import htsjdk.variant.vcf.VCFHeader;
import htsjdk.variant.vcf.VCFHeaderLine;
import htsjdk.variant.vcf.VCFHeaderLineCount;
+import htsjdk.variant.vcf.VCFInfoHeaderLine;
import jalview.analysis.AlignmentUtils;
import jalview.analysis.Dna;
*/
public class VCFLoader
{
+ /*
+ * keys to fields of VEP CSQ consequence data
+ * see https://www.ensembl.org/info/docs/tools/vep/vep_formats.html
+ */
+ private static final String ALLELE_KEY = "Allele";
+
+ private static final String ALLELE_NUM_KEY = "ALLELE_NUM"; // 0 (ref), 1...
+ private static final String FEATURE_KEY = "Feature"; // Ensembl stable id
+
+ /*
+ * what comes before column headings in CSQ Description field
+ */
+ private static final String FORMAT = "Format: ";
+
+ /*
+ * default VCF INFO key for VEP consequence data
+ * NB this can be overridden running VEP with --vcf_info_field
+ * - we don't handle this case (require CSQ identifier)
+ */
+ private static final String CSQ = "CSQ";
+
+ /*
+ * separator for fields in consequence data
+ */
+ private static final String PIPE = "|";
+
+ private static final String PIPE_REGEX = "\\" + PIPE;
+
+ /*
+ * key for Allele Frequency output by VEP
+ * see http://www.ensembl.org/info/docs/tools/vep/vep_formats.html
+ */
private static final String ALLELE_FREQUENCY_KEY = "AF";
+ /*
+ * delimiter that separates multiple consequence data blocks
+ */
private static final String COMMA = ",";
- private static final boolean FEATURE_PER_ALLELE = true;
-
+ /*
+ * the feature group assigned to a VCF variant in Jalview
+ */
private static final String FEATURE_GROUP_VCF = "VCF";
+ /*
+ * internal delimiter used to build keys for assemblyMappings
+ *
+ */
private static final String EXCL = "!";
/*
- * the alignment we are associated VCF data with
+ * the alignment we are associating VCF data with
*/
private AlignmentI al;
*/
private VCFHeader header;
+ /*
+ * the position (0...) of field in each block of
+ * CSQ (consequence) data (if declared in the VCF INFO header for CSQ)
+ * see http://www.ensembl.org/info/docs/tools/vep/vep_formats.html
+ */
+ private int csqAlleleFieldIndex = -1;
+ private int csqAlleleNumberFieldIndex = -1;
+ private int csqFeatureFieldIndex = -1;
+
/**
* Constructor given an alignment context
*
header = reader.getFileHeader();
VCFHeaderLine ref = header
.getOtherHeaderLine(VCFHeader.REFERENCE_KEY);
+
+ /*
+ * get offset of CSQ ALLELE_NUM and Feature if declared
+ */
+ locateCsqFields();
+
// check if reference is wrt assembly19 (GRCh37)
// todo may need to allow user to specify reference assembly?
boolean isRefGrch37 = (ref != null && ref.getValue().contains(
*/
for (SequenceI seq : al.getSequences())
{
- int added = loadVCF(seq, reader, isRefGrch37);
+ int added = loadSequenceVCF(seq, reader, isRefGrch37);
if (added > 0)
{
seqCount++;
}
/**
+ * Records the position of selected fields defined in the CSQ INFO header (if
+ * there is one). CSQ fields are declared in the CSQ INFO Description e.g.
+ * <p>
+ * Description="Consequence ...from ... VEP. Format: Allele|Consequence|...
+ */
+ protected void locateCsqFields()
+ {
+ VCFInfoHeaderLine csqInfo = header.getInfoHeaderLine(CSQ);
+ if (csqInfo == null)
+ {
+ return;
+ }
+
+ String desc = csqInfo.getDescription();
+ int formatPos = desc.indexOf(FORMAT);
+ if (formatPos == -1)
+ {
+ System.err.println("Parse error, failed to find " + FORMAT
+ + " in " + desc);
+ return;
+ }
+ desc = desc.substring(formatPos + FORMAT.length());
+
+ if (desc != null)
+ {
+ String[] format = desc.split(PIPE_REGEX);
+ int index = 0;
+ for (String field : format)
+ {
+ if (ALLELE_NUM_KEY.equals(field))
+ {
+ csqAlleleNumberFieldIndex = index;
+ }
+ if (ALLELE_KEY.equals(field))
+ {
+ csqAlleleFieldIndex = index;
+ }
+ if (FEATURE_KEY.equals(field))
+ {
+ csqFeatureFieldIndex = index;
+ }
+ index++;
+ }
+ }
+ }
+
+ /**
* Transfers VCF features to sequences to which this sequence has a mapping.
* If the mapping is 1:3, computes peptide variants from nucleotide variants.
*
/*
* nucleotide-to-nucleotide mapping e.g. transcript to CDS
*/
- // TODO no DBRef to CDS is added to transcripts
List<SequenceFeature> features = seq.getFeatures()
.getPositionalFeatures(SequenceOntologyI.SEQUENCE_VARIANT);
for (SequenceFeature sf : features)
* @param isVcfRefGrch37
* @return
*/
- protected int loadVCF(SequenceI seq, VCFReader reader,
+ protected int loadSequenceVCF(SequenceI seq, VCFReader reader,
boolean isVcfRefGrch37)
{
int count = 0;
GeneLociI seqCoords = seq.getGeneLoci();
if (seqCoords == null)
{
+ System.out.println(String.format(
+ "Can't query VCF for %s as chromosome coordinates not known",
+ seq.getName()));
return 0;
}
*/
VariantContext variant = variants.next();
- /*
- * we can only process SNP variants (which can be reported
- * as part of a MIXED variant record
- */
- if (!variant.isSNP() && !variant.isMixed())
- {
- continue;
- }
-
int start = variant.getStart() - offset;
int end = variant.getEnd() - offset;
/*
* convert chromosomal location to sequence coordinates
+ * - may be reverse strand (convert to forward for sequence feature)
* - null if a partially overlapping feature
*/
int[] seqLocation = mapping.locateInFrom(start, end);
if (seqLocation != null)
{
- count += addVariantFeature(seq, variant, seqLocation[0],
- seqLocation[1], forwardStrand);
+ int featureStart = Math.min(seqLocation[0], seqLocation[1]);
+ int featureEnd = Math.max(seqLocation[0], seqLocation[1]);
+ count += addAlleleFeatures(seq, variant, featureStart, featureEnd,
+ forwardStrand);
}
}
}
/**
- * Inspects the VCF variant record, and adds variant features to the sequence.
- * Only SNP variants are added, not INDELs. Returns the number of features
- * added.
- * <p>
- * If the sequence maps to the reverse strand of the chromosome, reference and
- * variant bases are recorded as their complements (C/G, A/T).
- *
- * @param seq
- * @param variant
- * @param featureStart
- * @param featureEnd
- * @param forwardStrand
- */
- protected int addVariantFeature(SequenceI seq, VariantContext variant,
- int featureStart, int featureEnd, boolean forwardStrand)
- {
- byte[] reference = variant.getReference().getBases();
- if (reference.length != 1)
- {
- /*
- * sorry, we don't handle INDEL variants
- */
- return 0;
- }
-
- if (FEATURE_PER_ALLELE)
- {
- return addAlleleFeatures(seq, variant, featureStart, featureEnd,
- forwardStrand);
- }
-
- /*
- * for now we extract allele frequency as feature score; note
- * this attribute is String for a simple SNP, but List<String> if
- * multiple alleles at the locus; we extract for the simple case only
- */
- float score = getAlleleFrequency(variant, 0);
-
- StringBuilder sb = new StringBuilder();
- sb.append(forwardStrand ? (char) reference[0] : complement(reference));
-
- /*
- * inspect alleles and record SNP variants (as the variant
- * record could be MIXED and include INDEL and SNP alleles)
- * warning: getAlleles gives no guarantee as to the order
- * in which they are returned
- */
- for (Allele allele : variant.getAlleles())
- {
- if (!allele.isReference())
- {
- byte[] alleleBase = allele.getBases();
- if (alleleBase.length == 1)
- {
- sb.append(COMMA).append(
- forwardStrand ? (char) alleleBase[0]
- : complement(alleleBase));
- }
- }
- }
- String alleles = sb.toString(); // e.g. G,A,C
-
- String type = SequenceOntologyI.SEQUENCE_VARIANT;
-
- SequenceFeature sf = new SequenceFeature(type, alleles, featureStart,
- featureEnd, score, FEATURE_GROUP_VCF);
-
- sf.setValue(Gff3Helper.ALLELES, alleles);
-
- Map<String, Object> atts = variant.getAttributes();
- for (Entry<String, Object> att : atts.entrySet())
- {
- sf.setValue(att.getKey(), att.getValue());
- }
- seq.addSequenceFeature(sf);
-
- return 1;
- }
-
- /**
* A convenience method to get the AF value for the given alternate allele
* index
*
}
/**
- * Adds one variant feature for each SNP allele in the VCF variant record, and
+ * Adds one variant feature for each allele in the VCF variant record, and
* returns the number of features added.
*
* @param seq
/**
* Inspects one allele and attempts to add a variant feature for it to the
- * sequence. Only SNP variants are added as features. We extract as much as
- * possible of the additional data associated with this allele to store in the
- * feature's key-value map. Answers the number of features added (0 or 1).
+ * sequence. We extract as much as possible of the additional data associated
+ * with this allele to store in the feature's key-value map. Answers the
+ * number of features added (0 or 1).
*
* @param seq
* @param variant
* @param altAlleleIndex
+ * (0, 1..)
* @param featureStart
* @param featureEnd
* @param forwardStrand
int altAlleleIndex, int featureStart, int featureEnd,
boolean forwardStrand)
{
- byte[] reference = variant.getReference().getBases();
+ String reference = variant.getReference().getBaseString();
Allele alt = variant.getAlternateAllele(altAlleleIndex);
- byte[] allele = alt.getBases();
- if (allele.length != 1)
- {
- /*
- * not a SNP variant
- */
- return 0;
- }
+ String allele = alt.getBaseString();
/*
- * build the ref,alt allele description e.g. "G,A"
+ * build the ref,alt allele description e.g. "G,A", using the base
+ * complement if the sequence is on the reverse strand
*/
+ // TODO check how structural variants are shown on reverse strand
StringBuilder sb = new StringBuilder();
- sb.append(forwardStrand ? (char) reference[0] : complement(reference));
+ sb.append(forwardStrand ? reference : Dna.reverseComplement(reference));
sb.append(COMMA);
- sb.append(forwardStrand ? (char) allele[0] : complement(allele));
+ sb.append(forwardStrand ? allele : Dna.reverseComplement(allele));
String alleles = sb.toString(); // e.g. G,A
String type = SequenceOntologyI.SEQUENCE_VARIANT;
sf.setValue(Gff3Helper.ALLELES, alleles);
- addAlleleProperties(variant, sf, altAlleleIndex);
+ addAlleleProperties(variant, seq, sf, altAlleleIndex);
seq.addSequenceFeature(sf);
* Add any allele-specific VCF key-value data to the sequence feature
*
* @param variant
+ * @param seq
* @param sf
* @param altAlelleIndex
+ * (0, 1..)
*/
- protected void addAlleleProperties(VariantContext variant,
+ protected void addAlleleProperties(VariantContext variant, SequenceI seq,
SequenceFeature sf, final int altAlelleIndex)
{
Map<String, Object> atts = variant.getAttributes();
- /*
- * process variant data, extracting values which are allele-specific
- * these may be per alternate allele (INFO[key].Number = 'A')
- * or per allele including reference (INFO[key].Number = 'R')
- */
for (Entry<String, Object> att : atts.entrySet())
{
String key = att.getKey();
- VCFHeaderLineCount number = header.getInfoHeaderLine(key)
- .getCountType();
+
+ /*
+ * extract Consequence data (if present) that we are able to
+ * associated with the allele for this variant feature
+ */
+ if (CSQ.equals(key))
+ {
+ addConsequences(variant, seq, sf, altAlelleIndex);
+ continue;
+ }
+
+ /*
+ * we extract values for other data which are allele-specific;
+ * these may be per alternate allele (INFO[key].Number = 'A')
+ * or per allele including reference (INFO[key].Number = 'R')
+ */
+ VCFInfoHeaderLine infoHeader = header.getInfoHeaderLine(key);
+ if (infoHeader == null)
+ {
+ /*
+ * can't be sure what data belongs to this allele, so
+ * play safe and don't take any
+ */
+ continue;
+ }
+
+ VCFHeaderLineCount number = infoHeader.getCountType();
int index = altAlelleIndex;
if (number == VCFHeaderLineCount.R)
{
*/
index++;
}
- /*
- * CSQ behaves as if Number=A but declares as Number=.
- * so give it special treatment
- */
- else if (!"CSQ".equals(key) && number != VCFHeaderLineCount.A)
+ else if (number != VCFHeaderLineCount.A)
{
/*
* don't save other values as not allele-related
}
/**
+ * Inspects CSQ data blocks (consequences) and adds attributes on the sequence
+ * feature for the current allele (and transcript if applicable)
+ * <p>
+ * Allele matching: if field ALLELE_NUM is present, it must match
+ * altAlleleIndex. If not present, then field Allele value must match the VCF
+ * Allele.
+ * <p>
+ * Transcript matching: if sequence name can be identified to at least one of
+ * the consequences' Feature values, then select only consequences that match
+ * the value (i.e. consequences for the current transcript sequence). If not,
+ * take all consequences (this is the case when adding features to the gene
+ * sequence).
+ *
+ * @param variant
+ * @param seq
+ * @param sf
+ * @param altAlelleIndex
+ * (0, 1..)
+ */
+ protected void addConsequences(VariantContext variant, SequenceI seq,
+ SequenceFeature sf, int altAlelleIndex)
+ {
+ Object value = variant.getAttribute(CSQ);
+
+ if (value == null || !(value instanceof ArrayList<?>))
+ {
+ return;
+ }
+
+ List<String> consequences = (List<String>) value;
+
+ /*
+ * if CSQ data includes 'Feature', and any value matches the sequence name,
+ * then restrict consequence data to only the matching value (transcript)
+ * i.e. just pick out consequences for the transcript the variant feature is on
+ */
+ String seqName = seq.getName()== null ? "" : seq.getName().toLowerCase();
+ String matchFeature = null;
+ if (csqFeatureFieldIndex > -1)
+ {
+ for (String consequence : consequences)
+ {
+ String[] csqFields = consequence.split(PIPE_REGEX);
+ if (csqFields.length > csqFeatureFieldIndex)
+ {
+ String featureIdentifier = csqFields[csqFeatureFieldIndex];
+ if (featureIdentifier.length() > 4
+ && seqName.indexOf(featureIdentifier.toLowerCase()) > -1)
+ {
+ matchFeature = featureIdentifier;
+ }
+ }
+ }
+ }
+
+ StringBuilder sb = new StringBuilder(128);
+ boolean found = false;
+
+ for (String consequence : consequences)
+ {
+ String[] csqFields = consequence.split(PIPE_REGEX);
+
+ if (includeConsequence(csqFields, matchFeature, variant,
+ altAlelleIndex))
+ {
+ if (found)
+ {
+ sb.append(COMMA);
+ }
+ found = true;
+ sb.append(consequence);
+ }
+ }
+
+ if (found)
+ {
+ sf.setValue(CSQ, sb.toString());
+ }
+ }
+
+ /**
+ * Answers true if we want to associate this block of consequence data with
+ * the specified alternate allele of the VCF variant.
+ * <p>
+ * If consequence data includes the ALLELE_NUM field, then this has to match
+ * altAlleleIndex. Otherwise the Allele field of the consequence data has to
+ * match the allele value.
+ * <p>
+ * Optionally (if matchFeature is not null), restrict to only include
+ * consequences whose Feature value matches. This allows us to attach
+ * consequences to their respective transcripts.
+ *
+ * @param csqFields
+ * @param matchFeature
+ * @param variant
+ * @param altAlelleIndex
+ * (0, 1..)
+ * @return
+ */
+ protected boolean includeConsequence(String[] csqFields,
+ String matchFeature, VariantContext variant, int altAlelleIndex)
+ {
+ /*
+ * check consequence is for the current transcript
+ */
+ if (matchFeature != null)
+ {
+ if (csqFields.length <= csqFeatureFieldIndex)
+ {
+ return false;
+ }
+ String featureIdentifier = csqFields[csqFeatureFieldIndex];
+ if (!featureIdentifier.equals(matchFeature))
+ {
+ return false; // consequence is for a different transcript
+ }
+ }
+
+ /*
+ * if ALLELE_NUM is present, it must match altAlleleIndex
+ * NB first alternate allele is 1 for ALLELE_NUM, 0 for altAlleleIndex
+ */
+ if (csqAlleleNumberFieldIndex > -1)
+ {
+ if (csqFields.length <= csqAlleleNumberFieldIndex)
+ {
+ return false;
+ }
+ String alleleNum = csqFields[csqAlleleNumberFieldIndex];
+ return String.valueOf(altAlelleIndex + 1).equals(alleleNum);
+ }
+
+ /*
+ * else consequence allele must match variant allele
+ */
+ if (csqAlleleFieldIndex > -1 && csqFields.length > csqAlleleFieldIndex)
+ {
+ String csqAllele = csqFields[csqAlleleFieldIndex];
+ String vcfAllele = variant.getAlternateAllele(altAlelleIndex)
+ .getBaseString();
+ return csqAllele.equals(vcfAllele);
+ }
+
+ return false;
+ }
+
+ /**
* A convenience method to complement a dna base and return the string value
* of its complement
*