6c4526f6b5dd50293b01dba4fbc9fe0bfce2fee4
[jalview.git] / src / jalview / io / vcf / VCFLoader.java
1 package jalview.io.vcf;
2
3 import htsjdk.samtools.util.CloseableIterator;
4 import htsjdk.variant.variantcontext.Allele;
5 import htsjdk.variant.variantcontext.VariantContext;
6 import htsjdk.variant.vcf.VCFHeader;
7 import htsjdk.variant.vcf.VCFHeaderLine;
8
9 import jalview.analysis.AlignmentUtils;
10 import jalview.analysis.Dna;
11 import jalview.api.AlignViewControllerGuiI;
12 import jalview.datamodel.AlignmentI;
13 import jalview.datamodel.DBRefEntry;
14 import jalview.datamodel.GeneLoci;
15 import jalview.datamodel.Mapping;
16 import jalview.datamodel.SequenceFeature;
17 import jalview.datamodel.SequenceI;
18 import jalview.ext.ensembl.EnsemblMap;
19 import jalview.ext.htsjdk.VCFReader;
20 import jalview.io.gff.Gff3Helper;
21 import jalview.io.gff.SequenceOntologyI;
22 import jalview.util.MapList;
23 import jalview.util.MappingUtils;
24
25 import java.io.IOException;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Map.Entry;
30
31 /**
32  * A class to read VCF data (using the htsjdk) and add variants as sequence
33  * features on dna and any related protein product sequences
34  * 
35  * @author gmcarstairs
36  */
37 public class VCFLoader
38 {
39   private static final String FEATURE_GROUP_VCF = "VCF";
40
41   private static final String EXCL = "!";
42
43   /*
44    * the alignment we are associated VCF data with
45    */
46   private AlignmentI al;
47
48   /*
49    * mappings between VCF and sequence reference assembly regions, as 
50    * key = "species!chromosome!fromAssembly!toAssembly
51    * value = Map{fromRange, toRange}
52    */
53   private Map<String, Map<int[], int[]>> assemblyMappings;
54
55   /**
56    * Constructor given an alignment context
57    * 
58    * @param alignment
59    */
60   public VCFLoader(AlignmentI alignment)
61   {
62     al = alignment;
63
64     // map of species!chromosome!fromAssembly!toAssembly to {fromRange, toRange}
65     assemblyMappings = new HashMap<String, Map<int[], int[]>>();
66   }
67
68   /**
69    * Loads VCF on to an alignment - provided it can be related to one or more
70    * sequence's chromosomal coordinates.
71    * <p>
72    * This method is not thread safe - concurrent threads should use separate
73    * instances of this class.
74    * 
75    * @param filePath
76    * @param status
77    */
78   public void loadVCF(String filePath, AlignViewControllerGuiI status)
79   {
80     VCFReader reader = null;
81
82     try
83     {
84       // long start = System.currentTimeMillis();
85       reader = new VCFReader(filePath);
86
87       VCFHeader header = reader.getFileHeader();
88       VCFHeaderLine ref = header
89               .getOtherHeaderLine(VCFHeader.REFERENCE_KEY);
90       // check if reference is wrt assembly19 (GRCh37)
91       // todo may need to allow user to specify reference assembly?
92       boolean isRefGrch37 = (ref != null && ref.getValue().contains(
93               "assembly19"));
94
95       int varCount = 0;
96       int seqCount = 0;
97
98       /*
99        * query for VCF overlapping each sequence in turn
100        */
101       for (SequenceI seq : al.getSequences())
102       {
103         int added = loadVCF(seq, reader, isRefGrch37);
104         if (added > 0)
105         {
106           seqCount++;
107           varCount += added;
108           transferAddedFeatures(seq);
109         }
110       }
111       // long elapsed = System.currentTimeMillis() - start;
112       String msg = String.format("Added %d VCF variants to %d sequence(s)",
113               varCount, seqCount);
114       if (status != null)
115       {
116         status.setStatus(msg);
117       }
118     } catch (Throwable e)
119     {
120       System.err.println("Error processing VCF: " + e.getMessage());
121       e.printStackTrace();
122     } finally
123     {
124       if (reader != null)
125       {
126         try
127         {
128           reader.close();
129         } catch (IOException e)
130         {
131           // ignore
132         }
133       }
134     }
135   }
136
137   /**
138    * Transfers VCF features to sequences to which this sequence has a mapping.
139    * If the mapping is 1:3, computes peptide variants from nucleotide variants.
140    * 
141    * @param seq
142    */
143   protected void transferAddedFeatures(SequenceI seq)
144   {
145     DBRefEntry[] dbrefs = seq.getDBRefs();
146     if (dbrefs == null)
147     {
148       return;
149     }
150     for (DBRefEntry dbref : dbrefs)
151     {
152       Mapping mapping = dbref.getMap();
153       if (mapping == null || mapping.getTo() == null)
154       {
155         continue;
156       }
157
158       SequenceI mapTo = mapping.getTo();
159       MapList map = mapping.getMap();
160       if (map.getFromRatio() == 3)
161       {
162         /*
163          * dna-to-peptide product mapping
164          */
165         AlignmentUtils.computeProteinFeatures(seq, mapTo, map);
166       }
167       else
168       {
169         /*
170          * nucleotide-to-nucleotide mapping e.g. transcript to CDS
171          */
172         // TODO no DBRef to CDS is added to transcripts
173         List<SequenceFeature> features = seq.getFeatures()
174                 .getPositionalFeatures(SequenceOntologyI.SEQUENCE_VARIANT);
175         for (SequenceFeature sf : features)
176         {
177           if (FEATURE_GROUP_VCF.equals(sf.getFeatureGroup()))
178           {
179             transferFeature(sf, mapTo, map);
180           }
181         }
182       }
183     }
184   }
185
186   /**
187    * Tries to add overlapping variants read from a VCF file to the given
188    * sequence, and returns the number of overlapping variants found. Note that
189    * this requires the sequence to hold information as to its chromosomal
190    * positions and reference, in order to be able to map the VCF variants to the
191    * sequence.
192    * 
193    * @param seq
194    * @param reader
195    * @param isVcfRefGrch37
196    * @return
197    */
198   protected int loadVCF(SequenceI seq, VCFReader reader,
199           boolean isVcfRefGrch37)
200   {
201     int count = 0;
202     GeneLoci seqCoords = seq.getGeneLoci();
203     if (seqCoords == null)
204     {
205       return 0;
206     }
207
208     List<int[]> seqChromosomalContigs = seqCoords.mapping.getToRanges();
209     for (int[] range : seqChromosomalContigs)
210     {
211       count += addVcfVariants(seq, reader, range, isVcfRefGrch37);
212     }
213
214     return count;
215   }
216
217   /**
218    * Queries the VCF reader for any variants that overlap the given chromosome
219    * region of the sequence, and adds as variant features. Returns the number of
220    * overlapping variants found.
221    * 
222    * @param seq
223    * @param reader
224    * @param range
225    *          start-end range of a sequence region in its chromosomal
226    *          coordinates
227    * @param isVcfRefGrch37
228    *          true if the VCF is with reference to GRCh37
229    * @return
230    */
231   protected int addVcfVariants(SequenceI seq, VCFReader reader,
232           int[] range, boolean isVcfRefGrch37)
233   {
234     GeneLoci seqCoords = seq.getGeneLoci();
235
236     String chromosome = seqCoords.chromosome;
237     String seqRef = seqCoords.assembly;
238     String species = seqCoords.species;
239
240     // TODO handle species properly
241     if ("".equals(species))
242     {
243       species = "human";
244     }
245
246     /*
247      * map chromosomal coordinates from GRCh38 (sequence) to
248      * GRCh37 (VCF) if necessary
249      */
250     // TODO generalise for other assemblies and species
251     int offset = 0;
252     String fromRef = "GRCh38";
253     if (fromRef.equalsIgnoreCase(seqRef) && isVcfRefGrch37)
254     {
255       String toRef = "GRCh37";
256       int[] newRange = mapReferenceRange(range, chromosome, species,
257               fromRef, toRef);
258       if (newRange == null)
259       {
260         System.err.println(String.format(
261                 "Failed to map %s:%s:%s:%d:%d to %s", species, chromosome,
262                 fromRef, range[0], range[1], toRef));
263         return 0;
264       }
265       offset = newRange[0] - range[0];
266       range = newRange;
267     }
268
269     boolean forwardStrand = range[0] <= range[1];
270
271     /*
272      * query the VCF for overlaps
273      * (convert a reverse strand range to forwards)
274      */
275     int count = 0;
276     MapList mapping = seqCoords.mapping;
277
278     int fromLocus = Math.min(range[0], range[1]);
279     int toLocus = Math.max(range[0], range[1]);
280     CloseableIterator<VariantContext> variants = reader.query(chromosome,
281             fromLocus, toLocus);
282     while (variants.hasNext())
283     {
284       /*
285        * get variant location in sequence chromosomal coordinates
286        */
287       VariantContext variant = variants.next();
288
289       /*
290        * we can only process SNP variants (which can be reported
291        * as part of a MIXED variant record
292        */
293       if (!variant.isSNP() && !variant.isMixed())
294       {
295         continue;
296       }
297
298       count++;
299       int start = variant.getStart() - offset;
300       int end = variant.getEnd() - offset;
301
302       /*
303        * convert chromosomal location to sequence coordinates
304        * - null if a partially overlapping feature
305        */
306       int[] seqLocation = mapping.locateInFrom(start, end);
307       if (seqLocation != null)
308       {
309         addVariantFeatures(seq, variant, seqLocation[0], seqLocation[1],
310                 forwardStrand);
311       }
312     }
313
314     variants.close();
315
316     return count;
317   }
318
319   /**
320    * Inspects the VCF variant record, and adds variant features to the sequence.
321    * Only SNP variants are added, not INDELs.
322    * <p>
323    * If the sequence maps to the reverse strand of the chromosome, reference and
324    * variant bases are recorded as their complements (C/G, A/T).
325    * 
326    * @param seq
327    * @param variant
328    * @param featureStart
329    * @param featureEnd
330    * @param forwardStrand
331    */
332   protected void addVariantFeatures(SequenceI seq, VariantContext variant,
333           int featureStart, int featureEnd, boolean forwardStrand)
334   {
335     byte[] reference = variant.getReference().getBases();
336     if (reference.length != 1)
337     {
338       /*
339        * sorry, we don't handle INDEL variants
340        */
341       return;
342     }
343
344     /*
345      * for now we extract allele frequency as feature score; note
346      * this attribute is String for a simple SNP, but List<String> if
347      * multiple alleles at the locus; we extract for the simple case only
348      */
349     Object af = variant.getAttribute("AF");
350     float score = 0f;
351     if (af instanceof String)
352     {
353       try
354       {
355         score = Float.parseFloat((String) af);
356       } catch (NumberFormatException e)
357       {
358         // leave as 0
359       }
360     }
361
362     StringBuilder sb = new StringBuilder();
363     sb.append(forwardStrand ? (char) reference[0] : complement(reference));
364
365     /*
366      * inspect alleles and record SNP variants (as the variant
367      * record could be MIXED and include INDEL and SNP alleles)
368      * warning: getAlleles gives no guarantee as to the order 
369      * in which they are returned
370      */
371     for (Allele allele : variant.getAlleles())
372     {
373       if (!allele.isReference())
374       {
375         byte[] alleleBase = allele.getBases();
376         if (alleleBase.length == 1)
377         {
378           sb.append(",").append(
379                   forwardStrand ? (char) alleleBase[0]
380                           : complement(alleleBase));
381         }
382       }
383     }
384     String alleles = sb.toString(); // e.g. G,A,C
385
386     String type = SequenceOntologyI.SEQUENCE_VARIANT;
387
388     SequenceFeature sf = new SequenceFeature(type, alleles, featureStart,
389             featureEnd, score, FEATURE_GROUP_VCF);
390
391     sf.setValue(Gff3Helper.ALLELES, alleles);
392
393     Map<String, Object> atts = variant.getAttributes();
394     for (Entry<String, Object> att : atts.entrySet())
395     {
396       sf.setValue(att.getKey(), att.getValue());
397     }
398     seq.addSequenceFeature(sf);
399   }
400
401   /**
402    * A convenience method to complement a dna base and return the string value
403    * of its complement
404    * 
405    * @param reference
406    * @return
407    */
408   protected String complement(byte[] reference)
409   {
410     return String.valueOf(Dna.getComplement((char) reference[0]));
411   }
412
413   /**
414    * Determines the location of the query range (chromosome positions) in a
415    * different reference assembly.
416    * <p>
417    * If the range is just a subregion of one for which we already have a mapping
418    * (for example, an exon sub-region of a gene), then the mapping is just
419    * computed arithmetically.
420    * <p>
421    * Otherwise, calls the Ensembl REST service that maps from one assembly
422    * reference's coordinates to another's
423    * 
424    * @param queryRange
425    *          start-end chromosomal range in 'fromRef' coordinates
426    * @param chromosome
427    * @param species
428    * @param fromRef
429    *          assembly reference for the query coordinates
430    * @param toRef
431    *          assembly reference we wish to translate to
432    * @return the start-end range in 'toRef' coordinates
433    */
434   protected int[] mapReferenceRange(int[] queryRange, String chromosome,
435           String species, String fromRef, String toRef)
436   {
437     /*
438      * first try shorcut of computing the mapping as a subregion of one
439      * we already have (e.g. for an exon, if we have the gene mapping)
440      */
441     int[] mappedRange = findSubsumedRangeMapping(queryRange, chromosome,
442             species, fromRef, toRef);
443     if (mappedRange != null)
444     {
445       return mappedRange;
446     }
447
448     /*
449      * call (e.g.) http://rest.ensembl.org/map/human/GRCh38/17:45051610..45109016:1/GRCh37
450      */
451     EnsemblMap mapper = new EnsemblMap();
452     int[] mapping = mapper.getMapping(species, chromosome, fromRef, toRef,
453             queryRange);
454
455     if (mapping == null)
456     {
457       // mapping service failure
458       return null;
459     }
460
461     /*
462      * save mapping for possible future re-use
463      */
464     String key = makeRangesKey(chromosome, species, fromRef, toRef);
465     if (!assemblyMappings.containsKey(key))
466     {
467       assemblyMappings.put(key, new HashMap<int[], int[]>());
468     }
469
470     assemblyMappings.get(key).put(queryRange, mapping);
471
472     return mapping;
473   }
474
475   /**
476    * If we already have a 1:1 contiguous mapping which subsumes the given query
477    * range, this method just calculates and returns the subset of that mapping,
478    * else it returns null. In practical terms, if a gene has a contiguous
479    * mapping between (for example) GRCh37 and GRCh38, then we assume that its
480    * subsidiary exons occupy unchanged relative positions, and just compute
481    * these as offsets, rather than do another lookup of the mapping.
482    * <p>
483    * If in future these assumptions prove invalid (e.g. for bacterial dna?!),
484    * simply remove this method or let it always return null.
485    * <p>
486    * Warning: many rapid calls to the /map service map result in a 429 overload
487    * error response
488    * 
489    * @param queryRange
490    * @param chromosome
491    * @param species
492    * @param fromRef
493    * @param toRef
494    * @return
495    */
496   protected int[] findSubsumedRangeMapping(int[] queryRange, String chromosome,
497           String species, String fromRef, String toRef)
498   {
499     String key = makeRangesKey(chromosome, species, fromRef, toRef);
500     if (assemblyMappings.containsKey(key))
501     {
502       Map<int[], int[]> mappedRanges = assemblyMappings.get(key);
503       for (Entry<int[], int[]> mappedRange : mappedRanges.entrySet())
504       {
505         int[] fromRange = mappedRange.getKey();
506         int[] toRange = mappedRange.getValue();
507         if (fromRange[1] - fromRange[0] == toRange[1] - toRange[0])
508         {
509           /*
510            * mapping is 1:1 in length, so we trust it to have no discontinuities
511            */
512           if (MappingUtils.rangeContains(fromRange, queryRange))
513           {
514             /*
515              * fromRange subsumes our query range
516              */
517             int offset = queryRange[0] - fromRange[0];
518             int mappedRangeFrom = toRange[0] + offset;
519             int mappedRangeTo = mappedRangeFrom + (queryRange[1] - queryRange[0]);
520             return new int[] { mappedRangeFrom, mappedRangeTo };
521           }
522         }
523       }
524     }
525     return null;
526   }
527
528   /**
529    * Transfers the sequence feature to the target sequence, locating its start
530    * and end range based on the mapping. Features which do not overlap the
531    * target sequence are ignored.
532    * 
533    * @param sf
534    * @param targetSequence
535    * @param mapping
536    *          mapping from the feature's coordinates to the target sequence
537    */
538   protected void transferFeature(SequenceFeature sf,
539           SequenceI targetSequence, MapList mapping)
540   {
541     int[] mappedRange = mapping.locateInTo(sf.getBegin(), sf.getEnd());
542   
543     if (mappedRange != null)
544     {
545       String group = sf.getFeatureGroup();
546       int newBegin = Math.min(mappedRange[0], mappedRange[1]);
547       int newEnd = Math.max(mappedRange[0], mappedRange[1]);
548       SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
549               group, sf.getScore());
550       targetSequence.addSequenceFeature(copy);
551     }
552   }
553
554   /**
555    * Formats a ranges map lookup key
556    * 
557    * @param chromosome
558    * @param species
559    * @param fromRef
560    * @param toRef
561    * @return
562    */
563   protected static String makeRangesKey(String chromosome, String species,
564           String fromRef, String toRef)
565   {
566     return species + EXCL + chromosome + EXCL + fromRef + EXCL
567             + toRef;
568   }
569 }