-
Notifications
You must be signed in to change notification settings - Fork 66
feat: add "recombinant" qc metric #1700
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
base: master
Are you sure you want to change the base?
Conversation
| #[derive(Clone, Debug, Default, Serialize, Deserialize, schemars::JsonSchema)] | ||
| #[serde(rename_all = "camelCase")] | ||
| pub struct QcResultRecombinants { | ||
| pub score: f64, | ||
| pub status: QcStatus, | ||
| pub total_private_mutations: usize, | ||
| pub total_reversion_substitutions: usize, | ||
| pub mutations_threshold: usize, | ||
| pub excess_mutations: usize, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Outputs of the metric (also contains a copy of the input config)
| let total_private_mutations = private_nuc_mutations.total_private_substitutions; | ||
| let total_reversion_substitutions = private_nuc_mutations.total_reversion_substitutions; | ||
|
|
||
| // Calculate the total mutations relevant for recombinant detection | ||
| // This includes all private substitutions and reversions, as both are indicators of recombination | ||
| let total_mutations_for_recombination = total_private_mutations + total_reversion_substitutions; | ||
|
|
||
| let excess_mutations = if total_mutations_for_recombination > config.mutations_threshold { | ||
| total_mutations_for_recombination - config.mutations_threshold | ||
| } else { | ||
| 0 | ||
| }; | ||
|
|
||
| // Calculate score based on excess mutations beyond threshold | ||
| let score = if total_mutations_for_recombination > config.mutations_threshold { | ||
| clamp_min(excess_mutations as f64 * *config.score_weight / config.mutations_threshold as f64, 0.0) | ||
| } else { | ||
| 0.0 | ||
| }; | ||
|
|
||
| let status = QcStatus::from_score(score); | ||
|
|
||
| Some(QcResultRecombinants { | ||
| score, | ||
| status, | ||
| total_private_mutations, | ||
| total_reversion_substitutions, | ||
| mutations_threshold: config.mutations_threshold, | ||
| excess_mutations, | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The actual calculation of the metric and it's score is here
| export function formatQCRecombinants<TFunction extends TFunctionInterface>( | ||
| t: TFunction, | ||
| recombinants?: DeepReadonly<QcResultRecombinants>, | ||
| ) { | ||
| if (!recombinants || recombinants.status === 'good') { | ||
| return undefined | ||
| } | ||
|
|
||
| const { score, totalPrivateMutations, totalReversionSubstitutions, mutationsThreshold, excessMutations, status } = | ||
| recombinants | ||
|
|
||
| const totalMutations = totalPrivateMutations + totalReversionSubstitutions | ||
|
|
||
| let message = t('Potentially recombinant sequence detected') | ||
| if (status === 'bad') { | ||
| message = t('Likely recombinant sequence detected') | ||
| } | ||
|
|
||
| return t( | ||
| '{{message}}. Seen {{totalMutations}} private mutations and reversions ({{excessMutations}} above threshold of {{mutationsThreshold}}). QC score: {{score}}', | ||
| { | ||
| message, | ||
| totalMutations, | ||
| excessMutations, | ||
| mutationsThreshold, | ||
| score: round(score), | ||
| }, | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way result is presented in the tooltip in the web UI is here
| #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, schemars::JsonSchema, Validate)] | ||
| #[serde(rename_all = "camelCase")] | ||
| #[serde(default)] | ||
| #[schemars(example = "QcRulesConfigRecombinants::example")] | ||
| pub struct QcRulesConfigRecombinants { | ||
| pub enabled: bool, | ||
| pub mutations_threshold: usize, | ||
| pub score_weight: OrderedFloat<f64>, | ||
| } | ||
|
|
||
| impl QcRulesConfigRecombinants { | ||
| pub const fn example() -> Self { | ||
| Self { | ||
| enabled: true, | ||
| mutations_threshold: 20, | ||
| score_weight: OrderedFloat(100.0), | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Configurable variables can be adjusted here (maps to fields in qc.recombinants of pathogen.json). They are passed to the function which calculates the metric.
Resolves #1699
Example data is in: nextstrain/nextclade_data#367
Test web app with: https://nextstrain--nextclade--pr-1700.previews.neherlab.click/?dataset-server=gh:@feat/qc-recombinants@&dataset-name=sars-cov-2&input-fasta=example