Skip to content

Commit 1338480

Browse files
Prihoda, DavidCopilot
andauthored
Add plugin config, small UI improvements (#14)
* Add plugin config, small UI improvements * Fix workflow diff * Reformat * Add link to job to pools table, visualization tweaks * Return dict in refolding input * Update ovo/app/components/input_components.py Co-authored-by: Copilot <[email protected]> * Update ovo/app/pages/designs/explorer.py Co-authored-by: Copilot <[email protected]> * Update ovo/core/utils/formatting.py Co-authored-by: Copilot <[email protected]> * Fix param metadata loop --------- Co-authored-by: Copilot <[email protected]>
1 parent bfd0dce commit 1338480

File tree

17 files changed

+163
-90
lines changed

17 files changed

+163
-90
lines changed

ovo/app/components/input_components.py

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@
66
from ovo.app.components.molstar_custom_component import molstar_custom_component, StructureVisualization
77
from ovo.app.components.molstar_custom_component.dataclasses import ContigSegment
88
from ovo.core.database import WorkflowTypes, Workflow
9-
from ovo.core.utils.residue_selection import parse_selections
9+
from ovo.core.utils.residue_selection import parse_selections, from_segments_to_hotspots
1010
from ovo.core.utils.formatting import safe_filename
1111
from ovo.core.utils.pdb import get_pdb, filter_pdb_str
1212

1313
from ovo import storage
1414
from ovo.core.utils.pdb import add_glycan_to_pdb
1515

1616

17-
def initialize_workflow(page_key: str, workflow_name: str, include_subclasses=False) -> Workflow:
17+
def initialize_workflow(page_key: str, workflow_name: str, include_subclasses: bool = False) -> Workflow:
1818
"""Initialize a workflow in the session state based on dropdown with available workflow variants (subclasses).
1919
2020
:param page_key: Key identifying the workflow page, used as key in session_state.workflows
2121
:param workflow_name: Base name of the workflow (e.g. "rfdiffusion-end-to-end")
22-
22+
:param include_subclasses: Whether to let user choose from subclasses of the base workflow as variants
2323
"""
2424
variant_names = WorkflowTypes.get_subclass_names(workflow_name) if include_subclasses else [workflow_name]
2525
if len(variant_names) > 1:
@@ -47,8 +47,14 @@ def initialize_workflow(page_key: str, workflow_name: str, include_subclasses=Fa
4747

4848
def pdb_input_component(old_pdb_code: str | None) -> tuple[str, bytes] | None:
4949
st.write("Enter PDB code, UniProt ID or upload your own structure file")
50-
with st.columns(2)[0]:
51-
new_pdb_code = st.text_input("PDB code or UniProt ID", value=old_pdb_code, key="input_pdb_code")
50+
with st.container(horizontal=True, vertical_alignment="bottom"):
51+
new_pdb_code = st.text_input(
52+
"Download by ID",
53+
placeholder="Input PDB code or UniProt ID...",
54+
value=old_pdb_code,
55+
key="input_pdb_code",
56+
width=270,
57+
)
5258
st.button("Confirm")
5359

5460
uploaded_file = st.file_uploader("...or upload a PDB file", type=["pdb", "pdb1"], key="input_pdb_file")
@@ -72,9 +78,21 @@ def sequence_selection_fragment(
7278
input_name: str,
7379
color="chain-id",
7480
representation_type="cartoon+ball-and-stick",
81+
write_segments: bool = True,
7582
fixed_segments: list[ContigSegment] | None = None,
7683
**selection_kwargs,
7784
):
85+
"""Fragment to display input structure and allow residue selection via molstar component.
86+
87+
:param page_key: Key identifying the workflow page, typically __file__
88+
:param input_name: Name of the input structure, used for download filename
89+
:param color: Initial color scheme for molstar component
90+
:param representation_type: Initial representation type for molstar component
91+
:param write_segments: Whether to write selected segments (True) or individual residues (False)
92+
:param fixed_segments: If provided, only these segments will be shown in the structure (others hidden)
93+
:param selection_kwargs: Additional kwargs passed to workflow.get/set_selected_segments methods
94+
"""
95+
7896
workflow = st.session_state.workflows[page_key]
7997

8098
if page_key not in st.session_state.selection_history:
@@ -85,25 +103,11 @@ def sequence_selection_fragment(
85103

86104
selection_history = st.session_state.selection_history[page_key][input_name]
87105

88-
st.write(
89-
"Click residues in sequence or structure to add them to the selection. Shift+Click to select a residue range."
90-
)
91-
92-
force_reload = False
93-
if (
94-
st.button(":material/undo: Undo selection", disabled=len(selection_history) <= 1)
95-
and len(selection_history) >= 2
96-
):
97-
# Go one selection backwards
98-
selection_history.pop(-1)
99-
previous_value = selection_history.pop(-1)
100-
st.write(f"Setting selection back to: {', '.join(previous_value)}")
101-
workflow.set_selected_segments(previous_value, **selection_kwargs)
102-
force_reload = True
103-
104106
pdb_input_string = storage.read_file_str(workflow.get_input_pdb_path())
105107

106-
if fixed_segments and not st.checkbox("Show full input structure"):
108+
if fixed_segments and not st.toggle(
109+
"Show full input structure", help="By default, only the fixed input segments are shown."
110+
):
107111
pdb_input_string = filter_pdb_str(
108112
pdb_input_string,
109113
segments=[seg.value for seg in fixed_segments],
@@ -115,8 +119,9 @@ def sequence_selection_fragment(
115119
if st.toggle(
116120
"Show glycosylation sites",
117121
key="show_glycosylation_sites_input",
118-
help="The positions of glycans are strictly based on the 5eli NAG template; "
119-
"nonexistent links might be displayed.",
122+
help="Visualize glycosylation sites on the structure based on glycosylation motifs in the sequence (NXS, NXT, X = any amino acid except P). "
123+
"The positions of glycans are approximate, based on the 5eli NAG template, "
124+
"nonexistent bonds might be displayed.",
120125
):
121126
pdb_input_string, glycosylated_residues = add_glycan_to_pdb(pdb_input_string)
122127
if glycosylated_residues:
@@ -153,6 +158,23 @@ def sequence_selection_fragment(
153158
label_visibility="collapsed",
154159
)
155160

161+
with st.container(horizontal=True, vertical_alignment="center"):
162+
force_reload = False
163+
if (
164+
st.button(":material/undo: Undo selection", disabled=len(selection_history) <= 1)
165+
and len(selection_history) >= 2
166+
):
167+
# Go one selection backwards
168+
selection_history.pop(-1)
169+
previous_value = selection_history.pop(-1)
170+
workflow.set_selected_segments(previous_value, **selection_kwargs)
171+
force_reload = True
172+
173+
selection_container = st.empty()
174+
selection_container.write(
175+
"Click residues in sequence or structure to add them to the selection. Shift+Click to select a residue range."
176+
)
177+
156178
selected_str = molstar_custom_component(
157179
structures=[
158180
StructureVisualization(
@@ -186,4 +208,9 @@ def sequence_selection_fragment(
186208
if not force_reload and selection and (not selection_history or selection != selection_history[-1]):
187209
selection_history.append(selection)
188210

189-
st.write(f"Selected segments: {', '.join(selection)}")
211+
if selection:
212+
if write_segments:
213+
selection_container.write(f"Selected segments: {'/'.join(selection)}")
214+
else:
215+
residues = from_segments_to_hotspots(selection)
216+
selection_container.write(f"Selected residues: {residues}")

ovo/app/components/molstar_custom_component/contigs_parser.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -172,28 +172,29 @@ def parse_contigs_trb(self, contigs_trb: str | dict = "") -> list[ContigSegment]
172172

173173
contigs = "/0 ".join(newContigs)
174174

175-
reference_tuples = []
176175
if "complex_con_ref_pdb_idx" in trb_file:
177-
for tuple in trb_file["complex_con_ref_pdb_idx"]:
178-
newList = [tuple[0], tuple[1], False]
179-
reference_tuples.append(newList)
176+
ref_idx = trb_file["complex_con_ref_pdb_idx"]
180177
else:
181178
assert "con_ref_pdb_idx" in trb_file, "Could not find con_ref_pdb_idx in the .trb file"
182-
for tuple in trb_file["con_ref_pdb_idx"]:
183-
newList = [tuple[0], tuple[1], False]
184-
reference_tuples.append(newList)
179+
ref_idx = trb_file["con_ref_pdb_idx"]
185180

186-
# Do the same for the mapped tuples.
187-
mapped_tuples = []
188181
if "complex_con_hal_pdb_idx" in trb_file:
189-
for tuple in trb_file["complex_con_hal_pdb_idx"]:
190-
newList = [tuple[0], tuple[1], False]
191-
mapped_tuples.append(newList)
182+
hal_idx = trb_file["complex_con_hal_pdb_idx"]
192183
else:
193-
assert "con_hal_pdb_idx" in trb_file, "Could not find con_ref_pdb_idx in the .trb file"
194-
for tuple in trb_file["con_hal_pdb_idx"]:
195-
newList = [tuple[0], tuple[1], False]
196-
mapped_tuples.append(newList)
184+
assert "con_hal_pdb_idx" in trb_file, "Could not find con_hal_pdb_idx in the .trb file"
185+
hal_idx = trb_file["con_hal_pdb_idx"]
186+
187+
return self.parse_contigs_ref(contigs, ref_idx=ref_idx, hal_idx=hal_idx)
188+
189+
def parse_contigs_ref(self, contigs: str, ref_idx: list[tuple], hal_idx: list[tuple]):
190+
"""Parse the contigs and return segments with output numbering."""
191+
reference_tuples = []
192+
for tuple in ref_idx:
193+
reference_tuples.append([tuple[0], tuple[1], False])
194+
195+
mapped_tuples = []
196+
for tuple in hal_idx:
197+
mapped_tuples.append([tuple[0], tuple[1], False])
197198

198199
return self._parse_contigs_with_tuples(
199200
contigs=contigs, mapped_tuples=mapped_tuples, reference_tuples=reference_tuples, include_generated=True

ovo/app/components/navigation.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def project_round_selector(
226226
return (selected_round_ids, None) if allow_design_input else selected_round_ids
227227

228228

229-
def pool_selector_table(pools_table: pd.DataFrame) -> list[str]:
229+
def pool_selector_table(pools_table: pd.DataFrame, project_id: str) -> list[str]:
230230
key = "_".join(pools_table["ID"])
231231

232232
# Prepare table for data editor
@@ -248,6 +248,10 @@ def pool_selector_table(pools_table: pd.DataFrame) -> list[str]:
248248
default_selection = st.query_params.get("pool_ids").split(",") if st.query_params.get("pool_ids") else []
249249
st.session_state[default_selection_key] = default_selection
250250
pools_table.insert(0, "Selected", [pool_id in default_selection for pool_id in pools_table["ID"]])
251+
pools_table["Job ID"] = [
252+
f"./jobs?pool_ids={pool_id}&project_id={project_id}" if not pd.isna(job_id) else None
253+
for pool_id, job_id in pools_table.set_index("ID")["Job ID"].items()
254+
]
251255

252256
edited = st.data_editor(
253257
pools_table,
@@ -257,6 +261,7 @@ def pool_selector_table(pools_table: pd.DataFrame) -> list[str]:
257261
disabled=data_columns,
258262
column_config={
259263
"Selected": "",
264+
"Job ID": st.column_config.LinkColumn(display_text=r"Job ↑", disabled=True),
260265
},
261266
)
262267

ovo/app/components/submission_components.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ def pool_submission_inputs(page_key: str):
2525
rounds_by_id = get_or_create_project_rounds(project_id=st.session_state.project.id)
2626
round_ids = list(rounds_by_id.keys())
2727

28-
left, right, _ = st.columns([1, 1, 2], vertical_alignment="bottom")
29-
3028
if st.session_state.pool_inputs.get(page_key):
3129
round_id, pool_name, pool_description = st.session_state.pool_inputs[page_key]
3230
if round_id is None:
@@ -39,6 +37,7 @@ def pool_submission_inputs(page_key: str):
3937
round_id = st.session_state["new_round_id"]
4038
del st.session_state["new_round_id"]
4139

40+
left, right, _ = st.columns([1, 1, 2], vertical_alignment="bottom")
4241
with left:
4342
round_id = st.selectbox(
4443
"Project Round",

ovo/app/components/workflow_visualization_components.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from ovo import db, storage
66
from ovo.app.components.custom_elements import wrapped_columns
7+
from ovo.app.components.molstar_custom_component.dataclasses import ContigSegment
78
from ovo.core.database import (
89
Design,
910
descriptors,
@@ -203,6 +204,8 @@ def rfdiffusion_scaffold_design_visualization(design_id: str | None):
203204
elif prediction_descriptor.b_factor_value == "fractional_plddt":
204205
pdb_str = pdb_to_mmcif(prediction_pdb, "-", True, fractional_plddt=True)
205206
description += " (colored by pLDDT confidence)"
207+
else:
208+
pdb_str = prediction_pdb
206209

207210
molstar_custom_component(
208211
structures=[
@@ -718,6 +721,27 @@ def visualize_rfdiffusion_design_sequence(design_id: str):
718721
if workflow.rfdiffusion_params.inpaint_seq
719722
else []
720723
)
724+
visualize_scaffold_alignment(
725+
input_seq_by_resno=input_seq_by_resno,
726+
designed_sequences=designed_sequences,
727+
parsed_segments=parsed_segments,
728+
inpainted_positions=inpainted_positions,
729+
)
730+
731+
732+
def visualize_scaffold_alignment(
733+
input_seq_by_resno: dict[str, dict[str, str]],
734+
designed_sequences: dict[str, str],
735+
parsed_segments: list[ContigSegment],
736+
inpainted_positions: list[str],
737+
):
738+
"""Visualize alignment between input and designed sequences based on contig segments.
739+
740+
:param input_seq_by_resno: Mapping from chain ID -> residue number (as string) -> amino acid.
741+
:param designed_sequences: Mapping from chain ID -> designed sequence.
742+
:param parsed_segments: List of ContigSegment objects representing the contig segments.
743+
:param inpainted_positions: List of positions (e.g., 'A12') that were inpainted.
744+
"""
721745
html = [
722746
f'<div style="font-family: monospace; font-size: 14px; margin: 10px 2px; display: inline-block;">'
723747
f"&nbsp;&nbsp;&nbsp;&nbsp;Region<br />"

ovo/app/pages/bindcraft/bindcraft_binder_design.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def hotspots_step():
160160
st.error("Please provide an input structure in the input structure step.")
161161
return
162162

163-
sequence_selection_fragment(__file__, workflow.input_name, color="hydrophobicity")
163+
sequence_selection_fragment(__file__, workflow.input_name, color="hydrophobicity", write_segments=False)
164164

165165

166166
@st.fragment()

ovo/app/pages/designs/designs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
st.write("No pools created yet in this round")
4646
st.stop()
4747

48-
selected_pool_ids = pool_selector_table(pools_table)
48+
selected_pool_ids = pool_selector_table(pools_table, st.session_state.project.id)
4949

5050
views = {
5151
"🔵 Explorer": "ovo.app.pages.designs.explorer:explorer_fragment",

ovo/app/pages/designs/explorer.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
)
1010
from ovo.app.components.navigation import design_navigation_selector
1111
from ovo.app.utils.cached_db import get_cached_design, get_cached_pool, get_cached_design_job
12-
from ovo.core.database.models import Design, Pool, DesignWorkflow, WorkflowTypes
12+
from ovo.core.database.models import Design, Pool, DesignWorkflow, WorkflowTypes, UnknownWorkflow
1313
from ovo.app.utils.cached_db import get_cached_pools, get_cached_design_jobs
1414

1515

@@ -70,6 +70,12 @@ def design_visualization_fragment(selected_design_ids: list[str]):
7070
design_job = get_cached_design_job(pool.design_job_id) if pool.design_job_id else None
7171
WorkflowType = type(design_job.workflow) if design_job and design_job.workflow else DesignWorkflow
7272

73+
if design_job and design_job.workflow.is_instance(UnknownWorkflow):
74+
st.warning(
75+
f"Falling back to basic structure and sequence visualization, "
76+
f"failed to load workflow information: {design_job.workflow.error}"
77+
)
78+
7379
WorkflowType.visualize_single_design_structures(design_id)
7480

7581
st.markdown("### Sequence")

ovo/app/pages/rfdiffusion/binder_design.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ def hotspots_step():
235235
st.error("Please provide an input structure in the input structure step.")
236236
return
237237

238-
sequence_selection_fragment(__file__, workflow.input_name, color="hydrophobicity")
238+
sequence_selection_fragment(__file__, workflow.input_name, color="hydrophobicity", write_segments=False)
239239

240240

241241
@st.fragment()

ovo/app/pages/rfdiffusion/binder_diversification.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,9 @@ def selection_step():
339339
)
340340
return
341341

342-
sequence_selection_fragment(__file__, workflow.input_name, contig_index=0, partial_diffusion=True)
342+
sequence_selection_fragment(
343+
__file__, workflow.input_name, write_segments=False, contig_index=0, partial_diffusion=True
344+
)
343345

344346

345347
@st.fragment()

0 commit comments

Comments
 (0)