import sys from typing import List, Tuple import re import numpy as np from abnumber.exceptions import ChainParseError try: from anarci.anarci import anarci except ImportError: # Only print the error without failing - required to import print('ANARCI module not available. Please install it separately or install AbNumber through Bioconda') print('See: https://abnumber.readthedocs.io/') sys.exit(1) POS_REGEX = re.compile(r'([HL]?)(\d+)([A-Z]?)') WHITESPACE = re.compile(r'\s+') def _validate_chain_type(chain_type): assert chain_type in ['H', 'L', 'K'], \ f'Invalid chain type "{chain_type}", it should be "H" (heavy), "L" (lambda light chian) or "K" (kappa light chain)' def _anarci_align(sequence, scheme, allowed_species, assign_germline=False) -> List[Tuple]: from abnumber.position import Position sequence = re.sub(WHITESPACE, '', sequence) all_numbered, all_ali, all_hits = anarci( [('id', sequence)], scheme=scheme, allowed_species=allowed_species, assign_germline=assign_germline ) seq_numbered = all_numbered[0] seq_ali = all_ali[0] if seq_numbered is None: raise ChainParseError(f'Variable chain sequence not recognized: "{sequence}"') assert len(seq_numbered) == len(seq_ali), 'Unexpected ANARCI output' results = [] for (positions, start, end), ali in zip(seq_numbered, seq_ali): chain_type = ali['chain_type'] species = ali['species'] v_gene = ali['germlines']['v_gene'][0][1] if assign_germline else None j_gene = ali['germlines']['j_gene'][0][1] if assign_germline else None aa_dict = {Position(chain_type=chain_type, number=num, letter=letter, scheme=scheme): aa for (num, letter), aa in positions if aa != '-'} tail = sequence[end+1:] results.append((aa_dict, chain_type, tail, species, v_gene, j_gene)) return results def _get_unique_chains(chains): seqs = set() chains_filtered = [] for chain in chains: if chain.seq in seqs: continue seqs.add(chain.seq) chains_filtered.append(chain) return chains_filtered # Based on positive score in Blosum62 SIMILAR_PAIRS = {'AA', 'AS', 'CC', 'DD', 'DE', 'DN', 'ED', 'EE', 'EK', 'EQ', 'FF', 'FW', 'FY', 'GG', 'HH', 'HN', 'HY', 'II', 'IL', 'IM', 'IV', 'KE', 'KK', 'KQ', 'KR', 'LI', 'LL', 'LM', 'LV', 'MI', 'ML', 'MM', 'MV', 'ND', 'NH', 'NN', 'NS', 'PP', 'QE', 'QK', 'QQ', 'QR', 'RK', 'RQ', 'RR', 'SA', 'SN', 'SS', 'ST', 'TS', 'TT', 'VI', 'VL', 'VM', 'VV', 'WF', 'WW', 'WY', 'YF', 'YH', 'YW', 'YY'} def is_similar_residue(a, b): if a == '-' or b == '-': return a == b return a+b in SIMILAR_PAIRS def is_integer(object): return isinstance(object, int) or isinstance(object, np.integer) SUPPORTED_SCHEMES = ['imgt', 'aho', 'chothia', 'kabat'] SUPPORTED_CDR_DEFINITIONS = ['imgt', 'chothia', 'kabat', 'north'] SCHEME_BORDERS = { # Start coordinates # CDR1, FR2, CDR2, FR3, CDR3, FR4 'imgt': [27, 39, 56, 66, 105, 118, 129], 'kabat_H': [31, 36, 50, 66, 95, 103, 114], 'kabat_K': [24, 35, 50, 57, 89, 98, 108], 'kabat_L': [24, 35, 50, 57, 89, 98, 108], 'chothia_H': [26, 33, 52, 57, 95, 103, 114], 'chothia_K': [24, 35, 50, 57, 89, 98, 108], 'chothia_L': [24, 35, 50, 57, 89, 98, 108], 'north_H': [23, 36, 50, 59, 93, 103, 114], 'north_K': [24, 35, 49, 57, 89, 98, 108], 'north_L': [24, 35, 49, 57, 89, 98, 108], } # { scheme -> { region -> list of position numbers } } SCHEME_REGIONS = { scheme: { 'FR1': list(range(1, borders[0])), 'CDR1': list(range(borders[0], borders[1])), 'FR2': list(range(borders[1], borders[2])), 'CDR2': list(range(borders[2], borders[3])), 'FR3': list(range(borders[3], borders[4])), 'CDR3': list(range(borders[4], borders[5])), 'FR4': list(range(borders[5], borders[6])), } for scheme, borders in SCHEME_BORDERS.items() } # { scheme -> { position number -> region } } SCHEME_POSITION_TO_REGION = { scheme: {pos_num: region for region, positions in regions.items() for pos_num in positions} \ for scheme, regions in SCHEME_REGIONS.items() } # { scheme -> set of vernier position numbers } SCHEME_VERNIER = { # 'imgt_H': frozenset([2, 52, 53, 54, 76, 78, 80, 82, 87, 118]), # 'chothia_H': frozenset([2, 47, 48, 49, 67, 69, 71, 73, 78, 93, 94, 103]), # 'north_H': frozenset([2, 47, 48, 49, 67, 69, 71, 73, 78, 93, 94, 103]), 'kabat_H': frozenset([2, 27, 28, 29, 30, 47, 48, 49, 67, 69, 71, 73, 78, 93, 94, 103]), # 'imgt_K': frozenset([2, 4, 41, 42, 52, 53, 54, 55, 78, 80, 84, 85, 87, 118]), # 'imgt_L': frozenset([2, 4, 41, 42, 52, 53, 54, 55, 78, 80, 84, 85, 87, 118]), # 'chothia_K': frozenset([2, 4, 35, 36, 46, 47, 48, 49, 64, 66, 68, 69, 71, 98]), # 'chothia_L': frozenset([2, 4, 35, 36, 46, 47, 48, 49, 64, 66, 68, 69, 71, 98]), # 'north_K': frozenset([2, 4, 35, 36, 46, 47, 48, 49, 64, 66, 68, 69, 71, 98]), # 'north_L': frozenset([2, 4, 35, 36, 46, 47, 48, 49, 64, 66, 68, 69, 71, 98]), 'kabat_K': frozenset([2, 4, 35, 36, 46, 47, 48, 49, 64, 66, 68, 69, 71, 98]), 'kabat_L': frozenset([2, 4, 35, 36, 46, 47, 48, 49, 64, 66, 68, 69, 71, 98]), } #'kabat_H': 31-35, 50-65, 95-102 #'kabat_K': 24-34, 50-56, 89-97