Skip to content

Commit

Permalink
Preprocessing pdb + toggle to show surface residues
Browse files Browse the repository at this point in the history
NGL does not like loading structure twice after a chain is selected.
  • Loading branch information
sverhoeven committed Apr 5, 2024
1 parent 0173584 commit fd59d16
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 25 deletions.
82 changes: 81 additions & 1 deletion app/haddock3-restraints-client/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 -<from_chain> | pdb_chain -<to_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<string, never>;
export interface components {
Expand Down Expand Up @@ -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: {
/**
Expand All @@ -139,7 +176,6 @@ export interface components {
/**
* Cutoff
* @description Relative cutoff for sidechain accessibility.
* @default 0.4
*/
cutoff: number;
};
Expand Down Expand Up @@ -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"];
};
};
};
};
}
12 changes: 9 additions & 3 deletions app/routes/api.h3restraints.$.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
7 changes: 6 additions & 1 deletion app/routes/scenarios.protein-protein.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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);
Expand All @@ -153,7 +156,7 @@ export default function ProteinProteinScenario() {
const [reference, setReference] = useState<Molecule | undefined>();
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<HTMLFormElement>) {
Expand Down Expand Up @@ -210,13 +213,15 @@ export default function ProteinProteinScenario() {
description="In example named data/e2a-hpr_1GGR.pdb"
actpass={protein1ActPass}
onActPassChange={setProtein1ActPass}
targetChain="A"
/>
<MoleculeSubForm
name="protein2"
legend="Second protein"
description="In example named data/hpr_ensemble.pdb"
actpass={protein2ActPass}
onActPassChange={setProtein2ActPass}
targetChain="B"
/>
</div>
<FormItem name="reference_fname" label="Reference structure">
Expand Down
96 changes: 86 additions & 10 deletions app/scenarios/MoleculeSubForm.client.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Structure } from "ngl";
import { Structure, autoLoad } from "ngl";
import { useState } from "react";

import { FormDescription } from "~/scenarios/FormDescription";
Expand All @@ -24,7 +24,8 @@ export type ActPassSelection = {

export async function passiveFromActive(
file: File,
activeResidues: ResidueSelection
activeResidues: ResidueSelection,
surface: number[]
) {
/*
On CLI
Expand All @@ -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,
Expand Down Expand Up @@ -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[],
Expand All @@ -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<Molecule | undefined>();
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,
Expand Down Expand Up @@ -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)
: []
}
/>
) : (
<p>Load a structure first</p>
Expand Down Expand Up @@ -195,6 +260,17 @@ export function MoleculeSubForm({
<label htmlFor="showpassive" className="">
Show passive restraints
</label>
<Checkbox
id="showsurface"
disabled={
!Object.keys(molecule!.chains).every((c) => c === targetChain)
}
defaultChecked={showSurface}
onCheckedChange={() => setShowSurface(!showSurface)}
/>
<label htmlFor="showsurface" className="">
Show surface residues
</label>
</div>
</>
) : (
Expand Down
7 changes: 6 additions & 1 deletion app/scenarios/ResiduesSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ export function ResiduesSelect({
<div
key={r.resno}
className="inline-block w-4 text-center font-mono"
title={`${r.resno}`}
title={
r.surface === false
? `${r.resno}, disabled due to not on surface`
: `${r.resno}`
}
>
<label
htmlFor={`residue-${r.resno}`}
Expand All @@ -99,6 +103,7 @@ export function ResiduesSelect({
<input
type="checkbox"
value={r.resno}
disabled={r.surface === false}
id={`residue-${r.resno}`}
checked={selected.includes(r.resno)}
onChange={(e) => handleChange(e, index)}
Expand Down
9 changes: 9 additions & 0 deletions app/scenarios/Viewer.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export function NGLComponent({
);

useEffect(() => {
stage.getComponentsByName(structure.name).dispose();
const component = stage.addComponentFromObject(structure);
if (!component) {
return;
Expand Down Expand Up @@ -192,11 +193,13 @@ export function Viewer({
chain,
active,
passive,
surface,
}: {
structure: Structure;
chain: string;
active: number[];
passive: number[];
surface: number[];
}) {
return (
<NGLStage>
Expand All @@ -219,6 +222,12 @@ export function Viewer({
opacity={0.3}
representation="spacefill"
/>
<NGLResidues
residues={surface}
color="orange"
opacity={0.7}
representation="surface"
/>
</NGLComponent>
</NGLStage>
);
Expand Down
Loading

0 comments on commit fd59d16

Please sign in to comment.