From fd59d162a62d0ff09b410e43da59ae9083cd45f4 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Fri, 5 Apr 2024 16:15:24 +0200 Subject: [PATCH] Preprocessing pdb + toggle to show surface residues NGL does not like loading structure twice after a chain is selected. --- app/haddock3-restraints-client/schema.d.ts | 82 +++++++++++++++++- app/routes/api.h3restraints.$.ts | 12 ++- app/routes/scenarios.protein-protein.tsx | 7 +- app/scenarios/MoleculeSubForm.client.tsx | 96 +++++++++++++++++++--- app/scenarios/ResiduesSelect.tsx | 7 +- app/scenarios/Viewer.client.tsx | 9 ++ app/scenarios/molecule.client.ts | 1 + package-lock.json | 16 ++-- package.json | 2 +- 9 files changed, 207 insertions(+), 25 deletions(-) diff --git a/app/haddock3-restraints-client/schema.d.ts b/app/haddock3-restraints-client/schema.d.ts index 0923baae..eb5b314d 100644 --- a/app/haddock3-restraints-client/schema.d.ts +++ b/app/haddock3-restraints-client/schema.d.ts @@ -90,6 +90,34 @@ export interface paths { patch?: never; trace?: never; }; + "/preprocess_pdb": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Preprocess Pdb + * @description Preprocess a PDB file. + * + * Runs the following [pdbtools](http://www.bonvinlab.org/pdb-tools/) pipeline: + * + * ```shell + * cat pdb | pdb_tidy -strict | pdb_selchain - | pdb_chain - | pdb_fixinsert | pdb_selaltloc | pdb_tidy -strict + * ``` + * + * The request body + */ + post: operations["preprocess_pdb_preprocess_pdb_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -129,6 +157,15 @@ export interface components { */ segid2: string; }; + /** Body_preprocess_pdb_preprocess_pdb_post */ + Body_preprocess_pdb_preprocess_pdb_post: { + /** + * Pdb + * Format: binary + * @description Gzip compressed PDB file to process + */ + pdb: Blob; + }; /** CalcAccessibilityRequest */ CalcAccessibilityRequest: { /** @@ -139,7 +176,6 @@ export interface components { /** * Cutoff * @description Relative cutoff for sidechain accessibility. - * @default 0.4 */ cutoff: number; }; @@ -392,4 +428,48 @@ export interface operations { }; }; }; + preprocess_pdb_preprocess_pdb_post: { + parameters: { + query: { + /** + * @description Chains to keep + * @example A + */ + from_chain: string; + /** + * @description New chain identifier + * @example A + */ + to_chain: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "multipart/form-data": components["schemas"]["Body_preprocess_pdb_preprocess_pdb_post"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/plain": string; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; } diff --git a/app/routes/api.h3restraints.$.ts b/app/routes/api.h3restraints.$.ts index a30aa1aa..1f4c1854 100644 --- a/app/routes/api.h3restraints.$.ts +++ b/app/routes/api.h3restraints.$.ts @@ -4,16 +4,22 @@ import { mustBeAllowedToSubmit } from "~/auth.server"; const HADDOCK3_RESTRAINTS_URL = process.env.HADDOCK3_RESTRAINTS_URL ?? "http://localhost:5000"; -// Proxy requests to the Haddock3 Restraints web service +// Reversze proxy requests to the Haddock3 Restraints web service export const loader = async ({ params, request }: LoaderFunctionArgs) => { const path = params["*"] || ""; + const { search } = new URL(request.url); await mustBeAllowedToSubmit(request); - const url = `${HADDOCK3_RESTRAINTS_URL}/${path}`; + const url = new URL(`${HADDOCK3_RESTRAINTS_URL}/${path}${search}`); const newRequest = new Request(url, { method: request.method, body: request.body, + // TODO sanitize headers, Accept, Content-* should be enough + headers: request.headers, }); - return fetch(newRequest); + // remix fetch is decompressing the response by default, as we want to reverse proxy we need to disable it + // also tsc does not know that remix is using a custom fetch implementation so we need to cast the init to RequestInit + const init = { compress: false } as RequestInit; + return fetch(newRequest, init); }; export const action = loader; diff --git a/app/routes/scenarios.protein-protein.tsx b/app/routes/scenarios.protein-protein.tsx index 22636bbd..2989b84e 100644 --- a/app/routes/scenarios.protein-protein.tsx +++ b/app/routes/scenarios.protein-protein.tsx @@ -62,6 +62,7 @@ molecules = [ [rigidbody] ambig_fname = "${data.ambig_fname.name}" +# unambig = restrain bodies of all molecules, dont set if there are none sampling = 1000 [caprieval] @@ -129,6 +130,8 @@ async function generateRestraintsFile( async function createZip(workflow: string, data: Schema) { const zip = new JSZip(); zip.file(WORKFLOW_CONFIG_FILENAME, workflow); + // TODO replace proteins with preprocessed versions + // also add remark inside that they were preprocessed zip.file(data.protein1.name, data.protein1); zip.file(data.protein2.name, data.protein2); zip.file(data.ambig_fname.name, data.ambig_fname); @@ -153,7 +156,7 @@ export default function ProteinProteinScenario() { const [reference, setReference] = useState(); function referenceLoaded(structure: NGL.Structure, file: File) { const chains = chainsFromStructure(structure); - setReference({ structure, chains, file }); + setReference({ structure, chains, file, originalFile: file }); } function onSubmit(event: React.FormEvent) { @@ -210,6 +213,7 @@ export default function ProteinProteinScenario() { description="In example named data/e2a-hpr_1GGR.pdb" actpass={protein1ActPass} onActPassChange={setProtein1ActPass} + targetChain="A" /> diff --git a/app/scenarios/MoleculeSubForm.client.tsx b/app/scenarios/MoleculeSubForm.client.tsx index 1d870727..5e876f28 100644 --- a/app/scenarios/MoleculeSubForm.client.tsx +++ b/app/scenarios/MoleculeSubForm.client.tsx @@ -1,4 +1,4 @@ -import { Structure } from "ngl"; +import { Structure, autoLoad } from "ngl"; import { useState } from "react"; import { FormDescription } from "~/scenarios/FormDescription"; @@ -24,7 +24,8 @@ export type ActPassSelection = { export async function passiveFromActive( file: File, - activeResidues: ResidueSelection + activeResidues: ResidueSelection, + surface: number[] ) { /* On CLI @@ -35,7 +36,7 @@ export async function passiveFromActive( structure: btoa(structure), chain: activeResidues.chain, active: activeResidues.resno, - surface: [], + surface, }; const { data, error } = await client.POST("/passive_from_active", { body, @@ -70,6 +71,39 @@ async function calculateAccessibility(file: File, chains: Chains) { }); } +async function preprocessPdb(file: File, fromChain: string, toChain: string) { + const cs = new CompressionStream("gzip"); + const compressedStream = file.stream().pipeThrough(cs); + // openapi-typescript does not transform request body with type=string+format=binary into blob + // so we cast it to string to avoid type errors, bypass the body serializer + // and use middleware to set the correct content type + const pdb = await new Response(compressedStream, { + headers: { "Content-Type": "application/gzip" }, + }).blob(); + const { error, data } = await client.POST("/preprocess_pdb", { + body: { + pdb, + }, + bodySerializer(body) { + const fd = new FormData(); + fd.append("pdb", body.pdb, file.name); + return fd; + }, + params: { + query: { + from_chain: fromChain, + to_chain: toChain, + }, + }, + parseAs: "text", + }); + if (error) { + console.error(error); + throw new Error("Could not preprocess pdb"); + } + return new File([data], file.name, { type: file.type }); +} + function filterBuriedResidues( chain: string, residues: number[], @@ -87,39 +121,63 @@ export function MoleculeSubForm({ description, actpass, onActPassChange, + targetChain, }: { name: string; legend: string; description: string; actpass: ActPassSelection; onActPassChange: (actpass: ActPassSelection) => void; + targetChain: string; }) { const [molecule, setMolecule] = useState(); const [showPassive, setShowPassive] = useState(false); + const [showSurface, setShowSurface] = useState(false); async function handleStructureLoad(structure: Structure, file: File) { const chains = chainsFromStructure(structure); - await calculateAccessibility(file, chains); - setMolecule({ structure, chains, file }); + + setMolecule({ structure, chains, file, originalFile: file }); } - function handleChainChange(chain: string) { + async function handleChainChange(chain: string) { + if (!molecule) { + return; + } + const processed = await preprocessPdb( + molecule.originalFile, + chain, + targetChain + ); + const structure: Structure = await autoLoad(processed); + const chains = chainsFromStructure(structure); + await calculateAccessibility(processed, chains); + setMolecule({ + structure, + chains, + file: processed, + originalFile: molecule.originalFile, + }); const newSelection = { - active: { chain, resno: [] }, - passive: { chain, resno: [] }, + active: { chain: targetChain, resno: [] }, + passive: { chain: targetChain, resno: [] }, }; onActPassChange(newSelection); } function handleActiveResiduesChange(activeResidues: number[]) { - passiveFromActive(molecule!.file, { + const surfaceResidues = molecule!.chains[targetChain] + .filter((r) => r.surface) + .map((r) => r.resno); + const activeSelection = { chain: actpass.active.chain, resno: filterBuriedResidues( actpass.active.chain, activeResidues, molecule!.chains ), - }) + }; + passiveFromActive(molecule!.file, activeSelection, surfaceResidues) .then((passiveResidues) => { const filteredPassiveResidues = filterBuriedResidues( actpass.passive.chain, @@ -162,6 +220,13 @@ export function MoleculeSubForm({ chain={actpass.active.chain} active={actpass.active.resno} passive={showPassive ? actpass.passive.resno : []} + surface={ + showSurface + ? molecule.chains[targetChain] + .filter((r) => r.surface) + .map((r) => r.resno) + : [] + } /> ) : (

Load a structure first

@@ -195,6 +260,17 @@ export function MoleculeSubForm({ + c === targetChain) + } + defaultChecked={showSurface} + onCheckedChange={() => setShowSurface(!showSurface)} + /> + ) : ( diff --git a/app/scenarios/ResiduesSelect.tsx b/app/scenarios/ResiduesSelect.tsx index 625a3c58..1952b4b6 100644 --- a/app/scenarios/ResiduesSelect.tsx +++ b/app/scenarios/ResiduesSelect.tsx @@ -88,7 +88,11 @@ export function ResiduesSelect({