"""
plmcal.py

PLM Calibration Routines. Aid grouping PLM points together based on several criteria
and managing collections of such points.

Classes: 'PLMCalibrationPoint', 'PLMCalibration'.

Lev, 04 Dec 2008
"""

import numpy, flib, timelog, aver

class PLMCalibrationPoint(object):
    """
    PLM calibration point - a bunch of PLM readings to be averaged together.
    Takes a 'plm' - 'PLMLog' object, containing the readings to combine.
    
    'time', 'T', 'M0', 'rawM0', 'stabi', 'bg', 'T_MCTNS', 'r_MCTNS' are 'Mean'
    objects created from averaging plm readings/settings.
    'xmit', 'gain', 'itime', 'idelay' must be the same for all the readings and
    'overload' is True if any of the readings overdrove PLM
    
    'T_set' is a temperature PID set point (T_MCTNS [mK]) or a negative value
    if no T_MCTNS stabilisation was running while taking this point.

    optional 'T_MCTNS' and 'r_MCTNS' allow to supply richer sets of MCTNS temperatures
    and ratios measured withing 5*T1 periods before the pulses if available (can
    be obtained from MCTNS or temperature PID logs).
    """
    def __init__(self, plm, T_set = -1, T_MCTNS = None, r_MCTNS = None):
        if plm is None:
            self.time      = aver.average(numpy.NaN)
            self.T         = aver.average(numpy.NaN)
            self.M0        = aver.average(numpy.NaN)
            self.rawM0     = aver.average(numpy.NaN)
            self.stabi     = aver.average(numpy.NaN)
            self.bg        = aver.average(numpy.NaN)
            self.T_MCTNS   = aver.average(numpy.NaN)
            self.r_MCTNS   = aver.average(numpy.NaN)
            self.xmit      = NaN
            self.gain      = NaN
            self.idelay    = NaN
            self.itime     = NaN
            self.overload = True
            self.T_set     = NaN
            return
        
        if T_MCTNS is None:
            T_MCTNS = plm.T_MCTNS()
        if r_MCTNS is None:
            r_MCTNS = plm.r_MCTNS()

        if len(plm) < 1: raise ValueError('An empty PLM log is insufficient to create a calibration point')
        if min(plm.xmit()) != max(plm.xmit()): raise ValueError('Points with different xmit given for a calibration point')
        if min(plm.gain()) != max(plm.gain()): raise ValueError('Points with different gain given for a calibration point')
        if min(plm.idelay()) != max(plm.idelay()): raise ValueError('Points with different int. delay given for a calibration point')
        if min(plm.itime()) != max(plm.itime()): raise ValueError('Points with different int. time given for a calibration point')
        
        self.time      = aver.average(plm.t())
        self.T         = aver.average(plm.T())
        self.M0        = aver.average(plm.M0()) + aver.Mean(0, 0.29 / numpy.mean(plm.stabi()), -0.5, 0.5)
        self.rawM0     = aver.average(plm.rawM0()) + aver.Mean(0, 0.29, -0.5, 0.5)
        self.stabi     = aver.average(plm.stabi())
        self.bg        = aver.average(plm.bg())
        self.T_MCTNS   = aver.average(plm.T_MCTNS())
        self.r_MCTNS   = aver.average(plm.r_MCTNS())
        self.xmit      = plm.xmit()[0]
        self.gain      = plm.gain()[0]
        self.idelay    = plm.idelay()[0]
        self.itime     = plm.itime()[0]
        self.overload  = any(plm.overload())
        self.T_set     = T_set

    def copy(self):
        "make a copy"
        copy = PLMCalibrationPoint(None)
        copy.time = self.time
        copy.T = self.T
        copy.M0 = self.M0
        copy.rawM0 = self.rawM0
        copy.stabi = self.stabi
        copy.bg = self.bg
        copy.T_MCTNS = self.T_MCTNS
        copy.r_MCTNS = self.r_MCTNS
        copy.xmit = self.xmit
        copy.gain = self.gain
        copy.idelay = self.idelay
        copy.itime = self.itime
        copy.overload = self.overload
        copy.T_set = self.T_set
        return copy

    def __repr__(self):
        s = "PLM calibration point: xmit=%d, gain=%d" % (self.xmit, self.gain)
        if self.T_set > 0:
            s += " @ %.2f mK" % self.T_set
        return s
        
    def totuple(self):
        """
        Convert a point into a 41 element tuple:
        [0:4]   - time
        [5:9]   - M0
        [10:14] - rawM0
        [15:19] - stabi
        [20:24] - bg
        [25:29] - T_MCTNS
        [30:34] - r_MCTNS
        [35]    - xmit
        [36]    - gain
        [37]    - idelay
        [38]    - itime
        [39]    - overload
        [40]    - T_set
        """
        return (self.time.totuple() + self.T.totuple() + self.M0.totuple() +
                self.rawM0.totuple() + self.stabi.totuple() + self.bg.totuple() +
                self.T_MCTNS.totuple() + self.r_MCTNS.totuple() +
                (self.xmit, self.gain, self.idelay, self.itime, self.overload, T_set))

    @staticmethod
    def fromtuple(array):
        """
        Convert a 41 element tuple/list/array into a calibration point:
        [0:4]   - time
        [5:9]   - M0
        [10:14] - rawM0
        [15:19] - stabi
        [20:24] - bg
        [25:29] - T_MCTNS
        [30:34] - r_MCTNS
        [35]    - xmit
        [36]    - gain
        [37]    - idelay
        [38]    - itime
        [39]    - overload
        [40]    - T_set
        """
        p = PLMCalibrationPoint(None)
        
        p.time      = Mean.fromtuple(array[0:4])
        p.M0        = Mean.fromtuple(array[5:9])
        p.rawM0     = Mean.fromtuple(array[10:14])
        p.stabi     = Mean.fromtuple(array[15:19])
        p.bg        = Mean.fromtuple(array[20:24])
        p.T_MCTNS   = Mean.fromtuple(array[25:29])
        p.r_MCTNS   = Mean.fromtuple(array[30:34])
        p.xmit      = array[35]
        p.gain      = array[36]
        p.idelay    = array[37]
        p.itime     = array[38]
        p.overload  = array[39]
        p.T_set     = array[40]
        
        return p

    def is_at(self, xmit = None, gain = None, idelay = None, itime = None, overload = None, T_set = None):
        """
        Return True if this point fulfils the specified criteria, such as
        given xmit pulse, gain, int. delay or int. time. If a variable is
        unspecified or set to None, no check on the corresponding field
        of the point is performed.
        """
        return ((xmit is None or self.xmit == xmit) and (gain is None or self.gain == gain)
                and (idelay is None or self.idelay == idelay) and (itime is None or self.itime == itime)
                and (overload is None or self.overload == overload) and (T_set is None or self.T_set == T_set))

class PLMCalibration(list):
    """
    A PLM calibration - a collection of PLMCalibrationPoints
    """

    @staticmethod
    def group(plm, pid, mctns=None, Korringa=30, maxDT=0.05):
        """
        Create a calibration with T_MCTNS PID
        """
        cal = PLMCalibration()
        
        for pid_period in pid.const_setpoint_periods():
            # cycle through PID set temperatures
            T = pid_period.x
            plm_T = plm.period(pid_period.start, pid_period.end)
            if len(plm_T) < 1: continue
            plm_T = plm_T.subset(numpy.abs(plm_T.T_MCTNS() - T) <= maxDT)
            
            for xmit in sorted(set(plm_T.xmit())):
                for gain in sorted(set(plm_T.gain())):
                    for idelay in sorted(set(plm_T.idelay())):
                        for itime in sorted(set(plm_T.itime())):
                            plm_p = plm_T.at(xmit, gain, idelay, itime)
                            if len(plm_p) < 1: continue
                            
                            # plm_p contains all points taken with similar PLM settings.
                            # Now points will be filtered depending on relevant readings
                            # A boolean array 'use' will tell which points to average
                            # together into a calibration points (True) and which to
                            # discard (False)
                            
                            use = numpy.ones([len(plm_p)], dtype=bool)
                            
                            safe_delay = 4.5 * Korringa / T
                            
                            T_MCTNS = []
                            r_MCTNS = []
                            
                            for n in range(len(plm_p)):
                                # discard a pulse if another PLM pulse was taken less than 4.5*T1 before it.
                                t = plm_p.t()[n] # time the pulse in question was taken
                                if any((plm.t() >= t - safe_delay) & (plm.t() < t)):
                                    print 'Discard a PLM pulse at %s due to other PLM pulse(s) within 4.5 T1' % flib.tm2str(t)
                                    use[n] = False
                                    continue
                                
                                # discard a pulse if any MCTNS temperature reading within 4.5*T1 was too far from
                                # the PID set point
                                TT_MCTNS = pid.period(t - safe_delay, t).x_measured() if mctns is None else mctns.period(t - safe_delay, t).T_grwl()
                                rr_MCTNS = numpy.asarray([plm_p.r_MCTNS()[n]]) if mctns is None else mctns.period(t - safe_delay, t).r()
                                    
                                DT = max(abs(TT_MCTNS - T)) if len(TT_MCTNS) > 0 else numpy.Inf
                                if DT > maxDT:
                                    print 'Discard a PLM pulse at %s due to a MCT reading %f mK away from PID set point within 4.5 T1' % (flib.tm2str(t), DT)
                                    use[n] = False
                                    continue
                                
                                T_MCTNS = numpy.concatenate([T_MCTNS, TT_MCTNS])
                                r_MCTNS = numpy.concatenate([r_MCTNS, rr_MCTNS])
                                
                            plm_p = plm_p.subset(use)
                            if len(plm_p) < 1: continue
                            
                            cal.append(PLMCalibrationPoint(plm_p, T, T_MCTNS, r_MCTNS))
        return cal
    
    def at(self, xmit=None, gain=None, idelay=None, itime=None, overload=None, T_set=None, copy=False):
        """
        Return a subset of the calibration obtained for at given settings. Any
        of the paramameters, xmit, gain, idelay, itime, overload is used for
        filtering points if it is specified (is not 'None').
        If 'copy' is true all points are duplicated.
        """
        
        subset = PLMCalibration()
        
        for point in self:
            if point.is_at(xmit, gain, idelay, itime, overload, T_set):
                subset.append(point.copy() if copy else point)
        return subset
    
    def toarray(self, property, subproperty = None):
        """
        Return a 'numpy.array' filled with certain parameters of the log
        if 'subproperty' is not specified, return an array of
        '.property' for every point. Otherwise return an array of
        '.property.subproperty' for every point.
        """
    
        if subproperty is None:
            return numpy.array([point.__getattribute__(property) for point in self])
        else:
            return numpy.array([point.__getattribute__(property).__getattribute__(subproperty) for point in self])

    def toset(self, *args, **vargs):
        """
        Similar to 'toarray' but similar values are only featured once,
        and elements are sorted incrementaly
        """
        return numpy.array(sorted(set(self.toarray(*args, **vargs))))
