Jhsmit commited on
Commit
dd15d3e
·
1 Parent(s): 3e76897

initial commit

Browse files
Files changed (3) hide show
  1. Dockerfile +28 -0
  2. app.py +366 -0
  3. requirements.txt +5 -0
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.13
2
+
3
+ # Set up a new user named "user" with user ID 1000
4
+ RUN useradd -m -u 1000 user
5
+
6
+ # Switch to the "user" user
7
+ USER user
8
+
9
+ # Set home to the user's home directory
10
+ ENV HOME=/home/user \
11
+ PATH=/home/user/.local/bin:$PATH
12
+
13
+ # Set the working directory to the user's home directory
14
+ WORKDIR $HOME/app
15
+
16
+ # Try and run pip command after setting the user with `USER user` to avoid permission issues with Python
17
+ RUN pip install --no-cache-dir --upgrade pip
18
+
19
+ # Copy the current directory contents into the container at $HOME/app setting the owner to the user
20
+ COPY --chown=user . $HOME/app
21
+
22
+ COPY --chown=user requirements.txt .
23
+
24
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
25
+
26
+ COPY --chown=user app.py .
27
+
28
+ ENTRYPOINT ["solara", "run", "app.py", "--host=0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import statistics
2
+ from dataclasses import asdict, dataclass
3
+ from io import StringIO
4
+ from pathlib import Path
5
+
6
+ import requests
7
+ import solara
8
+ import solara.lab
9
+ from Bio.PDB import Residue, Structure
10
+ from Bio.PDB.PDBParser import PDBParser
11
+ from ipymolstar.pdbemolstar import THEMES, PDBeMolstar
12
+ from matplotlib import colormaps
13
+ from matplotlib.colors import Normalize
14
+ from solara.alias import rv
15
+
16
+ parser = PDBParser(QUIET=True)
17
+ CHAIN_COLORS = [
18
+ "#1f77b4",
19
+ "#ff7f0e",
20
+ "#2ca02c",
21
+ "#d62728",
22
+ "#9467bd",
23
+ "#8c564b",
24
+ "#e377c2",
25
+ "#7f7f7f",
26
+ "#bcbd22",
27
+ "#17becf",
28
+ ]
29
+ AMINO_ACIDS = [
30
+ "ALA",
31
+ "ARG",
32
+ "ASN",
33
+ "ASP",
34
+ "CYS",
35
+ "GLN",
36
+ "GLU",
37
+ "GLY",
38
+ "HIS",
39
+ "ILE",
40
+ "LEU",
41
+ "LYS",
42
+ "MET",
43
+ "PHE",
44
+ "PRO",
45
+ "PYL",
46
+ "SEC",
47
+ "SER",
48
+ "THR",
49
+ "TRP",
50
+ "TYR",
51
+ "VAL",
52
+ ]
53
+ # use auth residue numbers or not
54
+ AUTH_RESIDUE_NUMBERS = {
55
+ "1QYN": False,
56
+ "2PE4": True,
57
+ }
58
+
59
+ pdb_ids = ["1QYN", "2PE4"]
60
+
61
+
62
+ def fetch_pdb(pdb_id) -> StringIO:
63
+ url = f"https://files.rcsb.org/download/{pdb_id}.pdb"
64
+ response = requests.get(url)
65
+ if response.status_code == 200:
66
+ sio = StringIO(response.text)
67
+ sio.seek(0)
68
+ return sio
69
+ else:
70
+ raise requests.HTTPError(f"Failed to download PDB file {pdb_id}")
71
+
72
+
73
+ structures = {p_id: parser.get_structure(p_id, fetch_pdb(p_id)) for p_id in pdb_ids}
74
+
75
+
76
+ @dataclass
77
+ class PDBeData:
78
+ molecule_id: str = "1qyn"
79
+ custom_data: dict | None = None
80
+ color_data: dict | None = None
81
+ bg_color: str = "#F7F7F7"
82
+ spin: bool = False
83
+ hide_polymer: bool = False
84
+ hide_water: bool = False
85
+ hide_heteroatoms: bool = False
86
+ hide_carbs: bool = False
87
+ hide_non_standard: bool = False
88
+ hide_coarse: bool = False
89
+
90
+ height: str = "700px"
91
+
92
+
93
+ visibility_cbs = ["polymer", "water", "heteroatoms", "carbs"]
94
+
95
+
96
+ def to_rgb(hex_color: str) -> dict:
97
+ return {
98
+ "r": int(hex_color[1:3], 16),
99
+ "g": int(hex_color[3:5], 16),
100
+ "b": int(hex_color[5:7], 16),
101
+ }
102
+
103
+
104
+ def color_chains(structure: Structure.Structure) -> dict:
105
+ data = [
106
+ {
107
+ "struct_asym_id": chain.id,
108
+ "color": to_rgb(hex_color),
109
+ }
110
+ for hex_color, chain in zip(CHAIN_COLORS, structure.get_chains())
111
+ ]
112
+
113
+ color_data = {"data": data, "nonSelectedColor": None}
114
+ return color_data
115
+
116
+
117
+ def color_residues(structure: Structure.Structure, auth: bool = False) -> dict:
118
+ _, resn, _ = zip(
119
+ *[r.id for r in structure.get_residues() if r.get_resname() in AMINO_ACIDS]
120
+ )
121
+
122
+ rmin, rmax = min(resn), max(resn)
123
+ # todo check for off by one errors
124
+ norm = Normalize(vmin=rmin, vmax=rmax)
125
+ auth_str = "_auth" if auth else ""
126
+
127
+ cmap = colormaps["rainbow"]
128
+ data = []
129
+ for i in range(rmin, rmax):
130
+ r, g, b, a = cmap(norm(i), bytes=True)
131
+ color = {"r": int(r), "g": int(g), "b": int(b)}
132
+ elem = {
133
+ f"start{auth_str}_residue_number": i,
134
+ f"end{auth_str}_residue_number": i,
135
+ "color": color,
136
+ "focus": False,
137
+ }
138
+ data.append(elem)
139
+
140
+ color_data = {"data": data, "nonSelectedColor": None}
141
+ return color_data
142
+
143
+
144
+ def get_bfactor(residue: Residue.Residue):
145
+ """returns the residue-average b-factor"""
146
+ return statistics.mean([atom.get_bfactor() for atom in residue])
147
+
148
+
149
+ def color_bfactor(structure: Structure.Structure, auth: bool = False) -> dict:
150
+ auth_str = "_auth" if auth else ""
151
+ value_data = []
152
+ for chain in structure.get_chains():
153
+ for r in chain.get_residues():
154
+ if r.get_resname() in AMINO_ACIDS:
155
+ bfactor = get_bfactor(r)
156
+ elem = {
157
+ f"start{auth_str}_residue_number": r.id[1],
158
+ f"end{auth_str}_residue_number": r.id[1],
159
+ "struct_asym_id": chain.id,
160
+ "value": bfactor,
161
+ }
162
+ value_data.append(elem)
163
+
164
+ all_values = [d["value"] for d in value_data]
165
+ vmin, vmax = min(all_values), max(all_values)
166
+
167
+ norm = Normalize(vmin=vmin, vmax=vmax)
168
+ cmap = colormaps["inferno"]
169
+ data = []
170
+ for v_elem in value_data:
171
+ elem = v_elem.copy()
172
+ r, g, b, a = cmap(norm(elem.pop("value")), bytes=True)
173
+ elem["color"] = {"r": int(r), "g": int(g), "b": int(b)}
174
+ data.append(elem)
175
+
176
+ color_data = {"data": data, "nonSelectedColor": None}
177
+ return color_data
178
+
179
+
180
+ def apply_coloring(pdb_id: str, color_mode: str):
181
+ structure = structures[pdb_id]
182
+ auth = AUTH_RESIDUE_NUMBERS[pdb_id]
183
+ if color_mode == "Chain":
184
+ color_data = color_chains(structure)
185
+ elif color_mode == "Residue":
186
+ color_data = color_residues(structure, auth)
187
+ elif color_mode == "β-factor":
188
+ color_data = color_bfactor(structure, auth)
189
+ return color_data
190
+
191
+
192
+ color_options = ["Chain", "Residue", "β-factor"]
193
+ color_data = apply_coloring(pdb_ids[0], color_options[0])
194
+ data = solara.Reactive(PDBeData(color_data=color_data))
195
+
196
+ molecule_store = {
197
+ "Glucose": dict(
198
+ url="https://pubchem.ncbi.nlm.nih.gov/rest/pug/conformers/000016A100000001/SDF?response_type=save&response_basename=Conformer3D_COMPOUND_CID_5793",
199
+ format="sdf",
200
+ binary=False,
201
+ ),
202
+ "ATP": dict(
203
+ url="https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/CID/5957/record/SDF?record_type=3d&response_type=save&response_basename=Conformer3D_COMPOUND_CID_5957",
204
+ format="sdf",
205
+ binary=False,
206
+ ),
207
+ "Caffeine": dict(
208
+ url="https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/CID/2519/record/SDF?record_type=3d&response_type=save&response_basename=Conformer3D_COMPOUND_CID_2519",
209
+ format="sdf",
210
+ binary=False,
211
+ ),
212
+ "Strychnine": dict(
213
+ url=" https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/CID/441071/record/SDF?record_type=3d&response_type=save&response_basename=Conformer3D_COMPOUND_CID_441071 ",
214
+ format="sdf",
215
+ binary=False,
216
+ ),
217
+ }
218
+
219
+
220
+ def download_pdb(pdb_id, fpath: Path):
221
+ url = f"https://files.rcsb.org/download/{pdb_id}.pdb"
222
+ response = requests.get(url)
223
+ if response.status_code == 200:
224
+ fpath.write_bytes(response.content)
225
+ return f"{pdb_id}.pdb"
226
+ else:
227
+ print("Failed to download PDB file")
228
+ return None
229
+
230
+
231
+ @solara.component
232
+ def ProteinView(dark_effective: bool):
233
+ with solara.Card("PDBeMol*"):
234
+ theme = "dark" if dark_effective else "light"
235
+ PDBeMolstar.element(**asdict(data.value), theme=theme)
236
+
237
+
238
+ @solara.component
239
+ def Page():
240
+ solara.Title("Solara App - PDBeMol*")
241
+ counter, set_counter = solara.use_state(0)
242
+ dark_effective = solara.lab.use_dark_effective()
243
+ dark_effective_previous = solara.use_previous(dark_effective)
244
+
245
+ structure_type = solara.use_reactive("Protein")
246
+ color_mode = solara.use_reactive(color_options[0])
247
+ protein_id = solara.use_reactive(pdb_ids[0])
248
+ molecule_key = solara.use_reactive(next(iter(molecule_store.keys())))
249
+
250
+ if dark_effective != dark_effective_previous:
251
+ if dark_effective:
252
+ data.update(bg_color=THEMES["dark"]["bg_color"])
253
+ else:
254
+ data.update(bg_color=THEMES["light"]["bg_color"])
255
+
256
+ def update_protein_id(value: str):
257
+ protein_id.set(value)
258
+ color_data = apply_coloring(protein_id.value, color_mode.value)
259
+ data.update(color_data=color_data, molecule_id=protein_id.value.lower())
260
+
261
+ def update_molecule_key(value: str):
262
+ molecule_key.set(value)
263
+ data.update(custom_data=molecule_store[molecule_key.value])
264
+
265
+ def update_structure_type(value: str):
266
+ structure_type.set(value)
267
+ if structure_type.value == "Protein":
268
+ color_data = apply_coloring(protein_id.value, color_mode.value)
269
+ data.update(color_data=color_data)
270
+ # currently there is a bug where coloring does not work anymore after switching from molecule back to protein
271
+ data.update(
272
+ color_data=color_data,
273
+ custom_data=None,
274
+ molecule_id=protein_id.value.lower(),
275
+ )
276
+ else:
277
+ color_data = {"data": [], "nonSelectedColor": None}
278
+ data.update(
279
+ molecule_id="",
280
+ custom_data=molecule_store[molecule_key.value],
281
+ color_data=color_data, # used to reset colors
282
+ )
283
+
284
+ def update_color_mode(value: str):
285
+ color_data = apply_coloring(protein_id.value, value)
286
+ color_mode.set(value)
287
+ print(id(color_data))
288
+ data.update(color_data=color_data)
289
+
290
+ with solara.AppBar():
291
+ solara.lab.ThemeToggle()
292
+
293
+ with solara.ColumnsResponsive([4, 8]):
294
+ with solara.Card("Controls"):
295
+ with solara.ToggleButtonsSingle(
296
+ value=structure_type.value,
297
+ on_value=update_structure_type,
298
+ classes=["d-flex", "flex-row"],
299
+ ):
300
+ solara.Button(label="Protein", classes=["flex-grow-1"])
301
+ solara.Button(label="Molecule", classes=["flex-grow-1"])
302
+
303
+ solara.Div(style="height: 20px")
304
+
305
+ if structure_type.value == "Protein":
306
+ solara.Select(
307
+ label="PDB id",
308
+ value=protein_id.value,
309
+ values=pdb_ids,
310
+ on_value=update_protein_id,
311
+ )
312
+
313
+ solara.Select(
314
+ label="Color mode",
315
+ value=color_mode.value,
316
+ on_value=update_color_mode,
317
+ values=color_options,
318
+ )
319
+ else:
320
+ solara.Select(
321
+ label="Molecule",
322
+ value=molecule_key.value,
323
+ values=list(molecule_store.keys()),
324
+ on_value=update_molecule_key,
325
+ )
326
+
327
+ solara.Checkbox(
328
+ label="spin",
329
+ value=data.value.spin,
330
+ on_value=lambda x: data.update(spin=x),
331
+ )
332
+
333
+ for struc_elem in visibility_cbs:
334
+ attr = f"hide_{struc_elem}"
335
+
336
+ def on_value(x, attr=attr):
337
+ data.update(**{attr: x})
338
+
339
+ solara.Checkbox(
340
+ label=f"hide {struc_elem}",
341
+ value=getattr(data.value, attr),
342
+ on_value=on_value,
343
+ )
344
+
345
+ btn = solara.Button("background color", block=True)
346
+ with solara.lab.Menu(activator=btn, close_on_content_click=False):
347
+ rv.ColorPicker(
348
+ v_model=data.value.bg_color,
349
+ on_v_model=lambda x: data.update(bg_color=x),
350
+ )
351
+
352
+ solara.Div(style="height: 20px")
353
+ solara.Button(
354
+ "redraw", on_click=lambda: set_counter(counter + 1), block=True
355
+ )
356
+
357
+ # with solara.Card("Protein view"):
358
+ # PDBeMolstar.element(**asdict(data.value)).key(f"molstar-{counter}")
359
+ key = f"{counter}_{dark_effective}"
360
+ ProteinView(dark_effective).key(key)
361
+
362
+
363
+ @solara.component
364
+ def Layout(children):
365
+ dark_effective = solara.lab.use_dark_effective()
366
+ return solara.AppLayout(children=children, toolbar_dark=dark_effective, color=None)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ solara
2
+ ipymolstar
3
+ matplotlib
4
+ biopython
5
+ uvicorn<0.30