@@ -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