Skip to content

Commit

Permalink
Merge pull request #144 from luk27official/limit-docking
Browse files Browse the repository at this point in the history
Limit docking tasks to 300 characters
  • Loading branch information
skodapetr authored May 20, 2024
2 parents eb7cc5d + 34c692b commit 68d37a3
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 29 deletions.
1 change: 0 additions & 1 deletion docker-compose-prankweb.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: "3.8"
services:
gateway:
build:
Expand Down
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: "3.8"
services:
gateway:
build:
Expand Down
87 changes: 77 additions & 10 deletions frontend/client/tasks/server-docking-task.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -108,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())
Expand Down Expand Up @@ -183,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;
}
52 changes: 37 additions & 15 deletions frontend/client/viewer/components/tasks-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -73,21 +73,38 @@ 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
if (smiles.length > 300) {
handleInvalidDockingInput("SMILES must be shorter than 300 characters.");
return;
}

handleInvalidDockingInput("");

let savedTasks = localStorage.getItem(`${props.predictionInfo.id}_serverTasks`);
if (!savedTasks) savedTasks = "[]";
Expand All @@ -103,10 +120,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"]
Expand All @@ -118,7 +138,8 @@ 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 [invalidInputMessage, setInvalidInputMessage] = React.useState<string>("");
const [dockingScript, setDockingScript] = React.useState<string>("");

const handleTaskTypeChange = (event: SelectChangeEvent) => {
const newTask = tasks.find(task => task.id == Number(event.target.value))!;
Expand Down Expand Up @@ -219,16 +240,17 @@ export default function TasksTab(props: { pockets: PocketData[], predictionInfo:
)
}
{
invalidInput &&
<tr>
<td colSpan={2}>
<Typography variant="body1" style={{ color: "red" }}>Error: The task could not be created. Check the formatting.</Typography>
</td>
</tr>
invalidInputMessage === "" ? null :
<tr>
<td colSpan={2}>
<Typography variant="body1" style={{ color: "red" }}>{invalidInputMessage}</Typography>
</td>
</tr>
}
<tr>
<td>
<Button variant="contained" onClick={handleSubmitButton}>Create task</Button>
{dockingScript !== "" && <>&nbsp;<Button onClick={() => downloadDockingBashScript(dockingScript)} variant="contained" color="warning">Download the script</Button></>}
</td>
<td>

Expand Down
4 changes: 2 additions & 2 deletions frontend/client/viewer/components/tasks-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -237,4 +237,4 @@ export function TasksTable(props: { pocket: PocketData | null, predictionInfo: P
</TableBody>
</Table>
);
}
};
17 changes: 17 additions & 0 deletions web-server/src/docking_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)):
Expand Down

0 comments on commit 68d37a3

Please sign in to comment.