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