Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b6c7e80
add geometry_operations.py and trimesh
3dot141592 Apr 21, 2026
1b3f70a
run black
3dot141592 Apr 21, 2026
ea4f913
LLM generated: Polyhedron visualizer
3dot141592 Apr 24, 2026
ff92441
Render amino acid spheres
3dot141592 May 6, 2026
ccb66da
feat: introduce parsing of _chem_comp table in cif-files
tE3m May 10, 2026
27a3c86
Add descriptions and rename
3dot141592 May 11, 2026
84857a5
WIP: fdw distance
3dot141592 May 11, 2026
9cf5e41
Add default step for ptm validation
Elena-kal May 11, 2026
7be7535
add functions to obtain ptms with all their atoms and their positions…
Elena-kal May 11, 2026
ec7e476
add tests for the ptm helper functions
Elena-kal May 11, 2026
c211ff9
format with black
Elena-kal May 11, 2026
6368442
Merge branch 'crosslinking' into molstar-viewer-adapter
Elena-kal May 14, 2026
d495cd3
Merge branch 'crosslinking' into molstar-viewer-adapter
Elena-kal May 14, 2026
d3e6085
finish vdw distance
3dot141592 May 16, 2026
5356af6
Merge branch 'molstar-viewer-adapter' into 407-get-all-ptms-with-coor…
3dot141592 May 17, 2026
eb764a2
Merge pull request #422 from cschlaffner/407-get-all-ptms-with-coordi…
3dot141592 May 17, 2026
a46eece
ptm bubble implementation
3dot141592 May 21, 2026
bcf9a03
PTM amino acid collision
3dot141592 May 26, 2026
fdb5a70
black
3dot141592 May 26, 2026
ba08b52
Add PTM collision output to validation step
3dot141592 May 28, 2026
3df865f
PR: Use CIF column constants
3dot141592 Jun 15, 2026
703bc29
PR: Code style
3dot141592 Jun 15, 2026
f174295
PR: rename ptm_validation_df to ptm_collisions_df
3dot141592 Jun 15, 2026
ae2e5e0
PR: warning when no PTMs found
3dot141592 Jun 15, 2026
29f1814
Merge crosslinking
3dot141592 Jun 15, 2026
bf09aaa
Add docstrings
3dot141592 Jun 15, 2026
6b492b1
fix tests
3dot141592 Jun 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions backend/main/views_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import pandas as pd

from backend.protzilla.constants.paths import SETTINGS_PATH
from backend.protzilla.data_analysis.amino_acid_spheres import (
calculate_amino_acid_spheres,
calculate_ptm_spheres,
)
from backend.protzilla.disk_operator import YamlOperator
from backend.protzilla.steps import Step
from backend.protzilla.step_manager import StepManager
Expand Down Expand Up @@ -211,6 +215,8 @@ def create_visualization(
cif_df: pd.DataFrame,
structure_entry_id: str,
crosslinking_df: pd.DataFrame | None = None,
include_ptm_spheres: bool = False,
ignored_neighbors: int = 0,
) -> dict:
"""
Create visualization data, by packaging a mmCIF string (converted from a CIF DataFrame) with its structure entry ID.
Expand All @@ -233,6 +239,14 @@ def create_visualization(

result = {"structureEntryId": structure_entry_id, "cifString": cif_string}

if include_ptm_spheres:
result["trimeshMeshes"] = calculate_amino_acid_spheres(
cif_df,
only_intersecting_ptms=True,
ignored_neighbors=int(ignored_neighbors),
)
result["trimeshMeshes"].extend(calculate_ptm_spheres(cif_df))

if crosslinking_df is not None:
result["crosslinks"] = extract_relevant_crosslink_information(crosslinking_df)

Expand Down
1 change: 1 addition & 0 deletions backend/protzilla/all_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
data_analysis.PTMDetailsVisualization,
data_analysis.CrosslinkingValidationWithAngstromDeviation,
data_analysis.CrosslinkingValidationWithAngstromDeviationForMultimer,
data_analysis.PtmValidation,
data_preprocessing.ImputationByMinPerSample,
data_integration.EnrichmentAnalysisGOAnalysisWithString,
data_integration.EnrichmentAnalysisGOAnalysisWithEnrichr,
Expand Down
1 change: 1 addition & 0 deletions backend/protzilla/constants/cif_columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class ATOM_SITE_COLUMNS(StrEnum):
AUTH_ASYM_ID = f"{ATOM_SITE_PREFIX}auth_asym_id"
AUTH_ATOM_ID = f"{ATOM_SITE_PREFIX}auth_atom_id"
PDBX_PDB_MODEL_NUM = f"{ATOM_SITE_PREFIX}pdbx_PDB_model_num"
PDBX_SIFTS_XREF_DB_ACC = f"{ATOM_SITE_PREFIX}pdbx_sifts_xref_db_acc"


ATOM_SITE_LABEL_COMP_ID = ATOM_SITE_COLUMNS.LABEL_COMP_ID
Expand Down
123 changes: 123 additions & 0 deletions backend/protzilla/constants/van_der_waals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
vdw_radii = {
"""
Van der Waals radius in Ångström
"""
"H": 1.20,
"He": 1.43,
"Li": 2.12,
"Be": 1.98,
"B": 1.91,
"C": 1.77,
"N": 1.66,
"O": 1.50,
"F": 1.46,
"Ne": 1.58,
"Na": 2.50,
"Mg": 2.51,
"Al": 2.25,
"Si": 2.19,
"P": 1.90,
"S": 1.89,
"Cl": 1.82,
"Ar": 1.83,
"K": 2.73,
"Ca": 2.62,
"Sc": 2.58,
"Ti": 2.46,
"V": 2.42,
"Cr": 2.45,
"Mn": 2.45,
"Fe": 2.44,
"Co": 2.40,
"Ni": 2.40,
"Cu": 2.38,
"Zn": 2.39,
"Ga": 2.32,
"Ge": 2.29,
"As": 1.88,
"Se": 1.82,
"Br": 1.86,
"Kr": 2.25,
"Rb": 3.21,
"Sr": 2.84,
"Y": 2.75,
"Zr": 2.52,
"Nb": 2.56,
"Mo": 2.45,
"Tc": 2.44,
"Ru": 2.46,
"Rh": 2.44,
"Pd": 2.15,
"Ag": 2.53,
"Cd": 2.49,
"In": 2.43,
"Sn": 2.42,
"Sb": 2.47,
"Te": 1.99,
"I": 2.04,
"Xe": 2.06,
"Cs": 3.48,
"Ba": 3.03,
"La": 2.98,
"Ce": 2.88,
"Pr": 2.92,
"Nd": 2.95,
"Pm": None,
"Sm": 2.90,
"Eu": 2.87,
"Gd": 2.83,
"Tb": 2.79,
"Dy": 2.87,
"Ho": 2.81,
"Er": 2.83,
"Tm": 2.79,
"Yb": 2.80,
"Lu": 2.74,
"Hf": 2.63,
"Ta": 2.53,
"W": 2.57,
"Re": 2.49,
"Os": 2.48,
"Ir": 2.41,
"Pt": 2.29,
"Au": 2.32,
"Hg": 2.45,
"Tl": 2.47,
"Pb": 2.60,
"Bi": 2.54,
"Po": None,
"At": None,
"Rn": None,
"Fr": None,
"Ra": None,
"Ac": 2.80,
"Th": 2.93,
"Pa": 2.88,
"U": 2.71,
"Np": 2.82,
"Pu": 2.81,
"Am": 2.83,
"Cm": 3.05,
"Bk": 3.40,
"Cf": 3.05,
"Es": 2.70,
"Fm": None,
"Md": None,
"No": None,
"Lr": None,
"Rf": None,
"Db": None,
"Sg": None,
"Bh": None,
"Hs": None,
"Mt": None,
"Ds": None,
"Rg": None,
"Cn": None,
"Nh": None,
"Fl": None,
"Mc": None,
"Lv": None,
"Ts": None,
"Og": None,
}
194 changes: 194 additions & 0 deletions backend/protzilla/data_analysis/amino_acid_spheres.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
from __future__ import annotations

import numpy as np
import pandas as pd
import trimesh

from backend.protzilla.constants.cif_columns import (
ATOM_SITE_COLUMNS,
CHEM_COMP_COLUMNS,
)
from backend.protzilla.data_analysis.geometry_operations import (
calculate_center_point,
extract_points_from_cif,
find_farthest_point_vdw,
find_intersecting_spheres,
mesh_to_polyhedron,
resolve_chain_column,
)
from backend.protzilla.utilities.ptm_helper import (
get_all_ptm_atoms_with_coordinates,
get_center_points_and_radius_for_each_ptm,
)


def _calculate_residue_spheres(
cif_df: pd.DataFrame, chain_id: str | None = None
) -> list[dict]:
residue_df = cif_df
chain_column = resolve_chain_column(cif_df)
if chain_id is not None and chain_column is not None:
residue_df = cif_df[cif_df[chain_column] == chain_id]

residue_positions = (
pd.to_numeric(residue_df[ATOM_SITE_COLUMNS.LABEL_SEQ_ID], errors="coerce")
.dropna()
.astype(int)
.drop_duplicates()
.sort_values()
)

spheres = []
for residue_position in residue_positions:
residue_points, residue_elements = extract_points_from_cif(
cif_df,
residue_range=(residue_position, residue_position),
chain_id=chain_id,
)
center = calculate_center_point(residue_points)
_, radius = find_farthest_point_vdw(residue_points, residue_elements, center)

spheres.append(
{
"chain": chain_id,
"position": residue_position,
"center": center.tolist(),
"radius": float(radius),
}
)

return spheres


def calculate_amino_acid_spheres(
cif_df: pd.DataFrame,
chain_id: str | None = None,
color: int = 0xFF8C00,
alpha: float = 0.25,
subdivisions: int = 1,
only_intersecting_ptms: bool = False,
ignored_neighbors: int = 0,
) -> list[dict]:
if only_intersecting_ptms:
residue_spheres = []
seen_residues = set()
for collision in find_ptm_amino_acid_sphere_collisions(
cif_df, ignored_neighbors=ignored_neighbors, chain_id=chain_id
):
for residue_sphere in collision["collisions"]:
residue_key = (residue_sphere["chain"], residue_sphere["position"])
if residue_key not in seen_residues:
seen_residues.add(residue_key)
residue_spheres.append(residue_sphere)
else:
residue_spheres = _calculate_residue_spheres(cif_df, chain_id)

spheres = []
for residue_sphere in residue_spheres:
sphere = trimesh.creation.icosphere(
subdivisions=subdivisions, radius=residue_sphere["radius"]
)
sphere.apply_translation(residue_sphere["center"])

spheres.append(
{
"label": f"Residue {residue_sphere['position']} sphere",
"mesh": mesh_to_polyhedron(sphere),
"color": color,
"alpha": alpha,
}
)

return spheres


def calculate_ptm_spheres(
cif_df: pd.DataFrame,
color: int = 0x00A6A6,
alpha: float = 0.35,
subdivisions: int = 1,
) -> list[dict]:
if CHEM_COMP_COLUMNS.MON_NSTD_FLAG not in cif_df.columns:
return []

ptms = get_center_points_and_radius_for_each_ptm(
get_all_ptm_atoms_with_coordinates(cif_df)
)

spheres = []
for ptm in ptms:
center = np.array(ptm["center_point"], dtype=float)
radius = float(ptm["radius"])

sphere = trimesh.creation.icosphere(subdivisions=subdivisions, radius=radius)
sphere.apply_translation(center)

spheres.append(
{
"label": f"PTM {ptm['ptm_name']} {ptm['chain']}:{ptm['position']} sphere",
"mesh": mesh_to_polyhedron(sphere),
"color": color,
"alpha": alpha,
}
)

return spheres


def find_ptm_amino_acid_sphere_collisions(
cif_df: pd.DataFrame,
ignored_neighbors: int = 0,
chain_id: str | None = None,
) -> list[dict]:
if CHEM_COMP_COLUMNS.MON_NSTD_FLAG not in cif_df.columns:
return []

amino_acid_df = cif_df[cif_df[CHEM_COMP_COLUMNS.MON_NSTD_FLAG] == True]
chain_column = resolve_chain_column(amino_acid_df)
if chain_id is not None:
chains = [chain_id]
elif chain_column is not None:
chains = amino_acid_df[chain_column].dropna().drop_duplicates()
else:
chains = [None]
amino_acid_spheres = [
sphere
for chain in chains
for sphere in _calculate_residue_spheres(amino_acid_df, chain)
]

ptms = get_center_points_and_radius_for_each_ptm(
get_all_ptm_atoms_with_coordinates(cif_df)
)
if chain_id is not None:
ptms = [ptm for ptm in ptms if ptm["chain"] == chain_id]

collisions = []
for ptm in ptms:
ptm_sphere = {
"ptm_name": ptm["ptm_name"],
"chain": ptm["chain"],
"position": int(ptm["position"]),
"center": ptm["center_point"],
"radius": float(ptm["radius"]),
}
checked_spheres = [
sphere
for sphere in amino_acid_spheres
if not (
sphere["chain"] == ptm_sphere["chain"]
and 0
< abs(sphere["position"] - ptm_sphere["position"])
<= ignored_neighbors
)
]
intersecting_spheres = find_intersecting_spheres(checked_spheres, ptm_sphere)

collisions.append(
{
**ptm_sphere,
"collisions": intersecting_spheres,
}
)

return collisions
Loading
Loading