remove cap
Browse files
app.py
CHANGED
|
@@ -16,7 +16,6 @@ from transformers.models.esmfold2.protein_utils import output_to_pdb, prepare_pr
|
|
| 16 |
# reasonable on a shared ZeroGPU slot. Swap for "biohub/ESMFold2" for MSA-conditioned
|
| 17 |
# accuracy on hard targets (slower, and needs an MSA pipeline this app doesn't provide).
|
| 18 |
MODEL_NAME = "biohub/ESMFold2-Fast"
|
| 19 |
-
MAX_TOTAL_RESIDUES = 600 # keeps inference within the ZeroGPU time/memory budget
|
| 20 |
MAX_ENTITY_ROWS = 8
|
| 21 |
VALID_AMINO_ACIDS = set("ACDEFGHIKLMNPQRSTVWYXBZUO")
|
| 22 |
ENTITY_TYPES = ["protein", "dna", "rna"]
|
|
@@ -118,8 +117,44 @@ def prepare_multi_chain_protein_features(sequences: list[str], device) -> dict[s
|
|
| 118 |
return {k: v.to(device) for k, v in combined.items()}
|
| 119 |
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
@spaces.GPU(duration=180)
|
| 122 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
chain_sequences: list[str] = []
|
| 124 |
for entity in entities:
|
| 125 |
sequence = clean_sequence(entity.get("sequence", ""))
|
|
@@ -140,12 +175,6 @@ def fold_entities(entities: list[dict], num_loops: int, num_sampling_steps: int,
|
|
| 140 |
raise gr.Error("Please enter at least one protein sequence.")
|
| 141 |
|
| 142 |
total_residues = sum(len(s) for s in chain_sequences)
|
| 143 |
-
if total_residues > MAX_TOTAL_RESIDUES:
|
| 144 |
-
raise gr.Error(
|
| 145 |
-
f"Total length is {total_residues} residues across {len(chain_sequences)} chain(s); this "
|
| 146 |
-
f"Space is limited to {MAX_TOTAL_RESIDUES} residues to keep inference within the ZeroGPU "
|
| 147 |
-
"time budget."
|
| 148 |
-
)
|
| 149 |
|
| 150 |
progress(0.1, desc="Moving model to GPU...")
|
| 151 |
model.to("cuda")
|
|
@@ -186,28 +215,6 @@ def fold_entities(entities: list[dict], num_loops: int, num_sampling_steps: int,
|
|
| 186 |
return viewer_html, " \n".join(summary_lines), pdb_path
|
| 187 |
|
| 188 |
|
| 189 |
-
def add_entity(entities: list[dict]) -> list[dict]:
|
| 190 |
-
if len(entities) >= MAX_ENTITY_ROWS:
|
| 191 |
-
return entities
|
| 192 |
-
return entities + [dict(DEFAULT_ENTITY)]
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
def remove_entity(entities: list[dict], idx: int) -> list[dict]:
|
| 196 |
-
if len(entities) <= 1:
|
| 197 |
-
return entities
|
| 198 |
-
return [e for i, e in enumerate(entities) if i != idx]
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
def update_entity_field(entities: list[dict], idx: int, field: str, value) -> list[dict]:
|
| 202 |
-
entities = [dict(e) for e in entities]
|
| 203 |
-
entities[idx][field] = value
|
| 204 |
-
return entities
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
def load_example(sequence: str) -> list[dict]:
|
| 208 |
-
return [{"type": "protein", "copies": 1, "sequence": sequence}]
|
| 209 |
-
|
| 210 |
-
|
| 211 |
with gr.Blocks(title="Foldy — ESMFold2 Protein Structure Prediction") as demo:
|
| 212 |
gr.Markdown(
|
| 213 |
f"""
|
|
@@ -226,48 +233,10 @@ with gr.Blocks(title="Foldy — ESMFold2 Protein Structure Prediction") as demo:
|
|
| 226 |
example1_btn = gr.Button("Load: Ubiquitin", size="sm")
|
| 227 |
example2_btn = gr.Button("Load: CAP protein", size="sm")
|
| 228 |
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
etype = gr.Dropdown(ENTITY_TYPES, value=entity["type"], label="Type", scale=2)
|
| 234 |
-
copies = gr.Number(
|
| 235 |
-
value=entity["copies"], label="Copies", precision=0, minimum=1, scale=1
|
| 236 |
-
)
|
| 237 |
-
seq = gr.Textbox(
|
| 238 |
-
value=entity["sequence"],
|
| 239 |
-
label="Sequence",
|
| 240 |
-
lines=4,
|
| 241 |
-
max_lines=4,
|
| 242 |
-
scale=6,
|
| 243 |
-
placeholder="Amino acid sequence...",
|
| 244 |
-
)
|
| 245 |
-
remove_btn = gr.Button("×", scale=0, min_width=40, visible=len(entities) > 1)
|
| 246 |
-
|
| 247 |
-
etype.change(
|
| 248 |
-
lambda val, ents, i=idx: update_entity_field(ents, i, "type", val),
|
| 249 |
-
inputs=[etype, entities_state],
|
| 250 |
-
outputs=entities_state,
|
| 251 |
-
)
|
| 252 |
-
copies.input(
|
| 253 |
-
lambda val, ents, i=idx: update_entity_field(ents, i, "copies", int(val or 1)),
|
| 254 |
-
inputs=[copies, entities_state],
|
| 255 |
-
outputs=entities_state,
|
| 256 |
-
)
|
| 257 |
-
seq.change(
|
| 258 |
-
lambda val, ents, i=idx: update_entity_field(ents, i, "sequence", val),
|
| 259 |
-
inputs=[seq, entities_state],
|
| 260 |
-
outputs=entities_state,
|
| 261 |
-
)
|
| 262 |
-
remove_btn.click(
|
| 263 |
-
lambda ents, i=idx: remove_entity(ents, i),
|
| 264 |
-
inputs=entities_state,
|
| 265 |
-
outputs=entities_state,
|
| 266 |
-
)
|
| 267 |
-
|
| 268 |
-
add_btn.click(add_entity, inputs=entities_state, outputs=entities_state)
|
| 269 |
-
example1_btn.click(lambda: load_example(EXAMPLE_UBIQUITIN), outputs=entities_state)
|
| 270 |
-
example2_btn.click(lambda: load_example(EXAMPLE_CAP), outputs=entities_state)
|
| 271 |
|
| 272 |
with gr.Accordion("Advanced settings", open=False):
|
| 273 |
num_loops = gr.Slider(1, 3, value=1, step=1, label="Recycling loops")
|
|
@@ -281,11 +250,58 @@ with gr.Blocks(title="Foldy — ESMFold2 Protein Structure Prediction") as demo:
|
|
| 281 |
with gr.Column(scale=2):
|
| 282 |
structure_output = gr.HTML(label="Predicted structure")
|
| 283 |
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
if __name__ == "__main__":
|
| 291 |
demo.queue(max_size=20).launch(theme=gr.themes.Soft())
|
|
|
|
| 16 |
# reasonable on a shared ZeroGPU slot. Swap for "biohub/ESMFold2" for MSA-conditioned
|
| 17 |
# accuracy on hard targets (slower, and needs an MSA pipeline this app doesn't provide).
|
| 18 |
MODEL_NAME = "biohub/ESMFold2-Fast"
|
|
|
|
| 19 |
MAX_ENTITY_ROWS = 8
|
| 20 |
VALID_AMINO_ACIDS = set("ACDEFGHIKLMNPQRSTVWYXBZUO")
|
| 21 |
ENTITY_TYPES = ["protein", "dna", "rna"]
|
|
|
|
| 117 |
return {k: v.to(device) for k, v in combined.items()}
|
| 118 |
|
| 119 |
|
| 120 |
+
def harvest_entities(etypes: list[str], copiess: list, seqs: list[str]) -> list[dict]:
|
| 121 |
+
"""Read the currently displayed value of every row's components directly.
|
| 122 |
+
|
| 123 |
+
Used instead of syncing a ``gr.State`` copy on every keystroke, so editing a field never
|
| 124 |
+
depends on a change/input event having already round-tripped to the server — the value the
|
| 125 |
+
user sees is always exactly the value Add/Remove/Fold act on.
|
| 126 |
+
"""
|
| 127 |
+
return [
|
| 128 |
+
{"type": t, "copies": max(1, int(c or 1)), "sequence": s or ""}
|
| 129 |
+
for t, c, s in zip(etypes, copiess, seqs)
|
| 130 |
+
]
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def add_entity_live(etypes: list[str], copiess: list, seqs: list[str]) -> list[dict]:
|
| 134 |
+
entities = harvest_entities(etypes, copiess, seqs)
|
| 135 |
+
if len(entities) < MAX_ENTITY_ROWS:
|
| 136 |
+
entities.append(dict(DEFAULT_ENTITY))
|
| 137 |
+
return entities
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def remove_entity_live(etypes: list[str], copiess: list, seqs: list[str], remove_idx: int) -> list[dict]:
|
| 141 |
+
entities = harvest_entities(etypes, copiess, seqs)
|
| 142 |
+
if len(entities) <= 1:
|
| 143 |
+
return entities
|
| 144 |
+
return [e for i, e in enumerate(entities) if i != remove_idx]
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def load_example(sequence: str) -> list[dict]:
|
| 148 |
+
return [{"type": "protein", "copies": 1, "sequence": sequence}]
|
| 149 |
+
|
| 150 |
+
|
| 151 |
@spaces.GPU(duration=180)
|
| 152 |
+
def fold_from_rows(*args, progress=gr.Progress()):
|
| 153 |
+
*row_values, num_loops, num_sampling_steps = args
|
| 154 |
+
num_rows = len(row_values) // 3
|
| 155 |
+
etypes, copiess, seqs = row_values[:num_rows], row_values[num_rows : 2 * num_rows], row_values[2 * num_rows :]
|
| 156 |
+
entities = harvest_entities(etypes, copiess, seqs)
|
| 157 |
+
|
| 158 |
chain_sequences: list[str] = []
|
| 159 |
for entity in entities:
|
| 160 |
sequence = clean_sequence(entity.get("sequence", ""))
|
|
|
|
| 175 |
raise gr.Error("Please enter at least one protein sequence.")
|
| 176 |
|
| 177 |
total_residues = sum(len(s) for s in chain_sequences)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
progress(0.1, desc="Moving model to GPU...")
|
| 180 |
model.to("cuda")
|
|
|
|
| 215 |
return viewer_html, " \n".join(summary_lines), pdb_path
|
| 216 |
|
| 217 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
with gr.Blocks(title="Foldy — ESMFold2 Protein Structure Prediction") as demo:
|
| 219 |
gr.Markdown(
|
| 220 |
f"""
|
|
|
|
| 233 |
example1_btn = gr.Button("Load: Ubiquitin", size="sm")
|
| 234 |
example2_btn = gr.Button("Load: CAP protein", size="sm")
|
| 235 |
|
| 236 |
+
# Empty placeholder, positioned here (above the settings/fold/download components
|
| 237 |
+
# below); filled in further down, once fold_btn etc. exist for render_entities to
|
| 238 |
+
# close over — see the ``with rows_container:`` block after this gr.Row().
|
| 239 |
+
rows_container = gr.Column()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
with gr.Accordion("Advanced settings", open=False):
|
| 242 |
num_loops = gr.Slider(1, 3, value=1, step=1, label="Recycling loops")
|
|
|
|
| 250 |
with gr.Column(scale=2):
|
| 251 |
structure_output = gr.HTML(label="Predicted structure")
|
| 252 |
|
| 253 |
+
example1_btn.click(lambda: load_example(EXAMPLE_UBIQUITIN), outputs=entities_state)
|
| 254 |
+
example2_btn.click(lambda: load_example(EXAMPLE_CAP), outputs=entities_state)
|
| 255 |
+
|
| 256 |
+
with rows_container:
|
| 257 |
+
|
| 258 |
+
@gr.render(inputs=entities_state)
|
| 259 |
+
def render_entities(entities):
|
| 260 |
+
etype_boxes, copies_boxes, seq_boxes, remove_btns = [], [], [], []
|
| 261 |
+
for idx, entity in enumerate(entities):
|
| 262 |
+
with gr.Row():
|
| 263 |
+
etype = gr.Dropdown(ENTITY_TYPES, value=entity["type"], label="Type", scale=2)
|
| 264 |
+
copies = gr.Number(
|
| 265 |
+
value=entity["copies"],
|
| 266 |
+
label="Copies",
|
| 267 |
+
precision=0,
|
| 268 |
+
minimum=1,
|
| 269 |
+
step=1,
|
| 270 |
+
scale=1,
|
| 271 |
+
)
|
| 272 |
+
seq = gr.Textbox(
|
| 273 |
+
value=entity["sequence"],
|
| 274 |
+
label="Sequence",
|
| 275 |
+
lines=4,
|
| 276 |
+
max_lines=4,
|
| 277 |
+
scale=6,
|
| 278 |
+
placeholder="Amino acid sequence...",
|
| 279 |
+
)
|
| 280 |
+
remove_btn = gr.Button("×", scale=0, min_width=40, visible=len(entities) > 1)
|
| 281 |
+
etype_boxes.append(etype)
|
| 282 |
+
copies_boxes.append(copies)
|
| 283 |
+
seq_boxes.append(seq)
|
| 284 |
+
remove_btns.append(remove_btn)
|
| 285 |
+
|
| 286 |
+
# No change/input handlers on the row fields themselves: Add/Remove/Fold all read
|
| 287 |
+
# the live, currently-displayed value of every row directly (below), so nothing
|
| 288 |
+
# depends on a change event having already round-tripped to the server first.
|
| 289 |
+
live_inputs = [*etype_boxes, *copies_boxes, *seq_boxes]
|
| 290 |
+
|
| 291 |
+
add_btn.click(add_entity_live, inputs=live_inputs, outputs=entities_state)
|
| 292 |
+
for idx, remove_btn in enumerate(remove_btns):
|
| 293 |
+
remove_btn.click(
|
| 294 |
+
lambda *vals, i=idx, n=len(entities): remove_entity_live(
|
| 295 |
+
vals[:n], vals[n : 2 * n], vals[2 * n :], i
|
| 296 |
+
),
|
| 297 |
+
inputs=live_inputs,
|
| 298 |
+
outputs=entities_state,
|
| 299 |
+
)
|
| 300 |
+
fold_btn.click(
|
| 301 |
+
fn=fold_from_rows,
|
| 302 |
+
inputs=[*live_inputs, num_loops, num_sampling_steps],
|
| 303 |
+
outputs=[structure_output, summary_output, pdb_output],
|
| 304 |
+
)
|
| 305 |
|
| 306 |
if __name__ == "__main__":
|
| 307 |
demo.queue(max_size=20).launch(theme=gr.themes.Soft())
|