## Copyright (C) 2014 Reed Essick
##
## This program is free software; you can redistribute it and/or modify it
## under the terms of the GNU General Public License as published by the
## Free Software Foundation; either version 2 of the License, or (at your
## option) any later version.
##
## This program is distributed in the hope that it will be useful, but
## WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
## Public License for more details.
##
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
#

## \addtogroup laldetchar_py_idq
## Synopsis
# ~~~
# from laldetchar.idq import calibration
# ~~~
# \author Reed Essick (<reed.essick@ligo.org>)


description = \
"""
a module to store functions useful when computing and checking calibration.
We expect that segments generated by thresholding on the FAP time-series at FAP=0.XYZ should correspond to an amount of time DURATION=0.XYZ*LIVETIME
"""

#===================================================================================================

import numpy as np
from laldetchar.idq import event
from laldetchar.idq import idq
#from laldetchar.idq import reed as idq

from laldetchar import git_version

__author__ = 'Reed Essick <reed.essick@ligo.org>'
__version__ = git_version.id
__date__ = git_version.date

## \addtogroup laldetchar_py_idq_calibration
# @{

#===================================================================================================

### used for calibration check output files
report_str = \
"""
        FAPthr   = %.5e
      stated FAP = %.5e
       deadtime  = %.5e
      difference = %.3e
   UL stated FAP = %.5e
     UL deadtime = %.5e
   UL difference = %.5e"""

#===================================================================================================
# weight functions
#===================================================================================================
def weights(gps, weight_type="uniform", **kwargs):
    """
    a delegation function to generate weights
    checks that the "weight_type" exists
    """
#    if not ("%s_weights"%weight_type in dir()):
    if not known_weights.has_key(weight_type):
        raise ValueError("could not find weight function for weight_type=%s"%weight_type)

    return known_weights[weight_type](gps, **kwargs)

def uniform_weights(gps):
    """
    a function that will return weights associated with events as a function of time.
    """
    return np.ones(len(gps), dtype="float")

def pareto_weights(gps, pow=1.0, tau=3600, gpso='max'):
    """
    returns a power law weight defined by w = ( (gps-gps)/tau )**-pow
    default for gpso = 'max' ==> max(gps)+1 (to prevent divergence)
    """
    if gpso == 'max':
        gpso = max(gps) + 1

    if not isisntance(gps, np.ndarray):
        gps = np.array(gps)

    return ((gps-gpso)/tau)**-pow

def sigmoid_weights(gps, gpso='max', tau=3600):
    """
    returns a sigmoid for weights w = (1 + np.exp(-(gps-gpso)/tau))**-1
    default for gpso = 'max' ==> max(gps)
    """
    if gpso == 'max':
        gpso = max(gps)

    if not isisntance(gps, np.ndarray):
        gps = np.array(gps)

    return ( 1 + np.exp( - (gps-gpso)/tau ) )**-1 

#===================================================================================================

### dictionary of known weight functions by type
known_weights = {"uniform":uniform_weights, "pareto":pareto_weights, "sigmoid":sigmoid_weights}

#===================================================================================================
# calibration 
#===================================================================================================
def timeseries_to_livetime(dt, ts, thr):
    """
    computes the livetime using truth arrays, rather than by creating segments. This should be faster than creating segments if you don't actually care about the segments
    assumes dt is constant for all samples in ts
    """
    truth = ts >= thr
    N = np.sum(truth)
    if N:
        return dt*N, np.min( ts[truth] )
    else:
        return dt*N, None


###
def check_calibration( segs, times, timeseries, FAPthrs):
    """
    checks the pipeline's calibration at each "FAPthr in FAPThrs"

    this may be sped up with a call to timeseries_to_livetime() instead of idq.timeseries_to_segments() -> event.livetime
    however, we currently use some segment logic and it is not clear that we can avoid actually generating segments
    """
    idq_livetime = event.livetime(segs)

    segments = []
    deadtimes = []
    statedFAPs = []
    errs = []
    for FAPthr in FAPthrs:
        SEGS = []
        max_statedFAP = 0.0
        for (t, ts) in zip(times, timeseries):
            (_segs, _min_ts) = idq.timeseries_to_segments(t, -ts, -FAPthr)  # we want FAP <= FAPthr <--> -FAP >= -FAPthr
            SEGS += _segs
            if _min_ts != None:
                statedFAP = -_min_ts
                if max_statedFAP < statedFAP:
                    max_statedFAP = statedFAP

        SEGS = event.andsegments([SEGS, segs])
        segments.append(SEGS)

        SEGS_livetime = event.livetime(SEGS)

        if not idq_livetime:
            if SEGS_livetime:
                raise ValueError("something is weird with segments... idq_livetime is zero but SEGS_livetime is not")
            else:
                deadtime = 0.0
        else:
            deadtime = 1.0 * SEGS_livetime / idq_livetime
            if deadtime > 1.0:
                raise ValueError("deadtime > 1.0, something is weird...\n  SEGS_livetime = %f\n  idq_livetime = %f"%(SEGS_livetime, idq_livetime))
        deadtimes.append( deadtime )

        statedFAPs.append(max_statedFAP)

        if max_statedFAP > 0:
            err = deadtime/max_statedFAP - 1
        elif deadtime:
            err = 1
        else:
            err = 0
        errs.append( err )

    return segments, deadtimes, statedFAPs, errs

###
def check_calibration_FAST( livetime, timeseries, FAPthrs, dt=1.0):
    """
    check the pitpline's calibration at each "FAPthr in FAPThrs"
    """
    deadtimes = []
    statedFAPs = []
    errs = []
    for FAPthr in FAPthrs:
        deadtime = 0.0
        statedFAP = 0.0
        for ts in timeseries:
            d, mF = timeseries_to_livetime(dt, -ts, -FAPthr) # we want FAP <= FAPthr <--> -FAP >= -FAPthr
            deadtime += d
            if mF != None:
                mF = -mF
                if mF > statedFAP:
                    statedFAP = mF
        
        if not livetime:
            if deadtime:
                raise ValueError("something is weird with segments... livetime is zero but deadtime is not")
#            else: deadtime = 0 ### not necessary because we know this must be the case from the first conditional
        else:
            deadtime /= 1.0*livetime
        deadtimes.append( deadtime )

        statedFAPs.append( statedFAP )

        if statedFAP > 0:
            err = deadtime/statedFAP - 1
        elif deadtime:
            err = 1
        else:
            err = 0
        errs.append( err )
       
    return deadtimes, statedFAPs, errs

##@}
