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 (
<>
+