diff --git a/backend/antigenapi/bioinformatics/blast.py b/backend/antigenapi/bioinformatics/blast.py index 20cf74e..d7cb6b8 100644 --- a/backend/antigenapi/bioinformatics/blast.py +++ b/backend/antigenapi/bioinformatics/blast.py @@ -11,7 +11,11 @@ BLAST_NUM_THREADS = 4 -def get_db_fasta(include_run: Optional[int] = None, exclude_run: Optional[int] = None): +def get_db_fasta( + include_run: Optional[int] = None, + exclude_run: Optional[int] = None, + query_type: str = "full", +): """Get the sequencing database in fasta format. Args: @@ -19,6 +23,8 @@ def get_db_fasta(include_run: Optional[int] = None, exclude_run: Optional[int] = Defaults to None. exclude_run (int, optional): Sequencing run ID to exclude. Defaults to None. + query_type (str): Query type - "full" sequence or "cdr3" + Defaults to "full". Returns: str: Sequencing run as a FASTA format string @@ -31,20 +37,35 @@ def get_db_fasta(include_run: Optional[int] = None, exclude_run: Optional[int] = query = query.exclude(sequencing_run_id=exclude_run) for sr in query: airr_file = read_airr_file( - sr.airr_file, usecols=("sequence_id", "sequence_alignment_aa") + sr.airr_file, + usecols=( + "sequence_id", + "cdr3_aa" if query_type == "cdr3" else "sequence_alignment_aa", + ), ) - airr_file = airr_file[airr_file.sequence_alignment_aa.notna()] + airr_file = airr_file[ + ( + airr_file.cdr3_aa.notna() + if query_type == "cdr3" + else airr_file.sequence_alignment_aa.notna() + ) + ] if not airr_file.empty: - for _, row in airr_file.iterrows(): - seq = row.sequence_alignment_aa.replace(".", "") - try: - if fasta_data[row.sequence_id] != seq: - raise ValueError( - f"Different sequences with same name! {row.sequence_id}" - ) - continue - except KeyError: - fasta_data[row.sequence_id] = seq + if query_type == "cdr3": + cdr3s = set(airr_file.cdr3_aa.unique()) + for cdr3 in cdr3s: + fasta_data[f"CDR3: {cdr3}"] = cdr3 + else: + for _, row in airr_file.iterrows(): + seq = row.sequence_alignment_aa.replace(".", "") + try: + if fasta_data[row.sequence_id] != seq: + raise ValueError( + f"Different sequences with same name! {row.sequence_id}" + ) + continue + except KeyError: + fasta_data[row.sequence_id] = seq fasta_files = as_fasta_files(fasta_data, max_file_size=None) if fasta_files: @@ -53,25 +74,29 @@ def get_db_fasta(include_run: Optional[int] = None, exclude_run: Optional[int] = return "" -def get_sequencing_run_fasta(sequencing_run_id: int): +def get_sequencing_run_fasta(sequencing_run_id: int, query_type: str): """Get sequencing run in BLAST format. Args: sequencing_run_id (int): Sequencing run ID + query_type (str): Query type - "full" sequence or "cdr3" Returns: str: Sequencing run as a FASTA format string """ - return get_db_fasta(include_run=sequencing_run_id) + return get_db_fasta(include_run=sequencing_run_id, query_type=query_type) def run_blastp( - sequencing_run_id: int, outfmt: str = BLAST_FMT_MULTIPLE_FILE_BLAST_JSON + sequencing_run_id: int, + query_type: str = "full", + outfmt: str = BLAST_FMT_MULTIPLE_FILE_BLAST_JSON, ): """Run blastp for a sequencing run vs rest of database. Args: sequencing_run_id (int): Sequencing run ID. + query_type (str): Query type - "full" sequence or "cdr3" Returns: JSONResponse: Single file BLAST JSON @@ -79,7 +104,7 @@ def run_blastp( db_data = get_db_fasta() if not db_data: return None - query_data = get_sequencing_run_fasta(sequencing_run_id) + query_data = get_sequencing_run_fasta(sequencing_run_id, query_type=query_type) if not query_data: return None diff --git a/backend/antigenapi/views_old.py b/backend/antigenapi/views_old.py index d82ccce..314bd82 100644 --- a/backend/antigenapi/views_old.py +++ b/backend/antigenapi/views_old.py @@ -1062,7 +1062,10 @@ def search_sequencing_run_results(self, request, query): ) def get_blast_sequencing_run(self, request, pk): """BLAST sequencing run vs database.""" - blast_str = run_blastp(pk) + query_type = self.request.query_params.get("queryType", "full") + if query_type not in ("full", "cdr3"): + raise ValueError(f"Unknown queryType: {query_type}") + blast_str = run_blastp(pk, query_type=query_type) if not blast_str: return JsonResponse({"hits": []}, status=status.HTTP_404_NOT_FOUND) @@ -1083,9 +1086,13 @@ def get_blast_sequencing_run(self, request, pk): for blast_hit_set in run_res["hits"]: subject_title = blast_hit_set["description"][0]["title"] query_title = run_res["query_title"] - query_cdr3 = airr_df.at[query_title, "cdr3_aa"] - if pd.isna(query_cdr3): - query_cdr3 = None + if query_type == "cdr3": + # TODO: Make more robust + query_cdr3 = query_title[6:] + else: + query_cdr3 = airr_df.at[query_title, "cdr3_aa"] + if pd.isna(query_cdr3): + query_cdr3 = None if subject_title.strip() == query_title.strip(): continue hsps = blast_hit_set["hsps"][0] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a65766e..063d0b2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,7 @@ "name": "frontend", "version": "0.1.0", "dependencies": { - "@headlessui/react": "^1.7.19", + "@headlessui/react": "2.1", "@heroicons/react": "^2.1.5", "@sentry/react": "^7.119.1", "@sentry/tracing": "^7.114.0", @@ -2439,20 +2439,76 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz", + "integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.25", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.25.tgz", + "integrity": "sha512-hZOmgN0NTOzOuZxI1oIrDu3Gcl8WViIkvPMpB4xdd4QD6xAMtwgwr3VPoiyH/bLtRcS1cDnhxLSD1NsMJmwh/A==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", + "license": "MIT" + }, "node_modules/@headlessui/react": { - "version": "1.7.19", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz", - "integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==", + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.1.10.tgz", + "integrity": "sha512-6mLa2fjMDAFQi+/R10B+zU3edsUk/MDtENB2zHho0lqKU1uzhAfJLUduWds4nCo8wbl3vULtC5rJfZAQ1yqIng==", + "license": "MIT", "dependencies": { - "@tanstack/react-virtual": "^3.0.0-beta.60", - "client-only": "^0.0.1" + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.17.1", + "@react-aria/interactions": "^3.21.3", + "@tanstack/react-virtual": "^3.8.1" }, "engines": { "node": ">=10" }, "peerDependencies": { - "react": "^16 || ^17 || ^18", - "react-dom": "^16 || ^17 || ^18" + "react": "^18", + "react-dom": "^18" } }, "node_modules/@heroicons/react": { @@ -3462,6 +3518,89 @@ } } }, + "node_modules/@react-aria/focus": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.4.tgz", + "integrity": "sha512-91J35077w9UNaMK1cpMUEFRkNNz0uZjnSwiyBCFuRdaVuivO53wNC9XtWSDNDdcO5cGy87vfJRVAiyoCn/mjqA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.22.4", + "@react-aria/utils": "^3.25.3", + "@react-types/shared": "^3.25.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.4.tgz", + "integrity": "sha512-E0vsgtpItmknq/MJELqYJwib+YN18Qag8nroqwjk1qOnBa9ROIkUhWJerLi1qs5diXq9LHKehZDXRlwPvdEFww==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.6", + "@react-aria/utils": "^3.25.3", + "@react-types/shared": "^3.25.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.6.tgz", + "integrity": "sha512-iLo82l82ilMiVGy342SELjshuWottlb5+VefO3jOQqQRNYnJBFpUSadswDPbRimSgJUZuFwIEYs6AabkP038fA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.25.3", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.25.3.tgz", + "integrity": "sha512-PR5H/2vaD8fSq0H/UB9inNbc8KDcVmW6fYAfSWkkn+OAdhTTMVKqXXrZuZBWyFfSD5Ze7VN6acr4hrOQm2bmrA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.6", + "@react-stately/utils": "^3.10.4", + "@react-types/shared": "^3.25.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.4", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.4.tgz", + "integrity": "sha512-gBEQEIMRh5f60KCm7QKQ2WfvhB2gLUr9b72sqUdIZ2EG+xuPgaIlCBeSicvjmjBvYZwOjoOEnmIkcx2GHp/HWw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-types/shared": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.25.0.tgz", + "integrity": "sha512-OZSyhzU6vTdW3eV/mz5i6hQwQUhkRs7xwY2d1aqPvTdMe0+2cY7Fwp45PAiwYLEj73i9ro2FxF9qC4DvHGSCgQ==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@remix-run/router": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz", @@ -3983,6 +4122,15 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@swc/helpers": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", + "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@tailwindcss/forms": { "version": "0.5.9", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.9.tgz", @@ -6063,11 +6211,6 @@ "node": ">=0.10.0" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" - }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -6078,6 +6221,15 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -16445,6 +16597,12 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.4.13", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2edc977..87d1d72 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@headlessui/react": "^1.7.19", + "@headlessui/react": "2.1", "@heroicons/react": "^2.1.5", "@sentry/react": "^7.119.1", "@sentry/tracing": "^7.114.0", diff --git a/frontend/src/crudtemplates/BlastResults.js b/frontend/src/crudtemplates/BlastResults.js index 51076d4..9796203 100644 --- a/frontend/src/crudtemplates/BlastResults.js +++ b/frontend/src/crudtemplates/BlastResults.js @@ -3,11 +3,19 @@ import { useState, useEffect } from "react"; import { useParams } from "react-router-dom"; import * as Sentry from "@sentry/browser"; import LoadingLlama from "../LoadingLlama.js"; +import WrappedRadioGroup from "./RadioGroup.js"; + +const blastQueryTypeOptions = [ + { id: "cdr3", name: "CDR3" }, + { id: "full", name: "Full Sequence" }, +]; const BlastResults = (props) => { const { recordId } = useParams(); const [blastResults, setBlastResults] = useState(); + const [queryType, setQueryType] = useState(blastQueryTypeOptions[0]); + function classNames(...classes) { return classes.filter(Boolean).join(" "); } @@ -31,14 +39,21 @@ const BlastResults = (props) => { ); }; - const fetchBlastResults = () => { - fetch(config.url.API_URL + "/sequencingrun/" + recordId + "/blast/", { - method: "GET", - headers: { - "Content-Type": "application/json", - "X-CSRFToken": props.csrfToken, + const fetchBlastResults = (queryType) => { + fetch( + config.url.API_URL + + "/sequencingrun/" + + recordId + + "/blast/?queryType=" + + queryType.id, + { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": props.csrfToken, + }, }, - }) + ) .then((res) => { res.json().then( (data) => { @@ -57,11 +72,19 @@ const BlastResults = (props) => { }; useEffect(() => { - fetchBlastResults(); - }, []); // eslint-disable-line react-hooks/exhaustive-deps + setBlastResults(); + fetchBlastResults(queryType); + }, [queryType]); // eslint-disable-line react-hooks/exhaustive-deps return ( <> + + {!blastResults && } {blastResults && (
diff --git a/frontend/src/crudtemplates/RadioGroup.js b/frontend/src/crudtemplates/RadioGroup.js new file mode 100644 index 0000000..5aabf79 --- /dev/null +++ b/frontend/src/crudtemplates/RadioGroup.js @@ -0,0 +1,41 @@ +"use client"; + +import { Radio, RadioGroup } from "@headlessui/react"; + +function classNames(...classes) { + return classes.filter(Boolean).join(" "); +} + +export default function WrappedRadioGroup(props) { + return ( +
+
+
+ {props.label} +
+
+ + + {props.options.map((option) => ( + + {option.name} + + ))} + +
+ ); +}