Chainsaw / model /utils /ndo_score.py
wuxing0105's picture
Upload folder using huggingface_hub
80a72c3 verified
Raw
History Blame Contribute Delete
4.07 kB
from collections import defaultdict
import copy
import numpy as np
def check_unique_assignments(assn):
res_counts = defaultdict(int) # check for repeated residues
for res_ids in assn.values():
for res_id in res_ids:
res_counts[res_id] += 1
assert all([v == 1 for v in res_counts.values()]), f"Non-unique domain assignment {assn}"
def make_domain_dict(domain_str, n_res):
"""
Converts string representation of domain boundaries to a dictionary
Delimit separate domains with commas , and discontinuous domains
with underscores _. Residue ranges separated by hyphens -, e.g. 1-100,101-200_300-340
:param domain_str: domain boundaries expressed zero-indexed sequential e.g. 1-100,101-200_300-340
:param n_res: number of residues in the sequence
:return:
"""
domain_dict = {}
bounds = domain_str.split(',')
assigned_res = set()
for i, bound in enumerate(bounds):
if len(bound):
for segment in bound.split('_'):
if '-' in segment:
start, end = segment.split('-')
else:
start = end = segment
segment_res = set(range(int(start), int(end) + 1))
assert len(segment_res.intersection(assigned_res)) == 0, f"Overlapping domain assignments {domain_str}"
assigned_res.update(segment_res)
domain_dict[f"D{i + 1}"] = domain_dict.get(f"D{i + 1}", []) + list(segment_res)
domain_dict['linker'] = list(set(range(n_res)).difference(assigned_res))
return domain_dict
def ndo_score(true: dict, pred: dict):
"""
Normalized Domain Overlap Score
Approximately corresponds to the fraction of residues that are assigned to the correct domain
(Tai, C.H., et al. Evaluation of domain prediction in CASP6. Proteins 2005;61:183-192.)
Full description of algorithm at https://ccrod.cancer.gov/confluence/display/CCRLEE/NDO]
https://ccrod.cancer.gov/confluence/display/CCRLEE/NDO
:param true: dict of domain assignments, with keys "linker" and "dX" where X is the domain number
:param pred: dict of domain assignments, with keys "linker" and "dX" where X is the domain number
example domain dictionary: {'linker':[0,1], 'D1':[2,3,4,8,9], 'D2':[5,6,7]}
"""
# domains definitions must be mutually exclusive.
check_unique_assignments(true)
check_unique_assignments(pred)
# alternative data structure would be a list of res ids for linker, plus a list of lists of res ids for domains.
true = copy.deepcopy(true)
pred = copy.deepcopy(pred)
n_dom_pred = len([k for k in pred.keys() if k != "linker"])
n_dom_gt = len([k for k in true.keys() if k != "linker"])
# linkers are treated specially, so put them at row/col 0 to make this easy
pred_linker = pred.pop("linker", [])
gt_linker = true.pop("linker", [])
gt_res_ids = [gt_linker] + list(true.values())
pred_res_ids = [pred_linker] + list(pred.values())
overlap = np.zeros((n_dom_pred + 1, n_dom_gt + 1))
assert len(gt_res_ids) == overlap.shape[1] and len(pred_res_ids) == overlap.shape[0]
for i, p_res in enumerate(pred_res_ids):
for j, gt_res in enumerate(gt_res_ids):
overlap[i, j] = len(set(p_res).intersection(set(gt_res)))
# modified from v0: max overlap for a domain cannot be with a linker (hence max slices start at 1)
if overlap.shape[0] == 1 or overlap.shape[1] == 1:
# either the predictions or the true labels (or both) are all linker
print('NDO score undefined for case where all linker')
return 0
row_scores = overlap[1:, 1:].max(axis=1)
row_scores -= (overlap[1:].sum(axis=1) - row_scores)
col_scores = overlap[1:, 1:].max(axis=0)
col_scores -= (overlap[:, 1:].sum(axis=0) - col_scores)
total_score = (row_scores.sum() + col_scores.sum()) / 2
# count number of domain (non-linker) residues to normalize
max_score = sum([len(gt_res) for gt_res in gt_res_ids[1:]])
return total_score / max_score