"""
Manipulating the Proportional-Integral-Differential Controller logs.

Log format:
# ULT fridge, PID pressure controller
# (1) Time (sec, since 1 January 1904)
# (2) P_proportional (W or nW, calculated)
# (3) P_integration (W or nW, calculated)
# (4) P_derivative (W or nW, calculated)
# (5) P_total (W or nW, dissipated)
# (6) V_DAC (mV)
# (7) DAC amplitude (uint16)
# (8) Gain (W/mbar)
# (9) Integral time const (sec)
# (10) Derivative time const (sec)
# (11) p_set (mbar)
# (12) p_measured (mbar)
# (13) P_max (W)

2 September 2008, Lev
"""

# Changelog
# 8 Jan 2009
# use ascii2numpy.loadascii

import os
import os.path
import time
import datetime
import numpy
import matplotlib.dates
import flib
import timelog
import ascii2numpy

class PIDLog(timelog.TimeLog):
    """
    Represents a ULT Proportional-Integral-Differential Controller log over
    a certain time. Two logs can be concatenated with a '&' sign

    'self.device' is the controller name, two logs must belong to the same
    device in order to concatenate them.

    log format:

    ULT fridge, PID pressure controller
    [:,0]  (1) Time (sec, since 1 January 1904)
    [:,1]  (2) P_proportional (W or nW, calculated)
    [:,2]  (3) P_integration (W or nW, calculated)
    [:,3]  (4) P_derivative (W or nW, calculated)
    [:,4]  (5) P_total (W/nW, dissipated)
    [:,5]  (6) V_DAC (mV)
    [:,6]  (7) DAC amplitude (uint16)
    [:,7]  (8) Gain (W/mbar or nW/mK)
    [:,8]  (9) Integral time const (sec)
    [:,9]  (10) Derivative time const (sec)
    [:,10] (11) p_set (mbar or mK)
    [:,11] (12) p_measured (mbar or mK)
    [:,12] (13) P_max (W or nW)
    
    Pressure and Temperature PID Controllers think in different units for heat
    and controller parameter. Their string representations can be obtained
    from self.powerUnits() (W or nW) and self.xUnits() (mbar or mK).
    """

    @staticmethod
    def fieldcount(): "return number of columns in the self.data"; return 13

    def power_prop(self): "power in proportional channel (calculated) [PU]"; return self.data[:,1]
    def power_int(self): "power in integral channel (calculatd) [PU]"; return self.data[:,2]
    def power_diff(self): "power in differential channel power (calculated) [PU]"; return self.data[:,3]
    def power_total(self): "total power (calculated) [PU]"; return self.data[:,4]

    def voltage(self): "DAC voltage [mV]"; return self.data[:,5]
    def DAC_value(self): "Raw DAC value 0-65535"; return self.data[:,6]
    
    def power(self, R_heater, R_leads):
        "Calculate dissipated power based on DAC voltage and resistances. Always measured in microwatts"
        return R_heater * (self.voltage()/(R_heater + R_leads))**2

    def gain(self): "PID Gain (PU/xu)"; return self.data[:,7]
    def T_int(self): "Integral Time Constant (sec)"; return self.data[:,8]
    def T_diff(self): "Differential Time Constant (sec)"; return self.data[:,8]
    
    def x_set(self): "set point for controlled parameter [xu]"; return self.data[:,10]
    def x_measured(self): "measured value of controlled parameter [xu]"; return self.data[:,11]

    def max_power(self): "power limit [PU]"; return self.data[:,12]

    def __repr__(self): return "%s Log %s" % (self.device, self.datespan())
    
    def powerUnits(self): "string representation of units powers are measured in [W or nW]"; return PIDLog.xUnitsDB[self.device]
    def xUnits(self): "string representation of units powers are measured in [W or nW]"; return PIDLog.powerUnitsDB[self.device]
    
    nameDB = {
        '_PID_P.dat': 'Pressure PID',
        '_PID_T.dat': 'Temperature PID'}

    xUnitsDB = {
        'Pressure PID': 'mbar',
        'Temperature PID': 'mK'}
        
    powerUnitsDB = {
        'Pressure PID': 'W',
        'Temperature PID': 'nW'}
    
    @staticmethod
    def load(filename):
        """
        Load a PID log from a given file. Controller is determined from
        filename suffix:
        
        filename suffix         controller name     x units     power units
            
        "_PID_P.dat             "Pressure PID"      "mbar"      "W"
        "_PID_T.dat             "Temperature PID"   "mK"        "nW"
        """

        basename = os.path.basename(filename)
        suffix = basename[8:]
        
        if not PIDLog.nameDB.has_key(suffix):
            return None

#        data = flib.loadascii(filename, usecols=range(PIDLog.fieldcount()))
        data = ascii2numpy.loadascii(filename, cols=PIDLog.fieldcount())

        # timestamps are stored as seconds since 00:00 1 Jan 1904.
        # Convert to seconds since 00:00 1 Jan 1970, C and UNIX time format.
        data[:,0] = flib.labview2tm(data[:,0])
        
        return PIDLog(data, PIDLog.nameDB[suffix])

    @staticmethod
    def loadPressurePID(run, start = None, end = None):
        "Return pressure PID log for a given run, withing [start, end] period if boundaries are specified."
        return PIDLog.loaddir(flib.smbpath('//phpc338/Run%d/PID-pressure' % run), start=start, end=end).period(start, end)

    @staticmethod
    def loadTemperaturePID(run, start = None, end = None):
        "Return temperature PID log for a given run, withing [start, end] period if boundaries are specified."
        return PIDLog.loaddir(flib.smbpath('//phpc338/Run%d/PID-temperature' % run), start=start, end=end).period(start, end)

    def const_setpoint_periods(self, minlength=10, maxgap=10):
        """
        Return an array of flib.Period's with constant set point.
        Stabilisation is considered interrupted if a gap between two points
        is over 10 seconds, then even if x is the same before and after,
        separate periods are returned.
        
        Each Period has the set point stored as '.x'
        """
        
        periods = []
        
        t = self.t()
        x = self.x_set()
        
        if len(self) > 0:
            start = t[0]
            lastx = x[0]
            
            for n in range(1, len(t)):
                #
                # 16/04/09: found a strange condition below:
                # "if lastx != x[n]# and x[n] - x[n-1] < 10:"
                # remove the second part
                if lastx != x[n] or t[n] - t[n-1] > maxgap:
                    periods += [flib.Period(start, t[n-1], x=lastx)]
                    start = t[n]
                    lastx = x[n]
            if t[len(t)-1] - start >= minlength:
                periods += [flib.Period(start, t[len(t)-1], x=lastx)]
        
        return numpy.array(periods)

    def const_power_periods(self, minlength=10, maxgap=10):
        """
        Return an array of flib.Period's with constant total power.
        Stabilisation is considered interrupted if a gap between two points
        is over 10 seconds, then even if x is the same before and after,
        separate periods are returned.
        
        Each Period has the set power stored as '.q'
        """
        
        periods = []
        
        t = self.t()
        q = self.power_total()
        
        if len(self) > 0:
            start = t[0]
            lastq = q[0]
            
            for n in range(1, len(t)):
                #
                # 16/04/09: found a strange condition below:
                # "if lastx != x[n]# and x[n] - x[n-1] < 10:"
                # remove the second part
                if lastq != q[n] or t[n] - t[n-1] > maxgap:
                    if t[n] - start >= minlength:
                        periods += [flib.Period(start, t[n-1], q=lastq)]
                    start = t[n]
                    lastq = q[n]
            
            if t[len(t)-1] - start >= minlength:
                periods += [flib.Period(start, t[len(t)-1], q=lastq)]
        
        return numpy.array(periods)

    def ramp_periods(self, shortest_steady = 3*60):
        """
        find periods of steady or constantly growing or decreasing total power.
        the criterium of a ramp is that during such a period the power
        advances in the same direction or stays the same, and the time
        intervals over which the power does not change are no longer
        than 'shortest_steady' [sec]. Otherwise a period of steady power is
        recognised.
        
        return an array of 'flib.Period' objects with a 'direction'
        field equal to -1/0/1 corresponding to decreasing/steady/increasing
        power.
        """
        if len(self) < 1:
            return numpy.array([], dtype=flib.Period)
        
        periods = []
        t = self.t()
        q = numpy.round(self.power_total(), 2)
        
        ii = numpy.diff(q).nonzero()[0]
        
        if len(ii) < 1:
            return numpy.array([flib.Period(min(t), max(t), direction = 0)])
        
        n = ii[0]
        if t[n] - t[0] > shortest_steady:
            periods += [flib.Period(t[0], t[n], direction = 0)]
            tstart = tprev = t[n+1]
        else:
            tstart = tprev = t[0]
        
        direction = numpy.sign(q[n+1] - q[n])
        
        for n in ii[1:]:
            if t[n] - tprev > shortest_steady:
                periods += [flib.Period(tstart, tprev, direction = direction)]            
                periods += [flib.Period(tprev, t[n], direction = 0)]
                tstart = t[n+1]
                direction = numpy.sign(q[n+1] - q[n])
            elif direction != numpy.sign(q[n+1] - q[n]):
                periods += [flib.Period(tstart, t[n], direction = direction)]
                tstart = t[n+1]
                direction = numpy.sign(q[n+1] - q[n])
            tprev = t[n+1]
        
        if t[len(t)-1] - tprev > shortest_steady:
            periods += [flib.Period(tstart, tprev, direction = direction)]
            tstart = tprev
            direction = 0
        
        periods += [flib.Period(tstart, t[len(t)-1], direction = direction)]
        return numpy.array(periods)
