# Rosie PoC: From Tumour Variants to Neoantigen Candidates

> A reproducible PoC in which an agent inspects synthetic tumour and normal DNA and RNA evidence step by step, then creates 9-mer neoantigen candidates from variants that pass the filters.

## 1. What will you practise?

This exercise asks one question.

> **Among variants found in a tumour, which mutant-peptide candidates remain after retaining only variants absent from normal tissue and actually expressed?**

Rosie's original sequencing data and seven final targets have not been published, so this exercise does not reproduce those results. Instead, it runs the central logic visible in public accounts, `tumour DNA variant → exclude normal DNA → confirm tumour RNA → generate mutant peptide`, directly on synthetic data.

We checked the public materials again on August 16, 2026, but found no download link leading to Rosie's FASTQ, VCF, DLA genotype, or seven target sequences. A separate open-source project with a similar name, [Project Rosie](https://github.com/shashank-padala/project-rosie), explicitly describes its canine demo as using a [synthetic VCF](https://github.com/shashank-padala/project-rosie/blob/cfeb3a0662b035bbebb79198753ea41013d9ff32/docs/explainers/05-canine-data-run.md) and [synthetic enriched candidates](https://github.com/shashank-padala/project-rosie/blob/cfeb3a0662b035bbebb79198753ea41013d9ff32/pipeline/scripts/build_demo.py). We therefore did not substitute that demo for Rosie's actual data.

| Input | Question asked in this exercise |
| --- | --- |
| Tumour DNA VAF | Is the variant observed sufficiently in the tumour sample? |
| Normal DNA VAF | Is it a germline variant carried throughout the body? |
| Mutant reads in tumour RNA | Is the variant allele observed in actual transcripts? |
| Tumour TPM | Is the gene expressed in the tumour? |
| Protein sequence surrounding the variant | Which peptides contain the mutated amino acid? |

The output is a table of variants that pass the evidence filters and the 9-mer peptides containing each variant. The canine gene system corresponding to human HLA is **DLA** (dog leukocyte antigen).[^dla] DLA-binding prediction is a later stage after the output of this PoC, so it is not calculated here.

### How does a variant become a peptide candidate?

When a single-base change in DNA changes one amino acid in a protein, it can create a short sequence absent from the normal protein. If a cell produces and cuts that protein and displays the fragment on DLA, a T cell may be able to see the mutant peptide. An antigen derived from such a tumour-specific variant is called a **neoantigen**.

The computational pipeline checks this biological process in reverse.

1. Observe a variant in tumour DNA.
2. Check whether the same position is also variant in normal DNA.
3. Check whether the variant allele was transcribed in tumour RNA.
4. Calculate the protein sequence changed by the variant.
5. Generate short peptides containing the mutated amino acid.
6. Evaluate their binding to the individual's DLA and the probability that they are actually presented.

This exercise takes a small TSV in which steps 1 through 4 have already been completed and runs through step 5. Because it does not perform step 6, its output consists of **peptide candidates before neoantigen prediction**. "Neoantigen candidates" in the title refers to the overall goal; these files alone do not establish validated neoantigens.

### Three kinds of evidence prevent different errors

Tumour DNA, normal DNA, and tumour RNA are not redundant checks that confirm the same fact three times.

| Evidence | What passing it supports | What it cannot establish alone |
| --- | --- | --- |
| Tumour DNA | The variant allele was observed in DNA reads from the tumour sample | Whether it is carried throughout the body or expressed |
| Normal DNA | The same variant signal is below the threshold in matched normal | Whether it is made into protein in the tumour |
| Mutant read in tumour RNA | The variant allele was directly observed in tumour transcripts | Whether it binds DLA and is displayed on the surface |
| Tumour TPM | Transcripts from that gene are present in the sample | Whether all that TPM came from the mutant allele |

The tumour-RNA values `tumor_rna_alt_reads` and `tumor_tpm` also measure different things. TPM can be high while no read directly supports the variant position; a few mutant reads can exist while overall gene expression is low. This PoC checks both conditions.

**This exercise stops at candidate filtering.** It applies four filters to synthetic input and generates 9-mer candidates. It does not include DLA genotyping, peptide-MHC binding prediction, normal-protein similarity checks, toxicity or autoimmunity assessment, manufacturing-sequence design, or experimental validation.

### What does the synthetic input look like?

Each row of the input TSV is one candidate variant. None is an actual Rosie variant. Gene names, protein changes, and numerical values are synthetic examples designed to demonstrate how the filters work.

| Column | Meaning |
| --- | --- |
| `variant_id` | Synthetic variant identifier |
| `wildtype_sequence`, `mutant_sequence` | Wild-type and mutant protein sequence surrounding the variant |
| `mutation_index` | Zero-based position of the changed amino acid in the surrounding sequence |
| `tumor_dna_vaf` | Fraction of tumour DNA reads carrying the variant allele |
| `normal_dna_vaf` | Fraction of normal DNA reads carrying the same variant allele |
| `tumor_rna_alt_reads` | Number of tumour RNA reads directly supporting the variant |
| `tumor_tpm` | Expression of that gene in the tumour |

A VAF of 0.42 means that approximately 42 of 100 tumour DNA reads covering that position support the variant. VAF alone cannot establish tumour-cell fraction or clonality because copy number and sample purity also affect it.

Excluding the sequences, the five actual input rows are:

| ID | gene | Protein change | Tumour DNA VAF | Normal DNA VAF | Tumour RNA mutant reads | Tumour TPM |
| --- | --- | --- | ---: | ---: | ---: | ---: |
| `SYN001` | `KIT` | `p.K9V` | 0.42 | 0.00 | 31 | 38.0 |
| `SYN002` | `TP53` | `p.T10Q` | 0.31 | 0.00 | 18 | 12.4 |
| `SYN003` | `PIK3CA` | `p.I11R` | 0.24 | 0.00 | 1 | 8.1 |
| `SYN004` | `KRAS` | `p.G10D` | 0.36 | 0.18 | 27 | 21.3 |
| `SYN005` | `BRAF` | `p.Q11E` | 0.08 | 0.00 | 9 | 4.2 |

`p.K9V` means that the ninth amino acid in the protein sequence changed from `K`, lysine, to `V`, valine. To suit Python, the TSV's `mutation_index` counts from zero, so this variant has the value `8`. Confusing human-readable protein position 9 with array index 8 places the wrong amino acid at the centre of the peptide.

From the table alone, `SYN004` looks like a strong candidate. Its tumour DNA VAF is 0.36 and it has 27 RNA mutant reads. However, its normal DNA VAF is 0.18, so it fails the tumour-specificity criterion. Conversely, `SYN003` has sufficient tumour DNA VAF and TPM but only one RNA read that directly supports the variant. These two rows demonstrate why each column is kept separate.

### What to check, in order

1. Do the TSV's numbers and protein sequences match the structure expected by the code?
2. Why does each row pass or fail when the four filters are applied?
3. How is the evidence score calculated for variants that pass?
4. Why does one mutated amino acid generate nine 9-mers?
5. Which candidates and risks enter together when one filter is relaxed?
6. What remains before this output can support claims about neoantigens and vaccine targets?

## 2. Ask an agent to prepare the PoC

Run Codex CLI or Claude Code in an empty working directory and give it the prompt below. The agent downloads the synthetic input and script into a separate project directory and checks only their structure. It does not run the candidate filter yet.

### Exercise environment setup prompt

```text
Prepare the Rosie neoantigen candidate-filtering PoC environment.

Requirements:
1. Check whether the current directory is already the rosie-neoantigen-poc project. If not, create rosie-neoantigen-poc under the current directory and work inside it.
2. Check that Python 3 is installed and print its version. The candidate filter shortlist.py uses only the standard library, so do not install external Python packages during this setup step. The later results-figure step may use matplotlib only.
3. Download the two files below. If a file with the same name already exists, do not overwrite it; inspect its size and contents first.
   - https://homegenomics.org/data/practice/rosie-neoantigen-poc/demo_variants.tsv
   - https://homegenomics.org/data/practice/rosie-neoantigen-poc/shortlist.py
4. Verify that demo_variants.tsv is a UTF-8 TSV with six lines including the header and the same number of columns in every data row.
5. Check syntax with python3 -m py_compile shortlist.py. Do not run shortlist.py yet.
6. Create an outputs directory and add __pycache__/ to .gitignore. Do not exclude the input TSV or Python script from Git.
7. Report in English the files created or checked, Python version, TSV row and column counts, and syntax-check result, then stop.

If a download fails, do not invent replacement data. Report the failed URL and error. If you cannot confirm whether an existing file matches the remote file, do not overwrite it; stop.
```

Immediately after setup, the following files exist.

- `demo_variants.tsv`: DNA, RNA, and protein-sequence evidence for five synthetic variants
- `shortlist.py`: filter, evidence-score, and 9-mer generation logic
- `outputs/`: directory for later results and sensitivity-analysis tables

The tables and figures on this page were reproduced on August 16, 2026 by downloading the same files into a separate working directory and running them again. Use the following files to compare the results directly.

- [TSV of the 18 peptides that passed](/data/practice/rosie-neoantigen-poc/results/candidate_peptides.tsv)
- [Per-variant filter audit TSV](/data/practice/rosie-neoantigen-poc/results/filter_audit.tsv)
- [TSV of the nine SYN001 windows](/data/practice/rosie-neoantigen-poc/results/syn001_peptide_windows.tsv)
- [Filter-sensitivity results TSV](/data/practice/rosie-neoantigen-poc/results/sensitivity.tsv)
- [analysis.py used to generate and recalculate the figures](/data/practice/rosie-neoantigen-poc/analysis.py) and its [pyproject.toml](/data/practice/rosie-neoantigen-poc/pyproject.toml)

Do not combine the prompts below into one. Give them to the agent one at a time from the top. Inspect the actual table and code evidence from each step before moving to the next.

## 3. Narrow candidates one step at a time with prompts

### 3-1. Check that the input table and code speak the same language

First read the input and code without running them. If the TSV column names do not match the names required by the code, stop before producing results.

### Step 1 prompt: inspect input and filter structure

```text
Read the Rosie PoC input TSV and shortlist.py and explain only their structure.

1. Read demo_variants.tsv with Python's csv standard library and summarize the column names, row count, and expected Python type of each column.
2. Show the five variants in a table ordered by variant_id, gene, protein_change, tumor_dna_vaf, normal_dna_vaf, tumor_rna_alt_reads, and tumor_tpm.
3. For each row, verify that wildtype_sequence and mutant_sequence have equal lengths, mutation_index is in range, and exactly one amino acid differs at that position.
4. Explain which input columns rejection_reason(), evidence_score(), and mutant_peptides() in shortlist.py read and what each returns.
5. Accurately tabulate the four filter thresholds and the order in which they are checked in the code.
6. Expand the three components and weights of evidence_score as formulas, and find evidence in the code that it is not an immunogenicity score.
7. Do not run shortlist.py or modify files yet. Report in English whether input and code disagree, then stop.
```

The default script requires all four conditions.

```text
normal DNA VAF ≤ 0.02
tumor DNA VAF ≥ 0.10
tumor RNA mutant read ≥ 3
tumor TPM ≥ 1.0
```

These values were chosen to make each filter visible in a five-row synthetic input. Other datasets require thresholds that account for sequencing depth, an error model, variant-caller quality values, tumour purity, and the study protocol.

The code separates filters from scores. `rejection_reason()` determines whether a candidate has the minimum evidence, and `evidence_score()` ranks candidates that pass by evidence strength. A variant that fails a filter does not produce output peptides even if its score is high.

The evidence score first constrains three inputs to the range from 0 to 1.

$$
d = \min\left(\frac{\text{tumor DNA VAF}}{0.5}, 1\right)
$$

$$
r = \min\left(\frac{\text{tumor RNA mutant reads}}{20}, 1\right)
$$

$$
e = \min\left(\frac{\log_2(\text{tumor TPM}+1)}{6}, 1\right)
$$

It then assigns 40% each to direct DNA and RNA evidence and 20% to overall gene expression.

$$
\text{evidence score} = 0.4d + 0.4r + 0.2e
$$

The weights and denominators are rules for the synthetic PoC, not a clinically validated model. A score of 0.9 must not be interpreted as a 90% probability of an immune response.

### 3-2. Run the default filters and inspect rejection reasons

In this step, focus first on **why each row remains or is rejected**, not on the final candidate count.

### Step 2 prompt: run the default candidate filters

```text
Run the default filters once on the validated synthetic input and audit the results.

1. Run python3 shortlist.py demo_variants.tsv -o outputs/candidate_peptides.tsv --lang en.
2. Preserve and show the PASS, REJECT, and WROTE lines from standard output.
3. Make an audit table applying all four filters to each variant. Use the columns variant_id, normal DNA, tumor DNA, mutant RNA, tumor TPM, final decision, and rejection reason, and save it as outputs/filter_audit.tsv.
4. If a row violates several conditions at once, compare the code with the actual output to determine whether all reasons are reported.
5. Confirm that outputs/candidate_peptides.tsv was created and inspect its row count, column names, and peptide count by variant.
6. As an assertion-like validation, confirm that only SYN001 and SYN002 pass and that exactly 18 peptides are produced in the default data. If not, investigate the cause and do not modify files arbitrarily.
7. Explain why SYN004 is rejected despite looking strong in tumor DNA and RNA, connecting the reason to the role of matched normal.
8. Plot the evidence_score of all five variants as horizontal bars and save it as outputs/evidence-score.png at 180 dpi. Distinguish PASS and REJECT with text as well as colour, and place SYN004's rejection reason beside its bar. Add matplotlib only in this step if the plot requires it.
9. Inspect the figure directly and confirm that its title and labels are not clipped.
10. Report the command run, actual output, and validation results in English, then stop.

Do not change filter thresholds or predict DLA binding yet.
```

The expected result is:

```text
PASS    SYN001  9 peptides
PASS    SYN002  9 peptides
REJECT  SYN003  insufficient mutant RNA reads
REJECT  SYN004  seen in normal DNA
REJECT  SYN005  insufficient tumor DNA VAF
WROTE   outputs/candidate_peptides.tsv  18 rows
```

`SYN004` looks strong in tumour DNA and RNA, but its normal DNA VAF is also 0.18. Ignoring this column could incorrectly retain a variant carried by normal cells as a tumour-specific candidate.

Read the decision on each of the five rows like this:

| ID | normal DNA | tumour DNA | mutant RNA | TPM | Score | Result |
| --- | --- | --- | --- | --- | ---: | --- |
| `SYN001` | 0.00, pass | 0.42, pass | 31, pass | 38.0, pass | 0.912 | PASS |
| `SYN002` | 0.00, pass | 0.31, pass | 18, pass | 12.4, pass | 0.733 | PASS |
| `SYN003` | 0.00, pass | 0.24, pass | 1, fail | 8.1, pass | 0.318 | REJECT |
| `SYN004` | 0.18, fail | 0.36, pass | 27, pass | 21.3, pass | 0.837 | REJECT |
| `SYN005` | 0.00, pass | 0.08, fail | 9, pass | 4.2, pass | 0.323 | REJECT |

[![Evidence scores for five synthetic variants. SYN004 has the second-highest score but fails the normal-DNA filter.](/images/practice/rosie-neoantigen-poc/evidence-score.png)](/images/practice/rosie-neoantigen-poc/evidence-score.png)

*The current experiment's `outputs/evidence-score.png`. Teal variants pass all four filters, while grey variants fail at least one. Although `SYN004`, at 0.837, scores higher than `SYN002`, at 0.733, it is excluded because of the normal-DNA evidence.*

Calculate the score for `SYN001` with the actual values. Its tumour DNA VAF of 0.42 becomes `0.42 ÷ 0.5 = 0.84`. Its 31 RNA mutant reads produce `31 ÷ 20`, which exceeds 1 and is therefore capped at 1. Its TPM of 38 becomes `log2(38 + 1) ÷ 6 ≈ 0.881`.

$$
0.4(0.84) + 0.4(1) + 0.2(0.881) \approx 0.912
$$

Normal DNA VAF does not enter this calculation. Normal DNA is a safety filter that determines whether a variant passes; after that, the evidence score uses only tumour DNA, tumour RNA, and TPM. That is why `SYN004`, which is also found in normal DNA, is excluded from the final results even though its score is high at 0.837.

The unit carrying the score of 0.912 is the `SYN001` variant, not an individual peptide. The nine peptides generated from the same variant have not yet been assessed for sequence-specific binding, so they all inherit the same score.

### 3-3. Trace the creation of 9-mers from one variant

MHC class I displays short peptides cut from intracellular proteins on the cell surface. This PoC fixes the peptide length at 9 and moves a window so that the mutated amino acid appears once in every position from the first through the last. With enough sequence on both sides, one variant generates nine candidates.

### Step 3 prompt: trace 9-mer generation logic

```text
Keep the default filter results and trace 9-mer generation for SYN001.

1. Read wildtype_sequence, mutant_sequence, and mutation_index for SYN001 from demo_variants.tsv. Do not modify the file.
2. Explain with the actual values that mutation_index is zero-based while output peptide_start is one-based.
3. Call shortlist.mutant_peptides() and show the nine windows generated for SYN001 in start-position order.
4. Include peptide_start, wildtype_peptide, mutant_peptide, and the one-based position of the mutated amino acid within the peptide in the table.
5. Verify that every peptide has length 9, that wild type and mutant differ by exactly one amino acid, and that the difference corresponds to the original mutation_index.
6. Save the table as outputs/syn001_peptide_windows.tsv and compare the SYN001 rows in outputs/candidate_peptides.tsv against the direct function output.
7. Draw the nine aligned windows over the 21-amino-acid mutant sequence and save the figure as outputs/syn001-peptide-windows.png at 180 dpi. Mark the mutant amino acid V with both colour and a black border.
8. Explain why all nine peptides from the same variant have the same evidence_score.
9. Compare the sequence, start, and mutation position within each peptide in the figure against the TSV.
10. Report the validation results and variant types unsupported by the code in English, then stop.

Do not install a new peptide-prediction tool or invent DLA-binding scores.
```

The first three output rows look like this:

```text
variant_id  gene  protein_change  peptide_start  wildtype_peptide  mutant_peptide  evidence_score
SYN001      KIT   p.K9V           1              KPMYEVQWK         KPMYEVQWV        0.912
SYN001      KIT   p.K9V           2              PMYEVQWKV         PMYEVQWVV        0.912
SYN001      KIT   p.K9V           3              MYEVQWKVV         MYEVQWVVV        0.912
```

Across all nine windows for `SYN001`, the `K → V` mutation moves one position at a time from the right end of the peptide to the left end.

| peptide start | wild-type 9-mer | mutant 9-mer | Mutation position in peptide |
| ---: | --- | --- | ---: |
| 1 | `KPMYEVQWK` | `KPMYEVQWV` | 9 |
| 2 | `PMYEVQWKV` | `PMYEVQWVV` | 8 |
| 3 | `MYEVQWKVV` | `MYEVQWVVV` | 7 |
| 4 | `YEVQWKVVE` | `YEVQWVVVE` | 6 |
| 5 | `EVQWKVVEE` | `EVQWVVVEE` | 5 |
| 6 | `VQWKVVEEI` | `VQWVVVEEI` | 4 |
| 7 | `QWKVVEEIN` | `QWVVVEEIN` | 3 |
| 8 | `WKVVEEING` | `WVVVEEING` | 2 |
| 9 | `KVVEEINGN` | `VVVEEINGN` | 1 |

[![Nine 9-mers created by moving the starting point one position at a time over the SYN001 mutant sequence. The orange V moves from peptide position 9 to position 1.](/images/practice/rosie-neoantigen-poc/syn001-peptide-windows.png)](/images/practice/rosie-neoantigen-poc/syn001-peptide-windows.png)

*The current experiment's `outputs/syn001-peptide-windows.png`. The top row is the 21-amino-acid mutant sequence, and the rows below are the 9-mers cut from it. The black-bordered `V` is the original protein's ninth amino acid in every row.*

The first window begins at the first amino acid of the surrounding sequence. The mutation originally at position 9 of the protein falls in the final position of this 9-mer. In the second window, whose start moves one position to the right, the same mutation falls in position 8. When the start position is 9, the mutation lies in the first position.

The code restricts possible starts so that windows do not cross either end of the sequence. If a mutation is near the beginning or end of the surrounding sequence, it produces fewer than nine 9-mers. Every variant in this synthetic input has enough amino acids on both sides to produce nine.

The one-amino-acid difference between `wildtype_peptide` and `mutant_peptide` is a candidate signal that the immune system could use to distinguish tumour from normal. It does not mean that every mutant peptide is actually produced or displayed on the cell surface.

`evidence_score` is a **ranking score** combining tumour DNA VAF, mutant RNA reads, and expression in the range from 0 to 1. The nine peptides from one variant share the score because peptide-specific DLA binding and processing have not yet been calculated.

The location of the mutation within a peptide can change its DLA binding because the DLA binding groove reads certain peptide positions strongly. The nine candidates therefore remain separate for the next binding-prediction stage rather than being discarded as duplicates. This PoC does not score one position as better than another.

`mutant_peptides()` raises an error if the wild-type and mutant sequences have different lengths. The current code therefore does not support variants such as insertions, deletions, and frameshifts that change protein length. Real neoantigen pipelines need separate logic for frameshift candidates, which can create an entirely new sequence downstream of a variant.

### 3-4. Compare the cost of relaxing filters

A filter is not a switch that finds the correct answer. It is a [choice between missed and incorrectly included candidates](/en/reference/classification-metrics/#case-evaluating-a-variant-candidate-filter). Compare the choices in a separate threshold-sensitivity analysis without modifying the original script or overwriting its results.

### Step 4 prompt: filter sensitivity experiment

```text
Add a filter sensitivity analysis without modifying the original demo_variants.tsv or shortlist.py.

1. Write a separate sensitivity.py that creates outputs/sensitivity.tsv. Use only the standard library for aggregation, and reuse only the matplotlib added in step 2 for the figure.
2. Apply these four scenarios to the same five input variants.
   - baseline: the original four thresholds
   - rna_relaxed: relax the minimum mutant RNA read count from 3 to 1
   - no_normal_filter: remove only the normal DNA threshold
   - tumor_vaf_relaxed: relax the minimum tumor DNA VAF from 0.10 to 0.05
3. Record the passing variant_id values, variant_id values newly added relative to baseline, variant count, and number of 9-mers that would be generated in each scenario.
4. Keep everything except the one stated threshold identical to baseline. Reuse calculations from the original functions where possible, but do not create a copy of the original file through string replacement.
5. Verify that SYN003 is added in rna_relaxed, SYN004 in no_normal_filter, and SYN005 in tumor_vaf_relaxed.
6. Plot the number of 9-mers in each scenario as horizontal bars and save the figure as outputs/sensitivity.png at 180 dpi. Distinguish baseline and relaxed scenarios with labels as well as colour, and write each newly admitted variant_id beside its bar.
7. Explain the broader candidate inclusion and the accompanying false-positive risk or loss of safety evidence for each relaxation, connecting it to the input values.
8. Rerun python3 sensitivity.py and compare the numbers in the figure with outputs/sensitivity.tsv. Report the files created or modified and the results, then stop.

Report only what these five rows show. Do not turn the scenario thresholds into recommended thresholds for another dataset, and do not invent peptide-MHC binding or treatment-effect scores.
```

Lowering the RNA criterion from three reads to one adds `SYN003`. Candidate inclusion becomes broader, but so does the risk of accepting a sequencing error or misalignment supported by only one read. Because this synthetic input has no validated truth labels, it cannot establish how much the classification metric recall, also called sensitivity, increased.

Removing the normal DNA filter lets `SYN004` pass. It increases the candidate count but discards important evidence of tumour specificity. Likewise, lowering the tumour DNA VAF threshold accepts weaker tumour-DNA signals but creates more room for error signals.

The expected core of `outputs/sensitivity.tsv` is:

| Scenario | Passing variants | Variant count | 9-mer count |
| --- | --- | ---: | ---: |
| `baseline` | `SYN001,SYN002` | 2 | 18 |
| `rna_relaxed` | `SYN001,SYN002,SYN003` | 3 | 27 |
| `no_normal_filter` | `SYN001,SYN002,SYN004` | 3 | 27 |
| `tumor_vaf_relaxed` | `SYN001,SYN002,SYN005` | 3 | 27 |

[![The number of 9-mers under the baseline filter and three relaxed scenarios. Each relaxation adds one variant and nine peptides.](/images/practice/rosie-neoantigen-poc/sensitivity.png)](/images/practice/rosie-neoantigen-poc/sensitivity.png)

*The current experiment's `outputs/sensitivity.png`. All three relaxed scenarios increase the count from 18 to 27, but each admits a different variant and gives up different evidence.*

Each relaxation adds one candidate and nine peptides, but the quality of information gained is not the same.

`rna_relaxed` rescues a variant that looks clear in DNA but has weak RNA evidence. This can help when low RNA sequencing depth causes a truly transcribed variant to be missed, but one read provides weak grounds for separating base-calling errors and alignment artifacts.

`no_normal_filter` means looking only at signals observed in the tumour. Ignoring `SYN004`'s normal DNA VAF of 0.18 risks calling a germline variant, or one also present in normal cells, tumour-specific. Of the three scenarios, this most directly removes safety evidence.

`tumor_vaf_relaxed` rescues `SYN005`, whose tumour DNA VAF is 0.08. It can capture more real variants in a low-purity tumour or a subclone, but it can also admit low-frequency sequencing noise. Real analysis examines not only VAF but total depth at the position, strand bias, base quality, and the caller's error model.

This table does not recommend thresholds. It is an experiment with five synthetic rows that separates **which failure each criterion prevents**. Real thresholds must be based on the assay's error rate and the cost of validation.

## 4. Stages before and after this PoC in a real pipeline

The PoC TSV has already completed several difficult stages. Starting from actual FASTQ adds the following work before and after it.

1. QC and reference-genome alignment of tumour and normal DNA and tumour RNA
2. Matched tumour-normal somatic variant calling
3. Transcript and coding-consequence annotation
4. Calculation of RNA expression and mutant-allele support
5. Inference of the individual's DLA genotype
6. Generation of mutant-protein sequences and peptides
7. Prioritization by DLA binding, processing, and similarity to the normal proteome
8. Specialist review and experimental validation

Each simple TSV column hides source evidence like this:

| TSV column | Information to record alongside it in a real pipeline |
| --- | --- |
| `tumor_dna_vaf` | Tumour alt-read count, total depth, base quality, strand bias, caller filter |
| `normal_dna_vaf` | Matched-normal alt-read count and depth, possible contamination |
| `tumor_rna_alt_reads` | RNA reference and alt-read counts, splice-aware alignment and mapping quality |
| `tumor_tpm` | Transcript annotation used, quantification tool and version |
| `protein_change` | Genome build, transcript ID, consequence-annotation version |
| Protein sequence | Selected transcript and method for applying normal and mutant alleles |

For example, a tumour DNA VAF of 0.10 can come from either `1/10` or `100/1000`. The ratio is the same, but the second has support from far more reads. This PoC lacks total depth and cannot distinguish the two situations.

`normal_dna_vaf = 0.00` also does not mean the variant is absolutely absent from normal cells. You must check that there was sufficient depth at that position and zero variant reads. A depth of zero means it was not observed, not that absence was established.

A protein change can differ by transcript as well. The same genome variant may be missense in one transcript and outside the coding region in another. A real pipeline records not only a gene symbol such as `TP53` but also the transcript ID and annotation release, so the same peptide can be generated again.

The public description of Rosie's project says that the team "checked whether sites of damage in DNA also appeared in the tumour RNA instructions." That phrase compresses steps 2 through 4 above. This exercise takes those results as a small table and proceeds through step 6.

### What changes at the DLA-binding stage?

In the current results, all nine peptides from `SYN001` have a score of 0.912. Adding DLA-binding prediction produces a different affinity or percentile rank for every peptide-sequence and DLA-allele combination. The mutation's position in the peptide also becomes important at this stage.

The same peptide can bind strongly or barely at all depending on the individual's DLA allele. Without Rosie's DLA genotype, peptide sequences alone cannot reproduce which candidates would be presented. Substituting another dog's DLA results does not reproduce Rosie's result.

Passing binding prediction is still not the end. The cell must actually cut the protein, the peptide must pass transporter and processing stages, and it must reach DLA on the cell surface. Finally, an experiment must confirm whether T cells recognize the peptide-DLA complex.

:::note[Why AlphaFold is absent]
The first gate for a neoantigen candidate is whether the mutant peptide binds the relevant DLA and is presented. A visible difference in the complete protein's three-dimensional structure cannot by itself determine that binding or a T-cell response. This exercise does not treat reports that AlphaFold was used in the Rosie project as equivalent to the role of a validated neoantigen predictor.
:::

## 5. What this PoC established and what remains

What this run establishes is small but clear. Four kinds of observations can be checked mechanically to exclude unsuitable variants and reproducibly generate mutant-peptide candidates from passing protein variants.

Questions still unanswered include:

- Does this peptide bind strongly to Rosie's DLA?
- Is it actually cut inside the cell and presented on the surface?
- Is it sufficiently different from the normal canine proteome?
- Do T cells recognize it and kill tumour cells?
- In what order and spacing should multiple targets be placed in an mRNA construct?

That is why the output file is called `candidate_peptides.tsv`, not `vaccine.tsv`. The computation narrowed candidates; it did not make a treatment.

### Boundaries to preserve when reading the result file

| Observed result | Supported statement | Statement not yet supported |
| --- | --- | --- |
| `SYN001` is PASS | It passed all four synthetic criteria | It is a validated somatic mutation in a real tumour |
| Nine mutant peptides generated | The 9-mers containing the mutated amino acid were enumerated | All nine are produced in the cell |
| Evidence score 0.912 | It ranks highly under this PoC's DNA and RNA rules | The probability of an immune response is 91.2% |
| Normal DNA VAF 0.00 | The normal alt fraction is 0 in the synthetic table | Germline possibility was completely excluded at sufficient depth |
| 31 mutant RNA reads | The synthetic table contains evidence of variant transcripts | Protein production and cell-surface presentation were confirmed |

Preserving these boundaries does not understate the computational result. It states precisely only what the current input and method actually support.

## Review questions

1. Why must a variant pass the normal DNA filter even when its tumour DNA VAF is high?
2. Why lower the priority of a DNA variant when it has zero mutant RNA reads?
3. Why generate multiple 9-mers from one variant?
4. Why can a peptide with a high `evidence_score` not be called highly immunogenic?
5. Which currently unpublished inputs are required to reproduce the actual Rosie analysis?

### Sources

- [UNSW: how seven neoantigen signals were found in Rosie's tumour and normal DNA and RNA](https://news.unsw.edu.au/en/meet-the-man-who-designed-a-cancer-vaccine-for-his-dog)
- [pVACtools: official documentation connecting variant annotation to MHC-binding prediction and candidate selection](https://pvactools.readthedocs.io/)
- [Research on canine MHC class I and tumour immunotherapy](https://pmc.ncbi.nlm.nih.gov/articles/PMC5362671/)
- [Project Rosie: synthetic-data explanation for a separate open-source canine demo](https://github.com/shashank-padala/project-rosie/blob/cfeb3a0662b035bbebb79198753ea41013d9ff32/docs/explainers/05-canine-data-run.md)

[^dla]: **DLA** (dog leukocyte antigen): the canine major histocompatibility complex, or MHC. It displays peptides cut from intracellular proteins on the cell surface, where T cells inspect the peptide-DLA complex. Because DLA alleles differ among individuals, the same peptide may or may not be presented in different dogs.