Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fix TMAlign file generation + restyle UI #222

Merged
merged 5 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions backend/src/api/protein.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from fastapi.exceptions import HTTPException

from ..api_types import ProteinEntry, UploadBody, UploadError, EditBody, CamelModel
from ..tmalign import tm_align_return
from ..auth import requires_authentication
from ..tmalign import tm_align
from io import BytesIO
from fastapi import APIRouter
from fastapi.responses import FileResponse, StreamingResponse
Expand Down Expand Up @@ -97,6 +97,14 @@ def pdb_to_fasta(pdb: PDB):
return ">{}\n{}".format(pdb.name, "".join(pdb.amino_acids()))


def str_as_file_stream(input_str: str, filename_as: str) -> StreamingResponse:
return StreamingResponse(
BytesIO(input_str.encode()),
media_type="text/plain",
headers={"Content-Disposition": f"attachment; filename={filename_as}"},
)


"""
ENDPOINTS TODO: add the other protein types here instead of in api_types.py
"""
Expand Down Expand Up @@ -137,7 +145,7 @@ def get_protein_entry(protein_name: str):
with Database() as db:
try:
query = """SELECT proteins.name,
proteins.description,
proteins.description,
proteins.length,
proteins.mass,
proteins.content,
Expand Down Expand Up @@ -355,15 +363,18 @@ def edit_protein_entry(body: EditBody, req: Request):


# /pdb with two attributes returns both PDBs, superimposed and with different colors.
@router.get("/protein/pdb/{proteinA:str}/{proteinB:str}")
@router.get("/protein/pdb/{proteinA:str}/{proteinB:str}", response_model=str)
def align_proteins(proteinA: str, proteinB: str):
try:
pdbA = stored_pdb_file_name(proteinA)
pdbB = stored_pdb_file_name(proteinB)

file = tm_align(proteinA, pdbA, proteinB, pdbB)
if not protein_name_found(proteinA) or not protein_name_found(proteinB):
raise HTTPException(
status_code=404, detail="One of the proteins provided is not found in DB"
)

return FileResponse(file, filename=proteinA + "_" + proteinB + ".pdb")
try:
filepath_pdbA = stored_pdb_file_name(proteinA)
filepath_pdbB = stored_pdb_file_name(proteinB)
superimposed_pdb = tm_align_return(filepath_pdbA, filepath_pdbB)
return str_as_file_stream(superimposed_pdb, f"{proteinA}_{proteinB}.pdb")
except Exception as e:
log.error(e)
raise HTTPException(status_code=500, detail=str(e))
68 changes: 64 additions & 4 deletions backend/src/tmalign.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,36 @@ def assert_tmalign_installed():
)


def parse_pdb(filepath: str) -> list[str]:
with open(filepath, "r") as f:
lines = f.readlines()
return lines
temp_dirs_active = 0


class UniqueTempDir:
"""
on opening scope will create directory of the given name
on closing scope will delete directory of the given name
uses the global `active_caches` above to create a unique dir name
"""

def __init__(self, base_path):
self.base_path = base_path

def __enter__(self):
global temp_dirs_active
temp_dirs_active += 1
self.temp_dir = os.path.join(self.base_path, f"temp_dir_{temp_dirs_active}")

# create the directory (and override existing one if exists)
bash_cmd("rm -rf " + self.temp_dir)
bash_cmd(f"mkdir {self.temp_dir}")

return self.temp_dir

def __exit__(self, *args):
global temp_dirs_active

# get rid of the temp directory
temp_dirs_active -= 1
bash_cmd("rm -rf " + self.temp_dir)


def tm_align(
Expand Down Expand Up @@ -73,3 +99,37 @@ def tm_align(
raise e

return desired_file


def tm_align_return(pdbA: str, pdbB: str) -> str:
"""
Description:
Returns two overlaid, aligned, and colored PDB structures in a single PDB file.
The ones without extensions appear to be PDB files.

Params:
pdbA:
The filepath of the first protein.
pdbB:
The filepath of the second protein.

Returns: the str contents of the pdbA superimposed on pdbB with TMAlgin
"""

assert_tmalign_installed()

with UniqueTempDir(base_path=TMALIGN_LOCATION) as temp_dir_path:
try:
output_location = os.path.join(temp_dir_path, "output")
cmd = f"{TMALIGN_EXECUTABLE} {pdbA} {pdbB} -o {output_location}"
bash_cmd(cmd)

tmalign_pdb_path = f"{output_location}_all_atm"

with open(tmalign_pdb_path, "r") as tmalign_pdb_file:
tmalign_pdb_file_str = tmalign_pdb_file.read()
return tmalign_pdb_file_str

except Exception as e:
log.warn(e)
raise e
6 changes: 4 additions & 2 deletions frontend/src/Router.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import Edit from "./routes/Edit.svelte";
import Tutorials from "./routes/Tutorials.svelte";
import ForceUploadThumbnails from "./routes/ForceUploadThumbnails.svelte";
import Compare from "./routes/Compare.svelte";
import Align from "./routes/Align.svelte";
</script>

<Router>
Expand All @@ -28,7 +28,9 @@
>
<Route path="/edit/:id" let:params><Edit urlId={params.id} /></Route>
<Route path="/force-upload-thumbnails"><ForceUploadThumbnails /></Route>
<Route path="/compare/:a/:b" let:params><Compare proteinA={params.a} proteinB={params.b}/></Route>
<Route path="/align/:a/:b" let:params
><Align proteinA={params.a} proteinB={params.b} /></Route
>
<Route path="/*"><Error /></Route>
</main>
</Router>
3 changes: 2 additions & 1 deletion frontend/src/lib/Molstar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
export let binary = false;
export let width = 500;
export let height = 500;
export let hideControls = true;
let m: PDBeMolstarPlugin;

let divEl: HTMLDivElement;
Expand All @@ -30,7 +31,7 @@
alphafoldView: true,
reactive: true,
sequencePanel: true,
hideControls: true,
hideControls,
hideCanvasControls: ["animation"],
});
}
Expand Down
18 changes: 9 additions & 9 deletions frontend/src/lib/SimilarProteins.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
<th class="name-cell"> Name </th>
<th class="evalue-cell"> E-Value </th>
<th class="prob-cell"> Prob. Match</th>
<th class="align-cell"> Region of Similarity </th>
<th class="compare-cell">TMAlign</th>
<th class="region-cell"> Region of Similarity </th>
<th class="align-cell">TMAlign</th>
</tr>
{#each similarProteins as protein, i}
<tr class="pdb-row">
Expand Down Expand Up @@ -79,7 +79,7 @@
</div></td
>
<td>
<div class="align-cell">
<div class="region-cell">
<AlignBlock
width={260}
height={20}
Expand All @@ -90,11 +90,11 @@
</div>
</td>
<td>
<div class="compare-cell">
<div class="align-cell">
<a
use:link
href="/compare/{queryProteinName}/{protein.name}"
>Compare <ArrowUpRightFromSquareOutline
href="/align/{queryProteinName}/{protein.name}"
>Align <ArrowUpRightFromSquareOutline
size="sm"
/></a
>
Expand Down Expand Up @@ -135,7 +135,7 @@
a {
display: flex;
gap: 1px;
align-items: center;
region-items: center;
}
/* width */
::-webkit-scrollbar {
Expand Down Expand Up @@ -166,12 +166,12 @@
.prob-cell {
width: 120px;
}
.align-cell {
.region-cell {
width: 290px;
padding-left: 10px;
padding-right: 30px;
}
.compare-cell {
.align-cell {
width: 100px;
}
</style>
4 changes: 2 additions & 2 deletions frontend/src/lib/openapi/services/DefaultService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,13 +256,13 @@ export class DefaultService {
* Align Proteins
* @param proteinA
* @param proteinB
* @returns any Successful Response
* @returns string Successful Response
* @throws ApiError
*/
public static alignProteins(
proteinA: string,
proteinB: string,
): CancelablePromise<any> {
): CancelablePromise<string> {
return __request(OpenAPI, {
method: 'GET',
url: '/protein/pdb/{proteinA}/{proteinB}',
Expand Down
105 changes: 105 additions & 0 deletions frontend/src/routes/Align.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<script lang="ts">
import TMAlignEntry from "./TMAlignEntry.svelte";

import { onMount } from "svelte";
import { Backend, BACKEND_URL, type ProteinEntry } from "../lib/backend";
import Molstar from "../lib/Molstar.svelte";
import DelayedSpinner from "../lib/DelayedSpinner.svelte";
import { DownloadOutline } from "flowbite-svelte-icons";
import { Button } from "flowbite-svelte";
import * as d3 from "d3";
import { undoFormatProteinName } from "../lib/format";

export let proteinA: string;
export let proteinB: string;
let combined = proteinA + "/" + proteinB;
let entryA: ProteinEntry | null = null;
let entryB: ProteinEntry | null = null;
let error = false;

const dark2green = d3.schemeDark2[0];
const dark2orange = d3.schemeDark2[1];

// when this component mounts, request protein wikipedia entry from backend
onMount(async () => {
// Request the protein from backend given ID
console.log(
"Requesting",
proteinA,
"and",
proteinB,
"info from backend"
);

entryA = await Backend.getProteinEntry(proteinA);
entryB = await Backend.getProteinEntry(proteinB);

// if we could not find the entry, the id is garbo
if (entryA == null || entryB == null) error = true;
console.log(entryA, entryB);
});
</script>

<svelte:head>
<title>Venome Protein {entryA ? entryA.name : ""}</title>
</svelte:head>

<section class="p-5">
<div class="flex gap-10">
{#if entryA && entryB}
<div class="flex gap-2 flex-col" style="width: 400px;">
<div>
<h1 id="title">Align</h1>
<p>
Aligned the following structures with <a
href="https://zhanggroup.org/TM-align/">TMAlign</a
>
</p>
</div>
<TMAlignEntry entry={entryA} color={dark2green} />
<TMAlignEntry entry={entryB} color={dark2orange} />
<div style="width: 300px;" class="mt-3">
<Button href="{BACKEND_URL}/protein/pdb/{combined}"
>Download Aligned PDB File<DownloadOutline
size="md"
class="ml-2"
/></Button
>
</div>
</div>
<div>
<Molstar
format="pdb"
url="{BACKEND_URL}/protein/pdb/{combined}"
width={1000}
height={900}
/>
</div>
{:else if !error}
<!-- Otherwise, tell user we tell the user we are loading -->
<h1><DelayedSpinner text="Loading Protein Entry" /></h1>
{:else if error}
<!-- if we error out, tell the user the id is shiza -->
<h1>Error</h1>
{/if}
</div>
</section>

<style>
#left-side {
width: 100%;
}
#right-side {
width: 450px;
}
#title {
font-size: 2.45rem;
font-weight: 500;
color: var(--darkblue);
}
.hide-ellipses {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
Loading
Loading