gemmi-protools 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of gemmi-protools might be problematic. Click here for more details.
- gemmi_protools/__init__.py +8 -0
- gemmi_protools/align.py +183 -0
- gemmi_protools/cif_opts.py +167 -0
- gemmi_protools/convert.py +96 -0
- gemmi_protools/dockq.py +139 -0
- gemmi_protools/parse_pdb_header.py +387 -0
- gemmi_protools/parser.py +279 -0
- gemmi_protools/pdb_opts.py +177 -0
- gemmi_protools/ppi.py +74 -0
- gemmi_protools/reader.py +371 -0
- gemmi_protools/struct_info.py +91 -0
- gemmi_protools-0.1.0.dist-info/METADATA +19 -0
- gemmi_protools-0.1.0.dist-info/RECORD +16 -0
- gemmi_protools-0.1.0.dist-info/WHEEL +5 -0
- gemmi_protools-0.1.0.dist-info/licenses/LICENSE +21 -0
- gemmi_protools-0.1.0.dist-info/top_level.txt +1 -0
gemmi_protools/align.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@Author: Luo Jiejian
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
import uuid
|
|
11
|
+
from copy import deepcopy
|
|
12
|
+
from typing import Union, Dict, Any, List, Optional
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from Bio.PDB import Superimposer
|
|
16
|
+
from typeguard import typechecked
|
|
17
|
+
|
|
18
|
+
from .convert import gemmi2bio, bio2gemmi
|
|
19
|
+
from .reader import StructureParser
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StructureAligner(object):
|
|
23
|
+
@typechecked
|
|
24
|
+
def __init__(self, query_path: Union[str, pathlib.Path], ref_path: Union[str, pathlib.Path]):
|
|
25
|
+
self._query_st = StructureParser()
|
|
26
|
+
self._query_st.load_from_file(query_path)
|
|
27
|
+
self._query_st.set_default_model()
|
|
28
|
+
|
|
29
|
+
self._ref_st = StructureParser()
|
|
30
|
+
self._ref_st.load_from_file(ref_path)
|
|
31
|
+
self._ref_st.set_default_model()
|
|
32
|
+
|
|
33
|
+
self.values = dict()
|
|
34
|
+
self.rot_mat = None
|
|
35
|
+
self.is_aligned = False
|
|
36
|
+
self.by_query = None
|
|
37
|
+
self.by_ref = None
|
|
38
|
+
self.query_path = query_path
|
|
39
|
+
self.ref_path = ref_path
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def __mmalign_path(self):
|
|
43
|
+
_path = shutil.which("MMAlign") or shutil.which("MMalign")
|
|
44
|
+
if _path is None:
|
|
45
|
+
raise RuntimeError("Executable program MMAlign is not found. "
|
|
46
|
+
"Download from https://zhanggroup.org/MM-align/ ."
|
|
47
|
+
"Build it and add MMAlign to environment PATH")
|
|
48
|
+
else:
|
|
49
|
+
return _path
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
@typechecked
|
|
53
|
+
def __parser_rotation_matrix(matrix_file: Union[str, pathlib.Path]):
|
|
54
|
+
rotation_matrix = []
|
|
55
|
+
translation_vector = []
|
|
56
|
+
|
|
57
|
+
with open(matrix_file, 'r') as file:
|
|
58
|
+
lines = file.readlines()
|
|
59
|
+
values = lines[2:5]
|
|
60
|
+
for cur_line in values:
|
|
61
|
+
tmp = re.split(pattern=r"\s+", string=cur_line.strip())
|
|
62
|
+
assert len(tmp) == 5
|
|
63
|
+
rotation_matrix.append(tmp[2:])
|
|
64
|
+
translation_vector.append(tmp[1])
|
|
65
|
+
return dict(R=np.array(rotation_matrix).astype(np.float32),
|
|
66
|
+
T=np.array(translation_vector).astype(np.float32))
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
@typechecked
|
|
70
|
+
def __parse_terminal_outputs(output_string: str) -> Dict[str, Any]:
|
|
71
|
+
lines = re.split(pattern=r"\n", string=output_string)
|
|
72
|
+
# chain mapping
|
|
73
|
+
patterns = dict(query_chain_ids=r"Structure_1.+\.pdb:([\w:]+)",
|
|
74
|
+
ref_chain_ids=r"Structure_2.+\.pdb:([\w:]+)",
|
|
75
|
+
query_total_length=r"Length of Structure_1.*?(\d+).*residues",
|
|
76
|
+
ref_total_length=r"Length of Structure_2.*?(\d+).*residues",
|
|
77
|
+
aligned_length=r"Aligned length=.*?(\d+)",
|
|
78
|
+
rmsd=r"RMSD=.*?([\d.]+)",
|
|
79
|
+
tmscore_by_query=r"TM-score=.*?([\d.]+).+Structure_1",
|
|
80
|
+
tmscore_by_ref=r"TM-score=.*?([\d.]+).+Structure_2",
|
|
81
|
+
aligned_seq_start=r"denotes other aligned residues",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
values = dict()
|
|
85
|
+
for idx, line in enumerate(lines):
|
|
86
|
+
current_keys = list(patterns.keys())
|
|
87
|
+
for key in current_keys:
|
|
88
|
+
tmp = re.search(patterns[key], line)
|
|
89
|
+
if tmp:
|
|
90
|
+
if key in ['query_chain_ids', 'ref_chain_ids']:
|
|
91
|
+
values[key] = re.split(pattern=":", string=tmp.groups()[0])
|
|
92
|
+
del patterns[key]
|
|
93
|
+
elif key in ['query_total_length', 'ref_total_length', 'aligned_length']:
|
|
94
|
+
values[key] = int(tmp.groups()[0])
|
|
95
|
+
del patterns[key]
|
|
96
|
+
elif key in ['rmsd', 'tmscore_by_query', 'tmscore_by_ref']:
|
|
97
|
+
values[key] = float(tmp.groups()[0])
|
|
98
|
+
del patterns[key]
|
|
99
|
+
elif key == "aligned_seq_start":
|
|
100
|
+
# idx + 1 and idx + 3 for aligned sequences 1 and 2
|
|
101
|
+
seq_1 = lines[idx + 1]
|
|
102
|
+
seq_2 = lines[idx + 3]
|
|
103
|
+
|
|
104
|
+
sp1 = re.split(pattern=r"\*", string=seq_1)
|
|
105
|
+
sp2 = re.split(pattern=r"\*", string=seq_2)
|
|
106
|
+
values["query_sequences"] = sp1[:-1] if "*" in seq_1 else sp1
|
|
107
|
+
values["ref_sequences"] = sp2[:-1] if "*" in seq_2 else sp2
|
|
108
|
+
del patterns[key]
|
|
109
|
+
return values
|
|
110
|
+
|
|
111
|
+
@typechecked
|
|
112
|
+
def make_alignment(self, query_chains: Optional[List[str]] = None,
|
|
113
|
+
ref_chains: Optional[List[str]] = None, timeout=300.0):
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
:param query_chains: list, None for all chains
|
|
117
|
+
:param ref_chains: list, None for all chains
|
|
118
|
+
:param timeout: default 300
|
|
119
|
+
:return:
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
program_path = self.__mmalign_path
|
|
123
|
+
|
|
124
|
+
# clone
|
|
125
|
+
q_st = deepcopy(self._query_st)
|
|
126
|
+
r_st = deepcopy(self._ref_st)
|
|
127
|
+
|
|
128
|
+
tmp_dir = os.path.join(tempfile.gettempdir(), str(uuid.uuid4()))
|
|
129
|
+
os.makedirs(tmp_dir)
|
|
130
|
+
|
|
131
|
+
if isinstance(query_chains, list):
|
|
132
|
+
q_st.pick_chains(query_chains)
|
|
133
|
+
|
|
134
|
+
if isinstance(ref_chains, list):
|
|
135
|
+
r_st.pick_chains(ref_chains)
|
|
136
|
+
|
|
137
|
+
_tmp_a = os.path.join(tmp_dir, "a.pdb")
|
|
138
|
+
q_st.to_pdb(_tmp_a)
|
|
139
|
+
|
|
140
|
+
_tmp_b = os.path.join(tmp_dir, "b.pdb")
|
|
141
|
+
r_st.to_pdb(_tmp_b)
|
|
142
|
+
|
|
143
|
+
matrix_file = os.path.join(tmp_dir, "m.txt")
|
|
144
|
+
_command = "%s %s %s -m %s" % (program_path, _tmp_a, _tmp_b, matrix_file)
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
result = subprocess.run(_command, shell=True, check=True,
|
|
148
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
149
|
+
timeout=timeout)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
print("%s: between files %s and %s; between chains: %s and %s" % (
|
|
152
|
+
str(e), self.query_path, self.ref_path,
|
|
153
|
+
str(q_st.chain_ids), str(r_st.chain_ids))
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
self.values = self.__parse_terminal_outputs(result.stdout.decode())
|
|
157
|
+
self.rot_mat = self.__parser_rotation_matrix(matrix_file)
|
|
158
|
+
self.is_aligned = True
|
|
159
|
+
self.by_query = q_st.chain_ids if query_chains is None else query_chains
|
|
160
|
+
self.by_ref = r_st.chain_ids if ref_chains is None else ref_chains
|
|
161
|
+
finally:
|
|
162
|
+
if os.path.isdir(tmp_dir):
|
|
163
|
+
shutil.rmtree(tmp_dir)
|
|
164
|
+
|
|
165
|
+
@typechecked
|
|
166
|
+
def save_aligned_query(self, out_file: str):
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
:param out_file: .cif file
|
|
170
|
+
:return:
|
|
171
|
+
"""
|
|
172
|
+
if not self.is_aligned:
|
|
173
|
+
raise RuntimeError("structure not aligned, run make_alignment first")
|
|
174
|
+
|
|
175
|
+
super_imposer = Superimposer()
|
|
176
|
+
super_imposer.rotran = (self.rot_mat["R"].T, self.rot_mat["T"])
|
|
177
|
+
|
|
178
|
+
bio_s = gemmi2bio(self._query_st.STRUCT)
|
|
179
|
+
super_imposer.apply(bio_s)
|
|
180
|
+
query_st_aligned = bio2gemmi(bio_s)
|
|
181
|
+
|
|
182
|
+
block = query_st_aligned.make_mmcif_block()
|
|
183
|
+
block.write_file(out_file)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@Author: Luo Jiejian
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pathlib
|
|
6
|
+
from typing import Union, Dict, Any
|
|
7
|
+
|
|
8
|
+
import gemmi
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from typeguard import typechecked
|
|
11
|
+
|
|
12
|
+
from .struct_info import Entity
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@typechecked
|
|
16
|
+
def _is_cif(path: Union[str, pathlib.Path]) -> bool:
|
|
17
|
+
if isinstance(path, str):
|
|
18
|
+
path = pathlib.Path(path)
|
|
19
|
+
if path.suffixes:
|
|
20
|
+
if path.suffixes[-1] == ".cif":
|
|
21
|
+
return True
|
|
22
|
+
elif "".join(path.suffixes[-2:]) == ".cif.gz":
|
|
23
|
+
return True
|
|
24
|
+
else:
|
|
25
|
+
return False
|
|
26
|
+
else:
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@typechecked
|
|
31
|
+
def _value_mapper_from_block(block: gemmi.cif.Block, category: str, column1: str, column2: str,
|
|
32
|
+
expand_column1: bool = False) -> Dict[str, Any]:
|
|
33
|
+
"""
|
|
34
|
+
mapper from column1 to column2
|
|
35
|
+
:param block:
|
|
36
|
+
:param category:
|
|
37
|
+
:param column1:
|
|
38
|
+
:param column2:
|
|
39
|
+
:param expand_column1: bool, if True, values joint by comma in column1 with be split
|
|
40
|
+
:return:
|
|
41
|
+
Only return a mapper when both column1 and column2 in category
|
|
42
|
+
"""
|
|
43
|
+
loop = block.find_mmcif_category(category)
|
|
44
|
+
tags = list(loop.tags)
|
|
45
|
+
|
|
46
|
+
results = dict()
|
|
47
|
+
if column1 in tags:
|
|
48
|
+
values1 = loop.column(tags.index(column1))
|
|
49
|
+
v1 = [values1.str(i) for i in range(len(values1))]
|
|
50
|
+
|
|
51
|
+
if column2 in tags:
|
|
52
|
+
values2 = loop.column(tags.index(column2))
|
|
53
|
+
v2 = [values2.str(i) for i in range(len(values2))]
|
|
54
|
+
else:
|
|
55
|
+
v2 = ["?"] * len(v1)
|
|
56
|
+
|
|
57
|
+
outputs = dict(zip(v1, v2))
|
|
58
|
+
|
|
59
|
+
if expand_column1:
|
|
60
|
+
outputs_ex = dict()
|
|
61
|
+
for key, val in outputs.items():
|
|
62
|
+
tmp = key.split(",")
|
|
63
|
+
for sk in tmp:
|
|
64
|
+
nk = sk.strip()
|
|
65
|
+
if nk:
|
|
66
|
+
outputs_ex[nk] = val
|
|
67
|
+
results = outputs_ex
|
|
68
|
+
else:
|
|
69
|
+
results = outputs
|
|
70
|
+
return results
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@typechecked
|
|
74
|
+
def _get_cif_resolution(block: gemmi.cif.Block) -> float:
|
|
75
|
+
resolution = 0.0
|
|
76
|
+
for key in ["_reflns.d_resolution_high",
|
|
77
|
+
"_refine.ls_d_res_high",
|
|
78
|
+
"_refine_hist.d_res_high",
|
|
79
|
+
"_em_3d_reconstruction.resolution",
|
|
80
|
+
]:
|
|
81
|
+
v = block.find_value(key)
|
|
82
|
+
try:
|
|
83
|
+
vf = float(v)
|
|
84
|
+
except (TypeError, ValueError):
|
|
85
|
+
continue
|
|
86
|
+
else:
|
|
87
|
+
resolution = vf
|
|
88
|
+
break
|
|
89
|
+
return resolution
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@typechecked
|
|
93
|
+
def _cif_entity_info(block: gemmi.cif.Block) -> Entity:
|
|
94
|
+
entity2description = _value_mapper_from_block(block, category="_entity.",
|
|
95
|
+
column1="_entity.id",
|
|
96
|
+
column2="_entity.pdbx_description")
|
|
97
|
+
|
|
98
|
+
polymer2entity = _value_mapper_from_block(block, category="_entity_poly.",
|
|
99
|
+
column1="_entity_poly.pdbx_strand_id",
|
|
100
|
+
column2="_entity_poly.entity_id",
|
|
101
|
+
expand_column1=True)
|
|
102
|
+
entity2species = _value_mapper_from_block(block, category="_entity_src_gen.",
|
|
103
|
+
column1="_entity_src_gen.entity_id",
|
|
104
|
+
column2="_entity_src_gen.pdbx_gene_src_scientific_name")
|
|
105
|
+
|
|
106
|
+
entity2species.update(_value_mapper_from_block(block, category="_pdbx_entity_src_syn.",
|
|
107
|
+
column1="_pdbx_entity_src_syn.entity_id",
|
|
108
|
+
column2="_pdbx_entity_src_syn.organism_scientific")
|
|
109
|
+
)
|
|
110
|
+
entity2species.update(_value_mapper_from_block(block, category="_entity_src_nat.",
|
|
111
|
+
column1="_entity_src_nat.entity_id",
|
|
112
|
+
column2="_entity_src_nat.pdbx_organism_scientific")
|
|
113
|
+
)
|
|
114
|
+
entity2taxid = _value_mapper_from_block(block, category="_entity_src_gen.",
|
|
115
|
+
column1="_entity_src_gen.entity_id",
|
|
116
|
+
column2="_entity_src_gen.pdbx_gene_src_ncbi_taxonomy_id")
|
|
117
|
+
entity2taxid.update(_value_mapper_from_block(block, category="_pdbx_entity_src_syn.",
|
|
118
|
+
column1="_pdbx_entity_src_syn.entity_id",
|
|
119
|
+
column2="_pdbx_entity_src_syn.ncbi_taxonomy_id")
|
|
120
|
+
)
|
|
121
|
+
entity2taxid.update(_value_mapper_from_block(block, category="_entity_src_nat.",
|
|
122
|
+
column1="_entity_src_nat.entity_id",
|
|
123
|
+
column2="_entity_src_nat.pdbx_ncbi_taxonomy_id")
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
vals = dict(eid2desc=entity2description,
|
|
127
|
+
eid2specie=entity2species,
|
|
128
|
+
eid2taxid=entity2taxid,
|
|
129
|
+
polymer2eid=polymer2entity
|
|
130
|
+
)
|
|
131
|
+
return Entity(**vals)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@typechecked
|
|
135
|
+
def _cif_block_for_output(structure: gemmi.Structure, entity: Entity) -> gemmi.cif.Block:
|
|
136
|
+
block = structure.make_mmcif_block()
|
|
137
|
+
|
|
138
|
+
ta = block.find_mmcif_category(category="_entity.")
|
|
139
|
+
da = pd.DataFrame(list(ta), columns=list(ta.tags))
|
|
140
|
+
if "_entity.id" in da.columns:
|
|
141
|
+
da["_entity.pdbx_description"] = da["_entity.id"].apply(
|
|
142
|
+
lambda i: entity["eid2desc"].get(i, "?").strip() or "?")
|
|
143
|
+
|
|
144
|
+
rows = []
|
|
145
|
+
for ar in da.to_numpy().tolist():
|
|
146
|
+
rows.append([gemmi.cif.quote(i) for i in ar])
|
|
147
|
+
|
|
148
|
+
if "_entity.pdbx_description" not in list(ta.tags):
|
|
149
|
+
ta.loop.add_columns(["_entity.pdbx_description"], "?")
|
|
150
|
+
|
|
151
|
+
ta = block.find_mmcif_category(category="_entity.")
|
|
152
|
+
for _ in range(len(ta)):
|
|
153
|
+
ta.remove_row(0)
|
|
154
|
+
for row in rows:
|
|
155
|
+
ta.append_row(row)
|
|
156
|
+
|
|
157
|
+
loop = block.init_loop("_entity_src_gen.", ["entity_id",
|
|
158
|
+
"pdbx_gene_src_scientific_name",
|
|
159
|
+
"pdbx_gene_src_ncbi_taxonomy_id"])
|
|
160
|
+
|
|
161
|
+
for k in entity["eid2specie"].keys():
|
|
162
|
+
loop.add_row([gemmi.cif.quote(k),
|
|
163
|
+
gemmi.cif.quote(entity["eid2specie"].get(k, "?")),
|
|
164
|
+
gemmi.cif.quote(entity["eid2taxid"].get(k, "?"))]
|
|
165
|
+
)
|
|
166
|
+
block.move_item(-1, 16)
|
|
167
|
+
return block
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@Author: Luo Jiejian
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import gemmi
|
|
6
|
+
import numpy as np
|
|
7
|
+
from Bio.PDB.Structure import Structure as BioStructure
|
|
8
|
+
from Bio.PDB.StructureBuilder import StructureBuilder
|
|
9
|
+
from typeguard import typechecked
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@typechecked
|
|
13
|
+
def gemmi2bio(gemmi_structure: gemmi.Structure) -> BioStructure:
|
|
14
|
+
"""
|
|
15
|
+
Convert gemmi structure to biopython structure
|
|
16
|
+
:param gemmi_structure:
|
|
17
|
+
:return:
|
|
18
|
+
return biopython structure
|
|
19
|
+
"""
|
|
20
|
+
structure_builder = StructureBuilder()
|
|
21
|
+
structure_builder.init_structure(structure_id=gemmi_structure.name)
|
|
22
|
+
|
|
23
|
+
for model_idx, gemmi_model in enumerate(gemmi_structure):
|
|
24
|
+
structure_builder.init_model(model_idx)
|
|
25
|
+
|
|
26
|
+
for gemmi_chain in gemmi_model:
|
|
27
|
+
structure_builder.init_chain(gemmi_chain.name)
|
|
28
|
+
|
|
29
|
+
for gemmi_residue in gemmi_chain:
|
|
30
|
+
if gemmi_residue.het_flag == "H":
|
|
31
|
+
if gemmi_residue.name in ["HOH", "WAT"]:
|
|
32
|
+
het_flag = "W"
|
|
33
|
+
else:
|
|
34
|
+
het_flag = "H"
|
|
35
|
+
else:
|
|
36
|
+
het_flag = " "
|
|
37
|
+
|
|
38
|
+
structure_builder.init_residue(resname=gemmi_residue.name, field=het_flag,
|
|
39
|
+
resseq=gemmi_residue.seqid.num, icode=gemmi_residue.seqid.icode)
|
|
40
|
+
for gemmi_atom in gemmi_residue:
|
|
41
|
+
coord = np.array([gemmi_atom.pos.x, gemmi_atom.pos.y, gemmi_atom.pos.z])
|
|
42
|
+
structure_builder.init_atom(name=gemmi_atom.name,
|
|
43
|
+
coord=coord,
|
|
44
|
+
b_factor=gemmi_atom.b_iso,
|
|
45
|
+
occupancy=gemmi_atom.occ,
|
|
46
|
+
altloc=gemmi_atom.altloc if gemmi_atom.has_altloc() else ' ',
|
|
47
|
+
fullname=gemmi_atom.name.center(4),
|
|
48
|
+
serial_number=gemmi_atom.serial,
|
|
49
|
+
element=gemmi_atom.element.name.upper())
|
|
50
|
+
|
|
51
|
+
bio_structure = structure_builder.get_structure()
|
|
52
|
+
return bio_structure
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@typechecked
|
|
56
|
+
def bio2gemmi(bio_structure: BioStructure) -> gemmi.Structure:
|
|
57
|
+
"""
|
|
58
|
+
Convert biopython structure to gemmi structure
|
|
59
|
+
:param bio_structure:
|
|
60
|
+
:return:
|
|
61
|
+
return gemmi structure
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
g_structure = gemmi.Structure()
|
|
65
|
+
g_structure.name = bio_structure.id
|
|
66
|
+
|
|
67
|
+
for bio_model in bio_structure:
|
|
68
|
+
# bio model start from 0, gemmi model start from 1
|
|
69
|
+
g_model = gemmi.Model(bio_model.id + 1)
|
|
70
|
+
for bio_chain in bio_model:
|
|
71
|
+
g_chain = gemmi.Chain(bio_chain.id)
|
|
72
|
+
for bio_residue in bio_chain:
|
|
73
|
+
g_residue = gemmi.Residue()
|
|
74
|
+
g_residue.name = bio_residue.resname
|
|
75
|
+
het_flag, r_num, i_code = bio_residue.id
|
|
76
|
+
g_residue.seqid.num = r_num
|
|
77
|
+
g_residue.seqid.icode = i_code
|
|
78
|
+
g_residue.het_flag = "A" if het_flag == " " else "H"
|
|
79
|
+
|
|
80
|
+
for bio_atom in bio_residue:
|
|
81
|
+
g_atom = gemmi.Atom()
|
|
82
|
+
g_atom.name = bio_atom.name
|
|
83
|
+
g_atom.b_iso = bio_atom.bfactor
|
|
84
|
+
g_atom.occ = bio_atom.occupancy
|
|
85
|
+
g_atom.altloc = "\x00" if bio_atom.altloc == " " else bio_atom.altloc
|
|
86
|
+
g_atom.element = gemmi.Element(bio_atom.element)
|
|
87
|
+
g_atom.serial = bio_atom.serial_number
|
|
88
|
+
px, py, pz = bio_atom.coord
|
|
89
|
+
g_atom.pos = gemmi.Position(px, py, pz)
|
|
90
|
+
g_residue.add_atom(g_atom)
|
|
91
|
+
g_chain.add_residue(g_residue)
|
|
92
|
+
g_model.add_chain(g_chain)
|
|
93
|
+
g_structure.add_model(g_model)
|
|
94
|
+
g_structure.setup_entities()
|
|
95
|
+
g_structure.assign_het_flags()
|
|
96
|
+
return g_structure
|
gemmi_protools/dockq.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@Author: Luo Jiejian
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import pathlib
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import Optional, Union
|
|
12
|
+
|
|
13
|
+
import pandas as pd
|
|
14
|
+
from typeguard import typechecked
|
|
15
|
+
|
|
16
|
+
from .reader import StructureParser
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@typechecked
|
|
20
|
+
def _read_model(model_file: Union[str, pathlib.Path]):
|
|
21
|
+
st = StructureParser()
|
|
22
|
+
st.load_from_file(model_file)
|
|
23
|
+
st.set_default_model()
|
|
24
|
+
return st
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@typechecked
|
|
28
|
+
def dockq_score(query_model: Union[str, pathlib.Path],
|
|
29
|
+
native_model: Union[str, pathlib.Path],
|
|
30
|
+
mapping: Optional[str] = None):
|
|
31
|
+
dockq_program = shutil.which("DockQ")
|
|
32
|
+
if dockq_program is None:
|
|
33
|
+
raise RuntimeError("DockQ is need")
|
|
34
|
+
|
|
35
|
+
q_st = _read_model(query_model)
|
|
36
|
+
n_st = _read_model(native_model)
|
|
37
|
+
|
|
38
|
+
tmp_dir = os.path.join(tempfile.gettempdir(), str(uuid.uuid4()))
|
|
39
|
+
os.makedirs(tmp_dir)
|
|
40
|
+
|
|
41
|
+
result_file = os.path.join(tmp_dir, "result.json")
|
|
42
|
+
q_file = os.path.join(tmp_dir, "q.pdb")
|
|
43
|
+
n_file = os.path.join(tmp_dir, "n.pdb")
|
|
44
|
+
q_st.to_pdb(q_file, write_minimal_pdb=True)
|
|
45
|
+
n_st.to_pdb(n_file, write_minimal_pdb=True)
|
|
46
|
+
if mapping is None:
|
|
47
|
+
cid = "".join(n_st.chain_ids)
|
|
48
|
+
mapping = cid + ":" + cid
|
|
49
|
+
|
|
50
|
+
_command = "%s --mapping %s --json %s %s %s" % (dockq_program, mapping, result_file, q_file, n_file)
|
|
51
|
+
metrics = ['DockQ', 'F1', 'chain1', 'chain2']
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
_ = subprocess.run(_command, shell=True, check=True,
|
|
55
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
56
|
+
timeout=300.0)
|
|
57
|
+
except subprocess.CalledProcessError as e:
|
|
58
|
+
# Handle errors in the called executable
|
|
59
|
+
msg = e.stderr.decode()
|
|
60
|
+
outputs = pd.DataFrame(columns=metrics)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
# Handle other exceptions such as file not found or permissions issues
|
|
63
|
+
msg = str(e)
|
|
64
|
+
outputs = pd.DataFrame(columns=metrics)
|
|
65
|
+
else:
|
|
66
|
+
with open(result_file, "r") as fin:
|
|
67
|
+
vals = json.load(fin)
|
|
68
|
+
msg = "Finished"
|
|
69
|
+
result = []
|
|
70
|
+
for v in vals["best_result"].values():
|
|
71
|
+
result.append(v)
|
|
72
|
+
outputs = pd.DataFrame(result)[metrics]
|
|
73
|
+
finally:
|
|
74
|
+
if os.path.isdir(tmp_dir):
|
|
75
|
+
shutil.rmtree(tmp_dir)
|
|
76
|
+
|
|
77
|
+
return dict(value=outputs,
|
|
78
|
+
msg=msg,
|
|
79
|
+
mapping=mapping,
|
|
80
|
+
model=query_model,
|
|
81
|
+
native=native_model
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def dockq_score_interface(query_model: Union[str, pathlib.Path],
|
|
86
|
+
native_model: Union[str, pathlib.Path],
|
|
87
|
+
chains_a: str,
|
|
88
|
+
chains_b: str):
|
|
89
|
+
ppi_if = chains_a + "@" + chains_b
|
|
90
|
+
chs_a = list(chains_a)
|
|
91
|
+
chs_b = list(chains_b)
|
|
92
|
+
|
|
93
|
+
# if multiple chains, merge to one
|
|
94
|
+
q_st = _read_model(query_model)
|
|
95
|
+
n_st = _read_model(native_model)
|
|
96
|
+
|
|
97
|
+
for c in chs_a + chs_b:
|
|
98
|
+
if c not in q_st.chain_ids:
|
|
99
|
+
raise RuntimeError("Chain %s is not in the query model: %s" % (c, query_model))
|
|
100
|
+
|
|
101
|
+
for c in chs_a + chs_b:
|
|
102
|
+
if c not in n_st.chain_ids:
|
|
103
|
+
raise RuntimeError("Chain %s is not in the native model: %s" % (c, native_model))
|
|
104
|
+
|
|
105
|
+
if len(chs_a) > 1:
|
|
106
|
+
q_st.merge_chains(chs_a)
|
|
107
|
+
n_st.merge_chains(chs_a)
|
|
108
|
+
|
|
109
|
+
if len(chs_b) > 1:
|
|
110
|
+
q_st.merge_chains(chs_b)
|
|
111
|
+
n_st.merge_chains(chs_b)
|
|
112
|
+
|
|
113
|
+
tmp_dir = os.path.join(tempfile.gettempdir(), str(uuid.uuid4()))
|
|
114
|
+
os.makedirs(tmp_dir)
|
|
115
|
+
|
|
116
|
+
q_file = os.path.join(tmp_dir, "qm.pdb")
|
|
117
|
+
n_file = os.path.join(tmp_dir, "nm.pdb")
|
|
118
|
+
q_st.to_pdb(q_file, write_minimal_pdb=True)
|
|
119
|
+
n_st.to_pdb(n_file, write_minimal_pdb=True)
|
|
120
|
+
|
|
121
|
+
chs = chs_a[0] + chs_b[0]
|
|
122
|
+
result = dockq_score(q_file, n_file, mapping="%s:%s" % (chs, chs))
|
|
123
|
+
|
|
124
|
+
if len(result["value"]) > 0:
|
|
125
|
+
q_score = round(result["value"].iloc[0]["DockQ"], 4)
|
|
126
|
+
f1 = round(result["value"].iloc[0]["F1"], 4)
|
|
127
|
+
else:
|
|
128
|
+
q_score = ""
|
|
129
|
+
f1 = ""
|
|
130
|
+
|
|
131
|
+
if os.path.isdir(tmp_dir):
|
|
132
|
+
shutil.rmtree(tmp_dir)
|
|
133
|
+
|
|
134
|
+
return dict(DockQ=q_score,
|
|
135
|
+
F1=f1,
|
|
136
|
+
interface=ppi_if,
|
|
137
|
+
model=query_model,
|
|
138
|
+
native=native_model
|
|
139
|
+
)
|