diff --git a/backend/src/api/protein.py b/backend/src/api/protein.py index 4e55a16..34b7920 100644 --- a/backend/src/api/protein.py +++ b/backend/src/api/protein.py @@ -14,6 +14,7 @@ from fastapi import APIRouter from fastapi.responses import FileResponse, StreamingResponse from fastapi.requests import Request +import re router = APIRouter() @@ -397,3 +398,64 @@ def align_proteins(proteinA: str, proteinB: str): except Exception as e: log.error(e) raise HTTPException(status_code=500, detail=str(e)) + + +class TMAlignInfo(CamelModel): + aligned_length: str | None + rmsd: str | None + seq_id: str | None + chain1_tm_score: str | None + chain2_tm_score: str | None + alignment_string: str + + +# Returns the alignment string info from TM Align's console log. +@router.get( + "/protein/tmalign/{proteinA:str}/{proteinB:str}", response_model=TMAlignInfo +) +def get_tm_info(proteinA: str, proteinB: str): + 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" + ) + try: + filepath_pdbA = stored_pdb_file_name(proteinA) + filepath_pdbB = stored_pdb_file_name(proteinB) + tmalign_output = tm_align_return(filepath_pdbA, filepath_pdbB, 1) + + log.warn("TM Align Output follows:") + # Split TMAlign data into an array format + tmalign_output_list = tmalign_output.splitlines() + log.warn(tmalign_output) + + # Grab aligned length, RMSD, and Seq ID + tmalign_tri = tmalign_output_list[12].split(", ") + # Note: \d+?.\d* means "match 1 or more numbers, 0 or 1 decimal points, and 0 or more numbers" in regex + aligned_length = re.search("\d+?.\d*", tmalign_tri[0]).group() + rmsd = re.search("\d+.?\d*", tmalign_tri[1]).group() + seq_id = re.search("\d+.?\d*", tmalign_tri[2]).group() + + # Grabs both TM scores + # NOTE: This is ONLY grabbing the TM-Score from the file. It's leaving out the LN and d0 stats. + chain1_normalized_tm_score = re.search( + "\d+.?\d*", tmalign_output_list[13] + ).group() + chain2_normalized_tm_score = re.search( + "\d+.?\d*", tmalign_output_list[14] + ).group() + + # Grabs TM Alignment String + tmalign_string = "\n".join(tmalign_output_list[18:21]) + log.warn(tmalign_string) + + return TMAlignInfo( + aligned_length=aligned_length, + rmsd=rmsd, + seq_id=seq_id, + chain1_tm_score=chain1_normalized_tm_score, + chain2_tm_score=chain2_normalized_tm_score, + alignment_string=tmalign_string, + ) + except Exception as e: + log.error(e) + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/src/tmalign.py b/backend/src/tmalign.py index ad0f727..7406a80 100644 --- a/backend/src/tmalign.py +++ b/backend/src/tmalign.py @@ -101,7 +101,7 @@ def tm_align( return desired_file -def tm_align_return(pdbA: str, pdbB: str) -> str: +def tm_align_return(pdbA: str, pdbB: str, consoleOutput=False) -> str: """ Description: Returns two overlaid, aligned, and colored PDB structures in a single PDB file. @@ -121,10 +121,14 @@ def tm_align_return(pdbA: str, pdbB: str) -> str: 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}" + cmd = f"{TMALIGN_EXECUTABLE} {pdbA} {pdbB} -o {output_location} > {output_location}.txt" bash_cmd(cmd) - tmalign_pdb_path = f"{output_location}_all_atm" + tmalign_pdb_path = ( + f"{output_location}_all_atm" + if not consoleOutput + else f"{output_location}.txt" + ) with open(tmalign_pdb_path, "r") as tmalign_pdb_file: tmalign_pdb_file_str = tmalign_pdb_file.read() diff --git a/frontend/src/lib/openapi/index.ts b/frontend/src/lib/openapi/index.ts index f94854a..c2a924f 100644 --- a/frontend/src/lib/openapi/index.ts +++ b/frontend/src/lib/openapi/index.ts @@ -29,6 +29,7 @@ export type { RangeFilter } from './models/RangeFilter'; export type { SearchProteinsBody } from './models/SearchProteinsBody'; export type { SearchProteinsResults } from './models/SearchProteinsResults'; export type { SimilarProtein } from './models/SimilarProtein'; +export type { TMAlignInfo } from './models/TMAlignInfo'; export type { UploadArticleImageComponent } from './models/UploadArticleImageComponent'; export type { UploadArticleProteinComponent } from './models/UploadArticleProteinComponent'; export type { UploadArticleTextComponent } from './models/UploadArticleTextComponent'; diff --git a/frontend/src/lib/openapi/models/TMAlignInfo.ts b/frontend/src/lib/openapi/models/TMAlignInfo.ts new file mode 100644 index 0000000..916c8c9 --- /dev/null +++ b/frontend/src/lib/openapi/models/TMAlignInfo.ts @@ -0,0 +1,13 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type TMAlignInfo = { + alignedLength: (string | null); + rmsd: (string | null); + seqId: (string | null); + chain1TmScore: (string | null); + chain2TmScore: (string | null); + alignmentString: string; +}; + diff --git a/frontend/src/lib/openapi/services/DefaultService.ts b/frontend/src/lib/openapi/services/DefaultService.ts index d125304..21ffde6 100644 --- a/frontend/src/lib/openapi/services/DefaultService.ts +++ b/frontend/src/lib/openapi/services/DefaultService.ts @@ -20,6 +20,7 @@ import type { RangeFilter } from '../models/RangeFilter'; import type { SearchProteinsBody } from '../models/SearchProteinsBody'; import type { SearchProteinsResults } from '../models/SearchProteinsResults'; import type { SimilarProtein } from '../models/SimilarProtein'; +import type { TMAlignInfo } from '../models/TMAlignInfo'; import type { UploadArticleImageComponent } from '../models/UploadArticleImageComponent'; import type { UploadArticleProteinComponent } from '../models/UploadArticleProteinComponent'; import type { UploadArticleTextComponent } from '../models/UploadArticleTextComponent'; @@ -329,6 +330,29 @@ export class DefaultService { }, }); } + /** + * Get Tm Info + * @param proteinA + * @param proteinB + * @returns TMAlignInfo Successful Response + * @throws ApiError + */ + public static getTmInfo( + proteinA: string, + proteinB: string, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/protein/tmalign/{proteinA}/{proteinB}', + path: { + 'proteinA': proteinA, + 'proteinB': proteinB, + }, + errors: { + 422: `Validation Error`, + }, + }); + } /** * Get Article * get_article diff --git a/frontend/src/routes/Align.svelte b/frontend/src/routes/Align.svelte index 20e2891..0022717 100644 --- a/frontend/src/routes/Align.svelte +++ b/frontend/src/routes/Align.svelte @@ -2,7 +2,7 @@ import TMAlignEntry from "../lib/ProteinLinkCard.svelte"; import { onMount } from "svelte"; - import { Backend, BACKEND_URL, type SimilarProtein, type ProteinEntry } from "../lib/backend"; + import { Backend, BACKEND_URL, type SimilarProtein, type ProteinEntry, type TMAlignInfo } from "../lib/backend"; import Molstar from "../lib/Molstar.svelte"; import DelayedSpinner from "../lib/DelayedSpinner.svelte"; import { DownloadOutline } from "flowbite-svelte-icons"; @@ -10,6 +10,7 @@ import * as d3 from "d3"; import { undoFormatProteinName } from "../lib/format"; import AlignBlock from "../lib/AlignBlock.svelte"; + import { AccordionItem, Accordion } from "flowbite-svelte"; export let proteinA: string; export let proteinB: string; @@ -19,6 +20,8 @@ let foldseekData: SimilarProtein; let foldseekError = false; let error = false; + let tmData: TMAlignInfo; + let tmDataError = false; const dark2green = d3.schemeDark2[0]; const dark2orange = d3.schemeDark2[1]; @@ -46,8 +49,16 @@ ); foldseekError = true; } - + try { + tmData = await Backend.getTmInfo(proteinA, proteinB) + } catch (e) { + console.error(e); + console.error("NEED TO DOWMLOAD T M ALIGN IN THE SERVER. SEE THE SERVER ERROR MESSAGE.") + tmDataError = true; + } + + // if we could not find the entry, the id is garbo if (entryA == null || entryB == null) error = true; console.log(entryA, entryB); @@ -72,33 +83,79 @@ -

- Foldseek Data -

- {#if foldseekData === undefined && !foldseekError} + + + TM-Align Data + {#if tmData === undefined && ! tmDataError} + + {:else if tmData !== undefined} +
+ Aligned Length: {tmData.alignedLength} +
+
+ RMSD: {tmData.rmsd} +
+
+ Seq_ID: {tmData.seqId} +
+
+ TM Score (Chain 1-Normalized): {tmData.chain1TmScore} +
+
+ TM Score (Chain 2-Normalized): {tmData.chain2TmScore} +
+
+ Alignment String +
+ {tmData.alignmentString} +
+
+ {/if} +
+
+ + + Foldseek Data + {#if foldseekData === undefined && !foldseekError} - {:else if foldseekData !== undefined} -
- Prob. Match: {foldseekData.prob} -
-
- E-Value: {foldseekData.evalue} -
-
- Region of Similarity - -
- {/if} + {:else if foldseekData !== undefined} +
+ Prob. Match: {foldseekData.prob} +
+
+ E-Value: {foldseekData.evalue} +
+
+ Region of Similarity + +
+ {/if} +
+