Skip to content
This repository was archived by the owner on Aug 2, 2026. It is now read-only.

Commit 09eb7f4

Browse files
Bc generalize attach barcodes (#57)
* first draft for generalizing attaching barcodes * updated generalized class * Fixed PR #1 * styling comment changes * styling comment changes * styling comment changes * barcode end postion bug * Fixed PR #2 * Fixed PR #2, made arg validation more human readable * add comments/documentation to the class * updated comments/documentation style for return values * Fixed PR #57 comments * Updated doc strings, error handling * provided descriptions for input files and purpose of class * updated class description
1 parent ad3fbc5 commit 09eb7f4

2 files changed

Lines changed: 316 additions & 0 deletions

File tree

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
],
3838
entry_points={
3939
'console_scripts': [
40+
'AttachBarcodes = sctools.platform:BarcodePlatform.attach_barcodes',
4041
'Attach10xBarcodes = sctools.platform:TenXV2.attach_barcodes',
4142
'SplitBam = sctools.platform:GenericPlatform.split_bam',
4243
'CalculateGeneMetrics = sctools.platform:GenericPlatform.calculate_gene_metrics',

src/sctools/platform.py

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,3 +629,318 @@ def attach_barcodes(cls, args=None):
629629
cls._tag_bamfile(args.u2, args.output_bamfile, tag_generators)
630630

631631
return 0
632+
633+
634+
class BarcodePlatform(GenericPlatform):
635+
"""Command Line Interface for extracting and attaching barcodes with specified positions
636+
generalizing TenXV2 attach barcodes
637+
638+
Sample, cell and/or molecule barcodes can be extracted and attached to an unmapped bam when the
639+
corresponding barcode's start position and and length are provided. The sample barcode is extracted
640+
from the index i7 fastq file and the cell and molecule barcode are extracted from the r1 fastq file
641+
642+
This class defines several methods that are created as CLI tools when sctools is installed
643+
(see setup.py)
644+
645+
Attributes
646+
----------
647+
cell_barcode : fastq.EmbeddedBarcode
648+
A data class that defines the start and end position of the cell barcode and the tags to
649+
assign the sequence and quality of the cell barcode
650+
molecule_barcode : fastq.EmbeddedBarcode
651+
A data class that defines the start and end position of the molecule barcode and the tags
652+
to assign the sequence and quality of the molecule barcode
653+
sample_barcode : fastq.EmbeddedBarcode
654+
A data class that defines the start and end position of the sample barcode and the tags
655+
to assign the sequence and quality of the sample barcode
656+
657+
Methods
658+
-------
659+
attach_barcodes()
660+
Attach barcodes from the forward (r1) and optionally index (i1) fastq files to the reverse
661+
(r2) bam file
662+
663+
"""
664+
cell_barcode = None
665+
molecule_barcode = None
666+
sample_barcode = None
667+
668+
@classmethod
669+
def _validate_barcode_args(cls, args):
670+
"""Validates that the barcode start position is greater than 0
671+
672+
Parameters
673+
----------
674+
args : object
675+
arguments list, The default value of None, when passed to `parser.parse_args`
676+
causes the parser to read `sys.argv`
677+
678+
Returns
679+
-------
680+
args : object
681+
return arguments list if valid
682+
683+
"""
684+
# check that if a barcode start position is provided, its length is also (and vice versa)
685+
cls._validate_barcode_length_and_position(args.cell_barcode_start_pos, args.cell_barcode_length)
686+
cls._validate_barcode_length_and_position(args.molecule_barcode_start_pos, args.molecule_barcode_length)
687+
cls._validate_barcode_length_and_position(args.sample_barcode_start_pos, args.sample_barcode_length)
688+
689+
# check that an index fastq is provided sample barcode length and position are given
690+
if args.i1 is None and args.sample_barcode_length:
691+
raise argparse.ArgumentError('An i7 index fastq file must be given to attach a sample barcode')
692+
693+
# check that cell and molecule barcodes don't overlap
694+
if args.cell_barcode_length and args.molecule_barcode_length:
695+
cls._validate_barcode_input(args.molecule_barcode_start_pos,
696+
args.cell_barcode_start_pos + args.cell_barcode_length)
697+
698+
return args
699+
700+
@classmethod
701+
def _validate_barcode_length_and_position(cls, barcode_start_position, barcode_length):
702+
"""Checks that either that both barcode length and position are given or that neither are given as arguments
703+
704+
Parameters
705+
----------
706+
barcode_start_position : int
707+
the user defined start position (base pairs) of the barcode
708+
709+
barcode_length : int
710+
the user defined length (base pairs) of the barcode
711+
712+
Returns
713+
-------
714+
given_value : int
715+
return given value if valid
716+
717+
"""
718+
barcode_start_pos_exists = bool(barcode_start_position) or (barcode_start_position == 0)
719+
barcode_length_exists = bool(barcode_length)
720+
# (XOR boolean logic)
721+
if (barcode_start_pos_exists != barcode_length_exists):
722+
raise argparse.ArgumentError('Invalid position/length, both position and length must be provided by the user together')
723+
724+
@classmethod
725+
def _validate_barcode_input(cls, given_value, min_value):
726+
"""Validates that the barcode input is greater than a min value
727+
728+
Parameters
729+
----------
730+
given_value : int
731+
the given value that must be greater than the min_value,
732+
(barcode length or barcode starting position)
733+
734+
min_value : int
735+
the min value that the given_value must be greater than
736+
737+
Returns
738+
-------
739+
given_value : int
740+
return given value if valid
741+
742+
"""
743+
if given_value < min_value:
744+
raise argparse.ArgumentTypeError('Invalid barcode length/position')
745+
return given_value
746+
747+
@classmethod
748+
def _validate_barcode_start_pos(cls, given_value):
749+
"""Validates that the barcode start position is greater than 0
750+
751+
Parameters
752+
----------
753+
given_value : Union[int, str]
754+
the given start position of the barcode to validate
755+
756+
Returns
757+
-------
758+
given_value : int
759+
returns the start position if it is valid
760+
761+
"""
762+
return cls._validate_barcode_input(int(given_value), 0)
763+
764+
@classmethod
765+
def _validate_barcode_length(cls, given_value):
766+
"""Validates that the barcode length is greater than 1
767+
768+
Parameters
769+
----------
770+
given_value : Union[int, str]
771+
the given length of the barcode to validate
772+
773+
Returns
774+
-------
775+
given_value : int
776+
returns the length if it is valid
777+
778+
"""
779+
return cls._validate_barcode_input(int(given_value), 1)
780+
781+
@classmethod
782+
def _tag_bamfile(cls,
783+
input_bamfile_name: str,
784+
output_bamfile_name: str,
785+
tag_generators: Iterable[fastq.EmbeddedBarcodeGenerator]) -> None:
786+
"""Adds tags from fastq file(s) to a bam file.
787+
788+
Attaches tags extracted from fastq files by `tag_generators`, attaches them to records from
789+
`input_bamfile_name`, and writes the result to `output_bamfile_name`
790+
791+
Parameters
792+
----------
793+
input_bamfile_name : str
794+
input bam
795+
output_bamfile_name : str
796+
output bam
797+
tag_generators : Iterable[fastq.EmbeddedBarcodeGenerator]
798+
Iterable of generators that yield barcodes from fastq files
799+
800+
"""
801+
bam_tagger = bam.Tagger(input_bamfile_name)
802+
bam_tagger.tag(output_bamfile_name, tag_generators)
803+
804+
@classmethod
805+
def _make_tag_generators(cls, r1, i1=None, whitelist=None) -> List[fastq.EmbeddedBarcodeGenerator]:
806+
"""Create tag generators from fastq files.
807+
808+
Tag generators are iterators that run over fastq records, they extract and yield all of the
809+
barcodes embedded in each fastq record. This means extracting the cell, umi, and/or the sample barcode.
810+
811+
Parameters
812+
----------
813+
r1 : str
814+
forward fastq file, where possibly the cell and/or molecule barcode is found
815+
i1 : str, optional
816+
index fastq file, where the sample barcode is found
817+
whitelist : str, optional
818+
A file that contains a list of acceptable cell barcodes
819+
820+
Returns
821+
-------
822+
tag_generators : List[EmbeddedBarcodeGenerator]
823+
EmbeddedBarcodeGenerators containing barcodes from the given fastq
824+
825+
"""
826+
tag_generators = []
827+
barcode_args = {'fastq_files': r1}
828+
829+
if i1:
830+
barcode_args['embedded_barcodes'] = [cls.sample_barcode]
831+
tag_generators.append(fastq.EmbeddedBarcodeGenerator(**barcode_args))
832+
833+
if whitelist:
834+
barcode_args['whitelist'] = whitelist
835+
if cls.cell_barcode:
836+
barcode_args['embedded_cell_barcode'] = cls.cell_barcode
837+
if cls.molecule_barcode:
838+
barcode_args['other_embedded_barcodes'] = cls.molecule_barcode
839+
tag_generators.append(fastq.BarcodeGeneratorWithCorrectedCellBarcodes(**barcode_args))
840+
841+
else:
842+
# for all the barcodes that have a length and starting position specified
843+
barcode_args['embedded_barcodes'] = [barcode for barcode in [cls.cell_barcode, cls.molecule_barcode] if barcode]
844+
tag_generators.append(fastq.EmbeddedBarcodeGenerator(**barcode_args))
845+
846+
return tag_generators
847+
848+
@classmethod
849+
def attach_barcodes(cls, args=None):
850+
"""Command line entrypoint for attaching barcodes to a bamfile.
851+
852+
Parameters
853+
----------
854+
args : Iterable[str], optional
855+
arguments list, The default value of None, when passed to `parser.parse_args`
856+
causes the parser to read `sys.argv`
857+
858+
Returns
859+
-------
860+
return_call : 0
861+
return call if the program completes successfully
862+
863+
"""
864+
parser = argparse.ArgumentParser()
865+
parser.add_argument('--r1',
866+
required=True,
867+
help='read 1 fastq file, where the cell and molecule barcode is found')
868+
parser.add_argument('--u2',
869+
required=True,
870+
help='unaligned bam, can be converted from fastq read 2'
871+
'using picard FastqToSam')
872+
parser.add_argument('-o',
873+
'--output-bamfile',
874+
required=True,
875+
help='filename for tagged bam')
876+
parser.add_argument('-w',
877+
'--whitelist',
878+
default=None,
879+
help='optional cell barcode whitelist. If provided, corrected barcodes '
880+
'will also be output when barcodes are observed within 1ED of a '
881+
'whitelisted barcode')
882+
parser.add_argument('--i1',
883+
default=None,
884+
help='(optional) i7 index fastq file, where the sample barcode is found')
885+
parser.add_argument('--sample-barcode-start-position',
886+
dest='sample_barcode_start_pos',
887+
default=None,
888+
help='the user defined start position (base pairs) of the sample barcode',
889+
type=cls._validate_barcode_start_pos)
890+
parser.add_argument('--sample-barcode-length',
891+
dest='sample_barcode_length',
892+
default=None,
893+
help='the user defined length (base pairs) of the sample barcode',
894+
type=cls._validate_barcode_length)
895+
parser.add_argument('--cell-barcode-start-position',
896+
dest='cell_barcode_start_pos',
897+
default=None,
898+
help='the user defined start position, in base pairs, of the cell barcode',
899+
type=cls._validate_barcode_start_pos)
900+
parser.add_argument('--cell-barcode-length',
901+
dest='cell_barcode_length',
902+
default=None,
903+
help='the user defined length, in base pairs, of the cell barcode',
904+
type=cls._validate_barcode_length)
905+
parser.add_argument('--molecule-barcode-start-position',
906+
dest='molecule_barcode_start_pos',
907+
default=None,
908+
help='the user defined start position, in base pairs, of the molecule barcode '
909+
'(must be not overlap cell barcode if cell barcode is provided)',
910+
type=cls._validate_barcode_start_pos)
911+
parser.add_argument('--molecule-barcode-length',
912+
dest='molecule_barcode_length',
913+
default=None,
914+
help='the user defined length, in base pairs, of the molecule barcode',
915+
type=cls._validate_barcode_length)
916+
917+
# parse and validate the args
918+
if args:
919+
args = parser.parse_args(args)
920+
else:
921+
args = parser.parse_args()
922+
cls._validate_barcode_args(args)
923+
924+
# if the length and there for the start pos have been given as args
925+
# get the appropriate barcodes
926+
if args.cell_barcode_length:
927+
cls.cell_barcode = fastq.EmbeddedBarcode(start=args.cell_barcode_start_pos,
928+
end=args.cell_barcode_start_pos + args.cell_barcode_length,
929+
quality_tag=consts.QUALITY_CELL_BARCODE_TAG_KEY,
930+
sequence_tag=consts.RAW_CELL_BARCODE_TAG_KEY)
931+
if args.molecule_barcode_length:
932+
cls.molecule_barcode = fastq.EmbeddedBarcode(start=args.molecule_barcode_start_pos,
933+
end=args.molecule_barcode_start_pos + args.molecule_barcode_length,
934+
quality_tag=consts.QUALITY_MOLECULE_BARCODE_TAG_KEY,
935+
sequence_tag=consts.RAW_MOLECULE_BARCODE_TAG_KEY)
936+
if args.sample_barcode_length:
937+
cls.sample_barcode = fastq.EmbeddedBarcode(start=args.sample_barcode_start_pos,
938+
end=args.sample_barcode_start_pos + args.sample_barcode_length,
939+
quality_tag=consts.QUALITY_SAMPLE_BARCODE_TAG_KEY,
940+
sequence_tag=consts.RAW_SAMPLE_BARCODE_TAG_KEY)
941+
942+
# make the tags and attach the barcodes
943+
tag_generators = cls._make_tag_generators(args.r1, args.i1, args.whitelist)
944+
cls._tag_bamfile(args.u2, args.output_bamfile, tag_generators)
945+
946+
return 0

0 commit comments

Comments
 (0)