From 5e75026e3d222870fea5635529b7d866276b33ed Mon Sep 17 00:00:00 2001 From: Lukas Polak Date: Mon, 29 Apr 2024 23:19:50 +0200 Subject: [PATCH 1/3] Add SMILES limitation for 300 chars --- frontend/client/tasks/server-docking-task.tsx | 9 ++++----- frontend/client/viewer/components/tasks-tab.tsx | 13 +++++++++++-- web-server/src/docking_task.py | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/frontend/client/tasks/server-docking-task.tsx b/frontend/client/tasks/server-docking-task.tsx index eecb388..10e610d 100644 --- a/frontend/client/tasks/server-docking-task.tsx +++ b/frontend/client/tasks/server-docking-task.tsx @@ -87,12 +87,11 @@ export async function computeDockingTaskOnBackend(prediction: PredictionInfo, po "bounding_box": box }), }).then((res) => { - console.log(res); - } - ).catch(err => { - console.log(err); + return res.json(); + }).catch(err => { + console.error(err); + return null; }); - return; } /** diff --git a/frontend/client/viewer/components/tasks-tab.tsx b/frontend/client/viewer/components/tasks-tab.tsx index 3c5ab2b..5d1f5f8 100644 --- a/frontend/client/viewer/components/tasks-tab.tsx +++ b/frontend/client/viewer/components/tasks-tab.tsx @@ -88,6 +88,12 @@ export default function TasksTab(props: { pockets: PocketData[], predictionInfo: setInvalidInput(false); const smiles = params[0].replaceAll(" ", ""); + // check if SMILES is < 300 characters, otherwise + // TODO: add an option to prepare a script that will run DODO with the ligand + if (smiles.length > 300) { + setInvalidInput(true); + return; + } let savedTasks = localStorage.getItem(`${props.predictionInfo.id}_serverTasks`); if (!savedTasks) savedTasks = "[]"; @@ -103,10 +109,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], smiles, props.plugin, exhaustiveness); + const taskPostRequest = computeDockingTaskOnBackend(props.predictionInfo, props.pockets[pocketIndex], smiles, props.plugin, exhaustiveness); + if (taskPostRequest === null) { + tasks[tasks.length - 1].status = "failed"; + } }, parameterDescriptions: [ - "Enter the molecule in SMILES format (e.g. c1ccccc1)", + "Enter the molecule in SMILES format (e.g. c1ccccc1), max 300 characters", "Enter the exhaustiveness for Autodock Vina (recommended: 32, allowed range: 1-64)" ], parameterDefaults: ["", "32"] diff --git a/web-server/src/docking_task.py b/web-server/src/docking_task.py index b7330cc..803ef3e 100644 --- a/web-server/src/docking_task.py +++ b/web-server/src/docking_task.py @@ -90,6 +90,23 @@ def post_task(self, prediction_id: str, data: dict): if data is None: #user did not provide any data with the post request return "", 400 + required_fields = ["hash", "pocket", "smiles", "exhaustiveness", "bounding_box"] + for field in required_fields: + if field not in data: + return f"Field {field} is missing.", 400 + + # those boundaries are arbitrary - given in the tsx task file + if len(data["smiles"]) > 300: + return "The requested SMILES is too long.", 400 + + try: + exhaustiveness = int(data["exhaustiveness"]) + # the exhaustiveness parameter must be a number between 1 and 64 + if exhaustiveness < 1 or exhaustiveness > 64: + raise ValueError + except ValueError: + return "The exhaustiveness parameter must be a number between 1 and 64.", 400 + taskinfo = TaskInfo(directory=directory, identifier=prediction_id, data=data) if os.path.exists(directory) and os.path.exists(_info_file(taskinfo)): From 08ba6369c4d3cdbb9ab813ba6a25dae9fa5ab843 Mon Sep 17 00:00:00 2001 From: Lukas Polak Date: Mon, 20 May 2024 00:07:05 +0200 Subject: [PATCH 2/3] Remove obsolete "version" from docker-compose --- docker-compose-prankweb.yml | 1 - docker-compose.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/docker-compose-prankweb.yml b/docker-compose-prankweb.yml index 1398673..ca825e4 100644 --- a/docker-compose-prankweb.yml +++ b/docker-compose-prankweb.yml @@ -1,4 +1,3 @@ -version: "3.8" services: gateway: build: diff --git a/docker-compose.yml b/docker-compose.yml index ac9f2f8..0527c28 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: "3.8" services: gateway: build: From 34c692bb1ab8b13230e751a093c9628b456953c1 Mon Sep 17 00:00:00 2001 From: Lukas Polak Date: Mon, 20 May 2024 14:35:10 +0200 Subject: [PATCH 3/3] Add script to run docking locally for wrong task inputs --- frontend/client/tasks/server-docking-task.tsx | 78 +++++++++++++++++-- .../client/viewer/components/tasks-tab.tsx | 43 ++++++---- .../client/viewer/components/tasks-table.tsx | 4 +- 3 files changed, 103 insertions(+), 22 deletions(-) diff --git a/frontend/client/tasks/server-docking-task.tsx b/frontend/client/tasks/server-docking-task.tsx index 10e610d..7086a77 100644 --- a/frontend/client/tasks/server-docking-task.tsx +++ b/frontend/client/tasks/server-docking-task.tsx @@ -1,4 +1,4 @@ -import { PredictionInfo } from "../prankweb-api"; +import { PredictionInfo, getApiEndpoint } from "../prankweb-api"; import { PocketData, Point3D, ServerTaskInfo, ServerTaskLocalStorageData } from "../custom-types"; import { getPocketAtomCoordinates } from "../viewer/molstar-visualise"; @@ -107,13 +107,10 @@ export async function dockingHash(pocket: string, smiles: string, exhaustiveness /** * Downloads the result of the task. - * @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 async function downloadDockingResult(smiles: string, fileURL: string, pocket: string, exhaustiveness: string) { +export async function downloadDockingResult(fileURL: string) { // https://stackoverflow.com/questions/50694881/how-to-download-file-in-react-js fetch(fileURL) .then((response) => response.blob()) @@ -182,3 +179,74 @@ export async function pollForDockingTask(predictionInfo: PredictionInfo) { } return localStorage.getItem(`${predictionInfo.id}_serverTasks`); } + +/** + * Generates a bash script that can be used to run the docking task locally. + * @param smiles SMILES identifier of the ligand + * @param pocket Pocket data + * @param plugin Mol* plugin + * @param prediction Prediction info + * @returns A bash script that can be used to run the docking task locally. + */ +export function generateBashScriptForDockingTask(smiles: string, pocket: PocketData, plugin: PluginUIContext, prediction: PredictionInfo) { + const box = computeBoundingBox(plugin, pocket); + const url = getApiEndpoint(prediction.database, prediction.id).replace(".", window.location.host) + `/public/${prediction.metadata.structureName}`; + + const script = + `#!/bin/bash +# This script runs the docking task locally. +# It requires Docker to be installed on the system. + +json_content='{ + "receptor": "${prediction.metadata.structureName}", + "ligand": "ligand.smi", + "output": "output.pdbqt", + "center": { + "x": ${box.center.x}, + "y": ${box.center.y}, + "z": ${box.center.z} + }, + "size": { + "x": ${box.size.x}, + "y": ${box.size.y}, + "z": ${box.size.z} + } +}' + +echo "$json_content" > docking_parameters.json +echo "${smiles}" > ligand.smi + +wget "${url}" -O ${prediction.metadata.structureName}.gz + +gunzip ${prediction.metadata.structureName}.gz + +docker run -it -v \$\{PWD\}/:/data ghcr.io/kiarka7/dodo:latest`; + + return script; +} + +/** + * Downloads the generated bash script with the current timestamp. + * @param script Bash script + * @returns void + */ +export async function downloadDockingBashScript(script: string) { + const today = new Date(); + const filename = `docking-task-${today.toISOString()}.sh`; + + const url = window.URL.createObjectURL( + new Blob([script]), + ); + const link = document.createElement('a'); + link.href = url; + link.setAttribute( + 'download', + filename, + ); + + document.body.appendChild(link); + link.click(); + link.parentNode!.removeChild(link); + + return; +} \ No newline at end of file diff --git a/frontend/client/viewer/components/tasks-tab.tsx b/frontend/client/viewer/components/tasks-tab.tsx index 5d1f5f8..a4798e1 100644 --- a/frontend/client/viewer/components/tasks-tab.tsx +++ b/frontend/client/viewer/components/tasks-tab.tsx @@ -9,7 +9,7 @@ import { Button, Paper, Typography } from "@mui/material"; import "./tasks-tab.css"; import { PredictionInfo } from "../../prankweb-api"; -import { computeDockingTaskOnBackend } from "../../tasks/server-docking-task"; +import { computeDockingTaskOnBackend, generateBashScriptForDockingTask, downloadDockingBashScript } from "../../tasks/server-docking-task"; import { PluginUIContext } from "molstar/lib/mol-plugin-ui/context"; import { computePocketVolume } from "../../tasks/client-atoms-volume"; import { TasksTable } from "./tasks-table"; @@ -73,28 +73,39 @@ export default function TasksTab(props: { pockets: PocketData[], predictionInfo: type: TaskType.Server, name: "Docking", compute: (params, customName, pocketIndex) => { + const smiles = params[0].replaceAll(" ", ""); + + const handleInvalidDockingInput = (baseMessage: string) => { + if (baseMessage === "") { + setInvalidInputMessage(""); + return; + } + + setInvalidInputMessage(`${baseMessage} Try running the script below to run docking locally.`); + setDockingScript(generateBashScriptForDockingTask(smiles, props.pockets[pocketIndex], props.plugin, props.predictionInfo)); + }; + // check if exhaustiveness is a number const exhaustiveness = params[1].replaceAll(",", ".").replaceAll(" ", ""); - if (isNaN(parseFloat(exhaustiveness))) { - setInvalidInput(true); + if (isNaN(parseInt(exhaustiveness))) { + handleInvalidDockingInput("Exhaustiveness must be an integer."); return; } // 1-64 is the allowed range if (Number(exhaustiveness) < 1 || Number(exhaustiveness) > 64) { - setInvalidInput(true); + handleInvalidDockingInput("Exhaustiveness must be in the range 1-64."); return; } - setInvalidInput(false); - const smiles = params[0].replaceAll(" ", ""); // check if SMILES is < 300 characters, otherwise - // TODO: add an option to prepare a script that will run DODO with the ligand if (smiles.length > 300) { - setInvalidInput(true); + handleInvalidDockingInput("SMILES must be shorter than 300 characters."); return; } + handleInvalidDockingInput(""); + let savedTasks = localStorage.getItem(`${props.predictionInfo.id}_serverTasks`); if (!savedTasks) savedTasks = "[]"; const tasks: ServerTaskLocalStorageData[] = JSON.parse(savedTasks); @@ -127,7 +138,8 @@ export default function TasksTab(props: { pockets: PocketData[], predictionInfo: const [name, setName] = React.useState(""); const [parameters, setParameters] = React.useState([]); const [forceUpdate, setForceUpdate] = React.useState(0); - const [invalidInput, setInvalidInput] = React.useState(false); + const [invalidInputMessage, setInvalidInputMessage] = React.useState(""); + const [dockingScript, setDockingScript] = React.useState(""); const handleTaskTypeChange = (event: SelectChangeEvent) => { const newTask = tasks.find(task => task.id == Number(event.target.value))!; @@ -228,16 +240,17 @@ export default function TasksTab(props: { pockets: PocketData[], predictionInfo: ) } { - invalidInput && - - - Error: The task could not be created. Check the formatting. - - + invalidInputMessage === "" ? null : + + + {invalidInputMessage} + + } + {dockingScript !== "" && <> } diff --git a/frontend/client/viewer/components/tasks-table.tsx b/frontend/client/viewer/components/tasks-table.tsx index 95aa3bb..4fe5bbf 100644 --- a/frontend/client/viewer/components/tasks-table.tsx +++ b/frontend/client/viewer/components/tasks-table.tsx @@ -116,7 +116,7 @@ export function TasksTable(props: { pocket: PocketData | null, predictionInfo: P const handleResultClick = (serverTask: ServerTaskLocalStorageData) => { switch (serverTask.type) { case ServerTaskType.Docking: - downloadDockingResult(serverTask.params[0], serverTask.responseData[0].url, serverTask.pocket.toString(), serverTask.params[1]); + downloadDockingResult(serverTask.responseData[0].url); break; default: break; @@ -237,4 +237,4 @@ export function TasksTable(props: { pocket: PocketData | null, predictionInfo: P ); -} \ No newline at end of file +}; \ No newline at end of file