Package rdkit :: Package Chem :: Module PandasTools

[hide private]
[frames] | no frames]

# Source Code for Module rdkit.Chem.PandasTools

      1  '''
      2  Importing pandasTools enables several features that allow for using RDKit molecules as columns of a Pandas dataframe.
      3  If the dataframe is containing a molecule format in a column (e.g. smiles), like in this example:
      4  >>> from rdkit.Chem import PandasTools
      5  >>> import pandas as pd
      6  >>> import os
      7  >>> from rdkit import RDConfig
      8  >>> antibiotics = pd.DataFrame(columns=['Name','Smiles'])
      9  >>> antibiotics = antibiotics.append({'Smiles':'CC1(C(N2C(S1)C(C2=O)NC(=O)CC3=CC=CC=C3)C(=O)O)C','Name':'Penicilline G'}, ignore_index=True)#Penicilline G
     10  >>> antibiotics = antibiotics.append({'Smiles':'CC1(C2CC3C(C(=O)C(=C(C3(C(=O)C2=C(C4=C1C=CC=C4O)O)O)O)C(=O)N)N(C)C)O','Name':'Tetracycline'}, ignore_index=True)#Tetracycline
     11  >>> antibiotics = antibiotics.append({'Smiles':'CC1(C(N2C(S1)C(C2=O)NC(=O)C(C3=CC=CC=C3)N)C(=O)O)C','Name':'Ampicilline'}, ignore_index=True)#Ampicilline
     12  >>> print([str(x) for x in  antibiotics.columns])
     13  ['Name', 'Smiles']
     14  >>> print(antibiotics)
     15              Name                                             Smiles
     16  0  Penicilline G    CC1(C(N2C(S1)C(C2=O)NC(=O)CC3=CC=CC=C3)C(=O)O)C
     17  1   Tetracycline  CC1(C2CC3C(C(=O)C(=C(C3(C(=O)C2=C(C4=C1C=CC=C4...
     18  2  Ampicilline  CC1(C(N2C(S1)C(C2=O)NC(=O)C(C3=CC=CC=C3)N)C(=O...
     19
     20  a new column can be created holding the respective RDKit molecule objects. The fingerprint can be included to accelerate substructure searches on the dataframe.
     21
     22  >>> PandasTools.AddMoleculeColumnToFrame(antibiotics,'Smiles','Molecule',includeFingerprints=True)
     23  >>> print([str(x) for x in  antibiotics.columns])
     24  ['Name', 'Smiles', 'Molecule']
     25
     26  A substructure filter can be applied on the dataframe using the RDKit molecule column, because the ">=" operator has been modified to work as a substructure check.
     27  Such the antibiotics containing the beta-lactam ring "C1C(=O)NC1" can be obtained by
     28
     29  >>> beta_lactam = Chem.MolFromSmiles('C1C(=O)NC1')
     30  >>> beta_lactam_antibiotics = antibiotics[antibiotics['Molecule'] >= beta_lactam]
     31  >>> print(beta_lactam_antibiotics[['Name','Smiles']])
     32              Name                                             Smiles
     33  0  Penicilline G    CC1(C(N2C(S1)C(C2=O)NC(=O)CC3=CC=CC=C3)C(=O)O)C
     34  2  Ampicilline  CC1(C(N2C(S1)C(C2=O)NC(=O)C(C3=CC=CC=C3)N)C(=O...
     35
     36
     37  It is also possible to load an SDF file can be load into a dataframe.
     38
     39  >>> sdfFile = os.path.join(RDConfig.RDDataDir,'NCI/first_200.props.sdf')
     40  >>> frame = PandasTools.LoadSDF(sdfFile,smilesName='SMILES',molColName='Molecule',includeFingerprints=True)
     41  >>> frame.info # doctest: +SKIP
     42  <bound method DataFrame.info of <class 'pandas.core.frame.DataFrame'>
     43  Int64Index: 200 entries, 0 to 199
     44  Data columns:
     45  AMW                       200  non-null values
     46  CLOGP                     200  non-null values
     47  CP                        200  non-null values
     48  CR                        200  non-null values
     49  DAYLIGHT.FPG              200  non-null values
     50  DAYLIGHT_CLOGP            200  non-null values
     51  FP                        200  non-null values
     52  ID                        200  non-null values
     53  ISM                       200  non-null values
     54  LIPINSKI_VIOLATIONS       200  non-null values
     55  NUM_HACCEPTORS            200  non-null values
     56  NUM_HDONORS               200  non-null values
     57  NUM_HETEROATOMS           200  non-null values
     58  NUM_LIPINSKIHACCEPTORS    200  non-null values
     59  NUM_LIPINSKIHDONORS       200  non-null values
     60  NUM_RINGS                 200  non-null values
     61  NUM_ROTATABLEBONDS        200  non-null values
     62  P1                        30  non-null values
     63  SMILES                    200  non-null values
     64  Molecule                  200  non-null values
     65  dtypes: object(20)>
     66
     67  Conversion to html is quite easy:
     68  >>> htm = frame.to_html()
     69  >>> str(htm[:36])
     70  '<table border="1" class="dataframe">'
     71
     72  In order to support rendering the molecules as images in the HTML export of the dataframe, the __str__ method is monkey-patched to return a base64 encoded PNG:
     73  >>> molX = Chem.MolFromSmiles('Fc1cNc2ccccc12')
     74  >>> print(molX) # doctest: +SKIP
     75  <img src="data:image/png;base64,..." alt="Mol"/>
     76  This can be reverted using the ChangeMoleculeRendering method
     77  >>> ChangeMoleculeRendering(renderer='String')
     78  >>> print(molX) # doctest: +SKIP
     79  <rdkit.Chem.rdchem.Mol object at 0x10d179440>
     80  >>> ChangeMoleculeRendering(renderer='PNG')
     81  >>> print(molX) # doctest: +SKIP
     82  <img src="data:image/png;base64,..." alt="Mol"/>
     83
     84  '''
     85  from __future__ import print_function
     86
     87  from base64 import b64encode
     88  import types,copy
     89
     90  from rdkit.six import BytesIO, string_types
     91  from rdkit import Chem
     92  from rdkit.Chem import Draw
     93
     94  try:
     95    import pandas as pd
     96    try:
     97      v = pd.__version__.split('.')
     98    except AttributeError:
     99      # support for older versions of pandas
    100      v = pd.version.version.split('.')
    101
    102
    103    if v[0]=='0' and int(v[1])<10:
    104      print("Pandas version %s not compatible with tests"%v, file=sys.stderr)
    105      pd = None
    106    else:
    107      if 'display.width' in  pd.core.config._registered_options:
    108        pd.set_option('display.width',1000000000)
    109      if 'display.max_rows' in  pd.core.config._registered_options:
    110        pd.set_option('display.max_rows',1000000000)
    111      elif 'display.height' in  pd.core.config._registered_options:
    112        pd.set_option('display.height',1000000000)
    113      if 'display.max_colwidth' in  pd.core.config._registered_options:
    114        pd.set_option('display.max_colwidth',1000000000)
    115      #saves the default pandas rendering to allow restauration
    116      defPandasRendering = pd.core.frame.DataFrame.to_html
    117  except ImportError:
    118    import traceback
    119    traceback.print_exc()
    120    pd = None
    121
    122  except Exception as e:
    123    import sys
    124    import traceback
    125    traceback.print_exc()
    126    pd = None
    127
    128  if pd:
    129      try:
    130          from pandas.formats import format as fmt
    131      except ImportError:
    132          from pandas.core import format as fmt # older versions
    133
    134
    135  highlightSubstructures=True
    136  molRepresentation = 'png' # supports also SVG
    137  molSize = (200,200)
    138
    139
    140 -def patchPandasHTMLrepr(self,**kwargs):
    141    '''
    142    Patched default escaping of HTML control characters to allow molecule image rendering dataframes
    143    '''
    144    formatter = fmt.DataFrameFormatter(self,buf=None,columns=None,col_space=None,colSpace=None,header=True,index=True,
    145                                                 na_rep='NaN',formatters=None,float_format=None,sparsify=None,index_names=True,
    146                                                 justify = None, force_unicode=None,bold_rows=True,classes=None,escape=False)
    147    formatter.to_html()
    148    html = formatter.buf.getvalue()
    149    return html
    150
    151 -def patchPandasHeadMethod(self,n=5):
    152    '''Ensure inheritance of patched to_html in "head" subframe
    153    '''
    154    df = self[:n]
    155    df.to_html = types.MethodType(patchPandasHTMLrepr,df)
    156    df.head = types.MethodType(patchPandasHeadMethod,df)
    157    return df
    158
    159 -def _get_image(x):
    160    """displayhook function for PIL Images, rendered as PNG"""
    161    import pandas as pd
    162    bio = BytesIO()
    163    x.save(bio,format='PNG')
    164    s = b64encode(bio.getvalue()).decode('ascii')
    165    pd.set_option('display.max_columns',len(s)+1000)
    166    pd.set_option('display.max_rows',len(s)+1000)
    167    if len(s)+100 > pd.get_option("display.max_colwidth"):
    168      pd.set_option("display.max_colwidth",len(s)+1000)
    169    return s
    170
    171 -def _get_svg_image(mol, size=(200,200), highlightAtoms=[]):
    172    """ mol rendered as SVG """
    173    from IPython.display import SVG
    174    from rdkit.Chem import rdDepictor
    175    from rdkit.Chem.Draw import rdMolDraw2D
    176    try:
    177      # If no coordinates, calculate 2D
    178      mol.GetConformer(-1)
    179    except ValueError:
    180      rdDepictor.Compute2DCoords(mol)
    181    drawer = rdMolDraw2D.MolDraw2DSVG(*size)
    182    drawer.DrawMolecule(mol,highlightAtoms=highlightAtoms)
    183    drawer.FinishDrawing()
    184    svg = drawer.GetDrawingText().replace('svg:','')
    185    return SVG(svg).data # IPython's SVG clears the svg text
    186
    187  from rdkit import DataStructs
    188
    189  try:
    190    from rdkit.Avalon import pyAvalonTools as pyAvalonTools
    191    _fingerprinter=lambda x,y:pyAvalonTools.GetAvalonFP(x,isQuery=y,bitFlags=pyAvalonTools.avalonSSSBits)
    192  except ImportError:
    193    _fingerprinter=lambda x,y:Chem.PatternFingerprint(x,fpSize=2048)
    194
    195 -def _molge(x,y):
    196    """Allows for substructure check using the >= operator (X has substructure Y -> X >= Y) by
    197    monkey-patching the __ge__ function
    198    This has the effect that the pandas/numpy rowfilter can be used for substructure filtering (filtered = dframe[dframe['RDKitColumn'] >= SubstructureMolecule])
    199    """
    200    if x is None or y is None: return False
    201    if hasattr(x,'_substructfp'):
    202      if not hasattr(y,'_substructfp'):
    203        y._substructfp=_fingerprinter(y,True)
    204      if not DataStructs.AllProbeBitsMatch(y._substructfp,x._substructfp):
    205        return False
    206    match = x.GetSubstructMatch(y)
    207    if match:
    208      if highlightSubstructures:
    209          x.__sssAtoms=list(match)
    210      else:
    211          x.__sssAtoms=[]
    212      return True
    213    else:
    214      return False
    215
    216
    217  Chem.Mol.__ge__ = _molge # lambda x,y: x.HasSubstructMatch(y)
    218
    219 -def PrintAsBase64PNGString(x,renderer = None):
    220    '''returns the molecules as base64 encoded PNG image
    221    '''
    222    if highlightSubstructures and hasattr(x,'__sssAtoms'):
    223        highlightAtoms=x.__sssAtoms
    224    else:
    225        highlightAtoms=[]
    226    if molRepresentation.lower() == 'svg':
    227      return _get_svg_image(x, highlightAtoms=highlightAtoms, size=molSize)
    228    else:
    229      return '<img src="data:image/png;base64,%s" alt="Mol"/>'%_get_image(Draw.MolToImage(x,highlightAtoms=highlightAtoms, size=molSize))
    230
    231
    232 -def PrintDefaultMolRep(x):
    233    return str(x.__repr__())
    234
    235  #Chem.Mol.__str__ = lambda x: '<img src="data:image/png;base64,%s" alt="Mol"/>'%get_image(Draw.MolToImage(x))
    236  Chem.Mol.__str__ = PrintAsBase64PNGString
    237
    238 -def _MolPlusFingerprint(m):
    239    '''Precomputes fingerprints and stores results in molecule objects to accelerate substructure matching
    240    '''
    241    #m = Chem.MolFromSmiles(smi)
    242    if m is not None:
    243      m._substructfp=_fingerprinter(m,False)
    244    return m
    245
    246 -def RenderImagesInAllDataFrames(images=True):
    247    '''Changes the default dataframe rendering to not escape HTML characters, thus allowing rendered images in all dataframes.
    248    IMPORTANT: THIS IS A GLOBAL CHANGE THAT WILL AFFECT TO COMPLETE PYTHON SESSION. If you want to change the rendering only
    249    for a single dataframe use the "ChangeMoleculeRendering" method instead.
    250    '''
    251    if images:
    252      pd.core.frame.DataFrame.to_html = patchPandasHTMLrepr
    253    else:
    254      pd.core.frame.DataFrame.to_html = defPandasRendering
    255
    256
    257 -def AddMoleculeColumnToFrame(frame, smilesCol='Smiles', molCol = 'ROMol',includeFingerprints=False):
    258    '''Converts the molecules contains in "smilesCol" to RDKit molecules and appends them to the dataframe "frame" using the specified column name.
    259    If desired, a fingerprint can be computed and stored with the molecule objects to accelerate substructure matching
    260    '''
    261    if not includeFingerprints:
    262      frame[molCol]=frame[smilesCol].map(Chem.MolFromSmiles)
    263    else:
    264      frame[molCol]=frame[smilesCol].map(lambda smiles: _MolPlusFingerprint(Chem.MolFromSmiles(smiles)))
    265    RenderImagesInAllDataFrames(images=True)
    266    #frame.to_html = types.MethodType(patchPandasHTMLrepr,frame)
    267    #frame.head = types.MethodType(patchPandasHeadMethod,frame)
    268
    269
    270 -def ChangeMoleculeRendering(frame=None, renderer='PNG'):
    271    '''Allows to change the rendering of the molecules between base64 PNG images and string representations.
    272    This serves two purposes: First it allows to avoid the generation of images if this is not desired and, secondly, it allows to enable image rendering for
    273    newly created dataframe that already contains molecules, without having to rerun the time-consuming AddMoleculeColumnToFrame. Note: this behaviour is, because some pandas methods, e.g. head()
    274    returns a new dataframe instance that uses the default pandas rendering (thus not drawing images for molecules) instead of the monkey-patched one.
    275    '''
    276    if renderer == 'String':
    277      Chem.Mol.__str__ = PrintDefaultMolRep
    278    else:
    279      Chem.Mol.__str__ = PrintAsBase64PNGString
    280    if frame is not None:
    281      frame.to_html = types.MethodType(patchPandasHTMLrepr,frame)
    282
    283 -def LoadSDF(filename, idName='ID',molColName = 'ROMol',includeFingerprints=False, isomericSmiles=False, smilesName=None, embedProps=False):
    284    """ Read file in SDF format and return as Pandas data frame. If embedProps=True all properties also get embedded in Mol objects in the molecule column. """
    285    df = None
    286    if isinstance(filename, string_types):
    287      if filename.lower()[-3:] == ".gz":
    288        import gzip
    289        f = gzip.open(filename, "rb")
    290      else:
    291        f = open(filename, 'rb')
    292      close = f.close
    293    else:
    294      f = filename
    295      close = None # don't close an open file that was passed in
    296    records = []
    297    indices = []
    298    for i, mol in enumerate(Chem.ForwardSDMolSupplier(f)):
    299      if mol is None: continue
    300      row = dict((k, mol.GetProp(k)) for k in mol.GetPropNames())
    301      if not embedProps:
    302        for prop in mol.GetPropNames():
    303          mol.ClearProp(prop)
    304      if mol.HasProp('_Name'): row[idName] = mol.GetProp('_Name')
    305      if smilesName is not None:
    306        row[smilesName] = Chem.MolToSmiles(mol, isomericSmiles=isomericSmiles)
    307      if not includeFingerprints:
    308          row[molColName] = mol
    309      else:
    310          row[molColName] = _MolPlusFingerprint(mol)
    311      records.append(row)
    312      indices.append(i)
    313
    314    if close is not None: close()
    315    RenderImagesInAllDataFrames(images=True)
    316    return pd.DataFrame(records, index=indices)
    317
    318  from rdkit.Chem import SDWriter
    319
    320 -def WriteSDF(df, out, molColName='ROMol', idName=None, properties=None, allNumeric=False):
    321    '''Write an SD file for the molecules in the dataframe. Dataframe columns can be exported as SDF tags if specified in the "properties" list. "properties=list(df.columns)" would export all columns.
    322    The "allNumeric" flag allows to automatically include all numeric columns in the output. User has to make sure that correct data type is assigned to column.
    323    "idName" can be used to select a column to serve as molecule title. It can be set to "RowID" to use the dataframe row key as title.
    324    '''
    325
    326    close = None
    327    if isinstance(out, string_types):
    328      if out.lower()[-3:] == ".gz":
    329        import gzip
    330        out = gzip.open(out, "wb")
    331        close = out.close
    332
    333    writer = SDWriter(out)
    334    if properties is None:
    335      properties=[]
    336    else:
    337      properties=list(properties)
    338    if allNumeric:
    339      properties.extend([dt for dt in df.dtypes.keys() if (np.issubdtype(df.dtypes[dt],float) or np.issubdtype(df.dtypes[dt],int))])
    340
    341    if molColName in properties:
    342      properties.remove(molColName)
    343    if idName in properties:
    344      properties.remove(idName)
    345    writer.SetProps(properties)
    346    for row in df.iterrows():
    347      # make a local copy I can modify
    348      mol = Chem.Mol(row[1][molColName])
    349
    350      if idName is not None:
    351        if idName == 'RowID':
    352          mol.SetProp('_Name',str(row[0]))
    353        else:
    354          mol.SetProp('_Name',str(row[1][idName]))
    355      for p in properties:
    356        cell_value = row[1][p]
    357        # Make sure float does not get formatted in E notation
    358        if np.issubdtype(type(cell_value),float):
    359          s = '{:f}'.format(cell_value).rstrip("0") # "f" will show 7.0 as 7.00000
    360          if s[-1] == ".":
    361            s += "0"  # put the "0" back on if it's something like "7."
    362          mol.SetProp(p, s)
    363        else:
    364          mol.SetProp(p,str(cell_value))
    365      writer.write(mol)
    366    writer.close()
    367    if close is not None: close()
    368
    369  _saltRemover = None
    370 -def RemoveSaltsFromFrame(frame, molCol = 'ROMol'):
    371    '''
    372    Removes salts from mols in pandas DataFrame's ROMol column
    373    '''
    374    global _saltRemover
    375    if _saltRemover is None:
    376        from rdkit.Chem import SaltRemover
    377        _saltRemover = SaltRemover.SaltRemover()
    378    frame[molCol] = frame.apply(lambda x: _saltRemover.StripMol(x[molCol]), axis = 1)
    379
    380 -def SaveSMILESFromFrame(frame, outFile, molCol='ROMol', NamesCol='', isomericSmiles=False):
    381    '''
    382    Saves smi file. SMILES are generated from column with RDKit molecules. Column with names is optional.
    383    '''
    384    w = Chem.SmilesWriter(outFile, isomericSmiles=isomericSmiles)
    385    if NamesCol != '':
    386      for m,n in zip(frame[molCol], map(str,frame[NamesCol])):
    387        m.SetProp('_Name',n)
    388        w.write(m)
    389      w.close()
    390    else:
    391      for m in frame[molCol]:
    392        w.write(m)
    393      w.close()
    394
    395  import numpy as np
    396  import os
    397  from rdkit.six.moves import cStringIO as StringIO
    398
    399 -def SaveXlsxFromFrame(frame, outFile, molCol='ROMol', size=(300,300)):
    400      """
    401      Saves pandas DataFrame as a xlsx file with embedded images.
    402      It maps numpy data types to excel cell types:
    403      int, float -> number
    404      datetime -> datetime
    405      object -> string (limited to 32k character - xlsx limitations)
    406
    407      Cells with compound images are a bit larger than images due to excel.
    408      Column width weirdness explained (from xlsxwriter docs):
    409      The width corresponds to the column width value that is specified in Excel.
    410      It is approximately equal to the length of a string in the default font of Calibri 11.
    411      Unfortunately, there is no way to specify "AutoFit" for a column in the Excel file format.
    412      This feature is only available at runtime from within Excel.
    413      """
    414
    415      import xlsxwriter # don't want to make this a RDKit dependency
    416
    417      cols = list(frame.columns)
    418      cols.remove(molCol)
    419      dataTypes = dict(frame.dtypes)
    420
    421      workbook = xlsxwriter.Workbook(outFile) # New workbook
    422      worksheet = workbook.add_worksheet() # New work sheet
    423      worksheet.set_column('A:A', size[0]/6.) # column width
    424
    425      # Write first row with column names
    426      c2 = 1
    427      for x in cols:
    428          worksheet.write_string(0, c2, x)
    429          c2 += 1
    430
    431      c = 1
    432      for index, row in frame.iterrows():
    433          image_data = StringIO()
    434          img = Draw.MolToImage(row[molCol], size=size)
    435          img.save(image_data, format='PNG')
    436
    437          worksheet.set_row(c, height=size[1]) # looks like height is not in px?
    438          worksheet.insert_image(c, 0, "f", {'image_data': image_data})
    439
    440          c2 = 1
    441          for x in cols:
    442              if str(dataTypes[x]) == "object":
    443                  worksheet.write_string(c, c2, str(row[x])[:32000]) # string length is limited in xlsx
    444              elif ('float' in str(dataTypes[x])) or ('int' in str(dataTypes[x])):
    445                  if (row[x] != np.nan) or (row[x] != np.inf):
    446                      worksheet.write_number(c, c2, row[x])
    447              elif 'datetime' in str(dataTypes[x]):
    448                  worksheet.write_datetime(c, c2, row[x])
    449              c2 += 1
    450          c += 1
    451
    452      workbook.close()
    453      image_data.close()
    454
    455
    456 -def FrameToGridImage(frame, column = 'ROMol', legendsCol=None, **kwargs):
    457    '''
    458    Draw grid image of mols in pandas DataFrame.
    459    '''
    460    if legendsCol:
    461      if legendsCol == frame.index.name:
    462        img = Draw.MolsToGridImage(frame[column], legends=list(map(str, list(frame.index))), **kwargs)
    463      else:
    464        img = Draw.MolsToGridImage(frame[column], legends=list(map(str, list(frame[legendsCol]))), **kwargs)
    465    else:
    466      img = Draw.MolsToGridImage(frame[column], **kwargs)
    467    return img
    468
    469  from rdkit.Chem.Scaffolds import MurckoScaffold
    470
    471 -def AddMurckoToFrame(frame, molCol = 'ROMol', MurckoCol = 'Murcko_SMILES', Generic = False):
    472    '''
    473    Adds column with SMILES of Murcko scaffolds to pandas DataFrame. Generic set to true results in SMILES of generic framework.
    474    '''
    475    if Generic:
    476      frame[MurckoCol] = frame.apply(lambda x: Chem.MolToSmiles(MurckoScaffold.MakeScaffoldGeneric(MurckoScaffold.GetScaffoldForMol(x[molCol]))), axis=1)
    477    else:
    478      frame[MurckoCol] = frame.apply(lambda x: Chem.MolToSmiles(MurckoScaffold.GetScaffoldForMol(x[molCol])), axis=1)
    479
    480
    481  from rdkit.Chem import AllChem
    482
    483 -def AlignMol(mol,scaffold):
    484    """
    485    Aligns mol (RDKit mol object) to scaffold (SMILES string)
    486    """
    487    scaffold = Chem.MolFromSmiles(scaffold)
    488    AllChem.Compute2DCoords(scaffold)
    489    AllChem.GenerateDepictionMatching2DStructure(mol,scaffold)
    490    return mol
    491
    492 -def AlignToScaffold(frame, molCol='ROMol', scaffoldCol='Murcko_SMILES'):
    493    '''
    494    Aligns molecules in molCol to scaffolds in scaffoldCol
    495    '''
    496    frame[molCol] = frame.apply(lambda x: AlignMol(x[molCol],x[scaffoldCol]), axis=1)
    497
    498
    499  if __name__ == "__main__":
    500    import sys
    501    if pd is None:
    502      print("pandas installation not found, skipping tests", file=sys.stderr)
    503    else:
    504      # version check
    505      try:
    506        v = pd.__version__.split('.')
    507      except AttributeError:
    508        # support for older versions of pandas
    509        v = pd.version.version.split('.')
    510
    511      if v[0]=='0' and int(v[1])<10:
    512        print("pandas installation >=0.10 not found, skipping tests",
    513              file=sys.stderr)
    514      else:
    515        import doctest
    516        failed,tried=doctest.testmod(optionflags=doctest.ELLIPSIS+doctest.NORMALIZE_WHITESPACE)
    517        if failed:
    518          sys.exit(failed)
    519
    520  # $Id$
    521  #
    522  #  Copyright (c) 2013, Novartis Institutes for BioMedical Research Inc.
    523  #  All rights reserved.
    524  #
    525  # Redistribution and use in source and binary forms, with or without
    526  # modification, are permitted provided that the following conditions are
    527  # met:
    528  #
    529  #     * Redistributions of source code must retain the above copyright
    530  #       notice, this list of conditions and the following disclaimer.
    531  #     * Redistributions in binary form must reproduce the above
    532  #       copyright notice, this list of conditions and the following
    533  #       disclaimer in the documentation and/or other materials provided
    534  #       with the distribution.
    535  #     * Neither the name of Novartis Institutes for BioMedical Research Inc.
    536  #       nor the names of its contributors may be used to endorse or promote
    537  #       products derived from this software without specific prior written permission.
    538  #
    539  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    540  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    541  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    542  # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
    543  # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    544  # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
    545  # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    546  # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    547  # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    548  # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    549  # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    550  #
    551