Jhsmit commited on
Commit
9b4ab03
1 Parent(s): 368c61c

initial commit

Browse files
Files changed (3) hide show
  1. Dockerfile +28 -0
  2. app.py +281 -0
  3. requirements.txt +5 -0
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11
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,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from dataclasses import dataclass
3
+ from io import StringIO
4
+ from pathlib import Path
5
+ from typing import Literal, Optional, TypedDict, cast
6
+ from zipfile import ZipFile
7
+
8
+ import polars as pl
9
+ import solara
10
+ from Bio.PDB import MMCIFParser, Structure
11
+ from ipymolstar import PDBeMolstar
12
+ from ipymolstar.widget import QueryParam
13
+ from polarify import polarify
14
+ from solara.components.file_drop import FileInfo
15
+
16
+
17
+ class ColorData(TypedDict):
18
+ data: list[QueryParam]
19
+ NonSelectedColor: None
20
+
21
+
22
+ class TooltipData(TypedDict):
23
+ data: list[QueryParam]
24
+
25
+
26
+ class CustomData(TypedDict):
27
+ data: str
28
+ format: Literal["cif"]
29
+ binary: Literal[False]
30
+
31
+
32
+ @dataclass
33
+ class AlphaFoldData:
34
+ name: str
35
+ structure: Structure
36
+ atom_data: pl.DataFrame
37
+ residue_data: pl.DataFrame
38
+
39
+ custom_data: CustomData
40
+ color_data: ColorData
41
+ tooltip_data: TooltipData
42
+
43
+ def write_atoms(self):
44
+ return self.atom_data.write_csv()
45
+
46
+
47
+ COLOR_LUT = {
48
+ "very-high": {"r": 16, "g": 109, "b": 255},
49
+ "confident": {"r": 16, "g": 207, "b": 241},
50
+ "low": {"r": 246, "g": 237, "b": 18},
51
+ "very-low": {"r": 239, "g": 130, "b": 30},
52
+ }
53
+ NO_COLOR_DATA = {"data": [], "nonSelectedColor": None}
54
+ NO_TOOLTIP_DATA = {"data": []}
55
+
56
+
57
+ PARSER = MMCIFParser()
58
+
59
+ result_index = solara.reactive(0)
60
+ file_info = solara.reactive(cast(Optional[FileInfo], None))
61
+
62
+
63
+ @polarify
64
+ def assign_confidence(x: pl.Expr) -> pl.Expr:
65
+ s = pl.lit("very-high")
66
+ if x < 50:
67
+ s = pl.lit("very-low")
68
+ elif x < 70:
69
+ s = pl.lit("low")
70
+ elif x < 90:
71
+ s = pl.lit("confident")
72
+
73
+ return s
74
+
75
+
76
+ @solara.lab.task
77
+ def load_result() -> Optional[AlphaFoldData]:
78
+ if file_info.value is None:
79
+ print("none done")
80
+ return "None"
81
+
82
+ f_idx = result_index.value
83
+ with ZipFile(file_info.value["file_obj"]) as zf:
84
+ files = zf.namelist()
85
+ structure_file = [f for f in files if f.endswith(".cif")][f_idx]
86
+ json_data_file = [f for f in files if "full_data" in f][f_idx]
87
+
88
+ with zf.open(json_data_file) as json_f:
89
+ json_load = json.load(json_f)
90
+
91
+ cif_str = zf.read(structure_file).decode("utf-8")
92
+
93
+ alphafold_name = structure_file.rstrip(f"_model_{f_idx}.cif")
94
+
95
+ sio = StringIO(cif_str)
96
+ sio.seek(0)
97
+ structure = PARSER.get_structure(structure_file.removesuffix(".cif"), sio)
98
+
99
+ names = pl.Series(
100
+ (atom.get_parent().resname for atom in structure.get_atoms()),
101
+ dtype=pl.Categorical,
102
+ )
103
+ resn = pl.Series(atom.get_parent().id[1] for atom in structure.get_atoms())
104
+ chain = pl.Series(json_load["atom_chain_ids"], dtype=pl.Categorical)
105
+
106
+ atoms_df = pl.DataFrame(
107
+ {
108
+ "name": names,
109
+ "resn": resn,
110
+ "chain": chain,
111
+ "plddt": json_load["atom_plddts"],
112
+ }
113
+ )
114
+
115
+ residue_df = (
116
+ atoms_df.group_by(["chain", "resn", "name"])
117
+ .agg(pl.col("plddt").mean().alias("mean_plddt"))
118
+ .sort(["chain", "resn"])
119
+ .with_columns(
120
+ assign_confidence(pl.col("mean_plddt"))
121
+ .alias("confidence")
122
+ .cast(pl.Categorical)
123
+ )
124
+ )
125
+
126
+ custom_data = {
127
+ "data": cif_str,
128
+ "format": "cif",
129
+ "binary": False,
130
+ }
131
+
132
+ color_query = []
133
+ tooltip_query = []
134
+ for chain, resn, alphafold_name, mean_plddt, confidence in residue_df.iter_rows():
135
+ res_color = {
136
+ "struct_asym_id": chain,
137
+ "residue_number": resn,
138
+ "color": COLOR_LUT[confidence],
139
+ }
140
+ res_tt = {
141
+ "struct_asym_id": chain,
142
+ "residue_number": resn,
143
+ "tooltip": f"Confidence: {confidence}; plddt: {mean_plddt:.2f}",
144
+ }
145
+ color_query.append(res_color)
146
+ tooltip_query.append(res_tt)
147
+
148
+ plddt_color_data = {"data": color_query, "nonSelectedColor": None}
149
+ plddt_tooltip_data = {"data": tooltip_query}
150
+
151
+ data = AlphaFoldData(
152
+ name=alphafold_name,
153
+ structure=structure,
154
+ atom_data=atoms_df,
155
+ residue_data=residue_df,
156
+ custom_data=custom_data,
157
+ color_data=plddt_color_data,
158
+ tooltip_data=plddt_tooltip_data,
159
+ )
160
+ return data
161
+
162
+
163
+ assets_dir = Path(__file__).parent.parent / "assets"
164
+ cif_file = assets_dir / "seca_secb" / "fold_seca_secb_model_0.cif"
165
+ json_file = assets_dir / "seca_secb" / "fold_seca_secb_full_data_0.json"
166
+
167
+ custom_data_initial = {
168
+ "data": cif_file.read_bytes(),
169
+ "format": "cif",
170
+ "binary": False,
171
+ }
172
+
173
+
174
+ @solara.component
175
+ def Page():
176
+ color_data = solara.use_reactive(NO_COLOR_DATA)
177
+ tooltip_data = solara.use_reactive(NO_TOOLTIP_DATA)
178
+
179
+ color_mode = solara.use_reactive("chain")
180
+ dark_effective = solara.lab.use_dark_effective()
181
+
182
+ def on_color_mode(value: str):
183
+ color_mode.set(value)
184
+ if value == "chain":
185
+ color_data.set(NO_COLOR_DATA)
186
+ tooltip_data.set(NO_TOOLTIP_DATA)
187
+ else:
188
+ color_data.set(load_result.value.color_data)
189
+ tooltip_data.set(load_result.value.tooltip_data)
190
+
191
+ def set_result_index(value: int):
192
+ result_index.set(value)
193
+ load_result()
194
+
195
+ solara.Title("Solarafold result viewer")
196
+ with solara.AppBar():
197
+ solara.lab.ThemeToggle()
198
+
199
+ with solara.Sidebar():
200
+ solara.FileDrop(label="Upload zip file", on_file=file_info.set, lazy=True)
201
+ solara.Button(
202
+ label="Load result",
203
+ on_click=load_result,
204
+ block=True,
205
+ disabled=file_info.value is None,
206
+ )
207
+
208
+ if not load_result.not_called:
209
+ disabled = load_result.pending
210
+ solara.Select(
211
+ label="Result index",
212
+ value=result_index.value,
213
+ on_value=set_result_index,
214
+ values=list(range(5)),
215
+ disabled=disabled,
216
+ )
217
+ solara.Select(
218
+ label="Color mode",
219
+ values=["chain", "plddt"],
220
+ value=color_mode.value,
221
+ on_value=on_color_mode,
222
+ disabled=disabled,
223
+ )
224
+
225
+ def write_atoms():
226
+ return load_result.value.atom_data.write_csv()
227
+
228
+ solara.FileDownload(
229
+ write_atoms,
230
+ filename="NA" if disabled else f"{load_result.value.name}_atoms.csv",
231
+ children=[
232
+ solara.Button(
233
+ "Download atom plddt",
234
+ block=True,
235
+ disabled=disabled,
236
+ )
237
+ ],
238
+ )
239
+
240
+ solara.Div(style={"height": "20px"})
241
+
242
+ def write_residues():
243
+ return load_result.value.residue_data.write_csv()
244
+
245
+ solara.FileDownload(
246
+ write_residues,
247
+ filename="NA" if disabled else f"{load_result.value.name}_residues.csv",
248
+ children=[
249
+ solara.Button(
250
+ "Download residue plddt",
251
+ block=True,
252
+ disabled=disabled,
253
+ )
254
+ ],
255
+ )
256
+
257
+ if load_result.not_called:
258
+ solara.Text("Drop and drag an alphafold3 result .zip file to get started")
259
+ elif load_result.pending:
260
+ solara.ProgressLinear(load_result.pending)
261
+ elif load_result.finished:
262
+ fold_data: AlphaFoldData = load_result.value
263
+
264
+ with solara.Card():
265
+ theme = "dark" if dark_effective else "light"
266
+ PDBeMolstar.element(
267
+ height="calc(100vh - 150px)",
268
+ custom_data=fold_data.custom_data,
269
+ color_data=color_data.value,
270
+ tooltips=tooltip_data.value,
271
+ show_water=False,
272
+ theme=theme,
273
+ ).key(f"pdbemolstar-{dark_effective}")
274
+
275
+
276
+ @solara.component
277
+ def Layout(children):
278
+ dark_effective = solara.lab.use_dark_effective()
279
+ return solara.AppLayout(
280
+ children=children, toolbar_dark=dark_effective, color=None
281
+ ) # if dark_effective else "primary")
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ polars
2
+ solara
3
+ biopython
4
+ ipymolstar
5
+ polarify