Skip to Content
Silent failuresMy pathogen results include host reads

My pathogen results include host reads

You sequenced a patient sample and called variants against the virus. Some of them are in places the virus is not variable, or are supported by a handful of reads, or look like human sequence once you go and look. The pipeline ran cleanly, the VCF is valid, and nothing said the sample was more than one organism.

What actually happened

A host–pathogen sample is not one organism. A plasma or PBMC sample from a person with HIV-1 contains the person’s nucleic acid as well as the virus’s. A metagenome contains many organisms. A xenograft contains two. The reads in the file are a mixture, and the alignment step has to decide where each one goes.

The standard way to handle that is a combined reference: the pathogen genome and the host genome in one FASTA, so that a host read has a host contig of its own to land on and does not get forced onto the virus. That is the arrangement this page follows. The reference covers two organisms. The alignment made against it is about the same two, because its reads came from both.

The trouble starts when the alignment is described as being about the pathogen alone, which is what the analysis is about and so what is natural to write down. From then on, whoever picks up that alignment is told it is virus-only. They proceed on that belief, and a later step treats everything in the file as viral.

Why nothing crashed

Before v0.2 (milestone M7), a dataset in Ligase could declare one organism. There was no way to write a combined reference down as what it was, and no way to say that an alignment against it covered two organisms. The only available choice was a single name, and the natural one is the pathogen. A reference declared organism="HIV-1" and an alignment declared organism="HIV-1" agree, so the comparison passed.

Everything else was also fine. The alignment’s @SQ header named contigs the reference has, with the lengths and checksums the reference has. The BAM was valid. Each individual fact was true; what was missing was a place to say that the data was about two organisms, and nothing could object to a statement that was never made.

An ordinary tool has the same blind spot for a plain reason. A variant caller sees reads and a reference. It has no concept of what organism the reads came from, so it cannot tell host reads from viral reads, and it has no business guessing.

What Ligase says

The organism dimension is a set. Here the reference says it is about HIV-1 and Homo sapiens, as a combined reference is. The alignment was declared as the pathogen alone, which is how it would have been written before.

DimensionErrorin stepcallligase check — nothing ran
aln.organism=HIV-1declared
ref.organism=HIV-1 and Homo sapiensdeclared
ref also lists Homo sapiens.
These must match. Did you pass data from the wrong organism?

Three things are worth noticing.

The two declarations are shown on adjacent lines, each against the name it is bound to: aln.organism first, then ref.organism. Each is marked (declared), because no alignment header and no FASTA records an organism, so these are statements that a person made. Nothing was inferred.

The line after the two declarations names the difference itself: ref also lists Homo sapiens. You are not left comparing two lists by eye to find the extra member. For a metagenome with a dozen organisms on one side and eleven on the other, that line is the part you read.

And the closing question is the organism dimension’s own, and it was written when organism was a single string. “Did you pass data from the wrong organism?” is a fair question, but it is only half of what the engine can say here. The engine cannot tell which reading applies. The alignment may be declared too narrowly, because it really is host–pathogen. The reference may be declared too broadly. Or the alignment may be the wrong data for this reference. It reports which sets differ and leaves the choice to the person who knows the sample.

The same check, the other way round

The message above shows the combined reference against a pathogen-only alignment. The reverse arrangement is also caught, and it is the one that matches the spurious-mapping worry. If the reference is the virus alone and the sample really is host–pathogen, the host reads that resemble the viral reference, because of a repeat, a low-complexity stretch or an endogenous element, have nowhere else to go and can map to it anyway. They reach the variant caller looking like viral reads that differ from the reference, and are reported as variants. Declare the alignment ["HIV-1", "Homo sapiens"] against that virus-only reference and the engine refuses the join in the same way, with the sides swapped. The detail line reads aln also lists Homo sapiens.

How it is caught

Pass a list wherever the data is about more than one organism, and say the same thing on both sides:

import ligase as lg ref = lg.reference( "data/refs/hxb2_plus_host.fasta", organism=["HIV-1", "Homo sapiens"], ) aln = lg.alignment( "data/alignments/sample01.bam", organism=["HIV-1", "Homo sapiens"], ) # Nothing has run. This reads the FASTA index and the BAM header. lg.check_step("call", {"aln": aln, "ref": ref})

That passes. Change the alignment to organism="HIV-1" alone, which is the declaration this page is about, and the same check_step call refuses with the message shown above, before anything runs. The two value lines follow the order the inputs are bound in, which is why aln comes before ref in the snippet: bind them the other way round and the same two lines come out swapped.

Two organism sets are equal when they name the same organisms, in any order, so ["Homo sapiens", "HIV-1"] and ["HIV-1", "Homo sapiens"] agree. A single string is a set of one.

The reasoning for making organism a set is recorded in the decision log. For how organism sits alongside the other dimensions, see the dimension catalogue; for why the engine compares dimensions rather than trusting files to agree, see types and dimensions.

Last updated on