"""KLIFS API client for kinase names, pocket residues, and KLIFS region annotations.
Provides KLIFS Swagger clients (:class:`KinaseNames`, :class:`KinaseInfo`) and
:class:`KLIFSPocket` for retrieving and structuring KLIFS pocket and region data.
"""
import logging
import re
from dataclasses import dataclass, field
from itertools import chain
from typing import Any
import numpy as np
from Bio import Align
from bravado.client import SwaggerClient
from mkt.databases.aligners import BL2UniProtAligner, Kincore2UniProtAligner
from mkt.databases.api_schema import SwaggerAPIClient
logger = logging.getLogger(__name__)
# start/end and colors courtesy of OpenCADD
# https://github.com/volkamerlab/opencadd/blob/master/opencadd/databases/klifs/schema.py
DICT_POCKET_KLIFS_REGIONS = {
"I": {
"start": 1,
"end": 3,
"contiguous": True,
"color": "khaki",
},
"g.l": {
"start": 4,
"end": 9,
"contiguous": True,
"color": "green",
},
"II": {
"start": 10,
"end": 13,
"contiguous": True,
"color": "khaki",
},
"III": {
"start": 14,
"end": 19,
"contiguous": False,
"color": "khaki",
},
"αC": {
"start": 20,
"end": 30,
"contiguous": True,
"color": "red",
},
"b.l": {
"start": 31,
"end": 37,
"contiguous": True,
"color": "green",
},
"IV": {
"start": 38,
"end": 41,
"contiguous": False,
"color": "khaki",
},
"V": {
"start": 42,
"end": 44,
"contiguous": True,
"color": "khaki",
},
"GK": {
"start": 45,
"end": 45,
"contiguous": True,
"color": "orange",
},
"hinge": {
"start": 46,
"end": 48,
"contiguous": True,
"color": "magenta",
},
"linker": {
"start": 49,
"end": 52,
"contiguous": True,
"color": "cyan",
},
"αD": {
"start": 53,
"end": 59,
"contiguous": False,
"color": "red",
},
"αE": {
"start": 60,
"end": 64,
"contiguous": True,
"color": "red",
},
"VI": {
"start": 65,
"end": 67,
"contiguous": True,
"color": "khaki",
},
"c.l": {
"start": 68,
"end": 75,
"contiguous": True,
"color": "darkorange",
},
"VII": {
"start": 76,
"end": 78,
"contiguous": False,
"color": "khaki",
},
"VIII": {
"start": 79,
"end": 79,
"contiguous": True,
"color": "khaki",
},
"xDFG": {
"start": 80,
"end": 83,
"contiguous": True,
"color": "cornflowerblue",
},
"a.l": {
"start": 84,
"end": 85,
"contiguous": False,
"color": "cornflowerblue",
},
}
"""dict[str, dict[str, int | bool | str]]: Mapping KLIFS pocket region to start and end indices, \
boolean denoting if subsequent regions are contiguous, and colors."""
LIST_KLIFS_REGION = list(
chain(
*[
[f"{key}:{i}" for i in range(val["start"], val["end"] + 1)]
for key, val in DICT_POCKET_KLIFS_REGIONS.items()
]
)
)
"""list[str]: List of string of all KLIFS pocket regions in format region:idx."""
# αC:b.l - {'BUB1B': 'E'} - need to skip this, seems to be in b.l gap region
LIST_INTER_REGIONS = [
"II:III",
"III:αC",
"IV:V",
"hinge:linker",
"αD:αE",
"αE:VI",
"VII:VIII",
]
"""list[str]: List of inter-region region gaps that exist given analysis."""
LIST_INTRA_REGIONS = [
"b.l_intra",
"linker_intra",
]
"""list[str]: List of intra-region region gaps that exist given analysis."""
[docs]
class KLIFS(SwaggerAPIClient):
"""Class to interact with the KLIFS API."""
[docs]
def __init__(self):
"""Initialize KLIFS Class object.
Upon initialization, KLIFS API is queried.
Attributes
----------
url : str
KLIFS API URL
_klifs : bravado.client.SwaggerClient
KLIFS API object
"""
self.url = "https://dev.klifs.net/swagger_v2/swagger.json"
self._klifs = self.query_api()
[docs]
def query_api(self):
"""Get KLIFS API as bravado.client.SwaggerClient object.
Returns
-------
bravado.client.SwaggerClient
KLIFS API object
"""
klifs_api = SwaggerClient.from_url(
self.url,
config={
"validate_requests": False,
"validate_responses": False,
"validate_swagger_spec": False,
},
)
self._stamp_now()
return klifs_api
[docs]
def get_url(self):
"""Get KLIFS API URL."""
return self.url
[docs]
def get_klifs(self):
"""Get KLIFS API object."""
return self._klifs
[docs]
class KinaseNames(KLIFS):
"""Class to get kinase names from KLIFS."""
[docs]
def __init__(
self,
species: str | None = "Human",
) -> None:
"""Initialize KinaseNames Class object.
Upon initialization, KLIFS API is queried and kinase names are retrieved.
Parameters
----------
species : str
Species of the kinase; default "Human" but can also be "Mouse"
_kinase_names: dict[str, dict[str, str | int]] | None
Llist of dictionaries of all kinase names info from KLIFS
Attributes
----------
_kinase_names : list[str]
List of kinase names
"""
super().__init__()
self.species = species
self._kinase_names = self.query_kinase_names()
[docs]
def query_kinase_names(self) -> dict[str, dict[str, str | int]] | None:
"""Get kinase names from KLIFS.
Returns
-------
list[str]
List of kinase names
"""
if self.species is None:
list_kinase_names = (
self._klifs.Information.get_kinase_names().response().result
)
elif self.species not in ["Human", "Mouse"]:
logger.warn(f"Species {self.species} not supported")
return None
else:
list_kinase_names = (
self._klifs.Information.get_kinase_names(
species=self.species,
)
.response()
.result
)
list_kinase_names = [
{i: getattr(result, i) for i in result} for result in list_kinase_names
]
return list_kinase_names
[docs]
def get_kinase_names(self):
"""Get kinase names from KLIFS query."""
return self._kinase_names
LIST_KINASEINFO_KEYS = [
"family",
"full_name",
"gene_name",
"group",
"iuphar",
"kinase_ID",
"name",
"pocket",
"species",
"subfamily",
"uniprot",
]
"""list[str]: List of keys for kinase information; use to populate if 404 error."""
[docs]
class KinaseInfo(KLIFS):
"""Class to get information about a kinase from KLIFS."""
[docs]
def __init__(
self,
search_term: str | None = None,
search_field: str | None = None,
species: str = "Human",
) -> None:
"""Initialize KinaseInfo Class object.
Upon initialization, KLIFS API is queried and kinase information for specificied kinase is retrieved.
Parameters
----------
search_term : str
Search term used to query KLIFS API; if None will return all kinases
search_field : str | None
Search field (optional; default: None);
only used to post-hoc annotate column with search term in case of missing data
species : str
Species of the kinase; default "Human" but can also be "Mouse"
Attributes
----------
search_term : str
Search term used to query KLIFS API
search_field : str | None
Search field (optional; default: None);
only used to post-hoc annotate column with search term in case of missing data
species : str
Species of the kinase
status_code : int | None
Status code of the query; None if query fails
_kinase_info : list[dict[str, str | int | None]] | None
List of KLIFS API object for search term
"""
super().__init__()
self.search_term = search_term
self.search_field = search_field
self.species = species
self.status_code = 200
self._kinase_info = self.query_kinase_info()
[docs]
def check_species(self) -> bool:
"""Check if species is supported."""
if self.species is not None and self.species not in ["Human", "Mouse"]:
logger.warn(f"Species {self.species} not supported")
return False
return True
[docs]
def call_bravado_function(self) -> list[Any]:
"""Check which bravado.client.CallableOperation to use."""
if self.search_term is None:
if self.species is None:
list_return = (
self._klifs.Information.get_kinase_information().response().result
)
else:
list_return = (
self._klifs.Information.get_kinase_information(
species=self.species,
)
.response()
.result
)
else:
if self.species is None:
list_return = (
self._klifs.Information.get_kinase_ID(
kinase_name=[self.search_term],
)
.response()
.result
)
else:
list_return = (
self._klifs.Information.get_kinase_ID(
kinase_name=[self.search_term],
species=self.species,
)
.response()
.result
)
return list_return
[docs]
def handle_page_not_found(self):
"""Handle page not found error."""
dict_kinase_info = dict(
zip(LIST_KINASEINFO_KEYS, [None] * len(LIST_KINASEINFO_KEYS))
)
if self.search_field is not None:
dict_kinase_info[self.search_field] = self.search_term
return list(dict_kinase_info)
[docs]
def query_kinase_info(self) -> list[dict[str, str | int | None]] | None:
"""Get information about a kinase from KLIFS.
Returns
-------
list[dict[str, str | int | None]] | None
List of dictionaries with information about the kinase
"""
if not self.check_species():
return None
try:
kinase_info = self.call_bravado_function()
except Exception as e:
if hasattr(e, "status_code"):
# 404 error: kinase not found in KLIFS
if e.status_code == 404:
logger.warning(
f"Kinase {self.search_term} "
f"(field: {self.search_field} not found in KLIFS"
)
list_kinase_info = self.handle_page_not_found()
self.status_code = 404
# 500 error: server error
elif 500 <= e.status_code < 600:
logger.warning(f"Server error: {e.status_code}")
self.status_code = e.status_code
list_kinase_info = None
# all other Exceptions
else:
logger.warning(
f"Error {e} in query_kinase_info for "
f"{self.search_term} (field: {self.search_field})"
)
self.status_code = e.status_code
list_kinase_info = None
else:
logger.warning(f"Error {e} in query_kinase_info for {self.search_term}")
self.status_code = None
if self.status_code == 200:
list_kinase_info = [
{i: getattr(result, i) for i in result} for result in kinase_info
]
# 400 error: bad request does not throw an Exception above
else:
list_kinase_info = None
return list_kinase_info
[docs]
def get_search_term(self):
"""Get search term used for query."""
return self.search_term
[docs]
def get_search_field(self):
"""Get search field used for query."""
return self.search_field
[docs]
def get_species(self):
"""Get species used for query."""
return self.species
[docs]
def get_status_code(self):
"""Get status code of the query."""
return self.status_code
[docs]
def get_kinase_info(self):
"""Get information about the kinase from KLIFS query."""
return self._kinase_info
[docs]
@dataclass
class KLIFSPocket:
"""Dataclass to hold KLIFS pocket alignment information per kinase.
Attributes
----------
uniprotSeq : str
UniProt canonical sequence
klifsSeq : str
KLIFS pocket sequence
idx_kd : tuple[int | None, int | None]
Index of kinase domain in UniProt sequence (start, end)
offset_bool : bool
If True, offset indices by 1 to match 1-based indexing; default True
list_klifs_region : list[str]
List of start and end regions of KLIFS pocket separated by ":"; end region will be the
same as start region if no concatenation necessary to find a single exact match
list_klifs_substr_actual : list[str | None]
List of substring of KLIFS pocket that maps to the *start region* of the KLIFS pocket
list_klifs_substr_match : list[str | None]
List of the actual substring used to match to the KLIFS pocket for the region(s) provided;
will be the same as list_klifs_substr_actual if no concatenation necessary to find a single exact match
list_substring_idxs : list[list[int] | None]
List of indices in UniProt sequence where KLIFS substring match starts;
offset by length of preceding KLIFS region with gaps removed
list_align : list[str | None] | None
List of final alignments to UniProt sequence
"""
uniprotSeq: str
klifsSeq: str
idx_kd: tuple[int | None, int | None]
offset_bool: bool = True
list_klifs_region: list[str | None] = field(default_factory=list)
list_klifs_substr_actual: list[str | None] = field(default_factory=list)
list_klifs_substr_match: list[str | None] = field(default_factory=list)
list_substring_idxs: list[list[int | None] | None] = field(default_factory=list)
list_align: list[str | None] | None = None
KLIFS2UniProtIdx: dict[str, int | None] = field(default_factory=dict)
KLIFS2UniProtSeq: dict[str, str | None] = field(default_factory=dict)
def __post_init__(self):
self.iterate_klifs_alignment()
self.generate_alignment_list(bool_offset=self.offset_bool)
if self.list_align is not None:
self.KLIFS2UniProtIdx.update(dict(zip(LIST_KLIFS_REGION, self.list_align)))
self.generate_alignment_list_including_gaps()
[docs]
@staticmethod
def remove_gaps_from_klifs(klifs_string: str) -> str:
"""Remove gaps from KLIFS pocket sequence.
Parameters
----------
klifs_pocket : str
KLIFS pocket sequence; can be entire sequence or substring
Returns
-------
klifs_pocket_narm : str
KLIFS pocket sequence without gaps (i.e., "-" removed)
"""
klifs_pocket_narm = "".join([i for i in klifs_string if i != "-"])
return klifs_pocket_narm
[docs]
@staticmethod
def return_idx_of_substring_in_superstring(
superstring: str,
substring: str,
) -> list[int] | None:
"""Returns the index where substring begins in superstring (does not require -1 offset).
Parameters
----------
superstring : str
String in which to find substring index
substring : str
String in which to find superstring index
Returns
-------
list_out : list[int] | None
Index where substring begins in superstring; None if substring not in superstring
"""
list_out = [
i for i in range(len(superstring)) if superstring.startswith(substring, i)
]
return list_out
[docs]
@staticmethod
def return_idx_of_alignment_match(
align: Align.PairwiseAlignments,
) -> list[int]:
"""Return indices of alignment match.
Parameters
----------
align : Align.PairwiseAlignments
Pairwise alignments
Returns
-------
list[int]
List of indices for alignment match
"""
# extract target (b.l) and query (UniProt) sequences
target = align.indices[0]
query = align.indices[1]
# where target is aligned, set to 1; where target is not aligned, set to np.nan
target[target >= 0] = 1
target = np.where(target == -1, np.nan, target)
# keep only indices where target is aligned to query
output = target * query
output = output[~np.isnan(output)]
output = [int(i) for i in output.tolist()]
return output
[docs]
def select_correct_alignment(
self,
alignments: Align.PairwiseAlignments,
bool_bl: bool = True,
) -> list[int]:
"""Select correct alignment for b.l region.
Parameters
----------
alignments : Align.PairwiseAlignments
Pairwise alignments
bool_bl : bool
If True, select correct alignment for b.l region; if False, select correct alignment for linker region
Returns
-------
list[int]
List of indices for correct alignment
"""
list_alignments = [
re.findall(r"[A-Z]+", alignment[0, :]) for alignment in alignments
]
if bool_bl:
# manual review showed 2 matches + gap + 5 matches in b.l region
list_idx = [
idx
for idx, i in enumerate(list_alignments)
if len(i) == 2 and len(i[0]) == 2
]
region = "b.l"
else:
# manual review showed 1 matches + gap + 3 matches in linker region
list_idx = [
idx
for idx, i in enumerate(list_alignments)
if len(i) == 2 and len(i[0]) == 1
]
region = "linker"
if len(list_idx) > 1:
logger.error(
f"{len(list_idx)} correct alignments found for {region} region\n{list_alignments}"
)
return None
# BUB1B and PIK3R4 have "-" in b.l region so will not obey heuristic in list_idx
elif len(list_idx) == 0:
if len(alignments) == 1:
alignment = alignments[0]
else:
logger.error(
f"{len(alignments)} non-heuristic alignments found for {region} region\n"
f""
f"{[print(i) for i in alignments]}"
)
return None
else:
alignment = alignments[list_idx[0]]
return self.return_idx_of_alignment_match(alignment)
[docs]
def align_klifs_pocket_to_uniprot_seq(
self,
idx_start: int,
idx_end: int,
) -> tuple[str, list[int] | None]:
"""Align KLIFS region to UniProt canonical Uniprot sequence.
Parameters
----------
idx_start : int
Start index of KLIFS region
idx_end : int
End index of KLIFS region
Returns
-------
substring_klifs : str
Substring of KLIFS pocket that maps to indices for the region(s) provided
list_idx : list[int] | None
List of indices in UniProt sequence where KLIFS region starts
"""
substring_klifs = self.klifsSeq[idx_start:idx_end]
substring_klifs_narm = self.remove_gaps_from_klifs(substring_klifs)
if len(substring_klifs_narm) == 0:
list_idx = None
else:
list_idx = self.return_idx_of_substring_in_superstring(
self.uniprotSeq, substring_klifs_narm
)
return substring_klifs, list_idx
[docs]
def find_start_or_end_idx_recursively(
self,
idx_in: int,
bool_start: bool = True,
) -> int:
"""Find the start or end indices in UniProt canonical sequence of flanking KLIFS regions recursively.
Parameters
----------
idx_in : int
Index of KLIFS region (e.g., I is 0, g.l is 1, etc.)
bool_start : bool
If True, find start index (default); if False, find end index
"""
# if looking for preceding region, start at idx_in - 1
if bool_start:
# if first region
if idx_in == 0:
# if KD start is None, return 0
if self.idx_kd[0] is None:
return 0
# if KD start is provided, return KD start
else:
return self.idx_kd[0]
idx_temp = self.list_substring_idxs[idx_in - 1]
str_temp = self.list_klifs_substr_actual[idx_in - 1]
if idx_temp is not None and len(idx_temp) == 1:
idx_out = idx_temp[0] + len(self.remove_gaps_from_klifs(str_temp))
else:
idx_out = self.find_start_or_end_idx_recursively(
idx_in - 1, bool_start=True
)
# if looking for subsequent region, start at idx_in + 1
else:
# if last region
if idx_in == len(DICT_POCKET_KLIFS_REGIONS) - 1:
# if KD end is None, return len(self.uniprotSeq) - 1
if self.idx_kd[1] is None:
return len(self.uniprotSeq) - 1
# if KD end is provided, return KD end
else:
return self.idx_kd[1]
idx_temp = self.list_substring_idxs[idx_in + 1]
if idx_temp is not None and len(idx_temp) == 1:
idx_out = idx_temp[0]
else:
idx_out = self.find_start_or_end_idx_recursively(
idx_in + 1, bool_start=False
)
return idx_out
[docs]
def return_partial_alignments(
self,
idx: int,
align_fn: Align.PairwiseAligner | None = None,
) -> tuple[int, int, Align.PairwiseAlignments | list[int | None] | None]:
"""Return partial alignments for b.l region.
Parameters
----------
idx : int
Index of region (e.g., I is 0, g.l is 1, etc.)
align_fn : Align.PairwiseAligner | None
Alignment function; if none provided will use exact match
Returns
-------
tuple[int, int, Align.PairwiseAlignments | list[int | None] | None]
Start, end, and alignments (either indices or alignments or None) for region
"""
start_idx = self.find_start_or_end_idx_recursively(idx, bool_start=True)
end_idx = self.find_start_or_end_idx_recursively(idx, bool_start=False)
str_klifs = self.remove_gaps_from_klifs(self.list_klifs_substr_actual[idx])
str_uniprot = self.uniprotSeq[start_idx:end_idx]
if len(str_klifs) == 0:
return start_idx, end_idx, None
else:
if align_fn is not None:
aligned = align_fn.align(str_klifs, str_uniprot)
else:
aligned = self.return_idx_of_substring_in_superstring(
str_uniprot, str_klifs
)
return start_idx, end_idx, aligned
[docs]
def iterate_klifs_alignment(
self,
) -> None:
"""Align KLIFS region to UniProt canonical Uniprot sequence."""
dict_klifs = DICT_POCKET_KLIFS_REGIONS
list_klifs = list(dict_klifs.keys())
for klifs_index, klifs_region in enumerate(list_klifs):
klifs_region_start, klifs_region_end = klifs_region, klifs_region
klifs_idx_start, klifs_idx_end = (
dict_klifs[klifs_region_start]["start"] - 1,
dict_klifs[klifs_region_end]["end"],
)
str_klifs, list_substring_idx = self.align_klifs_pocket_to_uniprot_seq(
idx_start=klifs_idx_start,
idx_end=klifs_idx_end,
)
self.list_klifs_substr_actual.append(str_klifs)
# if None KLIFS all "-" so disregard; if multiple idxs returned,
# concatenate with contiguous regions to identify single match
if list_substring_idx is not None and len(list_substring_idx) > 1:
bool_cont = dict_klifs[klifs_region_start]["contiguous"]
# if contiguous with subsequent, concatenate with susbequent region
if bool_cont:
klifs_region_end = list_klifs[klifs_index + 1]
# if not contiguous with subsequent, concatenate with previous region
else:
klifs_region_start = list_klifs[klifs_index - 1]
# need for offset later
len_klifs = len(self.remove_gaps_from_klifs(str_klifs))
klifs_idx_start, klifs_idx_end = (
dict_klifs[klifs_region_start]["start"] - 1,
dict_klifs[klifs_region_end]["end"],
)
str_klifs, list_substring_idx = self.align_klifs_pocket_to_uniprot_seq(
idx_start=klifs_idx_start,
idx_end=klifs_idx_end,
)
# if concat with previous, offset by length of preceding KLIFS region with gaps removed
if (
not bool_cont
and list_substring_idx is not None
and len(list_substring_idx) != 0
):
len_offset = len(self.remove_gaps_from_klifs(str_klifs)) - len_klifs
list_substring_idx = [i + len_offset for i in list_substring_idx]
# Q9UBF8 linker region maps exactly to earlier in seq; match not exact (linker)
# don't allow exact matches in locations < max idx previously found
try:
idx_max = max(self.list_substring_idxs)[0]
if max(list_substring_idx) < idx_max:
logger.warning(
f"Match at found at {list_substring_idx[0]} "
f"occurs before prior match at {idx_max}..."
)
list_substring_idx = []
except Exception as e:
logger.info(f"Error {e} in iterate_klifs_alignment. Continuing...")
pass
self.list_klifs_region.append(klifs_region_start + ":" + klifs_region_end)
self.list_klifs_substr_match.append(str_klifs)
self.list_substring_idxs.append(list_substring_idx)
# post-hoc adjustments
# b.l region non-contiguous alignment
idx_bl = [i for i, x in enumerate(list_klifs) if x == "b.l"][0]
# STK40 has no b.l region, so skip entirely
if self.list_substring_idxs[idx_bl] is None:
pass
else:
start, _, bl_alignments = self.return_partial_alignments(
idx=idx_bl,
align_fn=BL2UniProtAligner(),
)
list_bl = self.select_correct_alignment(bl_alignments)
self.list_substring_idxs[idx_bl] = [i + start for i in list_bl]
# interpolate multi-matching using previous and subsequent regions
for idx, substr_idx in enumerate(self.list_substring_idxs):
if idx != idx_bl and substr_idx is not None and len(substr_idx) > 1:
start = self.find_start_or_end_idx_recursively(idx, bool_start=True)
end = self.find_start_or_end_idx_recursively(idx, bool_start=False)
self.list_substring_idxs[idx] = [
i for i in substr_idx if i >= start and i <= end
]
# final partial alignment algorithm
for idx, substr_idx in enumerate(self.list_substring_idxs):
if substr_idx == []:
# check exact match
start_exact, _, align_exact = self.return_partial_alignments(idx=idx)
if align_exact != [] and len(align_exact) == 1:
self.list_substring_idxs[idx] = [
i + start_exact for i in align_exact
]
# if no exact match, try local alignment
else:
start_local, _, align_local = self.return_partial_alignments(
idx=idx,
align_fn=Kincore2UniProtAligner(),
)
# do not allow mismatches in local alignment
criteria1 = (
"-" not in align_local[0][0]
and align_local.sequences[0] != align_local.sequences[1]
)
# single local alignment where target == KLIFS pocket
criteria2 = len(align_local) == 1 and align_local[
0
].target == self.remove_gaps_from_klifs(align_local[0][0, :])
if criteria1:
logger.warning(
"Alignment mismatch in local alignment for "
f"{self.list_klifs_region[idx]} region\n"
f"{align_local.sequences[0]} vs.{align_local.sequences[1]}"
)
if not criteria1 and criteria2:
list_local = self.return_idx_of_alignment_match(align_local[0])
self.list_substring_idxs[idx] = [
i + start_local for i in list_local
]
# if no exact match, try global alignment
else:
start_global, _, align_global = self.return_partial_alignments(
idx=idx,
align_fn=BL2UniProtAligner(),
)
# all that remains is linker region where some gaps (1 + 3) occur
list_global = self.select_correct_alignment(
align_global, bool_bl=False
)
self.list_substring_idxs[idx] = [
i + start_global for i in list_global
]
[docs]
def generate_alignment_list(self, bool_offset: bool) -> list[str | None]:
"""Generate and validate alignment list.
Parameters
----------
bool_offset : bool
If True, offset indices by 1 to match 1-based indexing
Returns
-------
list[str | None]
List of alignments
"""
list_align = []
for list_idx, list_seq in zip(
self.list_substring_idxs, self.list_klifs_substr_actual
):
if list_idx is not None and len(list_idx) == 1:
if bool_offset:
temp_idx = list_idx[0] + 1
else:
temp_idx = list_idx[0]
for aa in list_seq:
if aa == "-":
list_align.append(None)
else:
list_align.append(temp_idx)
temp_idx += 1
elif list_idx is None:
list_align.extend([None] * len(list_seq))
else:
idx = 0
for aa in list_seq:
if aa == "-":
list_align.append(None)
else:
if bool_offset:
list_align.append(list_idx[idx] + 1)
else:
list_align.append(list_idx[idx])
idx += 1
for idx, i in enumerate(list_align):
if i is not None:
if bool_offset:
i = i - 1
if self.uniprotSeq[i] != self.klifsSeq[idx]:
logger.error(
f"Alignment mismatch at {idx} position in KLIFS pocket and {i} of UniProt\n"
f"UniProt: {self.uniprotSeq[i]}\n"
f"KLIFS: {self.klifsSeq[idx]}\n"
)
return None
else:
if self.klifsSeq[idx] != "-":
logger.error(
f"Alignment mismatch at {idx + 1} position in KLIFS pocket and {i} of UniProt\n"
f"UniProt: -\n"
f"KLIFS: {self.klifsSeq[idx]}\n"
)
return None
self.list_align = list_align
[docs]
def get_inter_region(self):
"""Get inter-region sequences."""
list_region = list(DICT_POCKET_KLIFS_REGIONS.keys())
dict_start_end = {
list_region[i - 1]: list_region[i] for i in range(1, len(list_region) - 1)
}
dict_cols = {
key: list(i for i in LIST_KLIFS_REGION if i.split(":")[0] == key)
for key in list_region
}
list_inter = []
for key1, val1 in dict_start_end.items():
keys_start, keys_end = dict_cols[key1], dict_cols[val1]
start = [
val for key, val in self.KLIFS2UniProtIdx.items() if key in keys_start
]
if all(v is None for v in start):
max_start = None
else:
max_start = np.nanmax(np.array(start, dtype=float)) + 1
end = [val for key, val in self.KLIFS2UniProtIdx.items() if key in keys_end]
if all(v is None for v in end):
min_end = None
else:
min_end = np.nanmin(np.array(end, dtype=float))
list_inter.append((max_start, min_end))
dict_inter = dict(
zip([f"{key}:{val}" for key, val in dict_start_end.items()], list_inter)
)
dict_fasta = {i: {} for i in LIST_INTER_REGIONS}
for region in LIST_INTER_REGIONS:
start, end = dict_inter[region][0], dict_inter[region][1]
if start is not None and end is not None:
if end - start == 0:
dict_fasta[region] = None
else:
dict_fasta[region] = self.uniprotSeq[int(start) - 1 : int(end) - 1]
else:
dict_fasta[region] = None
return dict_fasta
[docs]
def recursive_idx_search(
self,
idx: int,
in_dict: dict[str, int],
decreasing: bool,
):
"""Recursively search for index in dictionary.
Parameters
----------
idx : int
Index to start search
in_dict : dict[str, int]
Dictionary to search
decreasing : bool
If True, search in decreasing order; if False, search in increasing order
Returns
-------
idx : int
Index in dictionary
"""
if idx == 0:
return "NONE"
list_keys = list(in_dict.keys())
if in_dict[list_keys[idx]] is None:
if decreasing:
idx = self.recursive_idx_search(idx - 1, in_dict, True)
else:
idx = self.recursive_idx_search(idx + 1, in_dict, False)
return idx
[docs]
def find_intra_gaps(
self,
dict_in: dict[str, int],
bool_bl: bool = True,
) -> tuple[int, str] | None:
"""Find intra-pocket gaps in KLIFS pocket region.
Parameters
----------
dict_in : dict[str, int]
Dictionary of KLIFS regions and their corresponding indices
bool_bl : bool
If True, find intra-region gaps for b.l region; if False, find intra-region gaps for linker region
Returns
-------
tuple[str, str] | None
Tuple of intra-region gaps
"""
if bool_bl:
region, idx_in, idx_out = "b.l", 1, 2
else:
region, idx_in, idx_out = "linker", 0, 1
list_keys = list(dict_in.keys())
list_idx = [idx for idx, i in enumerate(dict_in.keys()) if region in i]
# ATR and CAMKK1 have inter hinge:linker region
start = list_idx[idx_in]
end = list_idx[idx_out]
if dict_in[list_keys[start]] is None:
start = self.recursive_idx_search(start - 1, dict_in, True)
if dict_in[list_keys[end]] is None:
end = self.recursive_idx_search(end + 1, dict_in, False)
# STK40 has no b.l region or preceding
if start == "NONE":
return None
return (dict_in[list_keys[start]], dict_in[list_keys[end]])
[docs]
def return_intra_gap_substr(self, bl_bool) -> str | None:
"""Return intra-region gap substring.
Parameters
----------
bl_bool : bool
If True, find intra-region gaps for b.l region; if False, find intra-region gaps for linker region
Returns
-------
str | None
Intra-region gap substring
"""
tuple_idx = self.find_intra_gaps(self.KLIFS2UniProtIdx, bl_bool)
if tuple_idx is None:
return None
else:
start, end = tuple_idx[0], tuple_idx[1]
if end - start == 1:
return None
else:
return self.uniprotSeq[start : end - 1]
[docs]
def get_intra_region(self):
"""Get intra-region sequences."""
list_seq = []
for region in LIST_INTRA_REGIONS:
if region.split("_")[0] == "b.l":
list_seq.append(self.return_intra_gap_substr(True))
else:
list_seq.append(self.return_intra_gap_substr(False))
return dict(zip(LIST_INTRA_REGIONS, list_seq))
[docs]
def generate_alignment_list_including_gaps(self):
"""Return fully aligned KLIFS pocket."""
list_region = list(DICT_POCKET_KLIFS_REGIONS.keys())
# inter region
dict_inter = self.get_inter_region()
list_inter_regions = list(dict_inter.keys())
list_idx_inter = list(
chain(
*[
list(
idx for idx, j in enumerate(list_region) if j == i.split(":")[0]
)
for i in list_inter_regions
]
)
)
list_region_combo = list(list_region)
i = 0
for idx, val in zip(list_idx_inter, list_inter_regions):
list_region_combo.insert(idx + i + 1, val)
i += 1
# intra region
dict_intra = self.get_intra_region()
idx = list_region_combo.index("b.l")
list_region_combo[idx : idx + 1] = "b.l_1", "b.l_intra", "b.l_2"
idx = list_region_combo.index("linker")
list_region_combo[idx : idx + 1] = "linker_1", "linker_intra", "linker_2"
dict_full_klifs_region = {region: None for region in list_region_combo}
dict_actual = dict(zip(list_region, self.list_klifs_substr_actual))
# for region in list_region_combo:KL
for region, seq in dict_actual.items():
if region == "b.l":
dict_full_klifs_region["b.l_1"] = seq[0:2]
dict_full_klifs_region["b.l_2"] = seq[2:]
pass
elif region == "linker":
dict_full_klifs_region["linker_1"] = seq[0:1]
dict_full_klifs_region["linker_2"] = seq[1:]
else:
dict_full_klifs_region[region] = seq
for region, seq in dict_inter.items():
dict_full_klifs_region[region] = seq
for region, seq in dict_intra.items():
dict_full_klifs_region[region] = seq
self.KLIFS2UniProtSeq = dict_full_klifs_region