view snpEff_cds_report.py @ 5:85b933b7d231

Make the reporting of the SnpEff Amino_Acid_change field an option
author Jim Johnson <jj@umn.edu>
date Fri, 24 May 2013 08:50:03 -0500
parents 429a6b4df5e5
children a64ef0611117
line wrap: on
line source

#!/usr/bin/python
## version 1.2
import sys,os,tempfile,re
import httplib, urllib
import optparse
#import vcfClass
#from vcfClass import *
#import tools
#from tools import *

debug = False
cds_anchor = 'cds_variation'
aa_anchor = 'aa_variation'

codon_map = {"UUU":"F", "UUC":"F", "UUA":"L", "UUG":"L",
    "UCU":"S", "UCC":"S", "UCA":"S", "UCG":"S",
    "UAU":"Y", "UAC":"Y", "UAA":"*", "UAG":"*",
    "UGU":"C", "UGC":"C", "UGA":"*", "UGG":"W",
    "CUU":"L", "CUC":"L", "CUA":"L", "CUG":"L",
    "CCU":"P", "CCC":"P", "CCA":"P", "CCG":"P",
    "CAU":"H", "CAC":"H", "CAA":"Q", "CAG":"Q",
    "CGU":"R", "CGC":"R", "CGA":"R", "CGG":"R",
    "AUU":"I", "AUC":"I", "AUA":"I", "AUG":"M",
    "ACU":"T", "ACC":"T", "ACA":"T", "ACG":"T",
    "AAU":"N", "AAC":"N", "AAA":"K", "AAG":"K",
    "AGU":"S", "AGC":"S", "AGA":"R", "AGG":"R",
    "GUU":"V", "GUC":"V", "GUA":"V", "GUG":"V",
    "GCU":"A", "GCC":"A", "GCA":"A", "GCG":"A",
    "GAU":"D", "GAC":"D", "GAA":"E", "GAG":"E",
    "GGU":"G", "GGC":"G", "GGA":"G", "GGG":"G",}

def reverseComplement(seq) :
  rev = seq[::-1].lower().replace('u','A').replace('t','A').replace('a','T').replace('c','G').replace('g','C').upper()
  return rev

def translate(seq) :
  rna = seq.upper().replace('T','U')
  aa = []
  for i in range(0,len(rna) - 2, 3):
    aa.append(codon_map[rna[i:i+3]])
  return ''.join(aa)

"""
SNfEffect vcf reported variations to the reference sequence, so need to reverse complement for coding sequences on the negative strand
Queries that request a sequence always return the sequence in the first column regardless of the order of attributes.
"""
query_ccds_template = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Query>
<Query  virtualSchemaName = "default" formatter = "TSV" header = "0" uniqueRows = "0" count = "" datasetConfigVersion = "0.6" >
	<Dataset name = "__ENSEMBL_DATASET_HERE__" interface = "default" >
		<Filter name = "ensembl_transcript_id" value = "__YOUR_ENSEMBL_TRANSCRIPT_ID_HERE__"/>
		<Filter name = "with_ccds" excluded = "0"/>
		<Attribute name = "ensembl_transcript_id" />
		<Attribute name = "ccds" />
	</Dataset>
</Query>
"""
ccds_filter = '<Filter name = "with_ccds" excluded = "0"/>'
query_template = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Query>
<Query  virtualSchemaName = "default" formatter = "TSV" header = "1" uniqueRows = "1" count = "" datasetConfigVersion = "0.6" >
        <Dataset name = "__ENSEMBL_DATASET_HERE__" interface = "default" >
                <Filter name = "ensembl_transcript_id" value = "__YOUR_ENSEMBL_TRANSCRIPT_ID_HERE__"/>
                <Filter name = "biotype" value = "protein_coding"/>
                __CCDS_FILTER_HERE__
                <Attribute name = "cdna" />
                <Attribute name = "ensembl_gene_id" />
                <Attribute name = "ensembl_transcript_id" />
                <Attribute name = "strand" />
                <Attribute name = "transcript_start" />
	        <Attribute name = "transcript_end"/>
	        <Attribute name = "exon_chrom_start"/>
	        <Attribute name = "exon_chrom_end"/>
		<Attribute name = "cdna_coding_start" />
		<Attribute name = "cdna_coding_end" />
	        <Attribute name = "cds_length"/>
	        <Attribute name = "rank"/>
                <Attribute name = "5_utr_start" />
                <Attribute name = "5_utr_end" />
                <Attribute name = "3_utr_start" />
                <Attribute name = "3_utr_end" />
	        <Attribute name = "ensembl_peptide_id"/>
		<Attribute name = "start_position" />
		<Attribute name = "end_position" />
        </Dataset>
</Query>
"""
"""
Columns(19):
['cDNA sequences', 'Ensembl Gene ID', 'Ensembl Transcript ID', 'Strand', 'Transcript Start (bp)', 'Transcript End (bp)', 'Exon Chr Start (bp)', 'Exon Chr End (bp)', 'cDNA coding start', 'cDNA coding end', 'CDS Length', 'Exon Rank in Transcript', "5' UTR Start", "5' UTR End", "3' UTR Start", "3' UTR End", 'Ensembl Protein ID', 'Gene Start (bp)', 'Gene End (bp)']

  0	cDNA sequence
  1	Ensembl Gene ID
  2	Ensembl Transcript ID
  3	Strand
- 4	Transcript Start (bp)
- 5	Transcript End (bp)
  6	Exon Chr Start (bp)
  7	Exon Chr End (bp)
  8	CDS Start
  9	CDS End
  10	CDS Length
- 11	Exon Rank in Transcript
  12	5' UTR Start
  13	5' UTR End
  14	3' UTR Start
  15	3' UTR End
- 16	Ensembl Protein ID
- 17	Gene Start (bp)
- 18	Gene End (bp)
"""

# return {transcript_id : ccds_id} 
def getCcdsIDs(bimoart_host, ensembl_dataset, transcript_ids):
  ccds_dict = dict()
  if transcript_ids:
    query = query_ccds_template
    query = re.sub('__ENSEMBL_DATASET_HERE__',ensembl_dataset if ensembl_dataset else 'hsapiens_gene_ensembl',query)
    query = re.sub('__YOUR_ENSEMBL_TRANSCRIPT_ID_HERE__',','.join(transcript_ids),query)
    params = urllib.urlencode({'query':query})
    headers = {"Accept": "text/plain"}
    if debug: print >> sys.stdout, "CCDS Query: %s\n" % (query)
    try:
      if debug: print >> sys.stdout, "Ensembl Host: %s\n" % (bimoart_host)
      conn = httplib.HTTPConnection(bimoart_host if bimoart_host != None else 'www.biomart.org')
      conn.request("POST", "/biomart/martservice", params, headers)
      response = conn.getresponse()
      data = response.read()
      if len(data) > 0:
        # if debug: print  >> sys.stdout, "%s\n\n" % data
        lines = data.split('\n')
        for line in lines:
          fields = line.split('\t')
          if len(fields) == 2:
            (transcript_id,ccds) = fields
            ccds_dict[transcript_id] = ccds
        if debug: print >> sys.stdout, "CCDS response: %s\n" % (lines)
    except Exception, e:
      sys.stdout.write( "transcript_ids: %s  %s\n" % (transcript_ids, e) )
  return ccds_dict

def getBiomartQuery(transcript_id,ensembl_dataset,filter_ccds=True):
  query = query_template
  query = re.sub('__ENSEMBL_DATASET_HERE__',ensembl_dataset if ensembl_dataset else 'hsapiens_gene_ensembl',query)
  query = re.sub('__YOUR_ENSEMBL_TRANSCRIPT_ID_HERE__',transcript_id,query)
  query = re.sub('__CCDS_FILTER_HERE__',ccds_filter if filter_ccds else '',query)
  return query

# return [ensembl_gene_id,ensembl_transcript_id,strand,cds_pos,cds_ref,cds_alt,coding_sequence, variant_sequence]
def getEnsemblInfo(snpEffect,bimoart_host,ensembl_dataset,filter_ccds=True):
  transcript_id = snpEffect.transcript
  chr_pos = snpEffect.pos
  query = getBiomartQuery(transcript_id,ensembl_dataset,filter_ccds)
  if debug: print  >> sys.stdout, "getEnsemblInfo:\n%s\n" % (query)
  params = urllib.urlencode({'query':query})
  headers = {"Accept": "text/plain"}
  pos = snpEffect.pos
  cds_pos = None  # zero based offset
  coding_sequence = ''
  cds_ref = snpEffect.ref
  cds_alt = snpEffect.alt
  try: 
    if debug: print >> sys.stdout, "Ensembl Host: %s\n" % (bimoart_host)
    conn = httplib.HTTPConnection(bimoart_host if bimoart_host != None else 'www.biomart.org')
    conn.request("POST", "/biomart/martservice", params, headers)
    response = conn.getresponse()
    data = response.read()
    if len(data) > 0:
      # if debug: print  >> sys.stdout, "%s\n\n" % data
      lines = data.split('\n')
      # Use the header line to determine the order of fields
      line = lines[0]
      # if debug: print  >> sys.stdout, "Headers:\n%s\n" % line
      colnames = line.split('\t')
      # for i in range(len(colnames)):
      #
      if debug: print  >> sys.stdout, "Columns(%d):\n%s\n" % (len(colnames), colnames)
      for line in lines[1:]:
        if line == None or len(line) < 2: 
          continue
        field = line.split('\t')
        if len(field) != len(colnames):
          continue
        if debug: print  >> sys.stdout, "Entry(%d):\n%s\n" % (len(field),line)
        if field[10] == None or len(field[10]) < 1:
          continue
        if debug:
          for i in range(len(colnames)):
            print  >> sys.stdout, "%d\t%s :\n%s\n" % (i,colnames[i],field[i])
        snpEffect.gene_id = field[1]
        strand = '+' if int(field[3]) > 0 else '-'
        snpEffect.strand = strand
        # coding_sequence is first
        if len(field) > 10 and re.match('[ATGCN]+',field[0]):
          if field[10] == None or len(field[10]) < 1:
            continue
          cdna_seq = field[0]
          in_utr = False
          # Could be mutliple UTRs exons - sum up lengths for cds offset into cdna sequence
          utr_5_starts = sorted(eval('[' + re.sub(';',',',field[12]) + ']'),reverse=(strand == '-'))
          utr_5_ends = sorted(eval('[' + re.sub(';',',',field[13]) + ']'),reverse=(strand == '-'))
          utr_5_lens = []
          for i in range(len(utr_5_starts)):
            utr_5_start = int(utr_5_starts[i])
            utr_5_end = int(utr_5_ends[i])
            utr_5_lens.append(abs(utr_5_end - utr_5_start) + 1)
            if utr_5_start <= pos  <= utr_5_end :
              in_utr = True
              if debug: print  >> sys.stdout, "in utr_5: %s     %s     %s\n" % (utr_5_start,pos,utr_5_end);
              break
          utr_3_starts = sorted(eval('[' + re.sub(';',',',field[14]) + ']'),reverse=(strand == '-'))
          utr_3_ends = sorted(eval('[' + re.sub(';',',',field[15]) + ']'),reverse=(strand == '-'))
          for i in range(len(utr_3_starts)):
            utr_3_start = int(utr_3_starts[i])
            utr_3_end = int(utr_3_ends[i])
            if utr_3_start <= pos  <= utr_3_end :
              in_utr = True
              if debug: print  >> sys.stdout, "in utr_3: %s     %s     %s\n" % (utr_3_start,pos,utr_3_end);
              break
          # Get coding sequence from cdna
          cdna_length = int(field[10])
          cdna_coding_start =  sorted(eval('[' + re.sub(';',',',field[8]) + ']'))
          cdna_coding_end  = sorted(eval('[' + re.sub(';',',',field[9]) + ']'))
          cdna_lens = [] 
          for i in range(len(cdna_coding_start)):
            cdna_lens.append(cdna_coding_end[i] - cdna_coding_start[i] + 1)
          if debug: print  >> sys.stdout, "cdna_coding (%d):\n %s\n %s\n %s\n" % (len(cdna_coding_start),cdna_coding_start,cdna_coding_end,cdna_lens)
          cdna_cds_offset = cdna_coding_start[0] - 1 # 0-based array offet
          for i in range(len(cdna_coding_start)):
            if debug: print  >> sys.stdout, "%s\n" % cdna_seq[cdna_coding_start[i]-1:cdna_coding_end[i]+1]
            coding_sequence += cdna_seq[cdna_coding_start[i]-1:cdna_coding_end[i]]
          snpEffect.cds = coding_sequence
          if coding_sequence and len(coding_sequence) >= 3:
            snpEffect.cds_stop_codon = coding_sequence[-3:]
          if debug: print  >> sys.stdout, "coding_seq (%d from %d):\n%s" % (len(coding_sequence),cdna_length,coding_sequence)
          if debug: print  >> sys.stdout, "cdna_coding (%d):\n %s\n %s\n %s\n" % (len(cdna_coding_start),cdna_coding_start,cdna_coding_end,cdna_lens)
          if debug: print  >> sys.stdout, "5_utr %s %s %s\n" % (utr_5_starts,utr_5_ends,utr_5_lens)
          if  not in_utr:
            exon_start = sorted(eval('[' + re.sub(';',',',field[6]) + ']'),reverse=(strand == '-'))
            exon_end = sorted(eval('[' + re.sub(';',',',field[7]) + ']'),reverse=(strand == '-'))
            exon_rank = sorted(eval('[' + re.sub(';',',',field[11]) + ']'),reverse=(strand == '-'))
            exon_lens = [] 
            for i in range(len(exon_start)):
              exon_lens.append(exon_end[i] - exon_start[i] + 1)
            if debug: print  >> sys.stdout, "exons (%d) strand = %s :\n %s\n %s\n %s\n" % (len(exon_start),strand,exon_start,exon_end, exon_lens)
            if debug: 
              bp_tot = 0
              for i in range(len(exon_start)):
                exon_len = exon_end[i] - exon_start[i] + 1
                bp_tot += exon_len
                print >> sys.stdout, "test: %s     %s     %s     %s     %s    %d   %d\n" % (exon_start[i],pos,exon_end[i],exon_start[i] < pos < exon_end[i], pos - exon_start[i], exon_len, bp_tot)
            cds_idx = 0
            for i in range(len(exon_start)):
              # Does this exon have cds bases - i.e. not entirely in UTR
              if len(utr_5_ends) > 0:
                if strand == '+' and len(utr_5_ends) > 0 and exon_end[i] <= utr_5_ends[-1]:
                  continue
                if strand == '-' and len(utr_5_starts) > 0 and exon_start[i] >= utr_5_starts[-1]:
                  continue
              exon_len = exon_end[i] - exon_start[i] + 1
              if exon_start[i] <= pos <= exon_end[i]:
                if strand == '+':
                  if cds_idx: # find offset from start of cdna_coding and exon
                    offset = pos - exon_start[i]
                  else: # find offset from end of cdna_coding and exon
                    offset = pos - ( exon_start[i] if len(utr_5_ends) < 1 else max(exon_start[i], utr_5_ends[-1]+1) )
                  cds_pos = cdna_coding_start[cds_idx] - cdna_coding_start[0] + offset
                else:  # negative strand
                  cds_ref = reverseComplement(snpEffect.ref)
                  cds_alt = reverseComplement(snpEffect.alt)
                  offset = ( exon_end[i] if len(utr_5_starts) < 1 else min(exon_end[i], utr_5_starts[-1] -1) ) - pos
                  cds_pos = cdna_coding_start[cds_idx] - cdna_coding_start[0] + offset - (len(cds_ref) - 1)
                snpEffect.cds_pos = cds_pos
                snpEffect.cds_ref = cds_ref
                snpEffect.cds_alt = cds_alt
                return snpEffect
              else:  
                cds_idx += 1
  except Exception, e:
    sys.stdout.write( "transcript_id: %s %s %s\n" % (transcript_id,chr_pos, e) )
  finally:
    if conn != None :
      conn.close()
  return None

"""
  Effect ( Effect_Impact | Functional_Class | Codon_Change | Amino_Acid_change| Amino_Acid_length | Gene_Name | Gene_BioType | Coding | Transcript | Exon [ | ERRORS | WARNINGS ] )
  FRAME_SHIFT(HIGH||||745|CHUK|protein_coding|CODING|ENST00000370397|exon_10_101964263_101964414)
"""
class SnpEffect( object ):
  report_headings = ['Gene','Variant Position','Reference','Variation','Penetrance','Sequencing Depth','Transcript','AA Position','AA change','AA Length','Stop Codon','Stop Region','AA Variation']
  def __init__(self,chrom,pos,ref,alt,freq,depth,effect = None, snpEffVersion = None, biomart_host = None, filter_ccds = False):
    self.chrom = chrom
    self.pos = int(pos)
    self.ref = ref
    self.alt = alt
    self.freq = float(freq) if freq else None
    self.depth = int(depth) if depth else None
    # From SnpEffect VCF String
    self.effect = None
    self.effect_impact = None
    self.functional_class = None
    self.codon_change = None
    self.amino_acid_change = None
    self.amino_acid_length = None
    self.gene_name = None
    self.gene_id = None
    self.gene_biotype = None
    self.coding = None
    self.transcript = None
    self.transcript_ids = None
    self.exon = None
    self.cds_stop_codon = None
    self.cds_stop_pos = None
    # retrieve from ensembl
    self.strand = None
    self.cds_pos = None
    self.cds_ref = None
    self.cds_alt = None
    self.cds = None
    self.aa_pos = None
    self.aa_len = None
    self.alt_stop_pos = None
    self.alt_stop_codon = None
    self.alt_stop_region = None  ## includes base before and after alt_stop_codon
    self.use_eff_aa_change = False
  def chrPos(self):
    return "%s:%s" % (self.chrom,self.pos)
  def setEffect(self, effect, snpEffVersion = None):
    if snpEffVersion and snpEffVersion == '2':
      (effect_impact,functional_class,codon_change,amino_acid_change,gene_name,gene_biotype,coding,transcript,exon) = effect[0:9]
    else: # SnpEffVersion 3  # adds Amino_Acid_length field
      (effect_impact,functional_class,codon_change,amino_acid_change,amino_acid_length,gene_name,gene_biotype,coding,transcript,exon) = effect[0:10]
      self.amino_acid_length = amino_acid_length
    self.effect_impact = effect_impact
    self.functional_class = functional_class
    self.codon_change = codon_change
    self.amino_acid_change = amino_acid_change
    self.gene_name = gene_name
    self.gene_biotype = gene_biotype
    self.coding = coding
    self.transcript = transcript
    self.exon = exon
  def parseEffect(self, effect, snpEffVersion = None):
    (eff,effs) = effect.rstrip(')').split('(')
    self.effect = eff
    eff_fields = effs.split('|')
    if debug: print >> sys.stdout, "parseEffect:\n\t%s\n\t%s\n" % (effect,eff_fields)
    self.setEffect(eff_fields, snpEffVersion)
  def fetchEnsemblInfo(self,biomart_host=None,ensembl_dataset=None,filter_ccds=False):
    getEnsemblInfo(self,biomart_host,ensembl_dataset,filter_ccds)
    return len(self.cds) > 0 if self.cds else False
  def score(self):
    return self.freq * self.depth if self.freq and self.depth else -1
  def getCodingSequence(self):
    return self.cds
  def getVariantSequence(self):
    if self.cds and self.cds_pos and self.cds_ref and self.cds_alt:
      return self.cds[:self.cds_pos] + self.cds_alt + self.cds[self.cds_pos+len(self.cds_ref):]
    else:
      if debug: print >> sys.stdout, "getVariantSequence:  %s\t%s\t%s\t%s\n" % (str(self.cds_pos) if self.cds_pos else 'no pos', self.cds_ref, self.cds_alt, self.cds)
      return None
  def getCodingAminoSequence(self):
    seq = translate(self.cds) if self.cds else None
    if seq:
      self.aa_len = len(seq)
    return seq
  def getVariantAminoSequence(self):
    variant_seq = self.getVariantSequence()
    return translate(variant_seq) if variant_seq else None
  def getVariantPeptide(self,toStopCodon=True):
    (coding_seq, alt_seq, pos, coding_aa, var_aa, var_aa_pos, var_aa_end) = self.getSequences()
    if var_aa:
      if toStopCodon:
        end_pos = var_aa_end
      else:
        end_pos = len(var_aa)
      novel_peptide = var_aa[var_aa_pos:end_pos]
      return novel_peptide
    return None
  # [preAA,varAA,postAA, varPeptide, varOffset, subAA]
  def getNonSynonymousPeptide(self,start_count,end_count,toStopCodon=True):
    (coding_seq, alt_seq, pos, coding_aa, var_aa, var_aa_pos, var_aa_end) = self.getSequences()
    if var_aa:
      start_pos = max(var_aa_pos - start_count,0) if start_count and int(start_count) >= 0 else 0
      if toStopCodon:
        end_pos = var_aa_end
      else:
        end_offset = end_count + 1
        end_pos = min(var_aa_pos+end_offset,len(var_aa)) if end_offset and int(end_offset) >= 0 else var_aa_end
      try:
        varAA = var_aa[var_aa_pos] if var_aa_pos < len(var_aa) else '_' 
        if debug: print >> sys.stdout, "HGVS %s %s pos:\t%d %d %d" % (self.transcript, self.effect, start_pos, var_aa_pos, end_pos)
        mutation = "%s%d%s" % (coding_aa[var_aa_pos],var_aa_pos+1,varAA)
        preAA = var_aa[start_pos:var_aa_pos] # N-term side
        postAA = var_aa[var_aa_pos+1:end_pos] if var_aa_pos+1 < len(var_aa) else '' # C-term side
        novel_peptide = var_aa[start_pos:end_pos]
        return [preAA,varAA,postAA,novel_peptide,var_aa_pos - start_pos,mutation]
      except Exception, e:
        sys.stdout.write( "getNonSynonymousPeptide:%s %s\n" % (self.transcript,e) )
    return None
  # [coding_dna, variant_dna, cds_pos, coding_aa, variant_aa, aa_start, aa_stop]
  # positions are 0-based array indexes 
  def getSequences(self):
    coding_dna = self.getCodingSequence()
    coding_aa = self.getCodingAminoSequence()
    var_dna = self.getVariantSequence()
    var_aa = self.getVariantAminoSequence()
    var_aa_pos = None
    var_aa_end = None
    if var_aa:
      var_aa_pos = self.cds_pos / 3
      for j in range(var_aa_pos,min(len(var_aa),len(coding_aa))):
        if var_aa[j] != coding_aa[j]:
          var_aa_pos = j
          break
      self.aa_pos = var_aa_pos
      var_stop = var_aa.find('*',var_aa_pos)
      if var_stop < 0:
        var_aa_end = len(var_aa)
      else:
        var_aa_end = var_stop + 1 # include the stop codon 
        stop_pos = var_stop * 3
        self.alt_stop_pos = stop_pos
        self.alt_stop_codon = var_dna[stop_pos:stop_pos+3]
        self.alt_stop_region = (var_dna[stop_pos - 1] + '-' if stop_pos > 0 else '') + \
                               self.alt_stop_codon + \
                               ( '-' + var_dna[stop_pos+3] if len(var_dna) > stop_pos+3  else '')
    return [coding_dna,var_dna,self.cds_pos,coding_aa,var_aa, var_aa_pos, var_aa_end]
  def inHomopolymer(self,polyA_limit):
    if polyA_limit:   ## library prep can results in inserting or deleting an A in a poly A region
      ## check if base changes at cds_pos involve A or T 
      bdiff = None # the difference between the cds_ref and cds_alt
      boff = 0 # the offset to the difference
      if len(self.cds_ref) < len(self.cds_alt):
        bdiff = re.sub(self.cds_ref,'',self.cds_alt)
        boff = self.cds_alt.find(bdiff)
      elif len(self.cds_alt) < len(self.cds_ref):
        bdiff = re.sub(self.cds_alt,'',self.cds_ref)
        boff = self.cds_ref.find(bdiff)
      if bdiff:
        ## check the number of similar base neighboring the change
        alt_seq = self.getVariantSequence()
        subseq = alt_seq[max(self.cds_pos-polyA_limit-2,0):min(self.cds_pos+polyA_limit+2,len(alt_seq))]
        ## adjust match length if the cps_pos is within polyA_limit form sequence end
        match_len = min(self.cds_pos,min(len(alt_seq)-self.cds_pos - boff,polyA_limit))
        if debug: print >> sys.stdout, "polyA bdiff %s   %s  %d  %d\n" % (bdiff,subseq, match_len, boff)
        ## Pattern checks for match of the repeated base between the polyA_limit and the length of the sub sequence times
        if bdiff.find('A') >= 0:
          pat = '^(.*?)(A{%d,%d})(.*?)$' % (match_len,len(subseq))
          match = re.match(pat,subseq)
          if debug: print >> sys.stdout, "%s %s %s  %s %s\n" % (self.transcript, self.cds_ref, self.cds_alt, subseq, match.groups() if match else 'no match')
          if match:
            return True
        if bdiff.find('T') >= 0:
          pat = '^(.*?)(T{%d,%d})(.*?)$' % (match_len,len(subseq))
          match = re.match(pat,subseq)
          if debug: print >> sys.stdout, "%s %s %s  %s %s\n" % (self.transcript, self.cds_ref, self.cds_alt, subseq, match.groups() if match else 'no match')
          if match:
            if debug: print >> sys.stdout, "%s %s %s  %s %s\n" % (self.transcript, self.cds_ref, self.cds_alt, subseq, match.groups())
            return True
    return False  
  def getReportHeader():
    return report_headings
  def getReportFields(self):
    gene_name = self.gene_name  if self.gene_name else ''
    cds_ref = self.cds_ref if self.cds_ref else ''
    cds_alt = self.cds_alt if self.cds_alt else ''
    hgvs = self.getNonSynonymousPeptide(10,10,toStopCodon=False)
    if debug: print >> sys.stdout, "HGVS %s" % hgvs
    var_aa = self.getVariantPeptide(toStopCodon=True)
    freq = "%2.2f" % self.freq if self.freq else ''
    depth = "%d" % self.depth if self.depth else ''
    aa_pos = "%d" % (self.aa_pos + 1) if self.aa_pos else ''
    aa_len = "%d" % self.aa_len if self.aa_len else ''
    gene_tx_id = '|'.join([self.gene_id,self.transcript]) if self.gene_id else self.transcript
    stop_codon = self.alt_stop_codon if self.alt_stop_codon else ''
    stop_region = self.alt_stop_region if self.alt_stop_region else ''
    chrpos = self.chrPos()
    aa_change = self.amino_acid_change if (self.use_eff_aa_change and self.amino_acid_change) else hgvs[5]
    return [gene_name,chrpos,cds_ref,cds_alt,freq,depth,gene_tx_id,aa_pos,aa_change,aa_len,stop_codon,stop_region,var_aa if var_aa else '']

def __main__():

  def printReportTsv(output_file, snpEffects):
    if output_file:
      print >> output_file, "# %s" % '\t'.join(SnpEffect.report_headings)
      for snpEffect in snpEffects:
        (gene_name,chrpos,cds_ref,cds_alt,freq,depth,transcript_name,alt_aa_pos,aa_change,aa_len,stop_codon,stop_region,novel_peptide) = snpEffect.getReportFields()
        if not snpEffect.effect == 'FRAME_SHIFT':
          (preAA,varAA,postAA, allSeq, varOffset, subAA) = snpEffect.getNonSynonymousPeptide(10,10,toStopCodon=False)
          novel_peptide = "%s_%s_%s" %  (preAA,varAA,postAA)
        print >> output_file, "%s" % '\t'.join([gene_name,chrpos,cds_ref,cds_alt,freq,depth,transcript_name,alt_aa_pos,aa_change,aa_len,stop_region,novel_peptide])

  """
  http://www.ensembl.org/Homo_sapiens/Search/Details?species=Homo_sapiens;idx=Gene;end=1;q=CCDC111
  http://www.ensembl.org/Homo_sapiens/Search/Results?species=Homo_sapiens;idx=;q=ENST00000314970
  http://www.ensembl.org/Homo_sapiens/Transcript/Summary?g=ENSG00000164306;r=4:185570821-185616117;t=ENST00000515774
  """
  def printReportHtml(output_file, detail_dir, snpEffects):
    if output_file:
      print >> output_file, '<HTML><BODY>\n'
      print >> output_file, '<TABLE BORDER="1">\n'
      print >> output_file, '<TR align="LEFT"><TH>Gene</TH><TH>Variant Position</TH><TH>Reference</TH><TH>Variation</TH><TH>Penetrance</TH><TH>Sequencing Depth</TH><TH>Transcript Details</TH><TH>AA Position</TH><TH>AA Change</TH><TH>AA Length</TH> <TH>Stop Codon</TH><TH>AA Variation</TH></TR>\n'
      for snpEffect in snpEffects:
        (gene_name,chrpos,cds_ref,cds_alt,freq,depth,transcript_name,alt_aa_pos,aa_change,aa_len,stop_codon,stop_region,novel_peptide) = snpEffect.getReportFields()
        refname = '_'.join([snpEffect.transcript,str(snpEffect.pos),snpEffect.ref,snpEffect.alt]) + '.html'
        aa_ref = '#'.join([refname,aa_anchor])
        refpath = os.path.join(detail_dir,refname)
        try:
          ref_file = open(refpath,'w')
          printEffDetailHtml(ref_file,snpEffect)
          ref_file.close()
        except Exception, e:
          sys.stderr.write( "SnpEffect:%s %s\n" % (refpath,e) )
        if snpEffect.effect == 'FRAME_SHIFT':
          print >> output_file, '<TR><TD>%s</TD><TD>%s</TD><TD>%s</TD><TD>%s</TD><TD ALIGN=RIGHT>%s</TD><TD ALIGN=RIGHT>%s</TD><TD><A HREF="%s">%s</A></TD><TD ALIGN=RIGHT>%s</TD><TD>%s</TD><TD ALIGN=RIGHT>%s</TD><TD>%s</TD><TD><A HREF="%s">%s</A></TD></TR>\n' % (gene_name,chrpos,cds_ref,cds_alt,freq,depth,refname,transcript_name,alt_aa_pos,aa_change,aa_len,stop_region,aa_ref,novel_peptide)
        else:
          (preAA,varAA,postAA, allSeq, varOffset, subAA) = snpEffect.getNonSynonymousPeptide(10,10,toStopCodon=False)
          print >> output_file, '<TR><TD>%s</TD><TD>%s</TD><TD>%s</TD><TD>%s</TD><TD ALIGN=RIGHT>%s</TD><TD ALIGN=RIGHT>%s</TD><TD><A HREF="%s">%s</A></TD><TD ALIGN=RIGHT>%s</TD><TD>%s</TD><TD ALIGN=RIGHT>%s</TD><TD>%s</TD><TD><A HREF="%s"><small><I>%s</I></small><B>%s</B><small><I>%s</I></small></A></TD></TR>\n' % (gene_name,chrpos,cds_ref,cds_alt,freq,depth,refname,transcript_name,alt_aa_pos,aa_change,aa_len,stop_region,aa_ref, preAA,varAA,postAA)
      print >> output_file, '</TABLE>\n'
      print >> output_file, '</BODY></HTML>\n'

  def printEffDetailHtml(output_file,snpEffect,wrap_len=60):
    (coding_seq, alt_seq, pos, coding_aa, alt_aa, alt_aa_pos, alt_aa_end) = snpEffect.getSequences()
    seq_len = len(coding_seq)
    print >> output_file, '<HTML><BODY>\n'
    print >> output_file, '<TABLE>\n'
    print >> output_file, '<TR><TD>Gene:</TD><TD>%s</TD></TR>\n' % snpEffect.gene_name
    print >> output_file, '<TR><TD>Allele:</TD><TD>%s/%s</TD></TR>\n' % (snpEffect.cds_ref,snpEffect.cds_alt)
    print >> output_file, '<TR><TD>Transcript:</TD><TD>%s|%s</TD></TR>\n' % (snpEffect.gene_id,snpEffect.transcript)
    print >> output_file, '<TR><TD>Transcript Strand:</TD><TD>%s</TD></TR>\n' % snpEffect.strand
    print >> output_file, '<TR><TD>Position in transcript:</TD><TD><A HREF="%s">%d</A></TD></TR>\n' % ("#%s" % cds_anchor,(snpEffect.cds_pos + 1))
    print >> output_file, '</TABLE>\n'
    if alt_aa:
      alt_aa_pos = pos / 3 
      if coding_aa:
        for j in range(alt_aa_pos,min(len(alt_aa),len(coding_aa))):
          if alt_aa[j] != coding_aa[j]:
            alt_aa_pos = j
            break
      alt_aa_end = 0
      for j in range(alt_aa_pos,len(alt_aa)):
        if alt_aa[j] == '*':
          alt_aa_end = j
          break
    seq = []
    for j in range(1,wrap_len+1):
      seq.append('-' if j % 10 else '|')
    hrule = '</TD><TD>'.join(seq)
    print >> output_file, '<TABLE cellspacing="0">'
    for i in range(0,seq_len,wrap_len):
      e = min(i + wrap_len,seq_len)
      sa = i/3 
      ea = (i+wrap_len)/3
      print >> output_file, "<TR STYLE=\"background:#DDD\"><TD ALIGN=RIGHT></TD><TD>%s</TD><TD></TD ALIGN=RIGHT></TR>\n" % (hrule)
      print >> output_file, "<TR><TD BGCOLOR=#DDD ALIGN=RIGHT>%d</TD>" % (i+1)
      for j in range(i,i + wrap_len):
        if j < len(coding_seq):
          if pos == j:
            print >> output_file, "<TD><FONT COLOR=\"#F00\"><A NAME=\"%s\">%s</A></FONT></TD>" % (cds_anchor,coding_seq[j])
          elif pos <= j < pos + len(ref):
            print >> output_file, "<TD><FONT COLOR=\"#F00\">%s</FONT></TD>" % coding_seq[j]
          else:
            print >> output_file, "<TD>%s</TD>" % coding_seq[j]
        else:
          print >> output_file, "<TD></TD>"
      print >> output_file, "<TD BGCOLOR=#DDD ALIGN=RIGHT>%d</TD></TR>\n" % e
      if coding_aa:
        print >> output_file, "<TR><TD BGCOLOR=#DDD ALIGN=RIGHT>%d</TD>" % (sa+1)
        for j in range(sa,ea):
          if j < len(coding_aa):
            print >> output_file, "<TD COLSPAN=\"3\">%s</TD>" % coding_aa[j]
          else:
            print >> output_file, "<TD COLSPAN=\"3\"></TD>"
        print >> output_file, "<TD BGCOLOR=#DDD ALIGN=RIGHT>%d</TD></TR>\n" % ea
      if alt_aa:
        print >> output_file, "<TR><TD BGCOLOR=#DDD ALIGN=RIGHT>%d</TD>" % (sa+1)
        for j in range(sa,ea):
          if j < len(alt_aa):
            if alt_aa_pos == j:
              print >> output_file, "<TD COLSPAN=\"3\" BGCOLOR=\"#8F8\"><A NAME=\"%s\">%s</A></TD>" % (aa_anchor,alt_aa[j])
            elif alt_aa_pos < j <= alt_aa_end:
              print >> output_file, "<TD COLSPAN=\"3\" BGCOLOR=\"#8F8\">%s</TD>" % alt_aa[j]
            else:
              print >> output_file, "<TD COLSPAN=\"3\">%s</TD>" % alt_aa[j]
          else:
            print >> output_file, "<TD COLSPAN=\"3\"></TD>"
        print >> output_file, "<TD BGCOLOR=#DDD ALIGN=RIGHT>%d</TD></TR>\n" % ea
      print >> output_file, "<TR><TD BGCOLOR=#DDD ALIGN=RIGHT>%d</TD>" % (i+1)
      for j in range(i,i + wrap_len):
        if j < len(alt_seq):
          if pos <= j < pos + len(alt):
            print >> output_file, "<TD><FONT COLOR=\"#00F\">%s</FONT></TD>" % alt_seq[j]
          else:
            print >> output_file, "<TD>%s</TD>" % alt_seq[j]
        else:
          print >> output_file, "<TD></TD>"
      print >> output_file, "<TD BGCOLOR=#DDD ALIGN=RIGHT>%d</TD></TR>\n" % e
    print >> output_file, "</TABLE>"
    print >> output_file, "</BODY></HTML>"

  def printSeqText(output_file,snpEffect, wrap_len=120):
    nw = 6
    (coding_seq, alt_seq, pos, coding_aa, alt_aa, alt_aa_pos, alt_aa_end) = snpEffect.getSequences()
    if coding_seq:
      seq_len = max(len(coding_seq),len(alt_seq) if alt_seq != None else 0)
      rule = '---------|---------|---------|---------|---------|---------|---------|---------|---------|---------|---------|---------|'
      for i in range(0,seq_len,wrap_len):
        e = min(i + wrap_len,seq_len)
        sa = i/3 
        ea = e/3
        print >> output_file, "%*s %-*s %*s" % (nw,'',wrap_len,rule[:wrap_len],nw,'')
        print >> output_file, "%*d %-*s %*d" % (nw,i+1,wrap_len,coding_seq[i:e],nw,e)
        if coding_aa:
          print >> output_file, "%*d %-*s %*d" % (nw,sa+1,wrap_len,'  '.join(coding_aa[sa:ea]),nw,ea)
        if alt_aa:
          print >> output_file, "%*d %-*s %*d" % (nw,sa+1,wrap_len,'  '.join(alt_aa[sa:ea]),nw,ea)
        if alt_seq:
          print >> output_file, "%*d %-*s %*d\n" % (nw,i+1,wrap_len,alt_seq[i:e],nw,e)

  # Parse the command line options
  usage = "Usage: snpEff_cds_report.py filter [options]"
  parser = optparse.OptionParser(usage = usage)
  parser.add_option("-i", "--in",
                    action="store", type="string",
                    dest="vcfFile", help="input vcf file")
  parser.add_option("-o", "--out",
                    action="store", type="string",
                    dest="output", help="output report file")
  parser.add_option("-H", "--html_report",
                    action="store", type="string",
                    dest="html_file", help="html output report file")
  parser.add_option("-D", "--html_dir",
                    action="store", type="string",
                    dest="html_dir", help="html output report file")
  parser.add_option("-t", "--tsv_file",
                    action="store", type="string",
                    dest="tsv_file", help="TAB Separated Value (.tsv) output report file")
  parser.add_option("-e", "--ensembl_host",
                    action="store", type="string", 
                    dest="ensembl_host", default='www.biomart.org', help='bimoart ensembl server default: www.biomart.org')
  parser.add_option("-f", "--effects_filter",
                    action="store", type="string", default=None,
                    dest="effects_filter", help="a list of effects to filter")
  parser.add_option("-O", "--ensembl_dataset",
                    action="store", type="string", 
                    dest="ensembl_dataset", default='hsapiens_gene_ensembl', help='bimoart ensembl dataset default: hsapiens_gene_ensembl')
  parser.add_option("-p", "--polyA_limit",
                    action="store", type="int", default=None, help='ignore variants that are in a poly A longer than this value' )
  parser.add_option('-S', '--snpeff_aa_change', dest='snpeff_aa_change', action='store_true', default=False, help='Report the SnpEff Amino_Acid_change field'  )
  parser.add_option('-a', '--all_effects', dest='all_effects', action='store_true', default=False, help='Search for all effects'  )
  parser.add_option('-c', '--with_ccds', dest='with_ccds', action='store_true', default=False, help='Only variants with CCDS entries'  )
  parser.add_option('-d', '--debug', dest='debug', action='store_true', default=False, help='Turn on wrapper debugging to stdout'  )

  (options, args) = parser.parse_args()

  debug = options.debug
  ensembl_host = options.ensembl_host

  # Check that a single vcf file is given.
  if options.vcfFile == None:
    parser.print_help()
    print >> sys.stderr, "\nInput vcf file (-i, --input) is required for variant report "
    exit(1)
  outputFile = None
  outputHtml = None
  detailDir = None
  outputTsv = None
  tmpHtmlName = None
  tmpHtml = None
  effects_list = []
  # Set the output file to stdout if no output file was specified.
  if options.output == None and options.html_file == None and options.tsv_file == None:
    outputFile = sys.stdout
  if options.output != None:
    output = os.path.abspath(options.output)
    outputFile = open(output, 'w')
  if options.tsv_file != None:
    output = os.path.abspath(options.tsv_file)
    outputTsv = open(output, 'w')
  if options.html_file != None:
    output = os.path.abspath(options.html_file)
    outputHtml = open(output, 'w')
    if options.html_dir == None:
      detailDir = os.path.dirname(os.path.abspath(output))
    else:
      detailDir = options.html_dir
  if detailDir:
    if not os.path.isdir(detailDir):
      os.makedirs(detailDir)
  if options.effects_filter:
    eff_codes = options.effects_filter.split(',')
    for val in eff_codes:
      code = val.strip()
      if code:
        effects_list.append(code)
      
  # 
  # Effect ( Effefct_Impact | Codon_Change | Amino_Acid_change | Gene_Name | Gene_BioType | Coding | Transcript | Exon [ | ERRORS | WARNINGS ] )
  try:
    snpEffects = []
    homopolymers = []
    polya_count = 0
    polya_list = []
    fh = open( options.vcfFile )
    while True:
      line = fh.readline()
      if not line:
        break #EOF
      if line:
        if outputFile:
          print >> outputFile, "%s\n" % line
        line = line.strip()
        if len(line) < 1:
          continue
        elif line.startswith('#'):
          # check for SnpEffVersion
          """
          ##SnpEffVersion="2.1a (build 2012-04-20), by Pablo Cingolani"
          ##SnpEffVersion="SnpEff 3.0a (build 2012-07-08), by Pablo Cingolani"
          """
          if line.startswith('##SnpEffVersion='):
            m = re.match('##SnpEffVersion="(SnpEff )*([1-9])+.*$',line)
            if m:
              SnpEffVersion = m.group(2)
          # check for SnpEff Info Description
          """
          ##SnpEffVersion="2.1a (build 2012-04-20), by Pablo Cingolani"
          ##INFO=<ID=EFF,Number=.,Type=String,Description="Predicted effects for this variant.Format: 'Effect ( Effect_Impact | Functional_Class | Codon_Change | Amino_Acid_change | Gene_Name | Gene_BioType | Coding | Transcript | Exon [ | ERRORS | WARNINGS ] )' ">
          ##SnpEffVersion="SnpEff 3.0a (build 2012-07-08), by Pablo Cingolani"
          ##INFO=<ID=EFF,Number=.,Type=String,Description="Predicted effects for this variant.Format: 'Effect ( Effect_Impact | Functional_Class | Codon_Change | Amino_Acid_change| Amino_Acid_length | Gene_Name | Gene_BioType | Coding | Transcript | Exon [ | ERRORS | WARNINGS ] )' ">
          """
          pass
        else:
          if debug: print >> sys.stdout, "%s\n" % line
          freq = None
          depth = None
          fields = line.split('\t')
          (chrom,pos,id,ref,alts,qual,filter,info) = fields[0:8]
          if debug: print  >> sys.stdout, "%s:%s-%s" % (chrom,int(pos)-10,int(pos)+10) 
          if options.debug: print(fields)
          for idx,alt in enumerate(alts.split(',')):
            if options.debug: print >> sys.stdout, "alt: %s from: %s" % (alt,alts)
            if not re.match('^[ATCG]*$',alt):
              print >> sys.stdout, "only simple variant currently supported, ignoring: %s:%s  %s\n" % (chrom,pos,alt)
            for info_item in info.split(';'):
              try:
                if info_item.find('=') < 0:
                  continue
                (key,val) = info_item.split('=',1)  
                if key == 'SAF':   # Usually a SAF for each variant: if  A,AG   then SAF=0.333333333333333,SAF=0.333333333333333;
                  freqs = info_item.split(',')
                  (k,v) = freqs[idx].split('=',1)
                  freq = v
                elif key == 'DP':
                  depth = val
                elif key == 'EFF':
                  transcript_ids = []
                  effects = val.split(',')
                  ccds_dict = None
                  for effect in effects:
                    (eff,effs) = effect.rstrip(')').split('(')
                    eff_fields = effs.split('|')
                    if SnpEffVersion == '2':
                      """
                      Effect ( Effect_Impact | Functional_Class | Codon_Change | Amino_Acid_change | Gene_Name | Gene_BioType | Coding | Transcript | Exon [ | ERRORS | WARNINGS ] )
                      EFF=FRAME_SHIFT(HIGH||||CHUK|protein_coding|CODING|ENST00000370397|exon_10_101964263_101964414)
                      """
                      (impact,functional_class,codon_change,aa_change,gene_name,biotype,coding,transcript,exon) = eff_fields[0:9]
                    else: # SnpEffVersion 3  # adds Amino_Acid_length field
                      """
                      Effect ( Effect_Impact | Functional_Class | Codon_Change | Amino_Acid_change| Amino_Acid_length | Gene_Name | Gene_BioType | Coding | Transcript | Exon [ | ERRORS | WARNINGS ] )
                      FRAME_SHIFT(HIGH||||745|CHUK|protein_coding|CODING|ENST00000370397|exon_10_101964263_101964414)
                      """
                      (impact,functional_class,codon_change,aa_change,aa_len,gene_name,biotype,coding,transcript,exon) = eff_fields[0:10]
                    if transcript:
                      transcript_ids.append(transcript)
                  if debug: print  >> sys.stdout, "transcripts: %s" % (transcript_ids) 
                  if options.with_ccds:
                    ccds_dict = getCcdsIDs(ensembl_host,options.ensembl_dataset,transcript_ids)
                    if debug: print  >> sys.stdout, "ccds_dict: %s" % (ccds_dict) 
                  for effect in effects:
                    snpEffect = SnpEffect(chrom,pos,ref,alt,freq,depth)
                    snpEffect.transcript_ids = transcript_ids
                    snpEffect.parseEffect(effect,snpEffVersion=SnpEffVersion)
                    if options.snpeff_aa_change:
                      snpEffect.use_eff_aa_change = True
                    if effects_list and not snpEffect.effect in effects_list:
                      continue
                    if snpEffect.transcript and (not options.with_ccds or snpEffect.transcript in ccds_dict):
                      if snpEffect.fetchEnsemblInfo(ensembl_host,options.ensembl_dataset,options.with_ccds):
                        if snpEffect.cds_pos:
                          if snpEffect.inHomopolymer(options.polyA_limit):
                            homopolymers.append(snpEffect)
                          else:
                            snpEffects.append(snpEffect)
                    if outputFile:
                      print >> outputFile, "%s" % '\t'.join(snpEffect.getReportFields())
                      printSeqText(outputFile,snpEffect)
                    if snpEffect.cds and snpEffect.cds_pos and not options.all_effects:
                      ## Just report the first sequence returned for a vcf line
                      break
              except Exception, e:
                sys.stderr.write( "line: %s err: %s\n" % (line,e) )
    print >> sys.stdout, "homopolymer changes not reported: %d" % len(homopolymers)
    # Sort snpEffects
    snpEffects.sort(cmp=lambda x,y: cmp(x.score(), y.score()),reverse=True)
    if outputHtml:
      printReportHtml(outputHtml, detailDir, snpEffects)
    if outputTsv:
      printReportTsv(outputTsv,snpEffects)
  except Exception, e:
    print(str(e))         

if __name__ == "__main__": __main__()