Skip to content

Commit c93bd90

Browse files
committed
Move anode viewer to dlstbx
1 parent 593b9a3 commit c93bd90

3 files changed

Lines changed: 355 additions & 20 deletions

File tree

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import shutil
5+
from pathlib import Path
6+
7+
import gemmi
8+
import molviewspec as mvs
9+
import numpy as np
10+
import pandas as pd
11+
12+
13+
def parse_anode(pdb_file, anode_log, cutoff_sigma):
14+
atoms = []
15+
with open(anode_log, "r") as f:
16+
st = gemmi.read_structure(pdb_file)
17+
cell = st.cell
18+
19+
lines = f.readlines()
20+
# Find the starting point
21+
start = None
22+
for i, line in enumerate(lines):
23+
if "Strongest unique anomalous peaks" in line:
24+
start = i + 4 # start 4 lines after
25+
break
26+
27+
if start is None:
28+
raise ValueError("Couldn't find 'Strongest unique anomalous peaks' in file")
29+
30+
for line in lines[start:]:
31+
stripped = line.strip()
32+
if not stripped:
33+
break # stop at first empty line
34+
parts = stripped.split()
35+
# if len(parts) == 8: # 8 columns
36+
37+
atom_name = parts[0]
38+
xf, yf, zf = map(float, parts[1:4])
39+
height = float(parts[4])
40+
if height < cutoff_sigma:
41+
break
42+
coords = cell.orthogonalize(gemmi.Fractional(xf, yf, zf))
43+
atoms.append([atom_name, coords[0], coords[1], coords[2], height])
44+
45+
atoms_df = pd.DataFrame(atoms, columns=["Atom", "x", "y", "z", "Height"])
46+
47+
return atoms_df
48+
49+
50+
def save_cropped_maps(
51+
pdb_file, map_file, atoms_df, peaks, radius, prefix, results_directory
52+
):
53+
st = gemmi.read_structure(pdb_file)
54+
cell = st.cell
55+
56+
tmpdir = Path(results_directory) / "tmp_molviewspec"
57+
tmpdir.mkdir(parents=False, exist_ok=True)
58+
59+
for i in range(peaks):
60+
m = gemmi.read_ccp4_map(map_file, setup=True)
61+
grid = m.grid
62+
63+
mask = grid.clone()
64+
mask.fill(0.0)
65+
66+
map_out = f"{prefix}{i + 1}.map" #
67+
center = gemmi.Position(
68+
atoms_df["x"][i], atoms_df["y"][i], atoms_df["z"][i]
69+
) # peak loc
70+
71+
mask.set_points_around(center, radius, 1.0, use_pbc=True) # spherical mask in Å
72+
73+
dl = gemmi.Position(radius, radius, radius) # box d/2
74+
box = gemmi.FractionalBox()
75+
box.extend(cell.fractionalize(center - dl))
76+
box.extend(cell.fractionalize(center + dl))
77+
78+
grid.array[:] *= mask.array
79+
m.set_extent(box)
80+
outfile = str(f"{tmpdir}/{map_out}")
81+
m.write_ccp4_map(outfile)
82+
83+
84+
def find_camera_pos(structure: gemmi.Structure):
85+
atoms = [
86+
atom
87+
for model in structure
88+
for chain in model
89+
for residue in chain
90+
for atom in residue
91+
]
92+
a, b, c, d = gemmi.find_best_plane(atoms)
93+
# normal = gemmi.Vec3(a, b, c)
94+
95+
targetp = gemmi.Position(0, 0, 0)
96+
# The center can be taken as the centroid of the input atoms projected onto the plane
97+
for atom in atoms:
98+
targetp += atom.pos
99+
targetp /= len(atoms)
100+
targetp
101+
102+
camera_pos = targetp - (120 * gemmi.Position(a, b, c))
103+
104+
return targetp.tolist(), camera_pos.tolist()
105+
106+
107+
def gen_html_anode(results_directory, cutoff_sigma):
108+
pdb_file = results_directory + "/final.pdb"
109+
mtz_file = results_directory + "/final.mtz"
110+
anode_map = results_directory + "/anode.map"
111+
anode_log = results_directory + "/anode.lsa"
112+
113+
atoms_df = parse_anode(pdb_file, anode_log, cutoff_sigma)
114+
peaks = len(atoms_df) # set peaks to length of atoms_df
115+
heights = atoms_df["Height"]
116+
117+
# convert dimple mtz to map format
118+
map_file = results_directory + "/final.map"
119+
os.system(f"gemmi sf2map {mtz_file} {map_file}")
120+
121+
save_cropped_maps(
122+
pdb_file,
123+
anode_map,
124+
atoms_df,
125+
peaks=peaks,
126+
radius=3,
127+
prefix="box",
128+
results_directory=results_directory,
129+
)
130+
131+
save_cropped_maps(
132+
pdb_file,
133+
map_file,
134+
atoms_df,
135+
peaks=peaks,
136+
radius=6,
137+
prefix="fbox",
138+
results_directory=results_directory,
139+
) # 2fofc
140+
141+
st = gemmi.read_structure(pdb_file)
142+
cell = st.cell
143+
144+
snapshot_list = []
145+
146+
for j in range(peaks):
147+
globals()["map_file" + str(j + 1)] = (
148+
f"{results_directory}/tmp_molviewspec/box{j + 1}.map"
149+
)
150+
globals()["fmap_file" + str(j + 1)] = (
151+
f"{results_directory}/tmp_molviewspec/fbox{j + 1}.map"
152+
)
153+
154+
with open(globals()["map_file" + str(j + 1)], mode="rb") as f:
155+
globals()["map_data" + str(j + 1)] = f.read()
156+
157+
with open(globals()["fmap_file" + str(j + 1)], mode="rb") as f:
158+
globals()["fmap_data" + str(j + 1)] = f.read()
159+
160+
targetp, camerap = find_camera_pos(st)
161+
ns = (
162+
gemmi.NeighborSearch(st[0], cell, 5).populate(include_h=False).populate()
163+
) # neighbour search
164+
165+
for i in range(peaks + 1):
166+
if i == 0: # main page is different to all the others
167+
builder = mvs.create_builder()
168+
169+
structure = (
170+
builder.download(url=pdb_file).parse(format="pdb").model_structure()
171+
) # symmetry_mates_structure()
172+
structure.component(selector="polymer").representation(
173+
type="surface", size_factor=0.9
174+
).opacity(opacity=0.2).color(color="#AABDF1")
175+
structure.component(selector="polymer").representation().opacity(
176+
opacity=0.25
177+
).color(custom={"molstar_color_theme_name": "chain_id"})
178+
structure.component(selector="ligand").representation(
179+
type="ball_and_stick"
180+
).color(custom={"molstar_color_theme_name": "element-symbol"})
181+
structure.component(selector="ligand").representation(
182+
type="surface"
183+
).opacity(opacity=0.1).color(
184+
custom={"molstar_color_theme_name": "element-symbol"}
185+
)
186+
187+
for j in range(peaks):
188+
peakcoords = np.array(
189+
[atoms_df["x"][j], atoms_df["y"][j], atoms_df["z"][j]]
190+
).tolist()
191+
labelcoords = np.array(
192+
[atoms_df["x"][j], atoms_df["y"][j], atoms_df["z"][j]]
193+
).tolist()
194+
builder.primitives(opacity=0.1).sphere(
195+
center=peakcoords,
196+
radius=1,
197+
color="#da21fa",
198+
tooltip=f"peak {j + 1}",
199+
).label(position=labelcoords, text=f"{j + 1}", label_size=5)
200+
201+
for k in range(peaks):
202+
ccp4 = builder.download(url=globals()["map_file" + str(k + 1)]).parse(
203+
format="map"
204+
)
205+
ccp4.volume().representation(
206+
type="isosurface",
207+
relative_isovalue=3,
208+
show_wireframe=True,
209+
show_faces=False,
210+
).color(color="#da21fa").opacity(opacity=0.25)
211+
212+
builder.camera(position=camerap, target=targetp, up=[0, 0, 1])
213+
214+
globals()["snapshot" + str(i + 1)] = builder.get_snapshot(
215+
title="Main View",
216+
description=f"## Anode Results: \n ### Summary \n - Anomalous difference map shown at 3σ, magenta for the top {peaks} sites listed in 'anode.lsa'",
217+
transition_duration_ms=700,
218+
linger_duration_ms=5000,
219+
key="Main",
220+
)
221+
222+
snapshot_list.append(globals()["snapshot" + str(i + 1)])
223+
224+
else:
225+
builder = mvs.create_builder()
226+
227+
structure = (
228+
builder.download(url=pdb_file).parse(format="pdb").model_structure()
229+
) # symmetry_mates_structure()
230+
structure.component(selector="polymer").representation(
231+
type="surface", size_factor=0.9
232+
).opacity(opacity=0.2).color(color="#AABDF1")
233+
structure.component(selector="polymer").representation().opacity(
234+
opacity=0.25
235+
).color(custom={"molstar_color_theme_name": "chain_id"})
236+
structure.component(selector="ligand").representation(
237+
type="ball_and_stick"
238+
).color(custom={"molstar_color_theme_name": "element-symbol"})
239+
structure.component(selector="ligand").representation(
240+
type="surface"
241+
).opacity(opacity=0.1).color(
242+
custom={"molstar_color_theme_name": "element-symbol"}
243+
)
244+
245+
for j in range(peaks):
246+
peakcoords = np.array(
247+
[atoms_df["x"][j], atoms_df["y"][j], atoms_df["z"][j]]
248+
).tolist()
249+
labelcoords = np.array(
250+
[atoms_df["x"][j], atoms_df["y"][j], atoms_df["z"][j]]
251+
).tolist()
252+
builder.primitives(opacity=0.1).sphere(
253+
center=peakcoords,
254+
radius=1,
255+
color="#da21fa",
256+
tooltip=f"peak {j + 1}",
257+
).label(position=labelcoords, text=f"{j + 1}", label_size=2) # spheres
258+
259+
nearest_atom_mark = ns.find_nearest_atom(
260+
gemmi.Position(
261+
atoms_df["x"][i - 1], atoms_df["y"][i - 1], atoms_df["z"][i - 1]
262+
)
263+
)
264+
residue = mvs.ComponentExpression(
265+
atom_id=st[0][nearest_atom_mark.chain_idx][
266+
nearest_atom_mark.residue_idx
267+
][nearest_atom_mark.atom_idx].serial
268+
) # nearest atom id
269+
structure.component(
270+
selector=residue,
271+
custom={
272+
"molstar_show_non_covalent_interactions": True,
273+
"molstar_non_covalent_interactions_radius_ang": 5.0,
274+
},
275+
).focus() # .label(text=f"{heights[i-1]} σ",)
276+
277+
for k in range(peaks):
278+
ccp4 = builder.download(url=globals()["map_file" + str(k + 1)]).parse(
279+
format="map"
280+
)
281+
ccp4.volume().representation(
282+
type="isosurface",
283+
relative_isovalue=3,
284+
show_wireframe=True,
285+
show_faces=False,
286+
).color(color="#da21fa").opacity(opacity=0.25)
287+
288+
ccp42 = builder.download(url=globals()["fmap_file" + str(k + 1)]).parse(
289+
format="map"
290+
) # 2fo-fc
291+
ccp42.volume().representation(
292+
type="isosurface",
293+
relative_isovalue=1.5,
294+
show_wireframe=True,
295+
show_faces=False,
296+
).color(color="#2f78d7").opacity(opacity=0.25)
297+
298+
globals()["snapshot" + str(i + 1)] = builder.get_snapshot(
299+
title=f"Site {i}",
300+
description=f"## Anode Results: \n ### Site {i} \n - Displaying unique anomalous peak, site {i}, height {heights[i - 1]} σ \n - Anomolous difference map 3σ, magenta \n - 2FO-FC at 1.5σ, blue \n \n [Back to Main Summary Page](#Main)",
301+
transition_duration_ms=700,
302+
linger_duration_ms=5000,
303+
key="site1",
304+
)
305+
306+
snapshot_list.append(globals()["snapshot" + str(i + 1)])
307+
308+
with open(pdb_file) as f:
309+
pdb_data = f.read()
310+
311+
# with open(map_file, mode="rb") as f:
312+
# map_data = f.read()
313+
314+
data_dict = {}
315+
for i in range(peaks):
316+
key = globals()["map_file" + str(i + 1)]
317+
value = globals()["map_data" + str(i + 1)]
318+
data_dict[key] = value
319+
320+
key = globals()["fmap_file" + str(i + 1)]
321+
value = globals()["fmap_data" + str(i + 1)]
322+
data_dict[key] = value
323+
324+
data_dict[pdb_file] = pdb_data
325+
326+
states = mvs.States(
327+
snapshots=snapshot_list, metadata=mvs.GlobalMetadata(description="anode")
328+
)
329+
html = mvs.molstar_widgets.molstar_html(states, data=data_dict, ui="stories")
330+
with open(f"{results_directory}/anode.html", "w") as f:
331+
f.write(html)
332+
333+
# clean up
334+
tmpdir = results_directory + "/tmp_molviewspec"
335+
shutil.rmtree(str(tmpdir))
336+
os.remove(map_file)

0 commit comments

Comments
 (0)