Skip to content

Commit

Permalink
Merge pull request #141 from luk27official/ph-no-vis
Browse files Browse the repository at this point in the history
Update
  • Loading branch information
skodapetr authored Mar 29, 2024
2 parents 8b40aee + 3d437f6 commit 7663403
Show file tree
Hide file tree
Showing 16 changed files with 2,515 additions and 802 deletions.
4 changes: 1 addition & 3 deletions documentation/prankweb.open-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ paths:
schema:
$ref: '#/components/schemas/TaskId'
requestBody:
description: The request needs to contain the hash and the pocket number.
description: The request needs to contain the hash of the docking task.
required: true
content:
application/json:
Expand Down Expand Up @@ -228,8 +228,6 @@ components:
properties:
hash:
type: string
pocket:
type: number
DockingResponse:
type: array
items:
Expand Down
2 changes: 1 addition & 1 deletion executor-docking/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ RUN sed -i 's/^ parser = MMCIFParser(filename, modelsAs=modelsAs)$/

# Install dodo (docking in docker)
# https://github.com/kiarka7/DODO
RUN wget -q https://raw.githubusercontent.com/kiarka7/DODO/28d8a35611a6086607f6abc1928a44961cb711ac/run_docking.py -O /opt/executor-docking/run_docking.py
RUN wget -q https://raw.githubusercontent.com/kiarka7/DODO/9d77b3b1f03a8b656eec5f5ae465ac9f2521965e/run_docking.py -O /opt/executor-docking/run_docking.py

COPY --chown=user:user ./executor-docking/requirements.txt ./
RUN pip3 install -r requirements.txt
Expand Down
3 changes: 2 additions & 1 deletion executor-docking/run_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def prepare_docking(input_file: str, structure_file_gzip: str, task_directory: s
ligandFile = os.path.join(task_directory, "ligand.smi")
with open(input_file) as inp, open(ligandFile, "w") as f:
input_json = json.load(inp)
f.write(input_json["hash"])
f.write(input_json["smiles"])

# prepare the input file
new_input_file = os.path.join(task_directory, "docking_parameters.json")
Expand All @@ -91,6 +91,7 @@ def prepare_docking(input_file: str, structure_file_gzip: str, task_directory: s
out_json["output"] = os.path.join(task_directory, "public", "out_vina.pdbqt")
out_json["center"] = input_json["bounding_box"]["center"]
out_json["size"] = input_json["bounding_box"]["size"]
out_json["exhaustiveness"] = input_json["exhaustiveness"]

json.dump(out_json, out)

Expand Down
8 changes: 5 additions & 3 deletions frontend/client/custom-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,11 @@ export interface ServerTaskInfo { // info about the task returned from the serve
lastChange: string;
status: string;
initialData: {
hash: string; //hash of the data
pocket: string; //pocket id
[key: string]: any; //other data
hash: string; //hash of the data
pocket: string; //pocket id
smiles: string; //SMILES representation of the ligand
exhaustiveness: string; //exhaustiveness value (for Autodock Vina)
[key: string]: any; //other data
}; //initial data
responseData: any; //response data
}
Expand Down
51 changes: 32 additions & 19 deletions frontend/client/tasks/server-docking-task.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { PocketData, Point3D, ServerTaskInfo, ServerTaskLocalStorageData } from

import { getPocketAtomCoordinates } from "../viewer/molstar-visualise";
import { PluginUIContext } from "molstar/lib/mol-plugin-ui/context";
import { md5 } from 'hash-wasm';

/**
* Computes distance of 2 points in 3D space.
Expand Down Expand Up @@ -47,9 +48,9 @@ function computeBoundingBox(plugin: PluginUIContext, pocket: PocketData) {
z: center.z
},
size: {
x: Math.ceil(sideLength),
y: Math.ceil(sideLength),
z: Math.ceil(sideLength)
x: Math.ceil(sideLength) + 5, // the bounding box is a bit larger than the pocket for more accurate results
y: Math.ceil(sideLength) + 5,
z: Math.ceil(sideLength) + 5
}
};
}
Expand All @@ -58,17 +59,20 @@ function computeBoundingBox(plugin: PluginUIContext, pocket: PocketData) {
* Sends requests to the backend to compute the docking task and periodically checks if the task is finished.
* @param prediction Prediction info
* @param pocket Pocket data
* @param hash Task identifier (hash)
* @param smiles SMILES ligand identifier
* @param plugin Mol* plugin
* @param exhaustiveness exhaustiveness value (for Autodock Vina)
* @returns Completed task data
*/
export async function computeDockingTaskOnBackend(prediction: PredictionInfo, pocket: PocketData, hash: string, plugin: PluginUIContext): Promise<any> {
if (hash === "") {
export async function computeDockingTaskOnBackend(prediction: PredictionInfo, pocket: PocketData, smiles: string, plugin: PluginUIContext, exhaustiveness: string): Promise<any> {
if (smiles === "") {
return;
}

const box = computeBoundingBox(plugin, pocket);

const hash = await dockingHash(pocket.rank, smiles, exhaustiveness);

await fetch(`./api/v2/docking/${prediction.database}/${prediction.id}/post`, {
method: 'POST',
headers: {
Expand All @@ -78,6 +82,8 @@ export async function computeDockingTaskOnBackend(prediction: PredictionInfo, po
body: JSON.stringify({
"hash": hash,
"pocket": pocket.rank,
"smiles": smiles,
"exhaustiveness": exhaustiveness,
"bounding_box": box
}),
}).then((res) => {
Expand All @@ -90,23 +96,27 @@ export async function computeDockingTaskOnBackend(prediction: PredictionInfo, po
}

/**
* Returns a hash that identifies this task, in this case directly the user input.
* @param prediction Prediction info
* @param pocket Pocket data
* @param formData Form data (user input)
* Returns a hash that identifies this task.
* @param pocket Pocket identifier
* @param smiles SMILES identifier
* @param exhaustiveness exhaustiveness value (for Autodock Vina)
* @returns Computed hash
*/
export function dockingHash(prediction: PredictionInfo, pocket: PocketData, formData: string) {
return formData;
export async function dockingHash(pocket: string, smiles: string, exhaustiveness: string) {
return await md5(`${pocket}_${smiles}_${exhaustiveness}`);
}

/**
* Downloads the result of the task.
* @param hash Task identifier (hash)
* @param smiles SMILES identifier
* @param fileURL URL to download the result from
* @param pocket Pocket identifier
* @param exhaustiveness exhaustiveness value (for Autodock Vina)
* @returns void
*/
export function downloadDockingResult(hash: string, fileURL: string, pocket: string) {
export async function downloadDockingResult(smiles: string, fileURL: string, pocket: string, exhaustiveness: string) {
const hash = await dockingHash(pocket, smiles, exhaustiveness);

// https://stackoverflow.com/questions/50694881/how-to-download-file-in-react-js
fetch(fileURL, {
method: 'POST',
Expand All @@ -115,8 +125,7 @@ export function downloadDockingResult(hash: string, fileURL: string, pocket: str
'Content-Type': 'application/json'
},
body: JSON.stringify({
"hash": hash,
"pocket": pocket
"hash": hash
})
})
.then((response) => response.blob())
Expand Down Expand Up @@ -156,26 +165,30 @@ export async function pollForDockingTask(predictionInfo: PredictionInfo) {
let savedTasks = localStorage.getItem(`${predictionInfo.id}_serverTasks`);
if (!savedTasks) savedTasks = "[]";
const tasks: ServerTaskLocalStorageData[] = JSON.parse(savedTasks);
if (tasks.length === 0) return;
if (tasks.every((task: ServerTaskLocalStorageData) => task.status === "successful" || task.status === "failed")) return;
tasks.forEach(async (task: ServerTaskLocalStorageData, i: number) => {
if (task.status === "successful" || task.status === "failed") return;

const individualTask: ServerTaskInfo = taskStatusJSON["tasks"].find((t: ServerTaskInfo) => t.initialData.hash === task.params[0] && t.initialData.pocket === task.pocket.toString());
const expectedHash = await dockingHash(task.pocket.toString(), task.params[0], task.params[1]);

const individualTask: ServerTaskInfo = taskStatusJSON["tasks"].find((t: ServerTaskInfo) => t.initialData.hash === expectedHash);
if (individualTask) {
if (individualTask.status !== task.status) {
//update the status
tasks[i].status = individualTask.status;

//download the computed data
if (individualTask.status === "successful") {
const hash = await dockingHash(task.pocket.toString(), individualTask.initialData.smiles, individualTask.initialData.exhaustiveness);
const data = await fetch(`./api/v2/docking/${predictionInfo.database}/${predictionInfo.id}/public/result.json`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"hash": task.params[0],
"pocket": task.pocket,
"hash": hash
})
}).then(res => res.json()).catch(err => console.log(err));
tasks[i].responseData = data;
Expand Down
49 changes: 28 additions & 21 deletions frontend/client/viewer/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,41 @@ import { PocketsViewType, PolymerColorType, PolymerViewType, PredictionData, Rea
import { DefaultPluginUISpec } from 'molstar/lib/mol-plugin-ui/spec';
import { createPluginUI } from 'molstar/lib/mol-plugin-ui';
import 'molstar/lib/mol-plugin-ui/skin/light.scss';
import { RcsbFv, RcsbFvTrackDataElementInterface } from "@rcsb/rcsb-saguaro";
import { RcsbFv, RcsbFvTrackDataElementInterface, RcsbFvTrackData } from "@rcsb/rcsb-saguaro";
import { highlightSurfaceAtomsInViewerLabelId, overPaintPolymer, updatePolymerView, showPocketInCurrentRepresentation } from './molstar-visualise';
import { renderReact18 } from 'molstar/lib/mol-plugin-ui/react18';

/**
* A function to render the actual PrankWeb viewer.
* @param predictionInfo Information about the prediction to be visualised.
*/
export async function renderProteinView(predictionInfo: PredictionInfo) {
const wrapper = document.getElementById('application-molstar')!;
const MolstarPlugin = await createPluginUI(wrapper, {
...DefaultPluginUISpec(),
layout: {
initial: {
isExpanded: false,
showControls: true,
controlsDisplay: "reactive",
regionState: {
top: "hidden", //sequence
left: (window.innerWidth > 1200) ? "collapsed" : "hidden",
//tree with some components, hide for small and medium screens
bottom: "hidden", //shows log information
right: "hidden" //structure tools
const MolstarPlugin = await createPluginUI(
{
target: wrapper,
render: renderReact18,
spec: {
...DefaultPluginUISpec(),
layout: {
initial: {
isExpanded: false,
showControls: true,
controlsDisplay: "reactive",
regionState: {
top: "hidden", //sequence
left: (window.innerWidth > 1200) ? "collapsed" : "hidden",
//tree with some components, hide for small and medium screens
bottom: "hidden", //shows log information
right: "hidden" //structure tools
}
}
},
components: {
remoteState: 'none'
}
}
},
components: {
remoteState: 'none'
}
});
});

// If the Mol* plugin is maximized, hide the React components.
MolstarPlugin.layout.events.updated.subscribe(() => {
Expand Down Expand Up @@ -199,9 +205,10 @@ export class Application extends React.Component<ReactApplicationProps, ReactApp
//resolve RCSB at first - do it by recoloring the pocket to the default color value
//currently there is no other way to "remove" one of the pockets without modyfing the others
const newColor = isVisible ? "#" + this.state.data.pockets[index].color : "#F9F9F9";
const track = this.state.pluginRcsb.getBoardData().find(e => e.trackId === "pocketsTrack");
const track = this.state.pluginRcsb.getBoardData().find(e => e.trackId.indexOf("pocketsTrack") !== -1);
if (track) {
track.trackData!.filter((e: RcsbFvTrackDataElementInterface) => e.provenanceName === `pocket${index + 1}`).forEach((foundPocket: RcsbFvTrackDataElementInterface) => (foundPocket.color = newColor));
// this shouldn't be any but I couldn't find a way to avoid it
track.trackData!.filter((e: any) => e.provenanceName === `pocket${index + 1}`).forEach((foundPocket: RcsbFvTrackDataElementInterface) => (foundPocket.color = newColor));
const newData = track.trackData;
this.state.pluginRcsb.updateTrackData("pocketsTrack", newData!);
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/client/viewer/components/prediction-info-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default function PredictionInfoTab(props: { predictionInfo: PredictionInf
},
{
name: "P2Rank version",
value: props.predictionData.metadata.p2rank_version || "unknown, possibly 2.4"
value: props.predictionData.metadata.p2rank_version || "< 2.4"
},
{
name: "",
Expand Down
35 changes: 32 additions & 3 deletions frontend/client/viewer/components/tasks-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type TaskTypeMenuItem = {
name: string,
compute: (params: string[], customName: string, pocketIndex: number) => void;
parameterDescriptions: string[];
parameterDefaults?: string[];
};

export default function TasksTab(props: { pockets: PocketData[], predictionInfo: PredictionInfo, plugin: PluginUIContext, initialPocket: number; }) {
Expand Down Expand Up @@ -72,6 +73,22 @@ export default function TasksTab(props: { pockets: PocketData[], predictionInfo:
type: TaskType.Server,
name: "Docking",
compute: (params, customName, pocketIndex) => {
// check if exhaustiveness is a number
const exhaustiveness = params[1].replaceAll(",", ".").replaceAll(" ", "");
if (isNaN(parseFloat(exhaustiveness))) {
setInvalidInput(true);
return;
}

// 1-64 is the allowed range
if (Number(exhaustiveness) < 1 || Number(exhaustiveness) > 64) {
setInvalidInput(true);
return;
}

setInvalidInput(false);
const smiles = params[0].replaceAll(" ", "");

let savedTasks = localStorage.getItem(`${props.predictionInfo.id}_serverTasks`);
if (!savedTasks) savedTasks = "[]";
const tasks: ServerTaskLocalStorageData[] = JSON.parse(savedTasks);
Expand All @@ -86,11 +103,13 @@ export default function TasksTab(props: { pockets: PocketData[], predictionInfo:
"discriminator": "server",
});
localStorage.setItem(`${props.predictionInfo.id}_serverTasks`, JSON.stringify(tasks));
computeDockingTaskOnBackend(props.predictionInfo, props.pockets[pocketIndex], params[0], props.plugin);
computeDockingTaskOnBackend(props.predictionInfo, props.pockets[pocketIndex], smiles, props.plugin, exhaustiveness);
},
parameterDescriptions: [
"Enter the molecule in SMILES format (e.g. c1ccccc1)",
]
"Enter the exhaustiveness for Autodock Vina (recommended: 32, allowed range: 1-64)"
],
parameterDefaults: ["", "32"]
}
];

Expand All @@ -99,11 +118,13 @@ export default function TasksTab(props: { pockets: PocketData[], predictionInfo:
const [name, setName] = React.useState<string>("");
const [parameters, setParameters] = React.useState<string[]>([]);
const [forceUpdate, setForceUpdate] = React.useState<number>(0);
const [invalidInput, setInvalidInput] = React.useState<boolean>(false);

const handleTaskTypeChange = (event: SelectChangeEvent) => {
const newTask = tasks.find(task => task.id == Number(event.target.value))!;
setTask(newTask);
setParameters(Array(newTask.parameterDescriptions.length).fill(""));
if (newTask.parameterDefaults) setParameters(newTask.parameterDefaults);
else setParameters(Array(newTask.parameterDescriptions.length).fill(""));
};

const handlePocketRankChange = (event: SelectChangeEvent) => {
Expand Down Expand Up @@ -197,6 +218,14 @@ export default function TasksTab(props: { pockets: PocketData[], predictionInfo:
</tr>
)
}
{
invalidInput &&
<tr>
<td colSpan={2}>
<Typography variant="body1" style={{ color: "red" }}>Error: The task could not be created. Check the formatting.</Typography>
</td>
</tr>
}
<tr>
<td>
<Button variant="contained" onClick={handleSubmitButton}>Create task</Button>
Expand Down
Loading

0 comments on commit 7663403

Please sign in to comment.