#!/usr/bin/python

#
# =============================================================================
#
#                                   Preamble
#
# =============================================================================
#


usage = \
'''
Computes detection statistic and stores to the specified column in the coinc table.
'''

from optparse import OptionParser
try:
    import sqlite3
except ImportError:
    # pre 2.5.x
    from pysqlite2 import dbapi2 as sqlite3
import sys
import os
import numpy

from glue.ligolw import dbtables
from glue.ligolw import lsctables
from glue.ligolw.utils import process

from pylal import ligolw_sqlutils as sqlutils

# =============================================================================
#
#                                   Set Options
#
# =============================================================================


def parse_command_line():
    """
    Parse the command line, return options and check for consistency among the
    options.
    """
    parser = OptionParser( version = "", usage = usage )

    # following are related to file input and output naming
    parser.add_option( "-i", "--input", action = "store", type = "string", default = None,
        help = "Input database to read. Can only input one at a time."
        )
    parser.add_option( "-o", "--output", action = "store", type = "string", default = None,
        help = "Name of output database to save to."
        )
    parser.add_option( "-t", "--tmp-space", action = "store", type = "string", default = None,
        metavar = "PATH",
        help =
            "Required. Location of local disk on which to do work. " +
            "This is used to enhance performance in a networked " +
            "environment, and to protect against accidentally " +
            "overwriting the input database."
        )
    parser.add_option( "-v", "--verbose", action = "store_true", default = False,
        help = "Print progress information"
        )
    # following are standard options for database-based programs
    parser.add_option( "", "--output-column", action = "store", type = "string", default = None,
        help = "Required. Column in the coinc_inspiral table to store output to."
        )
    parser.add_option("", "--sngl-table", action="store", type="string",
        default=None,
        help =
          "Should be set to sngl_inspiral or sngl_ringdown.(REQUIRED)"
        )
    parser.add_option("", "--coinc-table", action="store", type="string",
        default=None,
        help =
          "Required. Can be any table with a coinc_event_id and a time column. Ex. coinc_inspiral."
        )
    # following are specific to this program
    parser.add_option("", "--statistic-type", action = "store", type="string",
        default=None, metavar="STATTYPE",
        help =
            "type of single-ifo statistic to be computed: " +
            "allowed values are mean, snr_mean, snr, snr_sq, choppedl, choppedlforall, " +
            "snrchi, effective_snr, effsnrlike, newsnr, newsnrlike, " +
            "highstat, ratio"
        )
    parser.add_option("", "--nlow", action = "store", type = "float", default = 4.,
        help =
            "option for effsnrlike and newsnrlike statistics: power of snr followed by " +
            "contours of constant statistic at small snr"
        )
    parser.add_option("", "--nhigh", action = "store", type = "float", default = 2.,
        help =
            "option for effsnrlike and newsnrlike statistics: power of snr followed by " +
            "contours of constant statistic at large snr"
        )
    parser.add_option("", "--chisq-index", action="store", type="float", default = 6.,
        help = "chisq index q for newsnr; default is 6."
        )
    parser.add_option("", "--crossover-snr", action="store", type="float", default = 15.81,
        help = 
          "crossover snr value for effective snr and " +
          "effsnrlike, equivalent to sqrt of magic number; default is sqrt(250)=15.81."
        )
    parser.add_option("", "--duration-threshold", action="store", type="float", default = 0.2,
        help = "threshold for dividing bins in highstat statistic" 
        )
    parser.add_option("", "--snr-power", action="store", type="float", default = 2.,
        help =
          "What powers of single ifo statistics to add together. " +
          "To add the logs choose snr-power=0. Default is 2."
        )
    parser.add_option("", "--hanford-factor", action="store", type="float", default = 1.,
        help = "prefactor for power of H1 or H2 statistic in combined statistic; default is 1."
        )
    parser.add_option("", "--livingston-factor", action="store", type="float", default = 1.,
        help = "prefactor for power of L1 statistic in combined statistic; default is 1."
        )
    parser.add_option("", "--virgo-factor", action="store", type="float",  default = 1.,
        help = "prefactor for power of V1 statistic in combined statistic; default is 1."
        )
    parser.add_option("", "--virgo-run", action="store", type="int", default = 2,
        help = "which Virgo science run to tune highstat for; default is 2"
        )
    parser.add_option("", "--param", action="store", type="string",
        default=None,
        help =
          "Parameter that will be averaged for coinc table entry. Ex: ringdown frequency, quality." +
          "Required if mean or snr_mean is specified as statistic-type."
        )
    parser.add_option("", "--chopla", action="store", type="float",
        default=None,
        help =
          "Tunable parameter used in the chopped-L test." +
          "Required if choppedl or choppedlforall is specified as statistic-type."
          "Used for the ranking of double coincidences."
        )
    parser.add_option("", "--choplb", action="store", type="float",
        default=None,
        help =
          "Tunable parameter used in the chopped-L test." +
          "Required if choppedl or choppedlforall is specified as statistic-type."
          "Used for the ranking of double coincidences."
        )
    parser.add_option("", "--choplc", action="store", type="float",
        default=None,
        help =
          "Tunable parameter used in the chopped-L test." +
          "Required if choppedlforall is specified as statistic-type."
          "Used for the ranking of triple coincidences."
        )
    parser.add_option("", "--ratio-column", action="store", type="string",
        default=None,
        help =
          "Column in the sngl-table to use in the ratio calculation." +
          "Must be either eff_dist or snr. Required if ratio statistic"
          "is specified as statistic-type."
        )
    parser.add_option("", "--ratio-default", action="store", type="float",
        default=None,
        help =
          "Value to be used in the case that a coincidence does not contain" +
          "one of the IFOs used in the ratio statistic. For example, for an"
          "H1L1 coincidence, you would not be able to calculate an H1/H2"
          "effective distance ratio. Thus, you may want to specify a"
          "neutral default for this coincidence such as 1.0."
          "Required if ratio statistic is specified as statistic-type."
        )
    parser.add_option("", "--ifo1", action="store", type="string",
        default=None,
        help =
          "IFO used in the numerator if ratio statistic is specified." +
          "Required if ratio statistic is specified as statistic-type."
          "For example, H1, H2, L1, or V1."
        )
    parser.add_option("", "--ifo2", action="store", type="string",
        default=None,
        help =
          "IFO used in the denominator if ratio statistic is specified." +
          "Required if ratio statistic is specified as statistic-type."
          "For example, H1, H2, L1, or V1."
        )
    (options, args) = parser.parse_args()

    # check for required options and for self-consistency
    if not options.input:
        raise ValueError, "No input specified."
    if not options.output:
        raise ValueError, "No output specified."
    if not options.tmp_space:
        raise ValueError, "--tmp-space is a required argument."
    if not options.output_column:
        raise ValueError, "--output-column is a required argument."
    if not options.sngl_table:
        raise ValueError, "--sngl-table is a required argument."
    if not options.coinc_table:
        raise ValueError, "--coinc-table is a required argument."
    if options.statistic_type == 'highstat' and not options.virgo_run:
        raise ValueError, "please specify --virgo-run to get correct highstat tuning, \
        if not analyzing Virgo triggers set to your favourite integer"

    return options, sys.argv[1:]

# =============================================================================
#
#                       Function Definitions
#
# =============================================================================

def create_column( connection, table_name, column_name ):
    """
    Creates a column in the given table if it doesn't exist.
    """
    table_name = sqlutils.validate_option( table_name )
    column_name = sqlutils.validate_option( column_name )
    if column_name not in sqlutils.get_column_names_from_table( connection, table_name ):
        sqlquery = ''.join([ """
            ALTER TABLE
                """, table_name, """
            ADD COLUMN
                """, column_name ])
        connection.cursor().execute( sqlquery )


class CombiningFunctions:
    """
    Class that stores different functions that can be later used to calculate stats in sqlite.
    The functions are grouped together in this class because some of them require extra outside
    arguments not in the database, e.g., ifo prefactors. By storing things in a class, these extra
    variables can be set when the class is called, thus allowing them to automatically be used by sqlite
    when computing the results in the UPDATE SET command.
    """
    def __init__( self, q = None, nlow = None, nhigh = None, rhoc = None, \
      durthresh = None, power = 2., hfac = 1., lfac = 1., vfac = 1., vsr = 2, param = None, \
      chopla = None, choplb = None, choplc = None, rcolumn = None, \
      rdefault = None, ifo1 = None, ifo2 = None ):
        """
        Sets outside variables.
        """
        self.q = q
        self.nlow = nlow
        self.nhigh = nhigh
        self.rhoc = rhoc
        self.durthresh = durthresh
        self.power = power
        self.hfac = hfac
        self.lfac = lfac
        self.vfac = vfac
        self.vsr = vsr
        self.param = param
        self.chopla = chopla
        self.choplb = choplb
        self.choplc = choplc
        self.rcolumn = rcolumn
        self.rdefault = rdefault
        self.ifo1 = ifo1
        self.ifo2 = ifo2

    def compute_mean( self, coinc_sngls ):
        """
        Computes the mean param.
        """
        return sum([getattr(sngl, self.param) for sngl in coinc_sngls])/float(len(coinc_sngls))

    def compute_snr_weighted_mean( self, coinc_sngls ):
        """
        Computes the snr-weighted mean param.
        """
        return sum([getattr(sngl, self.param) * sngl.snr for sngl in coinc_sngls])/sum(sngl.snr for sngl in coinc_sngls)

    def compute_combined_snr( self, coinc_sngls ):
        """
        Computes a combined snr using the given coinc and whatever the powers and prefactors are set to.
        
        @coinc_sngls: a list of sngl rows associated with *one* coincident event
        """
#        for sngl in coinc_sngls: # diagnostic
#            print sngl.ifo[0].lower()
        if self.power == 0:
            return numpy.exp(sum( getattr(self, sngl.ifo[0].lower()+'fac') * numpy.log(sngl.snr) for sngl in coinc_sngls ))
        else:
            return (sum( getattr(self, sngl.ifo[0].lower()+'fac') * sngl.snr**self.power for sngl in coinc_sngls ))**(1./self.power)

    def compute_snr_sq( self, coinc_sngls ):
        """
        Computes the sum of the squares of the snrs.
        
        @coinc_sngls: a list of sngl rows associated with *one* coincident event
        """
        return (sum( sngl.snr**2 for sngl in coinc_sngls ))

    def compute_chopped_l_snr( self, coinc_sngls ):
        """
        Computes a combined snr for triple/quadruple coincs and uses chopped-L test to compute snr for double coincs.
        
        @coinc_sngls: a list of sngl rows associated with *one* coincident event
        """
        if len( coinc_sngls ) == 4:
            return ( self.compute_combined_snr( coinc_sngls ) )**2
        elif len( coinc_sngls ) == 3:
            return ( self.compute_combined_snr( coinc_sngls ) )**2
        elif len( coinc_sngls ) == 2:
            return min( sum( sngl.snr for sngl in coinc_sngls ), min(self.chopla * sngl.snr + self.choplb for sngl in coinc_sngls) )
        else:
            raise ValueError, "The compute_chopped_l_snr statistic can only be computed for doubles, triples, and quadruples."

    def compute_chopped_l_snr_for_all( self, coinc_sngls ):
        """
        Computes chopped_l_snr for doubles. For triples, uses a 3d chopped-like statistic that weights triggers along
        the axes amoung background triggers in the Gaussian spherical region at low SNR.

        @coinc_sngls: a list of sngl rows associated with *one* coincident event
        """
        if len( coinc_sngls ) == 3:
            ifo1_snr = None
            ifo2_snr = None
            ifo3_snr = None
            min_snr = None
            for sngl in coinc_sngls:
                if sngl.ifo == 'H1':
                    ifo1_snr = sngl.snr
                elif sngl.ifo == 'L1':
                    ifo2_snr = sngl.snr
                else:
                    ifo3_snr = sngl.snr
            min_snr = min( ifo1_snr + ifo2_snr + self.choplc, ifo1_snr + ifo3_snr + self.choplc, ifo2_snr + ifo3_snr + self.choplc )
            return ( min( numpy.sqrt( sum( sngl.snr**2 for sngl in coinc_sngls )), min_snr ) )**2
        elif len( coinc_sngls ) == 2:
            return self.compute_chopped_l_snr( coinc_sngls )
        else:
            raise ValueError, "The compute_chopped_l_snr_for_all statistic can only be computed for doubles and triples."

    def compute_combined_snrchi( self, coinc_sngls ):
        """
        Computes a combined snrchi using the given coinc and whatever the prefactors are set to.

        @coinc_sngls: a list of sngl rows associated with *one* coincident event
        """
        if self.power == 0:
            return numpy.exp(sum( getattr(self, sngl.ifo[0].lower()+'fac') * numpy.log(sngl.snr/numpy.sqrt(sngl.chisq/(2*sngl.chisq_dof-2)))
                for sngl in coinc_sngls ))
        else:
            return (sum( getattr(self, sngl.ifo[0].lower()+'fac') * (sngl.snr/numpy.sqrt(sngl.chisq/(2*sngl.chisq_dof-2)))\
                **self.power
                for sngl in coinc_sngls ))**(1./self.power)

    def compute_combined_effsnr( self, coinc_sngls ):
        """
        Computes a combined effective snr (magic number = rhoc^2) using the given coinc and 
        whatever the powers and prefactors are set to.
        
        @coinc_sngls: a list of sngl rows associated with *one* coincident event
        """
        if self.power == 0:
            return numpy.exp(sum( getattr(self, sngl.ifo[0].lower()+'fac') * numpy.log( sngl.get_effective_snr(fac=self.rhoc**2) ) \
                for sngl in coinc_sngls ))
        else:
            return (sum( getattr(self, sngl.ifo[0].lower()+'fac') * ( sngl.get_effective_snr(fac=self.rhoc**2) ) \
                for sngl in coinc_sngls ))**(1./self.power)

    def compute_combined_effsnrlike( self, coinc_sngls ):
        """
        Computes a combined effsnr-like statistic (crossover at rho=rhoc) using the given coinc and 
        whatever the powers and prefactors are set to.
        
        @coinc_sngls: a list of sngl rows associated with *one* coincident event
        """
        if self.power == 0:
            return numpy.exp(sum( getattr(self, sngl.ifo[0].lower()+'fac') * numpy.log( sngl.snr/((sngl.chisq/\
                  (2*sngl.chisq_dof-2))*(1+(sngl.snr/self.rhoc)**(self.nlow-self.nhigh)))**(1./self.nlow) ) 
                for sngl in coinc_sngls ))
        else:
            return (sum( getattr(self, sngl.ifo[0].lower()+'fac') * ( sngl.snr/((sngl.chisq/\
                  (2*sngl.chisq_dof-2))*(1+(sngl.snr/self.rhoc)**(self.nlow-self.nhigh)))**(1./self.nlow) ) **self.power 
                for sngl in coinc_sngls ))**(1./self.power)

    def compute_combined_newsnrlike( self, coinc_sngls ):
        """
        Computes a combined newsnr-like statistic (crossover at rchisq=1) using the given coinc and 
        whatever the powers and prefactors are set to.
        
        @coinc_sngls: a list of sngl rows associated with *one* coincident event
        """
        if self.power == 0:
            return numpy.exp(sum( getattr(self, sngl.ifo[0].lower()+'fac') * numpy.log( sngl.snr/(0.5*( \
                  (sngl.chisq/(2*sngl.chisq_dof-2))**(self.q/self.nlow) + (sngl.chisq/(2*sngl.chisq_dof-2))**(self.q/self.nhigh) ))**(1./self.q) ) 
                for sngl in coinc_sngls ))
        else:
            return (sum( getattr(self, sngl.ifo[0].lower()+'fac') * ( sngl.snr/(0.5*( (sngl.chisq/(2*sngl.chisq_dof-2))**(self.q/self.nlow) +\
                  (sngl.chisq/(2*sngl.chisq_dof-2))**(self.q/self.nhigh) ))**(1./self.q) ) **self.power 
                for sngl in coinc_sngls ))**(1./self.power)

    def compute_combined_newsnr( self, coinc_sngls ):
        """
        Computes a combined newsnr using the given coinc and whatever the powers and prefactors are set to.
        
        @coinc_sngls: a list of sngl rows associated with *one* coincident event
        """
        if self.power == 0:
            return numpy.exp(sum( getattr(self, sngl.ifo[0].lower()+'fac') * numpy.log( sngl.get_new_snr(index=self.q) )  for sngl in coinc_sngls ))
        else:
            return (sum( getattr(self, sngl.ifo[0].lower()+'fac') * (sngl.get_new_snr(index=self.q))**self.power for sngl in coinc_sngls ))**(1./self.power)

    def compute_highstat( self, coinc_sngls ):
        """
        Computes 'highstat' combining newsnrs if minimum template duration is above durthresh, else combining
        newsnr in V1 with effsnr in H1,H2,L1

        @coinc_sngls: a list of sngl rows associated with *one* coincident event
        """
        min_duration = min(sngl.template_duration for sngl in coinc_sngls)
        n = len(coinc_sngls)
        snglstat = numpy.zeros(n)
        snglfac = numpy.zeros(n)
        for i, sngl in zip(numpy.arange(n), coinc_sngls):
            snglfac[i] = getattr(self, sngl.ifo[0].lower()+'fac')
            if min_duration < self.durthresh and (sngl.ifo in ('H1', 'H2', 'L1') or self.vsr != 2):
                snglstat[i] = sngl.get_effective_snr(fac = self.rhoc**2)
            # apply effsnr to LIGO triggers and VSR3 triggers if the minimum duration is below threshold
            # but not to VSR2 triggers
            else:
                snglstat[i] = sngl.get_new_snr(index = self.q)
        if self.power == 0:
            return numpy.exp(sum ( snglfac[i] * numpy.log( snglstat[i] ) for i in numpy.arange(n) ))
        else:
            return (sum ( snglfac[i] * snglstat[i]**self.power for i in numpy.arange(n) ))**(1./self.power)

    def compute_ratio( self, coinc_sngls ):
        """
        Computes effective distance or SNR ratio (ifo1/ifo2) for coincidences that contain both an ifo1 trigger and
        an ifo2 trigger.
        Otherwise, set the value to the user-specified default.
        """
        if self.rcolumn == 'eff_dist':
            ifo1_eff_dist = None
            ifo2_eff_dist = None
            for sngl in coinc_sngls:
                if sngl.ifo == self.ifo1:
                    ifo1_eff_dist = sngl.eff_dist
                elif sngl.ifo == self.ifo2:
                    ifo2_eff_dist = sngl.eff_dist
            if ifo1_eff_dist and ifo2_eff_dist:
                return (ifo1_eff_dist / ifo2_eff_dist)
            else:
                return self.rdefault
        elif self.rcolumn == 'snr':
            ifo1_snr = None
            ifo2_snr = None
            for sngl in coinc_sngls:
                if sngl.ifo == self.ifo1:
                    ifo1_snr = sngl.snr
                elif sngl.ifo == self.ifo2:
                    ifo2_snr = sngl.snr
            if ifo1_snr and ifo2_snr:
                return (ifo1_snr / ifo2_snr)
            else:
                return self.rdefault
        else:
            raise ValueError, "The specied ratio column does not exist. Please use either eff_dist or snr."

# =============================================================================
#
#                                     Main
#
# =============================================================================

#
#       Generic Initialization
#

opts, args = parse_command_line()

# get input database filename
filename = opts.input
if not os.path.isfile( filename ):
    raise ValueError, "The input file, %s, cannot be found." % filename

# Setup working databases and connections
if opts.verbose: 
    print >> sys.stdout, "Setting up temp. database..."
working_filename = dbtables.get_connection_filename( 
    filename, tmp_path = opts.tmp_space, verbose = opts.verbose )
connection = sqlite3.connect( working_filename )
if opts.tmp_space:
    dbtables.set_temp_store_directory(connection, opts.tmp_space, verbose = opts.verbose)

sngl_table = sqlutils.validate_option( opts.sngl_table )
coinc_table = sqlutils.validate_option( opts.coinc_table )

#    
# Create the output column if it doesn't exist
#
output_column = sqlutils.validate_option( opts.output_column )
create_column( connection, coinc_table, output_column )

#
#   Initialize needed functions
#

# 1. initialize the combining function class
function_class = CombiningFunctions( q = opts.chisq_index, nlow = opts.nlow, nhigh = opts.nhigh, rhoc = opts.crossover_snr,
    durthresh = opts.duration_threshold, power = opts.snr_power, 
    hfac = opts.hanford_factor, lfac = opts.livingston_factor, vfac = opts.virgo_factor, vsr = opts.virgo_run,
    param = opts.param, chopla = opts.chopla, choplb = opts.choplb,
    choplc = opts.choplc,rcolumn = opts.ratio_column, rdefault = opts.ratio_default,
    ifo1 = opts.ifo1, ifo2 = opts.ifo2 )
opts.statistic_type = sqlutils.validate_option( opts.statistic_type )
if opts.statistic_type == 'mean':
    function = 'compute_mean'
elif opts.statistic_type == 'snr_mean':
    function = 'compute_snr_weighted_mean'
elif opts.statistic_type == 'snr':
    function = 'compute_combined_snr'
elif opts.statistic_type == 'snr_sq':
    function = 'compute_snr_sq'
elif opts.statistic_type == 'choppedl':
    function = 'compute_chopped_l_snr'
elif opts.statistic_type == 'choppedlforall':
    function = 'compute_chopped_l_snr_for_all'
elif opts.statistic_type == 'snrchi':
    function = 'compute_combined_snrchi'
elif opts.statistic_type == 'effective_snr':
    function = 'compute_combined_effsnr'
elif opts.statistic_type == 'effsnrlike':
    function = 'compute_combined_effsnrlike'
elif opts.statistic_type == 'newsnrlike':
    function = 'compute_combined_newsnrlike'
elif opts.statistic_type == 'newsnr':
    function = 'compute_combined_newsnr'
elif opts.statistic_type == 'highstat':
    function = 'compute_highstat'
elif opts.statistic_type == 'ratio':
    function = 'compute_ratio'

# 2. initialize the apply_function class in sqlite

# get the columns in the sngl table as they're stored in the database
sngl_columns = sqlutils.get_column_names_from_table( connection, sngl_table ) 

class ApplyCombiningFunction:
    """
    This class allows the combining stats to computed on the fly in sqlite instead of having to read all sngls
    into memory first.
    
    It works by first initializing a list to store all the single information about a coincident event.
    The step method is then used by sqlite to populate this list. Once all the singles in a group have
    been retrieved (a 'group' is determined by the GROUP BY command in the sqlite statement below, which in this
    case, things are grouped by coinc_event_ids) the finalize method is called. This is where the relevant
    function is applied and the resulting value returned. When sqlite goes onto the next group, the class
    is re-initialized and the process repeats.

    This all works in pysqlite by creating this class as an aggregate function. The step, finalize methods
    are standard ways for aggregate functions to work. For more information, see "Connection.create_aggregate"
    in the pysqlite documentation: http://docs.python.org/library/sqlite3.html
    """
    def __init__(self):
        """
        Initializes variables needed for the step process.
        """
        self.this_coinc = []

    def step(self, *args):
        """
        Populates self.this_coinc
        """
        this_sngl = lsctables.TableByName[ sngl_table ].RowType()
        [ setattr( this_sngl, column, data) for column, data in zip(sngl_columns, args) if column in this_sngl.__slots__ ]
        self.this_coinc.append( this_sngl )

    def finalize(self):
        """
        Once all the singles for the coinc have been gathered, applies
        the desired function to them and returns the result.
        """
        return getattr(function_class, function)( self.this_coinc )

# now create a sqlite aggregate function from the ApplyCombiningFunction class so that it can be applied
# on the fly; this requires knowing how many input arguments go into the step method, which is just the number
# of columns in the sngl table
nargs = len( sngl_columns )
connection.create_aggregate( 'apply_function', nargs, ApplyCombiningFunction )


#
#   Compute the desired statistic and update the coinc table with it
#

# for some reason sqlite doesn't like "table_name.*" in an aggregate function's arguments,
# so I'm sticking in all the columns by name into the query.
sngl_cols = str(','.join([sngl_table+"."+col for col in sngl_columns]))

sqlite3.enable_callback_tracebacks(True)
sqlquery = ''.join([ """
    UPDATE
        """, coinc_table, """
    SET
        """, output_column, """ = (
        SELECT
            apply_function(""", sngl_cols, """)
        FROM
            """, sngl_table, """
        JOIN
            coinc_event_map ON (
                """, sngl_table, """.event_id == coinc_event_map.event_id )
        WHERE
            coinc_event_map.coinc_event_id == """, coinc_table, """.coinc_event_id
        GROUP BY
            coinc_event_map.coinc_event_id
        )""" ])

if opts.verbose:
    print >> sys.stderr, "Sqlite query used is:\n%s" % sqlquery

connection.cursor().execute(sqlquery)

#
#       Save and Exit
#

connection.commit()
connection.close()

# write output database
dbtables.put_connection_filename(opts.output, working_filename, verbose = opts.verbose)

if opts.verbose: 
    print >> sys.stdout, "Finished!"

sys.exit(0)

