Skip to content

API

Scheme

A class representing a primer scheme with headers and primer bed lines.

A Scheme contains both the header lines (comments) and the primer bed lines that define a complete primer scheme for amplicon sequencing or qPCR.

Please use Scheme.from_str() or Scheme.from_file() for creation.

Attributes:

Name Type Description
headers list[str]

List of header/comment lines from the bed file

bedlines list[BedLine]

List of BedLine objects representing primers

Source code in primalbedtools/scheme.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
class Scheme:
    """A class representing a primer scheme with headers and primer bed lines.

    A Scheme contains both the header lines (comments) and the primer bed lines
    that define a complete primer scheme for amplicon sequencing or qPCR.

    Please use Scheme.from_str() or Scheme.from_file() for creation.

    Attributes:
        headers (list[str]): List of header/comment lines from the bed file
        bedlines (list[BedLine]): List of BedLine objects representing primers
    """

    headers: list[str]
    bedlines: list[BedLine]

    def __init__(self, headers: Optional[list[str]], bedlines: list[BedLine]):
        """Initialize a Scheme with headers and bedlines.

        Please use Scheme.from_str() or Scheme.from_file() for creation.

        Args:
            headers: Optional list of header strings. If None, defaults to empty list.
            bedlines: List of BedLine objects representing the primers in the scheme.
        """
        # Parse the headers
        if headers is None:
            headers = []

        self.headers = headers
        self.bedlines = bedlines

    # io
    @classmethod
    def from_str(cls, str: str):
        """Create a Scheme from a bed file string.

        Args:
            str: String containing bed file content with headers and primer lines.

        Returns:
            Scheme: A new Scheme object parsed from the string.
        """
        headers, bedlines = bedline_from_str(str)
        return cls(headers, bedlines)

    @classmethod
    def from_file(cls, file: str):
        """Create a Scheme from a bed file on disk.

        Args:
            file: Path to the bed file to read.

        Returns:
            Scheme: A new Scheme object loaded from the file.
        """
        headers, bedlines = read_bedfile(file)
        return cls(headers, bedlines)

    def to_str(self) -> str:
        """Convert the scheme to a bed file format string.

        Returns:
            str: String representation of the scheme in bed file format,
                 including headers and all primer lines.
        """
        return create_bedfile_str(self.headers, self.bedlines)

    def to_file(self, path: str):
        """Write the scheme to a bed file on disk.

        Args:
            path: File path where the bed file should be written.
        """
        return write_bedfile(path, self.headers, self.bedlines)

    # modifiers
    def sort_bedlines(self):
        """Sort the bedlines in canonical order in place.

        Sorts bedlines by chromosome, amplicon number, direction, and primer suffix
        to ensure consistent ordering across the scheme.
        """
        self.bedlines = sort_bedlines(self.bedlines)

    def merge_primers(self):
        """merges bedlines with the same chrom, amplicon number and class in place"""
        self.bedlines = merge_primers(self.bedlines)

    def update_primernames(self):
        """Updates PrimerNames into V2 format in place"""
        self.bedlines = update_primernames(self.bedlines)

    # properties
    @property
    def contains_probes(self) -> bool:
        """Check if the scheme contains any probe primers.

        Returns:
            bool: True if any bedlines are PROBE type primers, False otherwise.
        """
        for bedline in self.bedlines:
            if bedline.primer_class == PrimerClass.PROBE:
                return True
        return False

    @property
    def header_dict(self) -> dict:
        """Parse headers into a dictionary format.

        Returns:
            dict: Dictionary representation of the header lines, parsed according
                  to common header formats used in bed files.
        """
        return parse_headers_to_dict(self.headers)

    def to_delim_str(
        self, include_headers: bool = True, use_header_aliases: bool = False
    ):
        return to_delim_str(
            self, include_headers=include_headers, use_header_aliases=use_header_aliases
        )

contains_probes property

Check if the scheme contains any probe primers.

Returns:

Name Type Description
bool bool

True if any bedlines are PROBE type primers, False otherwise.

header_dict property

Parse headers into a dictionary format.

Returns:

Name Type Description
dict dict

Dictionary representation of the header lines, parsed according to common header formats used in bed files.

__init__(headers, bedlines)

Initialize a Scheme with headers and bedlines.

Please use Scheme.from_str() or Scheme.from_file() for creation.

Parameters:

Name Type Description Default
headers Optional[list[str]]

Optional list of header strings. If None, defaults to empty list.

required
bedlines list[BedLine]

List of BedLine objects representing the primers in the scheme.

required
Source code in primalbedtools/scheme.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def __init__(self, headers: Optional[list[str]], bedlines: list[BedLine]):
    """Initialize a Scheme with headers and bedlines.

    Please use Scheme.from_str() or Scheme.from_file() for creation.

    Args:
        headers: Optional list of header strings. If None, defaults to empty list.
        bedlines: List of BedLine objects representing the primers in the scheme.
    """
    # Parse the headers
    if headers is None:
        headers = []

    self.headers = headers
    self.bedlines = bedlines

from_file(file) classmethod

Create a Scheme from a bed file on disk.

Parameters:

Name Type Description Default
file str

Path to the bed file to read.

required

Returns:

Name Type Description
Scheme

A new Scheme object loaded from the file.

Source code in primalbedtools/scheme.py
77
78
79
80
81
82
83
84
85
86
87
88
@classmethod
def from_file(cls, file: str):
    """Create a Scheme from a bed file on disk.

    Args:
        file: Path to the bed file to read.

    Returns:
        Scheme: A new Scheme object loaded from the file.
    """
    headers, bedlines = read_bedfile(file)
    return cls(headers, bedlines)

from_str(str) classmethod

Create a Scheme from a bed file string.

Parameters:

Name Type Description Default
str str

String containing bed file content with headers and primer lines.

required

Returns:

Name Type Description
Scheme

A new Scheme object parsed from the string.

Source code in primalbedtools/scheme.py
64
65
66
67
68
69
70
71
72
73
74
75
@classmethod
def from_str(cls, str: str):
    """Create a Scheme from a bed file string.

    Args:
        str: String containing bed file content with headers and primer lines.

    Returns:
        Scheme: A new Scheme object parsed from the string.
    """
    headers, bedlines = bedline_from_str(str)
    return cls(headers, bedlines)

merge_primers()

merges bedlines with the same chrom, amplicon number and class in place

Source code in primalbedtools/scheme.py
116
117
118
def merge_primers(self):
    """merges bedlines with the same chrom, amplicon number and class in place"""
    self.bedlines = merge_primers(self.bedlines)

sort_bedlines()

Sort the bedlines in canonical order in place.

Sorts bedlines by chromosome, amplicon number, direction, and primer suffix to ensure consistent ordering across the scheme.

Source code in primalbedtools/scheme.py
108
109
110
111
112
113
114
def sort_bedlines(self):
    """Sort the bedlines in canonical order in place.

    Sorts bedlines by chromosome, amplicon number, direction, and primer suffix
    to ensure consistent ordering across the scheme.
    """
    self.bedlines = sort_bedlines(self.bedlines)

to_file(path)

Write the scheme to a bed file on disk.

Parameters:

Name Type Description Default
path str

File path where the bed file should be written.

required
Source code in primalbedtools/scheme.py
 99
100
101
102
103
104
105
def to_file(self, path: str):
    """Write the scheme to a bed file on disk.

    Args:
        path: File path where the bed file should be written.
    """
    return write_bedfile(path, self.headers, self.bedlines)

to_str()

Convert the scheme to a bed file format string.

Returns:

Name Type Description
str str

String representation of the scheme in bed file format, including headers and all primer lines.

Source code in primalbedtools/scheme.py
90
91
92
93
94
95
96
97
def to_str(self) -> str:
    """Convert the scheme to a bed file format string.

    Returns:
        str: String representation of the scheme in bed file format,
             including headers and all primer lines.
    """
    return create_bedfile_str(self.headers, self.bedlines)

update_primernames()

Updates PrimerNames into V2 format in place

Source code in primalbedtools/scheme.py
120
121
122
def update_primernames(self):
    """Updates PrimerNames into V2 format in place"""
    self.bedlines = update_primernames(self.bedlines)

to_delim_str(scheme, include_headers=True, use_header_aliases=False)

Turns a bedfile into a full expanded delim separated file

Source code in primalbedtools/scheme.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
def to_delim_str(
    scheme: Scheme, include_headers: bool = True, use_header_aliases: bool = False
) -> str:
    """
    Turns a bedfile into a full expanded delim separated file
    """
    # Define the default headers
    headers = DEFAULT_CSV_HEADERS

    lines_to_write: list[str] = []

    header_aliases = scheme.header_dict
    aliases_to_attr = {v: k for k, v in header_aliases.items()}

    # Parse the attr strings add new headers
    for bl in scheme.bedlines:
        for k in bl.attributes.keys():
            if use_header_aliases:
                k = header_aliases.get(k, k)
            if k not in headers:
                headers.append(k)

    # Create a csv line for each bedline
    if include_headers:
        lines_to_write.append(",".join(headers))

    for bl in scheme.bedlines:
        bl_csv: list[str] = []
        for h in headers:
            r = None
            try:
                r = bl.__getattribute__(h)
            except AttributeError:
                # Search _attribute dict
                if h in bl.attributes:
                    r = bl.attributes[h]
                elif h in aliases_to_attr:
                    r = bl.attributes.get(aliases_to_attr[h])

            bl_csv.append(str(r) if r is not None else "")

        lines_to_write.append(",".join(bl_csv))

    # write all complete lines
    return "\n".join(lines_to_write)

BedFileModifier

Collection of methods for modifying BED files.

This class provides static methods for common BED file operations such as updating primer names, sorting BED lines, and merging BED lines with the same characteristics.

Source code in primalbedtools/bedfiles.py
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
class BedFileModifier:
    """Collection of methods for modifying BED files.

    This class provides static methods for common BED file operations such as
    updating primer names, sorting BED lines, and merging BED lines with the
    same characteristics.
    """

    @staticmethod
    def update_primernames(
        bedlines: list[BedLine],
    ) -> list[BedLine]:
        """Updates primer names to v2 format in place.

        Converts all primer names in the provided BedLine objects to the v2 format
        (prefix_number_class_index). Groups primers by chromosome and amplicon
        number, then updates each group.

        Args:
            bedlines: A list of BedLine objects to update.

        Returns:
            list[BedLine]: The updated list of BedLine objects with v2 format names.

        Examples:
            >>> from primalbedtools.bedfiles import BedLine, BedFileModifier
            >>> bedlines = [BedLine(...)]  # List of BedLine objects
            >>> updated = BedFileModifier.update_primernames(bedlines)
        """
        return update_primernames(bedlines)

    @staticmethod
    def downgrade_primernames(
        bedlines: list[BedLine], merge_alts=False
    ) -> list[BedLine]:
        """Downgrades primer names to v1 format in place.

        Converts all primer names in the provided BedLine objects to the v1 format
        (prefix_number_class_ALT). Groups primers by chromosome and amplicon
        number, then updates each group.

        Args:
            bedlines: A list of BedLine objects to downgrade.

        Returns:
            list[BedLine]: The updated list of BedLine objects with v1 format names.
        """
        if merge_alts:
            # Merge the alt primers
            bedlines = merge_primers(bedlines)
        # Downgrade the primer names
        return downgrade_primernames(bedlines)

    @staticmethod
    def sort_bedlines(
        bedlines: list[BedLine],
    ) -> list[BedLine]:
        """Sorts the bedlines by chrom, amplicon number, class, and sequence.

        Groups BedLine objects into primer pairs, sorts those pairs by chromosome
        and amplicon number, then returns a flattened list of the sorted BedLine objects.

        Args:
            bedlines: A list of BedLine objects to sort.

        Returns:
            list[BedLine]: A new list containing the sorted original BedLine objects.

        Examples:
            >>> from primalbedtools.bedfiles import BedLine, BedFileModifier
            >>> bedlines = [BedLine(...)]  # List of BedLine objects
            >>> sorted_lines = BedFileModifier.sort_bedlines(bedlines)
        """
        return sort_bedlines(bedlines)

    @staticmethod
    def merge_primers(
        bedlines: list[BedLine],
    ) -> list[BedLine]:
        """Merges bedlines with the same chrom, amplicon number and class.

        Groups BedLine objects into primer pairs, then for each forward and reverse
        group, creates a merged BedLine with:
        - The earliest start position
        - The latest end position
        - The longest sequence
        - The amplicon prefix, number, and pool from the first BedLine

        Args:
            bedlines: A list of BedLine objects to merge.

        Returns:
            list[BedLine]: A new list containing new merged BedLine objects.

        Examples:
            >>> from primalbedtools.bedfiles import BedLine, BedFileModifier
            >>> bedlines = [BedLine(...)]  # List of BedLine objects
            >>> merged_lines = BedFileModifier.merge_primers(bedlines)
        """
        return merge_primers(bedlines)

downgrade_primernames(bedlines, merge_alts=False) staticmethod

Downgrades primer names to v1 format in place.

Converts all primer names in the provided BedLine objects to the v1 format (prefix_number_class_ALT). Groups primers by chromosome and amplicon number, then updates each group.

Parameters:

Name Type Description Default
bedlines list[BedLine]

A list of BedLine objects to downgrade.

required

Returns:

Type Description
list[BedLine]

list[BedLine]: The updated list of BedLine objects with v1 format names.

Source code in primalbedtools/bedfiles.py
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
@staticmethod
def downgrade_primernames(
    bedlines: list[BedLine], merge_alts=False
) -> list[BedLine]:
    """Downgrades primer names to v1 format in place.

    Converts all primer names in the provided BedLine objects to the v1 format
    (prefix_number_class_ALT). Groups primers by chromosome and amplicon
    number, then updates each group.

    Args:
        bedlines: A list of BedLine objects to downgrade.

    Returns:
        list[BedLine]: The updated list of BedLine objects with v1 format names.
    """
    if merge_alts:
        # Merge the alt primers
        bedlines = merge_primers(bedlines)
    # Downgrade the primer names
    return downgrade_primernames(bedlines)

merge_primers(bedlines) staticmethod

Merges bedlines with the same chrom, amplicon number and class.

Groups BedLine objects into primer pairs, then for each forward and reverse group, creates a merged BedLine with: - The earliest start position - The latest end position - The longest sequence - The amplicon prefix, number, and pool from the first BedLine

Parameters:

Name Type Description Default
bedlines list[BedLine]

A list of BedLine objects to merge.

required

Returns:

Type Description
list[BedLine]

list[BedLine]: A new list containing new merged BedLine objects.

Examples:

>>> from primalbedtools.bedfiles import BedLine, BedFileModifier
>>> bedlines = [BedLine(...)]  # List of BedLine objects
>>> merged_lines = BedFileModifier.merge_primers(bedlines)
Source code in primalbedtools/bedfiles.py
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
@staticmethod
def merge_primers(
    bedlines: list[BedLine],
) -> list[BedLine]:
    """Merges bedlines with the same chrom, amplicon number and class.

    Groups BedLine objects into primer pairs, then for each forward and reverse
    group, creates a merged BedLine with:
    - The earliest start position
    - The latest end position
    - The longest sequence
    - The amplicon prefix, number, and pool from the first BedLine

    Args:
        bedlines: A list of BedLine objects to merge.

    Returns:
        list[BedLine]: A new list containing new merged BedLine objects.

    Examples:
        >>> from primalbedtools.bedfiles import BedLine, BedFileModifier
        >>> bedlines = [BedLine(...)]  # List of BedLine objects
        >>> merged_lines = BedFileModifier.merge_primers(bedlines)
    """
    return merge_primers(bedlines)

sort_bedlines(bedlines) staticmethod

Sorts the bedlines by chrom, amplicon number, class, and sequence.

Groups BedLine objects into primer pairs, sorts those pairs by chromosome and amplicon number, then returns a flattened list of the sorted BedLine objects.

Parameters:

Name Type Description Default
bedlines list[BedLine]

A list of BedLine objects to sort.

required

Returns:

Type Description
list[BedLine]

list[BedLine]: A new list containing the sorted original BedLine objects.

Examples:

>>> from primalbedtools.bedfiles import BedLine, BedFileModifier
>>> bedlines = [BedLine(...)]  # List of BedLine objects
>>> sorted_lines = BedFileModifier.sort_bedlines(bedlines)
Source code in primalbedtools/bedfiles.py
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
@staticmethod
def sort_bedlines(
    bedlines: list[BedLine],
) -> list[BedLine]:
    """Sorts the bedlines by chrom, amplicon number, class, and sequence.

    Groups BedLine objects into primer pairs, sorts those pairs by chromosome
    and amplicon number, then returns a flattened list of the sorted BedLine objects.

    Args:
        bedlines: A list of BedLine objects to sort.

    Returns:
        list[BedLine]: A new list containing the sorted original BedLine objects.

    Examples:
        >>> from primalbedtools.bedfiles import BedLine, BedFileModifier
        >>> bedlines = [BedLine(...)]  # List of BedLine objects
        >>> sorted_lines = BedFileModifier.sort_bedlines(bedlines)
    """
    return sort_bedlines(bedlines)

update_primernames(bedlines) staticmethod

Updates primer names to v2 format in place.

Converts all primer names in the provided BedLine objects to the v2 format (prefix_number_class_index). Groups primers by chromosome and amplicon number, then updates each group.

Parameters:

Name Type Description Default
bedlines list[BedLine]

A list of BedLine objects to update.

required

Returns:

Type Description
list[BedLine]

list[BedLine]: The updated list of BedLine objects with v2 format names.

Examples:

>>> from primalbedtools.bedfiles import BedLine, BedFileModifier
>>> bedlines = [BedLine(...)]  # List of BedLine objects
>>> updated = BedFileModifier.update_primernames(bedlines)
Source code in primalbedtools/bedfiles.py
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
@staticmethod
def update_primernames(
    bedlines: list[BedLine],
) -> list[BedLine]:
    """Updates primer names to v2 format in place.

    Converts all primer names in the provided BedLine objects to the v2 format
    (prefix_number_class_index). Groups primers by chromosome and amplicon
    number, then updates each group.

    Args:
        bedlines: A list of BedLine objects to update.

    Returns:
        list[BedLine]: The updated list of BedLine objects with v2 format names.

    Examples:
        >>> from primalbedtools.bedfiles import BedLine, BedFileModifier
        >>> bedlines = [BedLine(...)]  # List of BedLine objects
        >>> updated = BedFileModifier.update_primernames(bedlines)
    """
    return update_primernames(bedlines)

BedLine

A class representing a single line in a primer.bed file.

BedLine stores and validates all attributes of a primer entry in a BED file, with support for primername version handling, position validation, and output formatting. It maintains internal consistency between related fields like strand and primer direction, and automatically parses complex primername formats.

Attributes:

Name Type Description
chrom str

Chromosome name, must match pattern [a-zA-Z0-9_.]+

start int

0-based start position of the primer

end int

End position of the primer

primername str

Name of the primer in either format v1 or v2

pool int

1-based pool number (use ipool for 0-based pool number)

strand str

Strand of the primer ("+" for forward, "-" for reverse)

sequence str

Sequence of the primer

attributes (dict[str, str | float], None)

Dict of primer attributes (e.g., primerWeights (pw) for rebalancing).

Properties

length (int): Length of the primer (end - start) amplicon_number (int): Amplicon number extracted from primername amplicon_prefix (str): Amplicon prefix extracted from primername primer_suffix (int, str, None): Suffix of the primer (integer index or alt string) primername_version (PrimerNameVersion): Version of the primername format ipool (int): 0-based pool number (pool - 1) direction_str (str): Direction as string ("LEFT" or "RIGHT")

Examples:

>>> from primalbedtools.bedfiles import BedLine
>>> bedline = BedLine(
...     chrom="chr1",
...     start=100,
...     end=120,
...     primername="scheme_1_LEFT_alt1",
...     pool=1,
...     strand="+",
...     sequence="ACGTACGTACGTACGTACGT",
... )
>>> print(bedline.length)
20
>>> print(bedline.primername_version)
PrimerNameVersion.V1
>>> print(bedline.to_bed())
chr1    100     120     scheme_1_LEFT_alt1      1       +       ACGTACGTACGTACGTACGT
>>> bedline.amplicon_prefix = "new-scheme"
>>> print(bedline.to_bed())
chr1    100     120     new-scheme_1_LEFT_alt1  1       +       ACGTACGTACGTACGTACGT
Source code in primalbedtools/bedfiles.py
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
class BedLine:
    """A class representing a single line in a primer.bed file.

    BedLine stores and validates all attributes of a primer entry in a BED file,
    with support for primername version handling, position validation, and
    output formatting. It maintains internal consistency between related fields
    like strand and primer direction, and automatically parses complex primername
    formats.

    Attributes:
        chrom (str): Chromosome name, must match pattern [a-zA-Z0-9_.]+
        start (int): 0-based start position of the primer
        end (int): End position of the primer
        primername (str): Name of the primer in either format v1 or v2
        pool (int): 1-based pool number (use ipool for 0-based pool number)
        strand (str): Strand of the primer ("+" for forward, "-" for reverse)
        sequence (str): Sequence of the primer
        attributes (dict[str,str|float], None): Dict of primer attributes (e.g., primerWeights (pw) for rebalancing).

    Properties:
        length (int): Length of the primer (end - start)
        amplicon_number (int): Amplicon number extracted from primername
        amplicon_prefix (str): Amplicon prefix extracted from primername
        primer_suffix (int, str, None): Suffix of the primer (integer index or alt string)
        primername_version (PrimerNameVersion): Version of the primername format
        ipool (int): 0-based pool number (pool - 1)
        direction_str (str): Direction as string ("LEFT" or "RIGHT")

    Examples:
        >>> from primalbedtools.bedfiles import BedLine
        >>> bedline = BedLine(
        ...     chrom="chr1",
        ...     start=100,
        ...     end=120,
        ...     primername="scheme_1_LEFT_alt1",
        ...     pool=1,
        ...     strand="+",
        ...     sequence="ACGTACGTACGTACGTACGT",
        ... )
        >>> print(bedline.length)
        20
        >>> print(bedline.primername_version)
        PrimerNameVersion.V1
        >>> print(bedline.to_bed())
        chr1	100	120	scheme_1_LEFT_alt1	1	+	ACGTACGTACGTACGTACGT
        >>> bedline.amplicon_prefix = "new-scheme"
        >>> print(bedline.to_bed())
        chr1    100     120     new-scheme_1_LEFT_alt1  1       +       ACGTACGTACGTACGTACGT
    """

    __slots__ = (
        "_chrom",
        "_start",
        "_end",
        "_pool",
        "_strand",
        "_sequence",
        "_attributes",
        "_amplicon_prefix",
        "_amplicon_number",
        "_primer_class",
        "_primer_suffix",
    )
    # properties
    _chrom: str
    _start: int
    # primername is a calculated property
    _end: int
    _pool: int
    _strand: str
    _sequence: str

    # primerAttributes
    _attributes: dict[str, Union[str, float]]

    # primernames components
    _amplicon_prefix: str
    _amplicon_number: int
    _primer_class: PrimerClass
    _primer_suffix: Union[int, str, None]

    def __init__(
        self,
        chrom: str,
        start: Union[int, str],
        end: Union[int, str],
        primername: str,
        pool: Union[int, str],
        strand: str,
        sequence: str,
        attributes: Union[dict[str, Union[str, float]], str, None, float] = None,
    ) -> None:
        self.chrom = chrom
        self.start = start
        self.end = end
        self.pool = pool
        self.sequence = sequence
        # Set attributes
        self.attributes = attributes  # Ensure the type matches the expected UnionType

        # use private fields to sidestep primer_class to strand validation
        self._strand = validate_strand(strand)
        (
            self.amplicon_prefix,
            self.amplicon_number,
            primer_class,
            self.primer_suffix,
        ) = validate_primer_name(primername)
        self._primer_class = primer_class_str_to_enum(primer_class)

        # Check the primer_class and strand are compatible
        if not check_valid_class_and_strand(self.primer_class_str, self.strand):
            raise ValueError(
                f"primername ({self.primername}) implies direction ({self.primer_class_str}), which is incompatible with ({strand})"
            )

    @property
    def chrom(self):
        """Return the chromosome of the primer"""
        return self._chrom

    @chrom.setter
    def chrom(self, v):
        v = "".join(str(v).split())  # strip all whitespace
        if re.match(CHROM_REGEX, v):
            self._chrom = v
        else:
            raise ValueError(f"chrom must match '{CHROM_REGEX}'. Got (`{v}`)")

    @property
    def start(self):
        """Return the start position of the primer"""
        return self._start

    @start.setter
    def start(self, v):
        try:
            v = int(v)
        except (ValueError, TypeError) as e:
            raise ValueError(f"start must be an int. Got ({v})") from e
        if v < 0:
            raise ValueError(f"start must be greater than or equal to 0. Got ({v})")
        self._start = v

    @property
    def end(self):
        return self._end

    @end.setter
    def end(self, v):
        try:
            v = int(v)
        except (ValueError, TypeError) as e:
            raise ValueError(f"end must be an int. Got ({v})") from e
        if v < 0:
            raise ValueError(f"end must be greater than or equal to 0. Got ({v})")
        self._end = v

    @property
    def amplicon_number(self) -> int:
        """Return the amplicon number of the primer"""
        return self._amplicon_number

    @property
    def amplicon_name(self) -> str:
        """Return the amplicon name of the primer"""
        return f"{self.amplicon_prefix}_{self.amplicon_number}"

    @amplicon_number.setter
    def amplicon_number(self, v):
        try:
            v = int(v)
        except ValueError as e:
            raise ValueError(f"amplicon_number must be an int. Got ({v})") from e
        if v < 0:
            raise ValueError(
                f"amplicon_number must be greater than or equal to 0. Got ({v})"
            )

        self._amplicon_number = v

    @property
    def amplicon_prefix(self) -> str:
        """Return the amplicon_prefix of the primer"""
        return self._amplicon_prefix

    @amplicon_prefix.setter
    def amplicon_prefix(self, v):
        try:
            v = str(v).strip()
        except ValueError as e:
            raise ValueError(f"amplicon_prefix must be a str. Got ({v})") from e

        if check_amplicon_prefix(v):
            self._amplicon_prefix = v
        else:
            raise ValueError(
                f"Invalid amplicon_prefix: ({v}). Must be alphanumeric or hyphen."
            )

    @property
    def primer_suffix(self) -> Union[int, str, None]:
        """Return the primer_suffix of the primer"""
        return self._primer_suffix

    @primer_suffix.setter
    def primer_suffix(self, v: Union[int, str, None]):
        self._primer_suffix = validate_primer_suffix(v)

    @property
    def primer_class(self) -> PrimerClass:
        """Return the PrimerDirection enum"""
        return self._primer_class

    @property
    def primer_class_str(self) -> str:
        """Return the string representation of PrimerDirection"""
        return self._primer_class.value

    @primer_class.setter
    def primer_class(self, v: str):
        new_primer_class = primer_class_str_to_enum(v)
        if check_valid_class_and_strand(new_primer_class.value, self.strand):
            self._primer_class = new_primer_class
        else:
            raise ValueError(
                f"The new primer_class ({new_primer_class.value}) is incompatible with current strand ({self.strand}). Please use method 'force_change' to update both."
            )

    @property
    def primername(self):
        """Return the primername of the primer"""
        return create_primername(
            self.amplicon_prefix,
            self.amplicon_number,
            self.primer_class,
            self.primer_suffix,
        )

    @primername.setter
    def primername(self, v, new_strand: Optional[str] = None):
        v = v.strip()
        if version_primername(v) == PrimerNameVersion.INVALID:
            raise ValueError(f"Invalid primername: ({v}). Must be in v1 or v2 format")

        # Parse the primername
        parts = v.split("_")
        self.amplicon_prefix = parts[0]
        self.amplicon_number = int(parts[1])

        # Get primer_class
        new_primer_class = primer_class_str_to_enum(parts[2])
        implied_strand: Optional[str] = None

        # if non-ambiguous set strand
        if new_primer_class != PrimerClass.PROBE:
            if new_primer_class == PrimerClass.LEFT:
                implied_strand = Strand.FORWARD.value
            elif new_primer_class == PrimerClass.RIGHT:
                implied_strand = Strand.REVERSE.value
            else:
                raise ValueError("Unknown strand!")

        # Set the strand and class
        if new_strand:
            self.force_change(new_primer_class.value, new_strand)
        elif implied_strand:
            self.force_change(new_primer_class.value, implied_strand)

        # Try to parse the primer_suffix
        try:
            self.primer_suffix = parts[3]
        except IndexError:
            self.primer_suffix = None

    @property
    def pool(self):
        """Return the 1-based pool number of the primer"""
        return self._pool

    @pool.setter
    def pool(self, v):
        try:
            v = int(v)
        except (ValueError, TypeError) as e:
            raise ValueError(f"pool must be an int. Got ({v})") from e
        if v < 1:
            raise ValueError(f"pool is 1-based pos int pool number. Got ({v})")
        self._pool = v

    @property
    def strand(self):
        """Return the strand of the primer"""
        return self._strand

    @property
    def strand_class(self) -> Strand:
        """Return the strand class of the primer"""
        if self.strand == "+":
            return Strand.FORWARD
        elif self.strand == "-":
            return Strand.REVERSE
        else:
            raise ValueError(
                f"Unknown strand value ({self.strand}) in {self.primername}"
            )

    @strand.setter
    def strand(self, v):
        new_s = validate_strand(v)

        if check_valid_class_and_strand(self.primer_class_str, new_s):
            self._strand = new_s
        else:
            raise ValueError(
                f"The new stand ({new_s}) is incompatible with current primer_class ({self.primer_class_str}). Please use method 'force_change' to update both."
            )

    @property
    def sequence(self):
        """Return the sequence of the primer"""
        return self._sequence

    @sequence.setter
    def sequence(self, v):
        if not isinstance(v, str):
            raise ValueError(f"sequence must be a str. Got ({v})")
        self._sequence = v.upper()

    @property
    def attributes(self):
        """Returns the primer attributes"""
        return self._attributes

    @attributes.setter
    def attributes(
        self, v: Optional[Union[dict[str, Union[str, float]], str, float]]
    ) -> None:
        # Parse string
        if isinstance(v, str):
            new_dict = parse_primer_attributes_str(v)
        # parse dict
        elif isinstance(v, dict):
            new_dict = v
        elif v is None:
            self._attributes = {}
            return
        else:
            raise ValueError(f"Invalid primer attributes. Got ({v})")

        if new_dict is None:
            self._attributes = {}
            return

        # Parse the new dict
        parsed_dict: dict[str, Union[str, float]] = {
            strip_all_white_space(str(k)): strip_all_white_space(str(v))
            for k, v in new_dict.items()
        }

        self._attributes = parsed_dict

        # Call to primer weight setter to validate
        if PRIMER_WEIGHT_KEY in parsed_dict:
            self.weight = parsed_dict[PRIMER_WEIGHT_KEY]

    @property
    def weight(self):
        """Return the weight of the primer from the attributes dict"""
        if self._attributes is None:
            return None
        return self._attributes.get(PRIMER_WEIGHT_KEY)

    @weight.setter
    def weight(self, v: Union[float, str, int, None]):
        # Catch Empty and None
        if v is None or v == "":
            if self._attributes is None:
                return
            # remove pw from the attributes
            self._attributes.pop(PRIMER_WEIGHT_KEY, None)
            return
        try:
            v = float(v)
        except (ValueError, TypeError) as e:
            raise ValueError(
                f"weight must be a float, None or empty str (''). Got ({v})"
            ) from e
        if v < 0:
            raise ValueError(f"weight must be greater than or equal to 0. Got ({v})")

        # Set primerWeights as pw in self._attributes
        if self._attributes is None:
            self._attributes = {}
        self._attributes[PRIMER_WEIGHT_KEY] = v

    # methods
    def force_change(self, primer_class: str, strand: str):
        """
        Updates compatible primerclass and strand simultaneously.
        """
        new_primer_class = primer_class_str_to_enum(primer_class)
        new_s = validate_strand(strand)

        if check_valid_class_and_strand(new_primer_class.value, new_s):
            self._primer_class = new_primer_class
            self._strand = new_s
        else:
            raise ValueError(
                f"The new primer_class ({new_primer_class.value}) is incompatible with current strand ({self.strand})."
            )

    # calculated properties
    @property
    def length(self):
        """Return the length of the primer"""
        return self.end - self.start

    @property
    def primername_version(self) -> PrimerNameVersion:
        return version_primername(self.primername)

    @property
    def ipool(self) -> int:
        """Return the 0-based pool number"""
        return self.pool - 1

    @property
    def direction_str(self) -> str:
        """Return 'LEFT' or 'RIGHT' based on strand"""
        return "LEFT" if self.strand == Strand.FORWARD.value else "RIGHT"

    def to_bed(self) -> str:
        """Convert the BedLine object to a BED formatted string."""
        # If a attributes is provided print. Else print empty string

        attribute_str = create_primer_attributes_str(self.attributes)
        if attribute_str is None:
            attribute_str = ""
        else:
            attribute_str = "\t" + attribute_str
        return f"{self.chrom}\t{self.start}\t{self.end}\t{self.primername}\t{self.pool}\t{self.strand}\t{self.sequence}{attribute_str}\n"

    def to_fasta(self, rc=False) -> str:
        """Convert the BedLine object to a FASTA formatted string."""
        if rc:
            return f">{self.primername}-rc\n{rc_seq(self.sequence)}\n"
        return f">{self.primername}\n{self.sequence}\n"

amplicon_name property

Return the amplicon name of the primer

amplicon_number property writable

Return the amplicon number of the primer

amplicon_prefix property writable

Return the amplicon_prefix of the primer

attributes property writable

Returns the primer attributes

chrom property writable

Return the chromosome of the primer

direction_str property

Return 'LEFT' or 'RIGHT' based on strand

ipool property

Return the 0-based pool number

length property

Return the length of the primer

pool property writable

Return the 1-based pool number of the primer

primer_class property writable

Return the PrimerDirection enum

primer_class_str property

Return the string representation of PrimerDirection

primer_suffix property writable

Return the primer_suffix of the primer

primername property writable

Return the primername of the primer

sequence property writable

Return the sequence of the primer

start property writable

Return the start position of the primer

strand property writable

Return the strand of the primer

strand_class property

Return the strand class of the primer

weight property writable

Return the weight of the primer from the attributes dict

force_change(primer_class, strand)

Updates compatible primerclass and strand simultaneously.

Source code in primalbedtools/bedfiles.py
691
692
693
694
695
696
697
698
699
700
701
702
703
704
def force_change(self, primer_class: str, strand: str):
    """
    Updates compatible primerclass and strand simultaneously.
    """
    new_primer_class = primer_class_str_to_enum(primer_class)
    new_s = validate_strand(strand)

    if check_valid_class_and_strand(new_primer_class.value, new_s):
        self._primer_class = new_primer_class
        self._strand = new_s
    else:
        raise ValueError(
            f"The new primer_class ({new_primer_class.value}) is incompatible with current strand ({self.strand})."
        )

to_bed()

Convert the BedLine object to a BED formatted string.

Source code in primalbedtools/bedfiles.py
726
727
728
729
730
731
732
733
734
735
def to_bed(self) -> str:
    """Convert the BedLine object to a BED formatted string."""
    # If a attributes is provided print. Else print empty string

    attribute_str = create_primer_attributes_str(self.attributes)
    if attribute_str is None:
        attribute_str = ""
    else:
        attribute_str = "\t" + attribute_str
    return f"{self.chrom}\t{self.start}\t{self.end}\t{self.primername}\t{self.pool}\t{self.strand}\t{self.sequence}{attribute_str}\n"

to_fasta(rc=False)

Convert the BedLine object to a FASTA formatted string.

Source code in primalbedtools/bedfiles.py
737
738
739
740
741
def to_fasta(self, rc=False) -> str:
    """Convert the BedLine object to a FASTA formatted string."""
    if rc:
        return f">{self.primername}-rc\n{rc_seq(self.sequence)}\n"
    return f">{self.primername}\n{self.sequence}\n"

BedLineParser

Collection of methods for BED file input/output operations.

This class provides static methods for parsing and writing BED files in various formats, handling both file system operations and string conversions.

Source code in primalbedtools/bedfiles.py
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
class BedLineParser:
    """Collection of methods for BED file input/output operations.

    This class provides static methods for parsing and writing BED files
    in various formats, handling both file system operations and string
    conversions.
    """

    @staticmethod
    def from_file(
        bedfile: typing.Union[str, pathlib.Path],
    ) -> tuple[list[str], list[BedLine]]:
        """Reads and parses a BED file from disk.

        Reads a BED file from the specified path and returns the header lines
        and parsed BedLine objects.

        Args:
            bedfile: Path to the BED file. Can be a string or Path object.

        Returns:
            tuple: A tuple containing: `list[str]`: Header lines from the BED file (lines starting with '#') `list[BedLine]`: Parsed BedLine objects from the file

        Raises:
            FileNotFoundError: If the specified file doesn't exist
            ValueError: If the file contains invalid BED entries

        Examples:
            >>> from primalbedtools.bedfiles import BedLineParser
            >>> headers, bedlines = BedLineParser.from_file("primers.bed")
            >>> print(f"Found {len(bedlines)} primer entries")
        """
        return read_bedfile(bedfile=bedfile)

    @staticmethod
    def from_str(bedfile_str: str) -> tuple[list[str], list[BedLine]]:
        """Parses a BED file from a string.

        Parses a string containing BED file content and returns the header lines
        and parsed BedLine objects.

        Args:
            bedfile_str: String containing BED file content.

        Returns:
            tuple: A tuple containing:
                - list[str]: Header lines from the BED string (lines starting with '#')
                - list[BedLine]: Parsed BedLine objects from the string

        Raises:
            ValueError: If the string contains invalid BED entries
        """
        return bedline_from_str(bedfile_str)

    @staticmethod
    def to_str(headers: typing.Optional[list[str]], bedlines: list[BedLine]) -> str:
        """Creates a BED file string from headers and BedLine objects.

        Combines header lines and BedLine objects into a properly formatted
        BED file string.

        Args:
            headers: List of header strings (with or without leading '#')
                    or None for no headers.
            bedlines: List of BedLine objects to format as strings.

        Returns:
            str: A formatted BED file string with headers and entries.

        Examples:
            >>> from primalbedtools.bedfiles import BedLine, BedLineParser
            >>> bedlines = [BedLine(...)]  # List of BedLine objects
            >>> headers = ["Track name=primers"]
            >>> bed_string = BedLineParser.to_str(headers, bedlines)
        """
        return create_bedfile_str(headers, bedlines)

    @staticmethod
    def to_file(
        bedfile: typing.Union[str, pathlib.Path],
        headers: typing.Optional[list[str]],
        bedlines: list[BedLine],
    ) -> None:
        """Writes headers and BedLine objects to a BED file.

        Creates or overwrites a BED file at the specified path with
        the provided headers and BedLine objects.

        Args:
            bedfile: Path where the BED file will be written.
                    Can be a string or Path object.
            headers: List of header strings (with or without leading '#')
                    or None for no headers.
            bedlines: List of BedLine objects to write to the file.

        Raises:
            IOError: If the file cannot be written
        """
        write_bedfile(bedfile, headers, bedlines)

from_file(bedfile) staticmethod

Reads and parses a BED file from disk.

Reads a BED file from the specified path and returns the header lines and parsed BedLine objects.

Parameters:

Name Type Description Default
bedfile Union[str, Path]

Path to the BED file. Can be a string or Path object.

required

Returns:

Name Type Description
tuple tuple[list[str], list[BedLine]]

A tuple containing: list[str]: Header lines from the BED file (lines starting with '#') list[BedLine]: Parsed BedLine objects from the file

Raises:

Type Description
FileNotFoundError

If the specified file doesn't exist

ValueError

If the file contains invalid BED entries

Examples:

>>> from primalbedtools.bedfiles import BedLineParser
>>> headers, bedlines = BedLineParser.from_file("primers.bed")
>>> print(f"Found {len(bedlines)} primer entries")
Source code in primalbedtools/bedfiles.py
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
@staticmethod
def from_file(
    bedfile: typing.Union[str, pathlib.Path],
) -> tuple[list[str], list[BedLine]]:
    """Reads and parses a BED file from disk.

    Reads a BED file from the specified path and returns the header lines
    and parsed BedLine objects.

    Args:
        bedfile: Path to the BED file. Can be a string or Path object.

    Returns:
        tuple: A tuple containing: `list[str]`: Header lines from the BED file (lines starting with '#') `list[BedLine]`: Parsed BedLine objects from the file

    Raises:
        FileNotFoundError: If the specified file doesn't exist
        ValueError: If the file contains invalid BED entries

    Examples:
        >>> from primalbedtools.bedfiles import BedLineParser
        >>> headers, bedlines = BedLineParser.from_file("primers.bed")
        >>> print(f"Found {len(bedlines)} primer entries")
    """
    return read_bedfile(bedfile=bedfile)

from_str(bedfile_str) staticmethod

Parses a BED file from a string.

Parses a string containing BED file content and returns the header lines and parsed BedLine objects.

Parameters:

Name Type Description Default
bedfile_str str

String containing BED file content.

required

Returns:

Name Type Description
tuple tuple[list[str], list[BedLine]]

A tuple containing: - list[str]: Header lines from the BED string (lines starting with '#') - list[BedLine]: Parsed BedLine objects from the string

Raises:

Type Description
ValueError

If the string contains invalid BED entries

Source code in primalbedtools/bedfiles.py
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
@staticmethod
def from_str(bedfile_str: str) -> tuple[list[str], list[BedLine]]:
    """Parses a BED file from a string.

    Parses a string containing BED file content and returns the header lines
    and parsed BedLine objects.

    Args:
        bedfile_str: String containing BED file content.

    Returns:
        tuple: A tuple containing:
            - list[str]: Header lines from the BED string (lines starting with '#')
            - list[BedLine]: Parsed BedLine objects from the string

    Raises:
        ValueError: If the string contains invalid BED entries
    """
    return bedline_from_str(bedfile_str)

to_file(bedfile, headers, bedlines) staticmethod

Writes headers and BedLine objects to a BED file.

Creates or overwrites a BED file at the specified path with the provided headers and BedLine objects.

Parameters:

Name Type Description Default
bedfile Union[str, Path]

Path where the BED file will be written. Can be a string or Path object.

required
headers Optional[list[str]]

List of header strings (with or without leading '#') or None for no headers.

required
bedlines list[BedLine]

List of BedLine objects to write to the file.

required

Raises:

Type Description
IOError

If the file cannot be written

Source code in primalbedtools/bedfiles.py
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
@staticmethod
def to_file(
    bedfile: typing.Union[str, pathlib.Path],
    headers: typing.Optional[list[str]],
    bedlines: list[BedLine],
) -> None:
    """Writes headers and BedLine objects to a BED file.

    Creates or overwrites a BED file at the specified path with
    the provided headers and BedLine objects.

    Args:
        bedfile: Path where the BED file will be written.
                Can be a string or Path object.
        headers: List of header strings (with or without leading '#')
                or None for no headers.
        bedlines: List of BedLine objects to write to the file.

    Raises:
        IOError: If the file cannot be written
    """
    write_bedfile(bedfile, headers, bedlines)

to_str(headers, bedlines) staticmethod

Creates a BED file string from headers and BedLine objects.

Combines header lines and BedLine objects into a properly formatted BED file string.

Parameters:

Name Type Description Default
headers Optional[list[str]]

List of header strings (with or without leading '#') or None for no headers.

required
bedlines list[BedLine]

List of BedLine objects to format as strings.

required

Returns:

Name Type Description
str str

A formatted BED file string with headers and entries.

Examples:

>>> from primalbedtools.bedfiles import BedLine, BedLineParser
>>> bedlines = [BedLine(...)]  # List of BedLine objects
>>> headers = ["Track name=primers"]
>>> bed_string = BedLineParser.to_str(headers, bedlines)
Source code in primalbedtools/bedfiles.py
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
@staticmethod
def to_str(headers: typing.Optional[list[str]], bedlines: list[BedLine]) -> str:
    """Creates a BED file string from headers and BedLine objects.

    Combines header lines and BedLine objects into a properly formatted
    BED file string.

    Args:
        headers: List of header strings (with or without leading '#')
                or None for no headers.
        bedlines: List of BedLine objects to format as strings.

    Returns:
        str: A formatted BED file string with headers and entries.

    Examples:
        >>> from primalbedtools.bedfiles import BedLine, BedLineParser
        >>> bedlines = [BedLine(...)]  # List of BedLine objects
        >>> headers = ["Track name=primers"]
        >>> bed_string = BedLineParser.to_str(headers, bedlines)
    """
    return create_bedfile_str(headers, bedlines)

bedline_from_str(bedline_str)

Create a list of BedLine objects from a BED string.

Source code in primalbedtools/bedfiles.py
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
def bedline_from_str(bedline_str: str) -> tuple[list[str], list[BedLine]]:
    """
    Create a list of BedLine objects from a BED string.
    """
    headers = []
    bedlines = []
    for line in bedline_str.strip().split("\n"):
        line = line.strip()

        # Handle headers
        if line.startswith("#"):
            headers.append(line)
        elif line:
            bedlines.append(create_bedline(line.split("\t")))

    return headers, bedlines

check_amplicon_prefix(amplicon_prefix)

Check if an amplicon prefix is valid.

Source code in primalbedtools/bedfiles.py
65
66
67
68
69
def check_amplicon_prefix(amplicon_prefix: str) -> bool:
    """
    Check if an amplicon prefix is valid.
    """
    return bool(re.match(AMPLICON_PREFIX, amplicon_prefix))

check_valid_class_and_strand(cls, strand)

This takes the PrimerClass str (LEFT|RIGHT|PROBE) and the strand str (+|-) and returns True is compatible.

Source code in primalbedtools/bedfiles.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def check_valid_class_and_strand(cls: str, strand: str) -> bool:
    """
    This takes the PrimerClass str (LEFT|RIGHT|PROBE) and the strand str (+|-)
    and returns True is compatible.
    """
    # Probe can be any
    if cls == PrimerClass.PROBE.value:
        return True
    # LEFT primers must be forwards
    elif cls == PrimerClass.LEFT.value and strand == Strand.FORWARD.value:
        return True
    # RIGHT primers must be Reverse
    elif cls == PrimerClass.RIGHT.value and strand == Strand.REVERSE.value:
        return True
    else:
        return False

create_bedline(bedline)

Creates a BedLine object from a list of string values.

:param bedline: list[str] A list of string values representing a BED line. The list should contain the following elements: - chrom: str, the chromosome name - start: str, the start position (will be converted to int) - end: str, the end position (will be converted to int) - primername: str, the name of the primer - pool: str, the pool number (will be converted to int) - strand: str, the strand ('+' or '-') - sequence: str, the sequence of the primer

:return: BedLine A BedLine object created from the provided values.

:raises ValueError: If any of the values cannot be converted to the appropriate type. :raises IndexError: If the provided list does not contain the correct number of elements.

Source code in primalbedtools/bedfiles.py
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
def create_bedline(bedline: list[str]) -> BedLine:
    """
    Creates a BedLine object from a list of string values.

    :param bedline: list[str]
        A list of string values representing a BED line. The list should contain the following elements:
        - chrom: str, the chromosome name
        - start: str, the start position (will be converted to int)
        - end: str, the end position (will be converted to int)
        - primername: str, the name of the primer
        - pool: str, the pool number (will be converted to int)
        - strand: str, the strand ('+' or '-')
        - sequence: str, the sequence of the primer

    :return: BedLine
        A BedLine object created from the provided values.

    :raises ValueError:
        If any of the values cannot be converted to the appropriate type.
    :raises IndexError:
        If the provided list does not contain the correct number of elements.
    """
    try:
        if len(bedline) < 8:
            attributes = None
        else:
            attributes = bedline[7]

        return BedLine(
            chrom=bedline[0],
            start=bedline[1],
            end=bedline[2],
            primername=bedline[3],
            pool=bedline[4],
            strand=bedline[5],
            sequence=bedline[6],
            attributes=attributes,
        )
    except IndexError as a:
        raise IndexError(
            f"Invalid BED line value: ({bedline}): has incorrect number of columns"
        ) from a

create_primer_attributes_str(primer_attributes)

Parses the dict into the ';' separated str. Strips all whitespace

Returns None on empty dict or None

Source code in primalbedtools/bedfiles.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def create_primer_attributes_str(
    primer_attributes: Union[dict[str, Union[str, float]], dict[str, str], None],
) -> Optional[str]:
    """
    Parses the dict into the ';' separated str. Strips all whitespace

    Returns None on empty dict or None
    """
    if primer_attributes is None or not primer_attributes:
        return None
    return ";".join(
        f"{strip_all_white_space(k)}={strip_all_white_space(str(v))}"
        for k, v in primer_attributes.items()
    )

create_primername(amplicon_prefix, amplicon_number, primer_class, primer_suffix)

Creates an unvalidated primername string.

Source code in primalbedtools/bedfiles.py
114
115
116
117
118
119
120
121
122
123
124
def create_primername(
    amplicon_prefix: str,
    amplicon_number: int,
    primer_class: PrimerClass,
    primer_suffix: Union[str, int, None],
):
    """
    Creates an unvalidated primername string.
    """
    values = [amplicon_prefix, amplicon_number, primer_class.value, primer_suffix]
    return "_".join([str(x) for x in values if x is not None])

downgrade_primernames(bedlines)

Downgrades primer names to v1 format in place.

Source code in primalbedtools/bedfiles.py
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
def downgrade_primernames(bedlines: list[BedLine]) -> list[BedLine]:
    """
    Downgrades primer names to v1 format in place.
    """
    # group the bedlines into Amplicons
    primer_pairs = group_primer_pairs(bedlines)

    # Update the primer names
    for left, right in primer_pairs:
        # Sort the bedlines by sequence
        left.sort(key=lambda x: x.sequence)
        for i, bedline in enumerate(left, start=1):
            alt = "" if i == 1 else f"_alt{i - 1}"
            bedline.primername = (
                f"{bedline.amplicon_prefix}_{bedline.amplicon_number}_LEFT{alt}"
            )

        right.sort(key=lambda x: x.sequence)
        for i, bedline in enumerate(right, start=1):
            alt = "" if i == 1 else f"_alt{i - 1}"
            bedline.primername = (
                f"{bedline.amplicon_prefix}_{bedline.amplicon_number}_RIGHT{alt}"
            )

    return bedlines

expand_bedlines(bedlines)

Expands ambiguous bases in the primer sequences to all possible combinations.

Source code in primalbedtools/bedfiles.py
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
def expand_bedlines(bedlines: list[BedLine]) -> list[BedLine]:
    """
    Expands ambiguous bases in the primer sequences to all possible combinations.
    """
    expanded_bedlines = []

    for bedline in bedlines:
        for expand_seq in expand_ambiguous_bases(bedline.sequence):
            expanded_bedlines.append(
                # Create a bunch of bedlines with the same name
                BedLine(
                    chrom=bedline.chrom,
                    start=bedline.start,
                    end=bedline.end,
                    primername=bedline.primername,
                    pool=bedline.pool,
                    strand=bedline.strand,
                    sequence=expand_seq,
                    attributes=bedline.attributes,
                )
            )
    # update the bedfile names
    expanded_bedlines = update_primernames(expanded_bedlines)
    return expanded_bedlines

group_amplicons(bedlines)

Groups all (including PROBES) BedLine objects into amplicons by chromosome and amplicon number.

This function takes a list of BedLine objects and groups them based on chromosome and amplicon number. For each amplicon number, it creates a dict containing the LEFT, PROBE, and RIGHT as the keys pointing to a list of corresponding Bedlines.

Parameters:

Name Type Description Default
bedlines list[BedLine]

A list of BedLine objects to group into primer pairs.

required

Returns:

Type Description
list[dict[str, list[BedLine]]]

A list of dicts, with the key being the primer class string, and value a list of BedLines.

Source code in primalbedtools/bedfiles.py
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
def group_amplicons(
    bedlines: list[BedLine],
) -> list[dict[str, list[BedLine]]]:
    """Groups all (including PROBES) BedLine objects into amplicons by chromosome and amplicon number.

    This function takes a list of BedLine objects and groups them based on chromosome
    and amplicon number. For each amplicon number, it creates a dict containing the LEFT,
    PROBE, and RIGHT as the keys pointing to a list of corresponding Bedlines.

    Args:
        bedlines: A list of BedLine objects to group into primer pairs.

    Returns:
        A list of dicts, with the key being the primer class string, and value a list of BedLines.

    """
    primer_pairs = []

    # Group by chrom
    for chrom_bedlines in group_by_chrom(bedlines).values():
        # Group by amplicon number
        for amplicon_number_bedlines in group_by_amplicon_number(
            chrom_bedlines
        ).values():
            # Generate primer pairs
            primer_pairs.append(group_by_class(amplicon_number_bedlines))

    return primer_pairs

group_by_amplicon_number(list_bedlines)

Groups a list of BedLine objects by amplicon number.

Takes a list of BedLine objects and organizes them into a dictionary where keys are amplicon numbers and values are lists of BedLine objects with that amplicon number.

Parameters:

Name Type Description Default
list_bedlines list[BedLine]

A list of BedLine objects to group.

required

Returns:

Type Description
dict[int, list[BedLine]]

dict[int, list[BedLine]]: A dictionary mapping amplicon numbers (int) to lists of BedLine objects.

Source code in primalbedtools/bedfiles.py
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
def group_by_amplicon_number(list_bedlines: list[BedLine]) -> dict[int, list[BedLine]]:
    """Groups a list of BedLine objects by amplicon number.

    Takes a list of BedLine objects and organizes them into a dictionary
    where keys are amplicon numbers and values are lists of BedLine objects
    with that amplicon number.

    Args:
        list_bedlines: A list of BedLine objects to group.

    Returns:
        dict[int, list[BedLine]]: A dictionary mapping amplicon numbers (int)
            to lists of BedLine objects.

    """
    bedlines_dict = {}
    for bedline in list_bedlines:
        if bedline.amplicon_number not in bedlines_dict:
            bedlines_dict[bedline.amplicon_number] = []
        bedlines_dict[bedline.amplicon_number].append(bedline)
    return bedlines_dict

group_by_chrom(list_bedlines)

Groups a list of BedLine objects by chromosome.

Takes a list of BedLine objects and organizes them into a dictionary where keys are chromosome names and values are lists of BedLine objects that belong to that chromosome.

Parameters:

Name Type Description Default
list_bedlines list[BedLine]

A list of BedLine objects to group.

required

Returns:

Type Description
dict[str, list[BedLine]]

dict[str, list[BedLine]]: A dictionary mapping chromosome names (str) to lists of BedLine objects.

Source code in primalbedtools/bedfiles.py
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
def group_by_chrom(list_bedlines: list[BedLine]) -> dict[str, list[BedLine]]:
    """Groups a list of BedLine objects by chromosome.

    Takes a list of BedLine objects and organizes them into a dictionary
    where keys are chromosome names and values are lists of BedLine objects
    that belong to that chromosome.

    Args:
        list_bedlines: A list of BedLine objects to group.

    Returns:
        dict[str, list[BedLine]]: A dictionary mapping chromosome names (str)
            to lists of BedLine objects.

    """
    bedlines_dict = {}
    for bedline in list_bedlines:
        if bedline.chrom not in bedlines_dict:
            bedlines_dict[bedline.chrom] = []
        bedlines_dict[bedline.chrom].append(bedline)
    return bedlines_dict

group_by_class(list_bedlines)

Groups a list of BedLine objects by primer class.

Takes a list of BedLine objects and organizes them into a dictionary where keys are class values and values are lists of BedLine objects in that class.

Parameters:

Name Type Description Default
list_bedlines list[BedLine]

A list of BedLine objects to group.

required

Returns:

Type Description
dict[str, list[BedLine]]

dict[str, list[BedLine]]: A dictionary mapping class values (str) to lists of BedLine objects.

Source code in primalbedtools/bedfiles.py
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
def group_by_class(
    list_bedlines: list[BedLine],
) -> dict[str, list[BedLine]]:
    """Groups a list of BedLine objects by primer class.

    Takes a list of BedLine objects and organizes them into a dictionary
    where keys are class values and values are lists of
    BedLine objects in that class.

    Args:
        list_bedlines: A list of BedLine objects to group.

    Returns:
        dict[str, list[BedLine]]: A dictionary mapping class values (str)
            to lists of BedLine objects.

    """
    bedlines_dict = {}
    for bedline in list_bedlines:
        if bedline.primer_class_str not in bedlines_dict:
            bedlines_dict[bedline.primer_class_str] = []
        bedlines_dict[bedline.primer_class_str].append(bedline)

    return bedlines_dict

group_by_pool(list_bedlines)

Groups a list of BedLine objects by pool number.

Takes a list of BedLine objects and organizes them into a dictionary where keys are pool numbers and values are lists of BedLine objects with that pool number.

Parameters:

Name Type Description Default
list_bedlines list[BedLine]

A list of BedLine objects to group.

required

Returns:

Type Description
dict[int, list[BedLine]]

dict[int, list[BedLine]]: A dictionary mapping pool numbers (int) to lists of BedLine objects.

Source code in primalbedtools/bedfiles.py
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
def group_by_pool(
    list_bedlines: list[BedLine],
) -> dict[int, list[BedLine]]:
    """Groups a list of BedLine objects by pool number.

    Takes a list of BedLine objects and organizes them into a dictionary
    where keys are pool numbers and values are lists of BedLine objects
    with that pool number.

    Args:
        list_bedlines: A list of BedLine objects to group.

    Returns:
        dict[int, list[BedLine]]: A dictionary mapping pool numbers (int)
            to lists of BedLine objects.

    """
    bedlines_dict = {}
    for bedline in list_bedlines:
        if bedline.pool not in bedlines_dict:
            bedlines_dict[bedline.pool] = []
        bedlines_dict[bedline.pool].append(bedline)
    return bedlines_dict

group_by_strand(list_bedlines)

Groups a list of BedLine objects by strand.

Takes a list of BedLine objects and organizes them into a dictionary where keys are strand values ("+" or "-") and values are lists of BedLine objects on that strand.

Parameters:

Name Type Description Default
list_bedlines list[BedLine]

A list of BedLine objects to group.

required

Returns:

Type Description
dict[str, list[BedLine]]

dict[str, list[BedLine]]: A dictionary mapping strand values (str) to lists of BedLine objects.

Source code in primalbedtools/bedfiles.py
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
def group_by_strand(list_bedlines: list[BedLine]) -> dict[str, list[BedLine]]:
    """Groups a list of BedLine objects by strand.

    Takes a list of BedLine objects and organizes them into a dictionary
    where keys are strand values ("+" or "-") and values are lists of
    BedLine objects on that strand.

    Args:
        list_bedlines: A list of BedLine objects to group.

    Returns:
        dict[str, list[BedLine]]: A dictionary mapping strand values (str)
            to lists of BedLine objects.

    """
    bedlines_dict = {}
    for bedline in list_bedlines:
        if bedline.strand not in bedlines_dict:
            bedlines_dict[bedline.strand] = []
        bedlines_dict[bedline.strand].append(bedline)
    return bedlines_dict

group_primer_pairs(bedlines)

Groups Primer BedLine objects into primer pairs by chromosome and amplicon number.

This function takes a list of BedLine objects and groups them based on chromosome and amplicon number. For each group, it creates a tuple containing the forward (LEFT) primers as the first element and the reverse (RIGHT) primers as the second element.

Parameters:

Name Type Description Default
bedlines list[BedLine]

A list of BedLine objects to group into primer pairs.

required

Returns:

Type Description
list[tuple[list[BedLine], list[BedLine]]]

A list of tuples, where each tuple contains: (First element: List of forward (LEFT) primers, Second element: List of reverse (RIGHT) primers)

Source code in primalbedtools/bedfiles.py
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
def group_primer_pairs(
    bedlines: list[BedLine],
) -> list[tuple[list[BedLine], list[BedLine]]]:
    """Groups Primer BedLine objects into primer pairs by chromosome and amplicon number.

    This function takes a list of BedLine objects and groups them based on chromosome
    and amplicon number. For each group, it creates a tuple containing the forward
    (LEFT) primers as the first element and the reverse (RIGHT) primers as the
    second element.

    Args:
        bedlines: A list of BedLine objects to group into primer pairs.

    Returns:
        A list of tuples, where each tuple contains: (First element: List of forward (LEFT) primers, Second element: List of reverse (RIGHT) primers)

    """
    primer_pairs = []

    # Group by chrom
    for chrom_bedlines in group_by_chrom(bedlines).values():
        # Group by amplicon number
        for amplicon_number_bedlines in group_by_amplicon_number(
            chrom_bedlines
        ).values():
            # Generate primer pairs
            strand_to_bedlines = group_by_class(amplicon_number_bedlines)
            primer_pairs.append(
                (
                    strand_to_bedlines.get(PrimerClass.LEFT.value, []),
                    strand_to_bedlines.get(PrimerClass.RIGHT.value, []),
                )
            )

    return primer_pairs

lr_string_to_strand_char(s)

Convert a LEFT/RIGHT string to a Strand.

Source code in primalbedtools/bedfiles.py
186
187
188
189
190
191
192
193
194
195
196
197
def lr_string_to_strand_char(s: str) -> str:
    """
    Convert a LEFT/RIGHT string to a Strand.
    """
    parsed_strand = s.upper().strip()

    if parsed_strand == "LEFT":
        return Strand.FORWARD.value
    elif parsed_strand == "RIGHT":
        return Strand.REVERSE.value
    else:
        raise ValueError(f"Invalid strand: {s}. Must be LEFT or RIGHT")

merge_primers(bedlines)

merges bedlines with the same chrom, amplicon number and class.

Source code in primalbedtools/bedfiles.py
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
def merge_primers(bedlines: list[BedLine]) -> list[BedLine]:
    """
    merges bedlines with the same chrom, amplicon number and class.
    """
    merged_bedlines = []

    for pdict in group_amplicons(bedlines):
        # Merge forward primers
        left = pdict.get(PrimerClass.LEFT.value, [])
        if left:
            fbedline_start = min([bedline.start for bedline in left])
            fbedline_end = max([bedline.end for bedline in left])
            fbedline_sequence = max([bedline.sequence for bedline in left], key=len)
            merged_bedlines.append(
                BedLine(
                    chrom=left[0].chrom,
                    start=fbedline_start,
                    end=fbedline_end,
                    primername=f"{left[0].amplicon_prefix}_{left[0].amplicon_number}_{PrimerClass.LEFT.value}_1",
                    pool=left[0].pool,
                    strand=Strand.FORWARD.value,
                    sequence=fbedline_sequence,
                )
            )

        # Merge probes
        probes = pdict.get(PrimerClass.PROBE.value, [])
        if probes:
            raise ValueError(
                f"can't merge schemes containing probes ({[p.primername for p in probes]})"
            )

        # Merge reverse primers
        right = pdict.get(PrimerClass.RIGHT.value, [])
        if right:
            rbedline_start = min([bedline.start for bedline in right])
            rbedline_end = max([bedline.end for bedline in right])
            rbedline_sequence = max([bedline.sequence for bedline in right], key=len)

            merged_bedlines.append(
                BedLine(
                    chrom=right[0].chrom,
                    start=rbedline_start,
                    end=rbedline_end,
                    primername=f"{right[0].amplicon_prefix}_{right[0].amplicon_number}_{PrimerClass.RIGHT.value}_1",
                    pool=right[0].pool,
                    strand=Strand.REVERSE.value,
                    sequence=rbedline_sequence,
                )
            )
    return merged_bedlines

parse_headers_to_dict(headers)

parses the header strings into a dict. - Removes the leading # and any padding white space. - splits the header line on the first '=', with the key, value being the left, or right|None

Source code in primalbedtools/bedfiles.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
def parse_headers_to_dict(headers: list[str]) -> dict[str, str]:
    """
    parses the header strings into a dict.
    - Removes the leading # and any padding white space.
    - splits the header line on the first '=', with the key, value being the left, or right|None
    """
    attr_dict = {}

    for header in headers:
        header = header.rstrip().lstrip()  # remove lr whitespace

        if header.startswith("#"):
            header = header[1:].lstrip()  # Remove # and any padding

        parts = header.split("=", 1)

        if len(parts) == 1:
            attr_dict[parts[0].rstrip()] = None
        else:
            attr_dict[parts[0].rstrip()] = parts[1]

    return attr_dict

sort_bedlines(bedlines)

Sorts bedlines by chrom, start, end, primername.

Source code in primalbedtools/bedfiles.py
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
def sort_bedlines(bedlines: list[BedLine]) -> list[BedLine]:
    """
    Sorts bedlines by chrom, start, end, primername.
    """
    amplicons = group_amplicons(bedlines)
    amplicons.sort(
        key=lambda x: (
            x[PrimerClass.LEFT.value][0].chrom,
            x[PrimerClass.LEFT.value][0].amplicon_number,
        )
    )  # Uses left primers

    # Sorted list
    sorted_list = []

    for dicts in amplicons:
        # Left primers
        lp = dicts.get(PrimerClass.LEFT.value, [])
        lp.sort(
            key=lambda x: x.primer_suffix if x.primer_suffix is not None else x.sequence
        )
        sorted_list.extend(lp)

        # Probes
        pp = dicts.get(PrimerClass.PROBE.value, [])
        pp.sort(
            key=lambda x: x.primer_suffix if x.primer_suffix is not None else x.sequence
        )
        sorted_list.extend(pp)

        # Right Primers
        rp = dicts.get(PrimerClass.RIGHT.value, [])
        rp.sort(
            key=lambda x: x.primer_suffix if x.primer_suffix is not None else x.sequence
        )
        sorted_list.extend(rp)

    return sorted_list

update_primernames(bedlines)

Update primer names to v2 format in place.

Source code in primalbedtools/bedfiles.py
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
def update_primernames(bedlines: list[BedLine]) -> list[BedLine]:
    """
    Update primer names to v2 format in place.
    """
    # group the bedlines into Amplicons
    primer_pairs = group_amplicons(bedlines)

    # Update the primer names
    for dicts in primer_pairs:
        # left primer
        lp = dicts.get(PrimerClass.LEFT.value, [])
        lp.sort(key=lambda x: x.sequence)
        for i, bedline in enumerate(lp, start=1):
            bedline.primername = f"{bedline.amplicon_prefix}_{bedline.amplicon_number}_{PrimerClass.LEFT.value}_{i}"

        # probes
        pp = dicts.get(PrimerClass.PROBE.value, [])
        pp.sort(key=lambda x: x.sequence)
        for i, bedline in enumerate(pp, start=1):
            bedline.primername = f"{bedline.amplicon_prefix}_{bedline.amplicon_number}_{PrimerClass.PROBE.value}_{i}"

        # right primers
        rp = dicts.get(PrimerClass.RIGHT.value, [])
        rp.sort(key=lambda x: x.sequence)
        for i, bedline in enumerate(rp, start=1):
            bedline.primername = f"{bedline.amplicon_prefix}_{bedline.amplicon_number}_{PrimerClass.RIGHT.value}_{i}"

    return bedlines

validate_amplicon_prefix(amplicon_prefix)

Validates and returns the amplicon_prefix. Raises ValueError

Source code in primalbedtools/bedfiles.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
def validate_amplicon_prefix(amplicon_prefix: str):
    """
    Validates and returns the amplicon_prefix. Raises ValueError
    """
    try:
        v = str(amplicon_prefix).strip()
    except ValueError as e:
        raise ValueError(
            f"amplicon_prefix must be a str. Got ({amplicon_prefix})"
        ) from e

    if check_amplicon_prefix(v):
        return v
    else:
        raise ValueError(
            f"Invalid amplicon_prefix: ({v}). Must be alphanumeric or hyphen."
        )

validate_primer_name(primername)

Validates the structure of the primer Name, and returns the unvalidated components. (Amplicon_prefix, Amplicon_number, Primer_class, Primer_suffix)

Source code in primalbedtools/bedfiles.py
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
def validate_primer_name(primername: str) -> tuple[str, str, str, Union[str, None]]:
    """
    Validates the structure of the primer Name, and returns the unvalidated components.
    (Amplicon_prefix, Amplicon_number, Primer_class, Primer_suffix)
    """

    primername = strip_all_white_space(primername)
    if version_primername(primername) == PrimerNameVersion.INVALID:
        raise ValueError(
            f"Invalid primername: ({primername}). Must be in v1 or v2 format"
        )

    parts = primername.split("_")
    if len(parts) == 3:
        parts.append(None)  # type: ignore

    return (parts[0], parts[1], parts[2], parts[3])

validate_strand(strand)

Validates and returns the strand. Raises ValueError

Source code in primalbedtools/bedfiles.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def validate_strand(strand: str) -> str:
    """
    Validates and returns the strand. Raises ValueError
    """
    try:
        v = str(strand).strip()
    except ValueError as e:
        raise ValueError(f"strand must be a str. Got ({strand})") from e

    if v not in {x.value for x in Strand}:
        raise ValueError(
            f"strand must be a str of ({[x.value for x in Strand]}). Got ({v})"
        )
    return v

version_primername(primername)

Check the version of a primername.

Source code in primalbedtools/bedfiles.py
53
54
55
56
57
58
59
60
61
62
def version_primername(primername: str) -> PrimerNameVersion:
    """
    Check the version of a primername.
    """
    if re.match(V1_PRIMERNAME, primername):
        return PrimerNameVersion.V1
    elif re.match(V2_PRIMERNAME, primername):
        return PrimerNameVersion.V2
    else:
        return PrimerNameVersion.INVALID

Amplicon

A class representing a PCR amplicon with forward and reverse primers, and optional probes.

An Amplicon object encapsulates all primers (LEFT, RIGHT, and optional PROBE) that belong to the same amplicon number and pool. It provides methods to calculate amplicon boundaries, coverage regions, and validate that all primers are consistent.

Attributes:

Name Type Description
left list[BedLine]

List of LEFT primer BedLine objects

right list[BedLine]

List of RIGHT primer BedLine objects

probes list[BedLine]

List of PROBE primer BedLine objects

chrom str

Chromosome name where the amplicon is located

pool int

1-based pool number

amplicon_number int

Amplicon number from primer names

prefix str

Amplicon prefix from primer names

Raises:

Type Description
ValueError

If primers have inconsistent chromosome, pool, or amplicon numbers

ValueError

If LEFT or RIGHT primers are missing

Examples:

>>> from primalbedtools.bedfiles import BedLine
>>> left_primer = BedLine(chrom="chr1", start=100, end=120,
...                       primername="scheme_1_LEFT_1", pool=1,
...                       strand="+", sequence="ACGT")
>>> right_primer = BedLine(chrom="chr1", start=200, end=220,
...                        primername="scheme_1_RIGHT_1", pool=1,
...                        strand="-", sequence="ACGT")
>>> amplicon = Amplicon(left=[left_primer], right=[right_primer])
>>> print(amplicon.amplicon_name)
scheme_1
>>> print(amplicon.amplicon_start, amplicon.amplicon_end)
100 220
Source code in primalbedtools/amplicons.py
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
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
class Amplicon:
    """A class representing a PCR amplicon with forward and reverse primers, and optional probes.

    An Amplicon object encapsulates all primers (LEFT, RIGHT, and optional PROBE) that belong
    to the same amplicon number and pool. It provides methods to calculate amplicon boundaries,
    coverage regions, and validate that all primers are consistent.

    Attributes:
        left (list[BedLine]): List of LEFT primer BedLine objects
        right (list[BedLine]): List of RIGHT primer BedLine objects
        probes (list[BedLine]): List of PROBE primer BedLine objects
        chrom (str): Chromosome name where the amplicon is located
        pool (int): 1-based pool number
        amplicon_number (int): Amplicon number from primer names
        prefix (str): Amplicon prefix from primer names

    Raises:
        ValueError: If primers have inconsistent chromosome, pool, or amplicon numbers
        ValueError: If LEFT or RIGHT primers are missing

    Examples:
        >>> from primalbedtools.bedfiles import BedLine
        >>> left_primer = BedLine(chrom="chr1", start=100, end=120,
        ...                       primername="scheme_1_LEFT_1", pool=1,
        ...                       strand="+", sequence="ACGT")
        >>> right_primer = BedLine(chrom="chr1", start=200, end=220,
        ...                        primername="scheme_1_RIGHT_1", pool=1,
        ...                        strand="-", sequence="ACGT")
        >>> amplicon = Amplicon(left=[left_primer], right=[right_primer])
        >>> print(amplicon.amplicon_name)
        scheme_1
        >>> print(amplicon.amplicon_start, amplicon.amplicon_end)
        100 220
    """

    left: list[BedLine]
    right: list[BedLine]
    probes: list[BedLine]

    chrom: str
    pool: int
    amplicon_number: int
    prefix: str

    def __init__(
        self,
        left: list[BedLine],
        right: list[BedLine],
        probes: Optional[list[BedLine]] = None,
    ):
        """Initialize an Amplicon with LEFT and RIGHT primers, and optional PROBE primers.

        Args:
            left: List of BedLine objects representing LEFT primers
            right: List of BedLine objects representing RIGHT primers
            probes: Optional list of BedLine objects representing PROBE primers

        Raises:
            ValueError: If primers have inconsistent chromosome, pool, or amplicon numbers
            ValueError: If LEFT or RIGHT primers are missing
        """
        self.left = left
        self.right = right

        if probes is None:
            probes = []
        self.probes = probes

        all_lines = left + right + probes

        # All prefixes must be the same
        prefixes = set([bedline.amplicon_prefix for bedline in all_lines])
        prefixes = sorted(prefixes)

        if len(prefixes) != 1:
            print(
                f"All bedlines must have the same prefix ({','.join(prefixes)}). Using the alphanumerically first one ({prefixes[0]})."
            )
        self.prefix = prefixes[0]

        # Check all chrom are the same
        chroms = set([bedline.chrom for bedline in all_lines])
        if len(chroms) != 1:
            raise ValueError(
                f"All bedlines must be on the same chromosome ({','.join(chroms)})"
            )
        self.chrom = chroms.pop()
        # Check all pools are the same
        pools = set([bedline.pool for bedline in all_lines])
        if len(pools) != 1:
            raise ValueError(
                f"All bedlines must be in the same pool ({','.join(map(str, pools))})"
            )
        self.pool = pools.pop()
        # Check all amplicon numbers are the same
        amplicon_numbers = set([bedline.amplicon_number for bedline in all_lines])
        if len(amplicon_numbers) != 1:
            raise ValueError(
                f"All bedlines must be the same amplicon ({','.join(map(str, amplicon_numbers))})"
            )
        self.amplicon_number = amplicon_numbers.pop()

        # Check both forward and reverse primers are present
        if not self.left:
            raise ValueError(
                f"No forward primers found for {self.prefix}_{self.amplicon_number}"
            )
        if not self.right:
            raise ValueError(
                f"No reverse primers found for {self.prefix}_{self.amplicon_number}"
            )

    def __lt__(self, other):
        if not isinstance(other, Amplicon):
            return NotImplemented
        return (self.chrom, self.amplicon_number, self.pool) < (
            other.chrom,
            other.amplicon_number,
            other.pool,
        )

    def __le__(self, other):
        if not isinstance(other, Amplicon):
            return NotImplemented
        return (self.chrom, self.amplicon_number, self.pool) <= (
            other.chrom,
            other.amplicon_number,
            other.pool,
        )

    def __gt__(self, other):
        if not isinstance(other, Amplicon):
            return NotImplemented
        return (self.chrom, self.amplicon_number, self.pool) > (
            other.chrom,
            other.amplicon_number,
            other.pool,
        )

    def __ge__(self, other):
        if not isinstance(other, Amplicon):
            return NotImplemented
        return (self.chrom, self.amplicon_number, self.pool) >= (
            other.chrom,
            other.amplicon_number,
            other.pool,
        )

    def __eq__(self, other):
        if not isinstance(other, Amplicon):
            return NotImplemented
        return (self.chrom, self.amplicon_number, self.pool) == (
            other.chrom,
            other.amplicon_number,
            other.pool,
        )

    def __ne__(self, other):
        if not isinstance(other, Amplicon):
            return NotImplemented
        return not self.__eq__(other)

    def __hash__(self) -> int:
        """Hash is based off the self.to_amplicon_str()"""
        return hash(self.to_amplicon_str())

    @property
    def ipool(self) -> int:
        """Get the 0-based pool number.

        Returns:
            int: Pool number converted from 1-based to 0-based indexing
        """
        return self.pool - 1

    @property
    def is_circular(self) -> bool:
        """Check if the amplicon appears to be circular (LEFT primer end > RIGHT primer start).

        This can indicate a circular genome where the amplicon spans the origin.

        Returns:
            bool: True if any LEFT primer end is greater than any RIGHT primer start
        """
        return self.left[0].end > self.right[0].start

    @property
    def amplicon_start(self) -> int:
        """Get the start position of the amplicon (earliest LEFT primer start).

        Returns:
            int: The smallest start position among all LEFT primers
        """
        return min(self.left, key=lambda x: x.start).start

    @property
    def amplicon_end(self) -> int:
        """Get the end position of the amplicon (latest RIGHT primer end).

        Returns:
            int: The largest end position among all RIGHT primers
        """
        return max(self.right, key=lambda x: x.end).end

    @property
    def coverage_start(self) -> int:
        """Get the start of the coverage region (latest LEFT primer end).

        This represents the first base that would be covered after primer trimming.

        Returns:
            int: The largest end position among all LEFT primers
        """
        return max(self.left, key=lambda x: x.end).end

    @property
    def coverage_end(self) -> int:
        """Get the end of the coverage region (earliest RIGHT primer start).

        This represents the last base that would be covered after primer trimming.

        Returns:
            int: The smallest start position among all RIGHT primers
        """
        return min(self.right, key=lambda x: x.start).start

    @property
    def amplicon_name(self) -> str:
        """Get the name of the amplicon.

        Returns:
            str: Amplicon name in format "prefix_number"
        """
        return f"{self.prefix}_{self.amplicon_number}"

    @property
    def probe_region(self) -> Optional[tuple[int, int]]:
        """Get the genomic region covered by PROBE primers.

        Returns:
            Optional[tuple[int, int]]: Half-open interval (start, end) of PROBE region,
                                     or None if no probes are present
        """
        if not self.probes:
            return None
        return (min(p.start for p in self.probes), max(p.end for p in self.probes))

    @property
    def left_region(self) -> tuple[int, int]:
        """Get the genomic region covered by LEFT primers.

        Returns:
            tuple[int, int]: Half-open interval (start, end) of LEFT primer region
        """
        return (min(lp.start for lp in self.left), max(lp.end for lp in self.left))

    @property
    def right_region(self) -> tuple[int, int]:
        """Get the genomic region covered by RIGHT primers.

        Returns:
            tuple[int, int]: Half-open interval (start, end) of RIGHT primer region
        """
        return (min(rp.start for rp in self.right), max(rp.end for rp in self.right))

    def to_amplicon_str(self) -> str:
        """Convert the amplicon to a BED format string representing the full amplicon.

        Returns:
            str: Tab-delimited string with chrom, amplicon_start, amplicon_end,
                 amplicon_name, and pool
        """
        return f"{self.chrom}\t{self.amplicon_start}\t{self.amplicon_end}\t{self.amplicon_name}\t{self.pool}"

    def to_primertrim_str(self) -> str:
        """Convert the amplicon to a BED format string representing the coverage region.

        This represents the region that would remain after primer trimming.

        Returns:
            str: Tab-delimited string with chrom, coverage_start, coverage_end,
                 amplicon_name, and pool
        """
        return f"{self.chrom}\t{self.coverage_start}\t{self.coverage_end}\t{self.amplicon_name}\t{self.pool}"

amplicon_end property

Get the end position of the amplicon (latest RIGHT primer end).

Returns:

Name Type Description
int int

The largest end position among all RIGHT primers

amplicon_name property

Get the name of the amplicon.

Returns:

Name Type Description
str str

Amplicon name in format "prefix_number"

amplicon_start property

Get the start position of the amplicon (earliest LEFT primer start).

Returns:

Name Type Description
int int

The smallest start position among all LEFT primers

coverage_end property

Get the end of the coverage region (earliest RIGHT primer start).

This represents the last base that would be covered after primer trimming.

Returns:

Name Type Description
int int

The smallest start position among all RIGHT primers

coverage_start property

Get the start of the coverage region (latest LEFT primer end).

This represents the first base that would be covered after primer trimming.

Returns:

Name Type Description
int int

The largest end position among all LEFT primers

ipool property

Get the 0-based pool number.

Returns:

Name Type Description
int int

Pool number converted from 1-based to 0-based indexing

is_circular property

Check if the amplicon appears to be circular (LEFT primer end > RIGHT primer start).

This can indicate a circular genome where the amplicon spans the origin.

Returns:

Name Type Description
bool bool

True if any LEFT primer end is greater than any RIGHT primer start

left_region property

Get the genomic region covered by LEFT primers.

Returns:

Type Description
tuple[int, int]

tuple[int, int]: Half-open interval (start, end) of LEFT primer region

probe_region property

Get the genomic region covered by PROBE primers.

Returns:

Type Description
Optional[tuple[int, int]]

Optional[tuple[int, int]]: Half-open interval (start, end) of PROBE region, or None if no probes are present

right_region property

Get the genomic region covered by RIGHT primers.

Returns:

Type Description
tuple[int, int]

tuple[int, int]: Half-open interval (start, end) of RIGHT primer region

__hash__()

Hash is based off the self.to_amplicon_str()

Source code in primalbedtools/amplicons.py
168
169
170
def __hash__(self) -> int:
    """Hash is based off the self.to_amplicon_str()"""
    return hash(self.to_amplicon_str())

__init__(left, right, probes=None)

Initialize an Amplicon with LEFT and RIGHT primers, and optional PROBE primers.

Parameters:

Name Type Description Default
left list[BedLine]

List of BedLine objects representing LEFT primers

required
right list[BedLine]

List of BedLine objects representing RIGHT primers

required
probes Optional[list[BedLine]]

Optional list of BedLine objects representing PROBE primers

None

Raises:

Type Description
ValueError

If primers have inconsistent chromosome, pool, or amplicon numbers

ValueError

If LEFT or RIGHT primers are missing

Source code in primalbedtools/amplicons.py
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def __init__(
    self,
    left: list[BedLine],
    right: list[BedLine],
    probes: Optional[list[BedLine]] = None,
):
    """Initialize an Amplicon with LEFT and RIGHT primers, and optional PROBE primers.

    Args:
        left: List of BedLine objects representing LEFT primers
        right: List of BedLine objects representing RIGHT primers
        probes: Optional list of BedLine objects representing PROBE primers

    Raises:
        ValueError: If primers have inconsistent chromosome, pool, or amplicon numbers
        ValueError: If LEFT or RIGHT primers are missing
    """
    self.left = left
    self.right = right

    if probes is None:
        probes = []
    self.probes = probes

    all_lines = left + right + probes

    # All prefixes must be the same
    prefixes = set([bedline.amplicon_prefix for bedline in all_lines])
    prefixes = sorted(prefixes)

    if len(prefixes) != 1:
        print(
            f"All bedlines must have the same prefix ({','.join(prefixes)}). Using the alphanumerically first one ({prefixes[0]})."
        )
    self.prefix = prefixes[0]

    # Check all chrom are the same
    chroms = set([bedline.chrom for bedline in all_lines])
    if len(chroms) != 1:
        raise ValueError(
            f"All bedlines must be on the same chromosome ({','.join(chroms)})"
        )
    self.chrom = chroms.pop()
    # Check all pools are the same
    pools = set([bedline.pool for bedline in all_lines])
    if len(pools) != 1:
        raise ValueError(
            f"All bedlines must be in the same pool ({','.join(map(str, pools))})"
        )
    self.pool = pools.pop()
    # Check all amplicon numbers are the same
    amplicon_numbers = set([bedline.amplicon_number for bedline in all_lines])
    if len(amplicon_numbers) != 1:
        raise ValueError(
            f"All bedlines must be the same amplicon ({','.join(map(str, amplicon_numbers))})"
        )
    self.amplicon_number = amplicon_numbers.pop()

    # Check both forward and reverse primers are present
    if not self.left:
        raise ValueError(
            f"No forward primers found for {self.prefix}_{self.amplicon_number}"
        )
    if not self.right:
        raise ValueError(
            f"No reverse primers found for {self.prefix}_{self.amplicon_number}"
        )

to_amplicon_str()

Convert the amplicon to a BED format string representing the full amplicon.

Returns:

Name Type Description
str str

Tab-delimited string with chrom, amplicon_start, amplicon_end, amplicon_name, and pool

Source code in primalbedtools/amplicons.py
271
272
273
274
275
276
277
278
def to_amplicon_str(self) -> str:
    """Convert the amplicon to a BED format string representing the full amplicon.

    Returns:
        str: Tab-delimited string with chrom, amplicon_start, amplicon_end,
             amplicon_name, and pool
    """
    return f"{self.chrom}\t{self.amplicon_start}\t{self.amplicon_end}\t{self.amplicon_name}\t{self.pool}"

to_primertrim_str()

Convert the amplicon to a BED format string representing the coverage region.

This represents the region that would remain after primer trimming.

Returns:

Name Type Description
str str

Tab-delimited string with chrom, coverage_start, coverage_end, amplicon_name, and pool

Source code in primalbedtools/amplicons.py
280
281
282
283
284
285
286
287
288
289
def to_primertrim_str(self) -> str:
    """Convert the amplicon to a BED format string representing the coverage region.

    This represents the region that would remain after primer trimming.

    Returns:
        str: Tab-delimited string with chrom, coverage_start, coverage_end,
             amplicon_name, and pool
    """
    return f"{self.chrom}\t{self.coverage_start}\t{self.coverage_end}\t{self.amplicon_name}\t{self.pool}"

create_amplicons(bedlines)

Group BedLine objects into Amplicon objects by chromosome, amplicon number, and pool.

Parameters:

Name Type Description Default
bedlines list[BedLine]

List of BedLine objects to group into amplicons

required

Returns:

Type Description
list[Amplicon]

list[Amplicon]: List of Amplicon objects created from the input bedlines

Raises:

Type Description
ValueError

If any amplicon is missing LEFT or RIGHT primers

ValueError

If primers within an amplicon have inconsistent attributes

Examples:

>>> from primalbedtools.bedfiles import BedLineParser
>>> headers, bedlines = BedLineParser.from_file("primers.bed")
>>> amplicons = create_amplicons(bedlines)
>>> print(f"Created {len(amplicons)} amplicons")
Source code in primalbedtools/amplicons.py
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
def create_amplicons(bedlines: list[BedLine]) -> list[Amplicon]:
    """Group BedLine objects into Amplicon objects by chromosome, amplicon number, and pool.

    Args:
        bedlines: List of BedLine objects to group into amplicons

    Returns:
        list[Amplicon]: List of Amplicon objects created from the input bedlines

    Raises:
        ValueError: If any amplicon is missing LEFT or RIGHT primers
        ValueError: If primers within an amplicon have inconsistent attributes

    Examples:
        >>> from primalbedtools.bedfiles import BedLineParser
        >>> headers, bedlines = BedLineParser.from_file("primers.bed")
        >>> amplicons = create_amplicons(bedlines)
        >>> print(f"Created {len(amplicons)} amplicons")
    """
    grouped_bedlines = group_amplicons(bedlines)
    primer_pairs = []
    for pdict in grouped_bedlines:
        primer_pairs.append(
            Amplicon(
                left=pdict.get(PrimerClass.LEFT.value, []),
                right=pdict.get(PrimerClass.RIGHT.value, []),
                probes=pdict.get(PrimerClass.PROBE.value, []),
            )
        )

    return primer_pairs

do_pp_ol(pp1, pp2)

Check if two amplicons have overlapping genomic regions.

Parameters:

Name Type Description Default
pp1 Amplicon

First amplicon to compare

required
pp2 Amplicon

Second amplicon to compare

required

Returns:

Name Type Description
bool bool

True if the amplicons have any overlapping genomic coordinates

Examples:

>>> amplicon1 = Amplicon(left=[...], right=[...])  # Covers 100-200
>>> amplicon2 = Amplicon(left=[...], right=[...])  # Covers 150-250
>>> do_pp_ol(amplicon1, amplicon2)
True
Source code in primalbedtools/amplicons.py
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
def do_pp_ol(pp1: Amplicon, pp2: Amplicon) -> bool:
    """Check if two amplicons have overlapping genomic regions.

    Args:
        pp1: First amplicon to compare
        pp2: Second amplicon to compare

    Returns:
        bool: True if the amplicons have any overlapping genomic coordinates

    Examples:
        >>> amplicon1 = Amplicon(left=[...], right=[...])  # Covers 100-200
        >>> amplicon2 = Amplicon(left=[...], right=[...])  # Covers 150-250
        >>> do_pp_ol(amplicon1, amplicon2)
        True
    """
    if range(
        max(pp1.amplicon_start, pp2.amplicon_start),
        min(pp1.amplicon_end, pp2.amplicon_end) + 1,
    ):
        return True
    else:
        return False