ipymolstar-demo / app.py
Jhsmit's picture
initial commit
dd15d3e
import statistics
from dataclasses import asdict, dataclass
from io import StringIO
from pathlib import Path
import requests
import solara
import solara.lab
from Bio.PDB import Residue, Structure
from Bio.PDB.PDBParser import PDBParser
from ipymolstar.pdbemolstar import THEMES, PDBeMolstar
from matplotlib import colormaps
from matplotlib.colors import Normalize
from solara.alias import rv
parser = PDBParser(QUIET=True)
CHAIN_COLORS = [
"#1f77b4",
"#ff7f0e",
"#2ca02c",
"#d62728",
"#9467bd",
"#8c564b",
"#e377c2",
"#7f7f7f",
"#bcbd22",
"#17becf",
]
AMINO_ACIDS = [
"ALA",
"ARG",
"ASN",
"ASP",
"CYS",
"GLN",
"GLU",
"GLY",
"HIS",
"ILE",
"LEU",
"LYS",
"MET",
"PHE",
"PRO",
"PYL",
"SEC",
"SER",
"THR",
"TRP",
"TYR",
"VAL",
]
# use auth residue numbers or not
AUTH_RESIDUE_NUMBERS = {
"1QYN": False,
"2PE4": True,
}
pdb_ids = ["1QYN", "2PE4"]
def fetch_pdb(pdb_id) -> StringIO:
url = f"https://files.rcsb.org/download/{pdb_id}.pdb"
response = requests.get(url)
if response.status_code == 200:
sio = StringIO(response.text)
sio.seek(0)
return sio
else:
raise requests.HTTPError(f"Failed to download PDB file {pdb_id}")
structures = {p_id: parser.get_structure(p_id, fetch_pdb(p_id)) for p_id in pdb_ids}
@dataclass
class PDBeData:
molecule_id: str = "1qyn"
custom_data: dict | None = None
color_data: dict | None = None
bg_color: str = "#F7F7F7"
spin: bool = False
hide_polymer: bool = False
hide_water: bool = False
hide_heteroatoms: bool = False
hide_carbs: bool = False
hide_non_standard: bool = False
hide_coarse: bool = False
height: str = "700px"
visibility_cbs = ["polymer", "water", "heteroatoms", "carbs"]
def to_rgb(hex_color: str) -> dict:
return {
"r": int(hex_color[1:3], 16),
"g": int(hex_color[3:5], 16),
"b": int(hex_color[5:7], 16),
}
def color_chains(structure: Structure.Structure) -> dict:
data = [
{
"struct_asym_id": chain.id,
"color": to_rgb(hex_color),
}
for hex_color, chain in zip(CHAIN_COLORS, structure.get_chains())
]
color_data = {"data": data, "nonSelectedColor": None}
return color_data
def color_residues(structure: Structure.Structure, auth: bool = False) -> dict:
_, resn, _ = zip(
*[r.id for r in structure.get_residues() if r.get_resname() in AMINO_ACIDS]
)
rmin, rmax = min(resn), max(resn)
# todo check for off by one errors
norm = Normalize(vmin=rmin, vmax=rmax)
auth_str = "_auth" if auth else ""
cmap = colormaps["rainbow"]
data = []
for i in range(rmin, rmax):
r, g, b, a = cmap(norm(i), bytes=True)
color = {"r": int(r), "g": int(g), "b": int(b)}
elem = {
f"start{auth_str}_residue_number": i,
f"end{auth_str}_residue_number": i,
"color": color,
"focus": False,
}
data.append(elem)
color_data = {"data": data, "nonSelectedColor": None}
return color_data
def get_bfactor(residue: Residue.Residue):
"""returns the residue-average b-factor"""
return statistics.mean([atom.get_bfactor() for atom in residue])
def color_bfactor(structure: Structure.Structure, auth: bool = False) -> dict:
auth_str = "_auth" if auth else ""
value_data = []
for chain in structure.get_chains():
for r in chain.get_residues():
if r.get_resname() in AMINO_ACIDS:
bfactor = get_bfactor(r)
elem = {
f"start{auth_str}_residue_number": r.id[1],
f"end{auth_str}_residue_number": r.id[1],
"struct_asym_id": chain.id,
"value": bfactor,
}
value_data.append(elem)
all_values = [d["value"] for d in value_data]
vmin, vmax = min(all_values), max(all_values)
norm = Normalize(vmin=vmin, vmax=vmax)
cmap = colormaps["inferno"]
data = []
for v_elem in value_data:
elem = v_elem.copy()
r, g, b, a = cmap(norm(elem.pop("value")), bytes=True)
elem["color"] = {"r": int(r), "g": int(g), "b": int(b)}
data.append(elem)
color_data = {"data": data, "nonSelectedColor": None}
return color_data
def apply_coloring(pdb_id: str, color_mode: str):
structure = structures[pdb_id]
auth = AUTH_RESIDUE_NUMBERS[pdb_id]
if color_mode == "Chain":
color_data = color_chains(structure)
elif color_mode == "Residue":
color_data = color_residues(structure, auth)
elif color_mode == "β-factor":
color_data = color_bfactor(structure, auth)
return color_data
color_options = ["Chain", "Residue", "β-factor"]
color_data = apply_coloring(pdb_ids[0], color_options[0])
data = solara.Reactive(PDBeData(color_data=color_data))
molecule_store = {
"Glucose": dict(
url="https://pubchem.ncbi.nlm.nih.gov/rest/pug/conformers/000016A100000001/SDF?response_type=save&response_basename=Conformer3D_COMPOUND_CID_5793",
format="sdf",
binary=False,
),
"ATP": dict(
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",
format="sdf",
binary=False,
),
"Caffeine": dict(
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",
format="sdf",
binary=False,
),
"Strychnine": dict(
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 ",
format="sdf",
binary=False,
),
}
def download_pdb(pdb_id, fpath: Path):
url = f"https://files.rcsb.org/download/{pdb_id}.pdb"
response = requests.get(url)
if response.status_code == 200:
fpath.write_bytes(response.content)
return f"{pdb_id}.pdb"
else:
print("Failed to download PDB file")
return None
@solara.component
def ProteinView(dark_effective: bool):
with solara.Card("PDBeMol*"):
theme = "dark" if dark_effective else "light"
PDBeMolstar.element(**asdict(data.value), theme=theme)
@solara.component
def Page():
solara.Title("Solara App - PDBeMol*")
counter, set_counter = solara.use_state(0)
dark_effective = solara.lab.use_dark_effective()
dark_effective_previous = solara.use_previous(dark_effective)
structure_type = solara.use_reactive("Protein")
color_mode = solara.use_reactive(color_options[0])
protein_id = solara.use_reactive(pdb_ids[0])
molecule_key = solara.use_reactive(next(iter(molecule_store.keys())))
if dark_effective != dark_effective_previous:
if dark_effective:
data.update(bg_color=THEMES["dark"]["bg_color"])
else:
data.update(bg_color=THEMES["light"]["bg_color"])
def update_protein_id(value: str):
protein_id.set(value)
color_data = apply_coloring(protein_id.value, color_mode.value)
data.update(color_data=color_data, molecule_id=protein_id.value.lower())
def update_molecule_key(value: str):
molecule_key.set(value)
data.update(custom_data=molecule_store[molecule_key.value])
def update_structure_type(value: str):
structure_type.set(value)
if structure_type.value == "Protein":
color_data = apply_coloring(protein_id.value, color_mode.value)
data.update(color_data=color_data)
# currently there is a bug where coloring does not work anymore after switching from molecule back to protein
data.update(
color_data=color_data,
custom_data=None,
molecule_id=protein_id.value.lower(),
)
else:
color_data = {"data": [], "nonSelectedColor": None}
data.update(
molecule_id="",
custom_data=molecule_store[molecule_key.value],
color_data=color_data, # used to reset colors
)
def update_color_mode(value: str):
color_data = apply_coloring(protein_id.value, value)
color_mode.set(value)
print(id(color_data))
data.update(color_data=color_data)
with solara.AppBar():
solara.lab.ThemeToggle()
with solara.ColumnsResponsive([4, 8]):
with solara.Card("Controls"):
with solara.ToggleButtonsSingle(
value=structure_type.value,
on_value=update_structure_type,
classes=["d-flex", "flex-row"],
):
solara.Button(label="Protein", classes=["flex-grow-1"])
solara.Button(label="Molecule", classes=["flex-grow-1"])
solara.Div(style="height: 20px")
if structure_type.value == "Protein":
solara.Select(
label="PDB id",
value=protein_id.value,
values=pdb_ids,
on_value=update_protein_id,
)
solara.Select(
label="Color mode",
value=color_mode.value,
on_value=update_color_mode,
values=color_options,
)
else:
solara.Select(
label="Molecule",
value=molecule_key.value,
values=list(molecule_store.keys()),
on_value=update_molecule_key,
)
solara.Checkbox(
label="spin",
value=data.value.spin,
on_value=lambda x: data.update(spin=x),
)
for struc_elem in visibility_cbs:
attr = f"hide_{struc_elem}"
def on_value(x, attr=attr):
data.update(**{attr: x})
solara.Checkbox(
label=f"hide {struc_elem}",
value=getattr(data.value, attr),
on_value=on_value,
)
btn = solara.Button("background color", block=True)
with solara.lab.Menu(activator=btn, close_on_content_click=False):
rv.ColorPicker(
v_model=data.value.bg_color,
on_v_model=lambda x: data.update(bg_color=x),
)
solara.Div(style="height: 20px")
solara.Button(
"redraw", on_click=lambda: set_counter(counter + 1), block=True
)
# with solara.Card("Protein view"):
# PDBeMolstar.element(**asdict(data.value)).key(f"molstar-{counter}")
key = f"{counter}_{dark_effective}"
ProteinView(dark_effective).key(key)
@solara.component
def Layout(children):
dark_effective = solara.lab.use_dark_effective()
return solara.AppLayout(children=children, toolbar_dark=dark_effective, color=None)