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
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@Author: Luo Jiejian
|
|
3
|
+
"""
|
|
4
|
+
import gzip
|
|
5
|
+
import io
|
|
6
|
+
import pathlib
|
|
7
|
+
import re
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
from typing import Dict, Union, List
|
|
10
|
+
|
|
11
|
+
from typeguard import typechecked
|
|
12
|
+
|
|
13
|
+
from .parse_pdb_header import _parse_pdb_header_list
|
|
14
|
+
from .struct_info import Entity
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@typechecked
|
|
18
|
+
def _molecule_information(header_dict: Dict) -> Entity:
|
|
19
|
+
entity2description = dict()
|
|
20
|
+
entity2species = dict()
|
|
21
|
+
entity2taxid = dict()
|
|
22
|
+
|
|
23
|
+
for idx in header_dict["compound"].keys():
|
|
24
|
+
compound = header_dict["compound"][idx]
|
|
25
|
+
if "chain" in compound:
|
|
26
|
+
chain = re.sub(pattern=r"\s+", repl="", string=compound["chain"])
|
|
27
|
+
if chain != "":
|
|
28
|
+
tmp = chain.split(",")
|
|
29
|
+
tmp.sort()
|
|
30
|
+
key = ",".join(tmp)
|
|
31
|
+
|
|
32
|
+
molecule = compound.get("molecule", "")
|
|
33
|
+
|
|
34
|
+
if idx in header_dict["source"]:
|
|
35
|
+
source = header_dict["source"][idx]
|
|
36
|
+
specie = source.get("organism_scientific", "")
|
|
37
|
+
taxid = source.get("organism_taxid", "")
|
|
38
|
+
else:
|
|
39
|
+
specie = ""
|
|
40
|
+
taxid = ""
|
|
41
|
+
|
|
42
|
+
entity2description[key] = molecule
|
|
43
|
+
entity2species[key] = specie
|
|
44
|
+
entity2taxid[key] = taxid
|
|
45
|
+
|
|
46
|
+
vals = dict(eid2desc=entity2description,
|
|
47
|
+
eid2specie=entity2species,
|
|
48
|
+
eid2taxid=entity2taxid,
|
|
49
|
+
polymer2eid=dict()
|
|
50
|
+
)
|
|
51
|
+
return Entity(**vals)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@typechecked
|
|
55
|
+
def _is_pdb(path: Union[str, pathlib.Path]) -> bool:
|
|
56
|
+
if isinstance(path, str):
|
|
57
|
+
path = pathlib.Path(path)
|
|
58
|
+
if path.suffixes:
|
|
59
|
+
if path.suffixes[-1] == ".pdb":
|
|
60
|
+
return True
|
|
61
|
+
elif "".join(path.suffixes[-2:]) == ".pdb.gz":
|
|
62
|
+
return True
|
|
63
|
+
else:
|
|
64
|
+
return False
|
|
65
|
+
else:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# add by Ljj
|
|
70
|
+
@typechecked
|
|
71
|
+
def _pdb_entity_info(path: Union[str, pathlib.Path]) -> Entity:
|
|
72
|
+
if _is_pdb(path):
|
|
73
|
+
cur_path = pathlib.Path(path)
|
|
74
|
+
if cur_path.suffixes[-1] == ".pdb":
|
|
75
|
+
with open(path, "r") as text_io:
|
|
76
|
+
lines = text_io.readlines()
|
|
77
|
+
else:
|
|
78
|
+
with gzip.open(path, "rb") as gz_handle:
|
|
79
|
+
with io.TextIOWrapper(gz_handle, encoding="utf-8") as text_io:
|
|
80
|
+
lines = text_io.readlines()
|
|
81
|
+
else:
|
|
82
|
+
raise ValueError("Only support .pdb or .pdb.gz file, but got %s" % path)
|
|
83
|
+
|
|
84
|
+
i = 0
|
|
85
|
+
for i in range(len(lines)):
|
|
86
|
+
line = lines[i]
|
|
87
|
+
record_type = line[0:6]
|
|
88
|
+
if record_type in ("ATOM ", "HETATM", "MODEL "):
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
header = lines[0:i]
|
|
92
|
+
info = _parse_pdb_header_list(header)
|
|
93
|
+
return _molecule_information(info)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@typechecked
|
|
97
|
+
def _get_pdb_resolution(remark_lines: List[str]) -> float:
|
|
98
|
+
resolutions = []
|
|
99
|
+
for line in remark_lines:
|
|
100
|
+
tmp = re.search(r"REMARK.+RESOLUTION.+?([\d\.]+|NOT APPLICABLE)", line)
|
|
101
|
+
if tmp:
|
|
102
|
+
v = tmp.groups()[0]
|
|
103
|
+
try:
|
|
104
|
+
vf = float(v)
|
|
105
|
+
except (TypeError, ValueError):
|
|
106
|
+
continue
|
|
107
|
+
else:
|
|
108
|
+
resolutions.append(vf)
|
|
109
|
+
if resolutions:
|
|
110
|
+
return min(resolutions)
|
|
111
|
+
else:
|
|
112
|
+
return 0.0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@typechecked
|
|
116
|
+
def _compound_source_string(entity: Entity) -> List[str]:
|
|
117
|
+
entity2polymer = defaultdict(list)
|
|
118
|
+
for k, v in entity["polymer2eid"].items():
|
|
119
|
+
entity2polymer[v].append(k)
|
|
120
|
+
entity_labels = list(entity2polymer.keys())
|
|
121
|
+
entity_labels.sort()
|
|
122
|
+
|
|
123
|
+
values = []
|
|
124
|
+
for i, el in enumerate(entity_labels):
|
|
125
|
+
values.append(dict(mol_id=str(i + 1),
|
|
126
|
+
chain=", ".join(entity2polymer[el]),
|
|
127
|
+
molecule=entity["eid2desc"].get(el, "?"),
|
|
128
|
+
organism_scientific=entity["eid2specie"].get(el, "?"),
|
|
129
|
+
organism_taxid=entity["eid2taxid"].get(el, "?")
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
outputs = []
|
|
133
|
+
# compound
|
|
134
|
+
compound_mol0 = "COMPND MOL_ID: {mol_id};"
|
|
135
|
+
compound_mol1 = "COMPND {n_line:>3} MOL_ID: {mol_id};"
|
|
136
|
+
compound_molecule = "COMPND {n_line:>3} MOLECULE: {molecule};"
|
|
137
|
+
compound_chain = "COMPND {n_line:>3} CHAIN: {chain};"
|
|
138
|
+
|
|
139
|
+
i = 0
|
|
140
|
+
for val in values:
|
|
141
|
+
if i == 0:
|
|
142
|
+
outputs.append(compound_mol0.format(**val))
|
|
143
|
+
i += 1
|
|
144
|
+
for c_str in [compound_molecule, compound_chain]:
|
|
145
|
+
cur_val = val.copy()
|
|
146
|
+
cur_val["n_line"] = i
|
|
147
|
+
outputs.append(c_str.format(**cur_val))
|
|
148
|
+
i += 1
|
|
149
|
+
else:
|
|
150
|
+
for c_str in [compound_mol1, compound_molecule, compound_chain]:
|
|
151
|
+
cur_val = val.copy()
|
|
152
|
+
cur_val["n_line"] = i
|
|
153
|
+
outputs.append(c_str.format(**cur_val))
|
|
154
|
+
i += 1
|
|
155
|
+
|
|
156
|
+
source_mol0 = "SOURCE MOL_ID: {mol_id};"
|
|
157
|
+
source_mol1 = "SOURCE {n_line:>3} MOL_ID: {mol_id};"
|
|
158
|
+
source_scientific = "SOURCE {n_line:>3} ORGANISM_SCIENTIFIC: {organism_scientific};"
|
|
159
|
+
source_taxid = "SOURCE {n_line:>3} ORGANISM_TAXID: {organism_taxid};"
|
|
160
|
+
|
|
161
|
+
i = 0
|
|
162
|
+
for val in values:
|
|
163
|
+
if i == 0:
|
|
164
|
+
outputs.append(source_mol0.format(**val))
|
|
165
|
+
i += 1
|
|
166
|
+
for c_str in [source_scientific, source_taxid]:
|
|
167
|
+
cur_val = val.copy()
|
|
168
|
+
cur_val["n_line"] = i
|
|
169
|
+
outputs.append(c_str.format(**cur_val))
|
|
170
|
+
i += 1
|
|
171
|
+
else:
|
|
172
|
+
for c_str in [source_mol1, source_scientific, source_taxid]:
|
|
173
|
+
cur_val = val.copy()
|
|
174
|
+
cur_val["n_line"] = i
|
|
175
|
+
outputs.append(c_str.format(**cur_val))
|
|
176
|
+
i += 1
|
|
177
|
+
return outputs
|
gemmi_protools/ppi.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@Author: Luo Jiejian
|
|
3
|
+
"""
|
|
4
|
+
import pathlib
|
|
5
|
+
from typing import Union, List
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from scipy.spatial import cKDTree
|
|
9
|
+
|
|
10
|
+
from .reader import StructureParser
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _ppi_atoms(struct, chains):
|
|
14
|
+
"""
|
|
15
|
+
Load atoms for N and O of backbone and N, O, P, S of side chains, only for PPI searching
|
|
16
|
+
:param struct:
|
|
17
|
+
:param chains:
|
|
18
|
+
:return:
|
|
19
|
+
"""
|
|
20
|
+
protein_atoms = ['N', 'ND1', 'ND2', 'NE', 'NE1', 'NE2', 'NH1', 'NH2', 'NZ',
|
|
21
|
+
'O', 'OD1', 'OD2', 'OE1', 'OE2', 'OG', 'OG1', 'OH',
|
|
22
|
+
'SD', 'SG']
|
|
23
|
+
xna_atoms = ['N1', 'N2', 'N3', 'N4', 'N6', 'N7', 'N9',
|
|
24
|
+
'O2', "O2'", "O3'", 'O4', "O4'", "O5'", 'O6',
|
|
25
|
+
'OP1', 'OP2', 'OP3', 'P']
|
|
26
|
+
pro_chs = []
|
|
27
|
+
xna_chs = []
|
|
28
|
+
for c in chains:
|
|
29
|
+
t = struct.chain_types.get(c, "")
|
|
30
|
+
if t == "protein":
|
|
31
|
+
pro_chs.append(c)
|
|
32
|
+
elif t in ["dna", "rna"]:
|
|
33
|
+
xna_chs.append(c)
|
|
34
|
+
|
|
35
|
+
pro_coord, pro_id = struct.get_atom_coords(pro_chs, protein_atoms)
|
|
36
|
+
xna_coord, xna_id = struct.get_atom_coords(xna_chs, xna_atoms)
|
|
37
|
+
return np.concatenate([pro_coord, xna_coord], axis=0), np.concatenate([pro_id, xna_id], axis=0)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def ppi_interface_residues(in_file: Union[str, pathlib.Path],
|
|
41
|
+
chains_x: List[str],
|
|
42
|
+
chains_y: List[str],
|
|
43
|
+
threshold: float = 4.0):
|
|
44
|
+
"""
|
|
45
|
+
identify PPI among protein, DNA, RNA
|
|
46
|
+
:param in_file:
|
|
47
|
+
:param chains_x:
|
|
48
|
+
:param chains_y:
|
|
49
|
+
:param threshold:
|
|
50
|
+
:return:
|
|
51
|
+
PPI residues of chains_x, PPI residues of chains_y
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
st = StructureParser()
|
|
55
|
+
st.load_from_file(in_file)
|
|
56
|
+
st.set_default_model()
|
|
57
|
+
st.STRUCT.remove_alternative_conformations()
|
|
58
|
+
st.STRUCT.remove_ligands_and_waters()
|
|
59
|
+
st.STRUCT.remove_hydrogens()
|
|
60
|
+
st.STRUCT.remove_empty_chains()
|
|
61
|
+
st.update_entity()
|
|
62
|
+
|
|
63
|
+
x_coord, x_id = _ppi_atoms(st, chains_x)
|
|
64
|
+
y_coord, y_id = _ppi_atoms(st, chains_y)
|
|
65
|
+
|
|
66
|
+
kd_tree_x = cKDTree(x_coord)
|
|
67
|
+
kd_tree_y = cKDTree(y_coord)
|
|
68
|
+
|
|
69
|
+
pairs = kd_tree_x.sparse_distance_matrix(kd_tree_y, threshold, output_type='coo_matrix')
|
|
70
|
+
|
|
71
|
+
x_res = np.unique(x_id[pairs.row][["ch_name", 'res_num', 'res_icode', 'res_name']])
|
|
72
|
+
y_res = np.unique(y_id[pairs.col][["ch_name", 'res_num', 'res_icode', 'res_name']])
|
|
73
|
+
|
|
74
|
+
return x_res, y_res
|
gemmi_protools/reader.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@Author: Luo Jiejian
|
|
3
|
+
"""
|
|
4
|
+
import pathlib
|
|
5
|
+
import re
|
|
6
|
+
import string
|
|
7
|
+
import warnings
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
from typing import Union, Optional, List
|
|
10
|
+
|
|
11
|
+
import gemmi
|
|
12
|
+
import numpy as np
|
|
13
|
+
from typeguard import typechecked
|
|
14
|
+
|
|
15
|
+
from .cif_opts import _cif_block_for_output, _is_cif
|
|
16
|
+
from .parser import (_assign_digital_entity_names, _ent_from_structure,
|
|
17
|
+
pdb_parser, cif_parser, _chain_type, _chain_names2one_letter,
|
|
18
|
+
_assert_unique_chain_names_in_models, get_assembly)
|
|
19
|
+
from .pdb_opts import _compound_source_string, _is_pdb
|
|
20
|
+
from .struct_info import Info
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StructureParser(object):
|
|
24
|
+
"""
|
|
25
|
+
Enhance Structure reader for .cif, .cif.gz, .pdb or .pdb.gz
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, structure: gemmi.Structure = None):
|
|
29
|
+
if not isinstance(structure, (type(None), gemmi.Structure)):
|
|
30
|
+
raise ValueError("structure must be gemmi.Structure or None")
|
|
31
|
+
if structure is None:
|
|
32
|
+
self.STRUCT = gemmi.Structure()
|
|
33
|
+
elif isinstance(structure, gemmi.Structure):
|
|
34
|
+
_assert_unique_chain_names_in_models(structure)
|
|
35
|
+
self.STRUCT = structure.clone()
|
|
36
|
+
else:
|
|
37
|
+
raise ValueError("structure must be gemmi.Structure or None")
|
|
38
|
+
self.STRUCT.setup_entities()
|
|
39
|
+
_assign_digital_entity_names(self.STRUCT)
|
|
40
|
+
|
|
41
|
+
self.INFO = Info()
|
|
42
|
+
self.INFO.from_gemmi_structure_infomap(self.STRUCT.info)
|
|
43
|
+
self.ENTITY = _ent_from_structure(self.STRUCT)
|
|
44
|
+
self.update_entity()
|
|
45
|
+
|
|
46
|
+
@typechecked
|
|
47
|
+
def load_from_file(self, path: Union[str, pathlib.PosixPath]):
|
|
48
|
+
if _is_pdb(path):
|
|
49
|
+
struct, entity = pdb_parser(path)
|
|
50
|
+
elif _is_cif(path):
|
|
51
|
+
struct, entity = cif_parser(path)
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError("Only support .cif, .cif.gz, .pdb or .pdb.gz file, but got %s" % path)
|
|
54
|
+
|
|
55
|
+
_assert_unique_chain_names_in_models(struct)
|
|
56
|
+
self.STRUCT, self.ENTITY = struct, entity
|
|
57
|
+
self.INFO.from_gemmi_structure_infomap(self.STRUCT.info)
|
|
58
|
+
self.update_entity()
|
|
59
|
+
|
|
60
|
+
@typechecked
|
|
61
|
+
def to_pdb(self, outfile: str, write_minimal_pdb=False):
|
|
62
|
+
compound_source = _compound_source_string(self.ENTITY)
|
|
63
|
+
struct = self.STRUCT.clone()
|
|
64
|
+
struct.raw_remarks = compound_source + struct.raw_remarks
|
|
65
|
+
if write_minimal_pdb:
|
|
66
|
+
struct.write_minimal_pdb(outfile)
|
|
67
|
+
else:
|
|
68
|
+
struct.write_pdb(outfile)
|
|
69
|
+
|
|
70
|
+
@typechecked
|
|
71
|
+
def to_cif(self, outfile: str):
|
|
72
|
+
out_block = _cif_block_for_output(self.STRUCT, self.ENTITY)
|
|
73
|
+
out_block.write_file(outfile)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def chain_ids(self):
|
|
77
|
+
vals = []
|
|
78
|
+
for m in self.STRUCT:
|
|
79
|
+
for c in m:
|
|
80
|
+
vals.append(c.name)
|
|
81
|
+
vals.sort()
|
|
82
|
+
return vals
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def model_numbers(self):
|
|
86
|
+
return [m.num for m in self.STRUCT]
|
|
87
|
+
|
|
88
|
+
@typechecked
|
|
89
|
+
def set_default_model(self, num: Optional[int] = None):
|
|
90
|
+
"""
|
|
91
|
+
Set the first model as default
|
|
92
|
+
:param num:
|
|
93
|
+
:return:
|
|
94
|
+
"""
|
|
95
|
+
if len(self.STRUCT) == 0:
|
|
96
|
+
raise RuntimeError("There is no model in structure")
|
|
97
|
+
|
|
98
|
+
keep_model = None
|
|
99
|
+
if num is None:
|
|
100
|
+
# default first model
|
|
101
|
+
keep_model = self.STRUCT[0]
|
|
102
|
+
else:
|
|
103
|
+
for model in self.STRUCT:
|
|
104
|
+
if model.num == num:
|
|
105
|
+
keep_model = model
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
if keep_model is None:
|
|
109
|
+
raise RuntimeError("Model %d not found in structure" % num)
|
|
110
|
+
|
|
111
|
+
# del, reversed order indexes
|
|
112
|
+
indexes_to_del = [i for i, model in enumerate(self.STRUCT) if model.num != keep_model.num]
|
|
113
|
+
indexes_to_del.sort(reverse=True)
|
|
114
|
+
|
|
115
|
+
for cur_index in indexes_to_del:
|
|
116
|
+
del self.STRUCT[cur_index]
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def chain_types(self):
|
|
120
|
+
return {c: _chain_type(self.STRUCT, c) for c in self.chain_ids}
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def assembly_names(self):
|
|
124
|
+
return [assem.name for assem in self.STRUCT.assemblies]
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def polymer_sequences(self):
|
|
128
|
+
cts = self.chain_types
|
|
129
|
+
out = dict()
|
|
130
|
+
for model in self.STRUCT:
|
|
131
|
+
for chain in model:
|
|
132
|
+
ct = cts.get(chain.name, "other")
|
|
133
|
+
if ct != "other":
|
|
134
|
+
out[chain.name] = re.sub("-", "", chain.get_polymer().make_one_letter_sequence())
|
|
135
|
+
return out
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def polymer_residue_numbers(self):
|
|
139
|
+
cts = self.chain_types
|
|
140
|
+
out = dict()
|
|
141
|
+
id_type = np.dtype([
|
|
142
|
+
("ch_name", "U5"),
|
|
143
|
+
("res_num", "i4"),
|
|
144
|
+
("res_icode", "U3"),
|
|
145
|
+
("res_name", "U5"),
|
|
146
|
+
])
|
|
147
|
+
for model in self.STRUCT:
|
|
148
|
+
for chain in model:
|
|
149
|
+
ct = cts.get(chain.name, "other")
|
|
150
|
+
if ct != "other":
|
|
151
|
+
out[chain.name] = np.array([(chain.name, r.seqid.num, r.seqid.icode, r.name)
|
|
152
|
+
for r in chain.get_polymer()], dtype=id_type)
|
|
153
|
+
return out
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def chain_residues(self):
|
|
157
|
+
out = dict()
|
|
158
|
+
for model in self.STRUCT:
|
|
159
|
+
for chain in model:
|
|
160
|
+
out[chain.name] = [r.name for r in chain]
|
|
161
|
+
return out
|
|
162
|
+
|
|
163
|
+
def update_entity(self):
|
|
164
|
+
"""
|
|
165
|
+
Update ENTITY, .entities .assemblies according to subchains
|
|
166
|
+
:return:
|
|
167
|
+
"""
|
|
168
|
+
subchains = []
|
|
169
|
+
for model in self.STRUCT:
|
|
170
|
+
for chain in model:
|
|
171
|
+
subchains.extend([sc.subchain_id() for sc in chain.subchains()])
|
|
172
|
+
|
|
173
|
+
# update .entities
|
|
174
|
+
new_entities = gemmi.EntityList()
|
|
175
|
+
ent_names = [] # keep
|
|
176
|
+
for ent in self.STRUCT.entities:
|
|
177
|
+
tmp = [i for i in ent.subchains if i in subchains]
|
|
178
|
+
if tmp:
|
|
179
|
+
ent.subchains = tmp
|
|
180
|
+
new_entities.append(ent)
|
|
181
|
+
ent_names.append(ent.name)
|
|
182
|
+
self.STRUCT.entities = new_entities
|
|
183
|
+
|
|
184
|
+
# update .ENTITY
|
|
185
|
+
for super_key in ["eid2desc", "eid2specie", "eid2taxid"]:
|
|
186
|
+
for eid in list(self.ENTITY[super_key].keys()):
|
|
187
|
+
if eid not in ent_names:
|
|
188
|
+
del self.ENTITY[super_key][eid]
|
|
189
|
+
|
|
190
|
+
for cid, eid in list(self.ENTITY["polymer2eid"].items()):
|
|
191
|
+
if eid not in ent_names or cid not in self.chain_ids:
|
|
192
|
+
del self.ENTITY["polymer2eid"][cid]
|
|
193
|
+
|
|
194
|
+
# update .assemblies
|
|
195
|
+
all_cid = self.chain_ids
|
|
196
|
+
del_assembly_indexes = []
|
|
197
|
+
|
|
198
|
+
for a_i, assembly in enumerate(self.STRUCT.assemblies):
|
|
199
|
+
del_gen_indexes = []
|
|
200
|
+
for g_i, gen in enumerate(assembly.generators):
|
|
201
|
+
# chains
|
|
202
|
+
tmp1 = [i for i in gen.chains if i in all_cid]
|
|
203
|
+
gen.chains = tmp1
|
|
204
|
+
|
|
205
|
+
tmp2 = [i for i in gen.subchains if i in subchains]
|
|
206
|
+
gen.subchains = tmp2
|
|
207
|
+
# empty gen
|
|
208
|
+
if gen.chains == [] and gen.subchains == []:
|
|
209
|
+
del_gen_indexes.append(g_i)
|
|
210
|
+
|
|
211
|
+
del_gen_indexes.sort(reverse=True)
|
|
212
|
+
for dgi in del_gen_indexes:
|
|
213
|
+
del assembly.generators[dgi]
|
|
214
|
+
|
|
215
|
+
if len(del_gen_indexes) == len(assembly.generators):
|
|
216
|
+
del_assembly_indexes.append(a_i)
|
|
217
|
+
|
|
218
|
+
del_assembly_indexes.sort(reverse=True)
|
|
219
|
+
for dai in del_assembly_indexes:
|
|
220
|
+
del self.STRUCT.assemblies[dai]
|
|
221
|
+
|
|
222
|
+
@typechecked
|
|
223
|
+
def rename_chain(self, origin_name: str, target_name: str):
|
|
224
|
+
if origin_name not in self.chain_ids:
|
|
225
|
+
raise ValueError("chain %s not found" % origin_name)
|
|
226
|
+
other_chain_names = set(self.chain_ids) - {origin_name}
|
|
227
|
+
|
|
228
|
+
if target_name in other_chain_names:
|
|
229
|
+
raise ValueError("target chain name %s has existed, change to a different one." % target_name)
|
|
230
|
+
|
|
231
|
+
self.STRUCT.rename_chain(origin_name, target_name)
|
|
232
|
+
|
|
233
|
+
# update .polymer2eid if exist
|
|
234
|
+
if origin_name in self.ENTITY.polymer2eid:
|
|
235
|
+
val = self.ENTITY.polymer2eid[origin_name]
|
|
236
|
+
del self.ENTITY.polymer2eid[origin_name]
|
|
237
|
+
self.ENTITY.polymer2eid[target_name] = val
|
|
238
|
+
|
|
239
|
+
# update .assemblies.generator.chain if exists, for .pdb loading structure
|
|
240
|
+
for assembly in self.STRUCT.assemblies:
|
|
241
|
+
for gen in assembly.generators:
|
|
242
|
+
tmp = [target_name if c == origin_name else c for c in gen.chains]
|
|
243
|
+
gen.chains = tmp
|
|
244
|
+
|
|
245
|
+
@typechecked
|
|
246
|
+
def switch_chain_names(self, chain_name_1: str, chain_name_2: str):
|
|
247
|
+
if chain_name_1 not in self.chain_ids:
|
|
248
|
+
raise ValueError("chain_name_2 %s not in structure" % chain_name_1)
|
|
249
|
+
if chain_name_2 not in self.chain_ids:
|
|
250
|
+
raise ValueError("chain_name_2 %s not in structure" % chain_name_2)
|
|
251
|
+
|
|
252
|
+
l3 = [i + j + k for i in string.ascii_uppercase for j in string.ascii_uppercase for k in string.ascii_uppercase]
|
|
253
|
+
l3.sort(reverse=True)
|
|
254
|
+
|
|
255
|
+
current_names = set(self.chain_ids)
|
|
256
|
+
l3_l = [n for n in l3 if n not in current_names]
|
|
257
|
+
sw_name = l3_l.pop()
|
|
258
|
+
self.rename_chain(chain_name_1, sw_name)
|
|
259
|
+
self.rename_chain(chain_name_2, chain_name_1)
|
|
260
|
+
self.rename_chain(sw_name, chain_name_2)
|
|
261
|
+
|
|
262
|
+
@typechecked
|
|
263
|
+
def pick_chains(self, chain_names: List[str]):
|
|
264
|
+
self.set_default_model()
|
|
265
|
+
|
|
266
|
+
if chain_names:
|
|
267
|
+
missing = [c for c in chain_names if c not in self.chain_ids]
|
|
268
|
+
if missing:
|
|
269
|
+
raise ValueError("Chains %s not found" % ",".join(missing))
|
|
270
|
+
else:
|
|
271
|
+
del_chain_names = set(self.chain_ids) - set(chain_names)
|
|
272
|
+
del_chain_indexes = [i for i, ch in enumerate(self.STRUCT[0]) if ch.name in del_chain_names]
|
|
273
|
+
del_chain_indexes.sort(reverse=True)
|
|
274
|
+
for di in del_chain_indexes:
|
|
275
|
+
del self.STRUCT[0][di]
|
|
276
|
+
self.update_entity()
|
|
277
|
+
else:
|
|
278
|
+
raise ValueError("No chain is given")
|
|
279
|
+
|
|
280
|
+
@typechecked
|
|
281
|
+
def make_chain_names_to_one_letter(self, only_uppercase: bool = True):
|
|
282
|
+
_mapper = _chain_names2one_letter(self.STRUCT, only_uppercase)
|
|
283
|
+
for origin_name, target_name in _mapper.items():
|
|
284
|
+
self.rename_chain(origin_name, target_name)
|
|
285
|
+
return _mapper
|
|
286
|
+
|
|
287
|
+
@typechecked
|
|
288
|
+
def get_assembly(self, assembly_name: str):
|
|
289
|
+
if assembly_name not in self.assembly_names:
|
|
290
|
+
raise ValueError("assembly %s is not found" % assembly_name)
|
|
291
|
+
|
|
292
|
+
struct, polymer2eid = get_assembly(self.STRUCT, assembly_name, gemmi.HowToNameCopiedChain.Short)
|
|
293
|
+
out = StructureParser(struct)
|
|
294
|
+
out.ENTITY = deepcopy(self.ENTITY)
|
|
295
|
+
out.ENTITY.polymer2eid = polymer2eid
|
|
296
|
+
|
|
297
|
+
# update info
|
|
298
|
+
prefix = "[Assembly %s] " % assembly_name
|
|
299
|
+
out.INFO.title = prefix + out.INFO.title
|
|
300
|
+
out.STRUCT.info = out.INFO.to_gemmi_structure_infomap()
|
|
301
|
+
return out
|
|
302
|
+
|
|
303
|
+
@typechecked
|
|
304
|
+
def merge_chains(self, chains: List[str]):
|
|
305
|
+
"""
|
|
306
|
+
Merge a list of chains, target chain id is chains[0]
|
|
307
|
+
|
|
308
|
+
Renumber the new chain from 1
|
|
309
|
+
|
|
310
|
+
[No fix the Entity and some other information of structure]
|
|
311
|
+
:param chains:
|
|
312
|
+
:return:
|
|
313
|
+
GemmiLoader
|
|
314
|
+
"""
|
|
315
|
+
for c in chains:
|
|
316
|
+
if c not in self.chain_ids:
|
|
317
|
+
raise RuntimeError("Chain %s is not in the structure" % c)
|
|
318
|
+
if len(self.STRUCT) > 1:
|
|
319
|
+
print("Multiple models in structure, do nothing")
|
|
320
|
+
elif len(chains) < 2:
|
|
321
|
+
print("Query chains less than 2, do nothing")
|
|
322
|
+
else:
|
|
323
|
+
new_chain = gemmi.Chain(chains[0])
|
|
324
|
+
residue_index = 1
|
|
325
|
+
|
|
326
|
+
model = self.STRUCT[0]
|
|
327
|
+
|
|
328
|
+
for ch in model:
|
|
329
|
+
if ch.name in chains:
|
|
330
|
+
for res in ch:
|
|
331
|
+
nr = deepcopy(res)
|
|
332
|
+
nr.seqid.icode = " "
|
|
333
|
+
nr.seqid.num = residue_index
|
|
334
|
+
new_chain.add_residue(nr)
|
|
335
|
+
residue_index += 1
|
|
336
|
+
|
|
337
|
+
for c in chains:
|
|
338
|
+
self.STRUCT[0].remove_chain(c)
|
|
339
|
+
|
|
340
|
+
self.STRUCT[0].add_chain(new_chain, unique_name=True)
|
|
341
|
+
|
|
342
|
+
def get_atom_coords(self, chains: List[str], atoms: Optional[List[str]] = None):
|
|
343
|
+
for c in chains:
|
|
344
|
+
if c not in self.chain_ids:
|
|
345
|
+
warnings.warn("Chain %s is not in the structure" % c)
|
|
346
|
+
|
|
347
|
+
coord = []
|
|
348
|
+
atom_id = []
|
|
349
|
+
id_type = np.dtype([
|
|
350
|
+
("ch_name", "U5"),
|
|
351
|
+
("res_num", "i4"),
|
|
352
|
+
("res_icode", "U3"),
|
|
353
|
+
("res_name", "U5"),
|
|
354
|
+
("atom_name", "U5")
|
|
355
|
+
])
|
|
356
|
+
|
|
357
|
+
model = self.STRUCT[0]
|
|
358
|
+
for ch in model:
|
|
359
|
+
if ch.name in chains:
|
|
360
|
+
for res in ch:
|
|
361
|
+
for atom in res:
|
|
362
|
+
if atoms is None or atom.name in atoms:
|
|
363
|
+
cur_id = (ch.name, res.seqid.num, res.seqid.icode, res.name, atom.name)
|
|
364
|
+
cur_pos = atom.pos.tolist()
|
|
365
|
+
coord.append(cur_pos)
|
|
366
|
+
atom_id.append(cur_id)
|
|
367
|
+
|
|
368
|
+
if coord:
|
|
369
|
+
return np.array(coord, dtype=np.float32), np.array(atom_id, dtype=id_type)
|
|
370
|
+
else:
|
|
371
|
+
return np.empty(shape=(0, 3), dtype=np.float32), np.array(atom_id, dtype=id_type)
|