"""
Manipulating the Paroscientific pressure logs.

Log format:
[:,0] (1) Time (sec, since 1 January 1970)
[:,1] (2) T_p (usec)
[:,3] (3) T_t (usec)
[:,4] (4) T_p zero (usec),
[:,5] (5) T_t zero (usec)
[:,6] (6) P (mbar)
[:,7] (7) T (degC)

27 May 2008, Lev
"""

# Changelog
# 5 Jan 2008, Lev
# Add 'loadMCT', 'loadLowP'
#
# 8 Jan 2009, Lev
# use ascii2numpy.loadascii
#
# 11 Jul 2010, Lev
# add calibration support
# high P cell gauge is recallibrated automatically upon loading using pressure zero-offset relevant in may-june 2010
#
# Jan 2012, Lev
# add a Zeolite log

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

class ParoLog(timelog.TimeLog):
    """
    Represents a ULT Paroscientific log over a certain time.
    Two logs can be concatenated with a '&' sign

    'self.device' is the gauge name, two logs must have the same gauge names in order to concatenate them.

    log format:

    [:,0] (1) Time (sec, since 1 January 1970)
    [:,1] (2) T_p (usec)
    [:,2] (3) T_t (usec)
    [:,3] (4) T_p zero (usec)
    [:,4] (5) T_t zero (usec)
    [:,5] (6) P (mbar)
    [:,6] (7) T (degC)
    """

    @staticmethod
    def fieldcount(): "return number of columns in the self.data"; return 7
    
    def P(self): "pressure [mbar]"; return self.data[:,5]
    def T(self): "temperature [degC]"; return self.data[:,6]
    def T_p(self): "pressure period raw reading [usec]"; return self.data[:,1]
    def T_t(self): "temperature period raw reading [usec]"; return self.data[:,2]
    def T_p0(self): "zero-offset pressure period raw reading [usec]"; return self.data[:,3]
    def T_t0(self): "zero-offset temperature period raw reading [usec]"; return self.data[:,4]

    def __repr__(self): return "Paroscientific Log %s, gauge='%s'" % (self.datespan(), self.device)
    
    gaugeNames = {
        '_Paro_high_P_sample.txt': 'highP',
        '_Paro_MCT.txt': 'MCT',
        '_Paro_2D_sample.txt': 'lowP'}
    
    pressureFactors = {
        '_Paro_high_P_sample.txt': 1.0,
        '_Paro_MCT.txt': 1.0e3,
        '_Paro_2D_sample.txt': 1.0}

    @staticmethod
    def load(filename):
        """
        Load a Paroscientific log from a given file
        A gauge is infered from the filename suffix. The following gauges are known:
        
            filename suffix            gauge name           pressure units
            
        "_Paro_high_P_sample.txt"       "highP"             mbar   
        "_Paro_MCT.txt"                 "MCT"               bar
        "_Paro_2D_sample.txt"           "lowP"              mbar
        """
        
        basename = os.path.basename(filename)
        suffix = basename[8:]
        
        if not ParoLog.gaugeNames.has_key(suffix):
            return None
        
#        data = flib.loadascii(filename, usecols=range(ParoLog.fieldcount()))
        data = ascii2numpy.loadascii(filename, cols=ParoLog.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])
        
        # MCT pressure is stored in bar, others in mbar
        # do a conversion into mbar
        data[:,5] *= ParoLog.pressureFactors[suffix]
        
        return ParoLog(data, ParoLog.gaugeNames[suffix])

    def only_Tt_repeats(self):
        """
        return a subset of the log with only those entries, where T_t is
        the same as in the previous line. This is a nasty way to remove
        entries, when temperature was measured, rather than pressure.
        However if temperature is stable, no detection is made.
        """
        Tt = self.T_t()
        Tt_before = numpy.concatenate([Tt[0:1], Tt[:-1]])
        return self.subset(Tt == Tt_before)

    def recalibrate(self, cal, P0):
        """
        change pressure and temperature in place using the parocientific periods,
        a given calibration (a string name or ParoLog.Calibration object).
        The zero pressure offset [mbar] is given by P0 that is either a number
        of a function P0(time, T), where 'time' is in sec. since Unix epoch and
        'T' is the gauge temperature. T_p0, T_t0 are set to NaN.
        """
        if isinstance(cal, basestring):
            cal = ParoLog.Calibration.load_cal(cal)
        T, P = cal.convert(self.T_t(), self.T_p())
        if not numpy.isscalar(P0):
            P0 = P0(self.t(), T)
        self.data[:,5] = P - P0
        self.data[:,6] = T
        self.data[:,3:4] = numpy.nan

    def remove_spikes(self, spike_factor=10, min_dP=2):
        """
        Return a subset of the log with pressure spikes removed.
        A spike is defined as a point which is different from its two
        adjucent points by more than 'spike_factor' times the difference between them
        and than 'min_dP' at the same time.
        """
        P0 = self.P()[0:-3]
        P1 = self.P()[1:-2]
        P2 = self.P()[2:-1]
        ii = ~((numpy.abs(P1 - P0) > spike_factor*numpy.abs(P2-P0)) & (numpy.abs(P1-P0) > min_dP))
        ii = numpy.concatenate([[False], ii, [False]])
        return self.subset(ii)

    @staticmethod
    def loadHighP(run, start=None, end=None, calname="Serial No 86110 ULT high P sample", P0=lambda t, T: 2.841e-07 * (t - 1249534540.0) + 61.7):
        """
        Return high pressure paroscientific log for a given run, withing [start, end] period if boundaries are specified.
        """
        p = ParoLog.loaddir(flib.smbpath('//phpc338/Run%d/Paroscientific/highP-cell-paro' % run), start=start, end=end).period(start, end)
        if calname is not None:
            p.recalibrate(calname, P0)
        return p

    @staticmethod
    def loadMCT(run, start=None, end=None):
        """
        Return MCT paroscientific log for a given run, withing [start, end] period if boundaries are specified.
        """
        return ParoLog.loaddir(flib.smbpath('//phpc338/Run%d/Paroscientific/MCT_paro' % run), start=start, end=end).period(start, end)

    @staticmethod
    def loadLowP(run, start=None, end=None):
        """
        Return low pressure paroscientific log for a given run, withing [start, end] period if boundaries are specified.
        """
        return ParoLog.loaddir(flib.smbpath('//phpc338/Run%d/Paroscientific/low-field-NMR-paro' % run), start=start, end=end).period(start, end)
    
    @staticmethod
    def loadZeolite(run, start=None, end=None):
        """
        Return zeolite pressure (temporarily hooked up) paroscientific log for a given run, withing [start, end] period if boundaries are specified.
        """
        return ParoLog.loaddir(flib.smbpath('//phpc338/Run%d/Paroscientific/Zeo_paro' % run), start=start, end=end).period(start, end)
    
    # 1 psi = 68.94757290 mbar
    @staticmethod
    def psi2mbar(): "psi to mbar conversion factor"; return 68.94757290
    
    class Calibration(object):
        "paroscientific calibrations"
        def __init__(self, name, maxPpsi, U0, Y1, Y2, Y3, C1, C2, C3, D1, D2, T1, T2, T3, T4, T5):
            self.name = name
            self.maxP = maxPpsi * ParoLog.psi2mbar()
            self.U0 = U0
            self.Y1 = Y1
            self.Y2 = Y2
            self.Y3 = Y3
            self.C1 = C1
            self.C2 = C2
            self.C3 = C3
            self.D1 = D1
            self.D2 = D2
            self.T1 = T1
            self.T2 = T2
            self.T3 = T3
            self.T4 = T4
            self.T5 = T5

        # Paroscientific gauge calibration
        # [0] gauge name
        # [1] max pressure [psi]
        # [2] U0 [usec]
        # [3] Y1 [degC/usec]
        # [4] Y2 [degC/usec^2]
        # [5] Y3 [degC/usec^3]
        # [6] C1 [psi]
        # [7] C2 [psi/usec]
        # [8] C3 [psi/usec^2]
        # [9] D1
        # [10] D2 [1/usec]
        # [11] T1 [usec]
        # [12] T2 [usec/usec]
        # [13] T3 [usec/usec^2]
        # [14] T4 [usec/usec^3]
        # [15] T5 [usec/usec^4]
        calibrations = [
            ("Serial No 30517 mK fridge MCT", 9.0000000000E+2, 5.8395150000E+0, -3.9547040000E+3, -8.8176770000E+3, 5.4031580000E+4, 5.0796180000E+3, -5.7832810000E+2, -1.5006390000E+4, -8.1900000000E-4, -3.0477000000E-1, 2.4811420000E+1, 1.8423900000E-1, 1.5281670000E+1, 0.0000000000E+0, 0.0000000000E+0),
            ("Serial No 32503 top of mK fridge", 1.5000000000E+1, 5.8739060000E+0, -3.8804540000E+3, -1.0734880000E+4, -1.1622760000E+4, 7.0019470000E+1, 7.6188790000E+0, -7.6868540000E+1, 3.6081000000E-2, 0.0000000000E+0, 2.6608790000E+1, 6.8987300000E-1, 1.9650340000E+1, 0.0000000000E+0, 0.0000000000E+0),
            ("Serial No 88855 MgO GHS", 1.5000000000E+1, 5.8551540000E+0, -3.9120860000E+3, -1.0848450000E+4, 0.0000000000E+0, 9.6851260000E+1, 3.9928410000E+0, -1.6011610000E+2, 3.3908000000E-2, 0.0000000000E+0, 2.7756950000E+1, 8.5295200000E-1, 2.0019850000E+1, 3.1028800000E+1, 0.0000000000E+0),
            ("Serial No 95720 zeolithe GHS", 1.5000000000E+1, 5.8182870000E+0, -3.8916130000E+3, -1.0415240000E+4, 0.0000000000E+0, 9.9321590000E+1, 4.0915350000E+0, -1.2839530000E+2, 2.9694000000E-2, 0.0000000000E+0, 2.7580840000E+1, 8.2430000000E-1, 2.0003550000E+1, 3.3516130000E+1, 0.0000000000E+0),
            ("INCORRECT Serial No 30517 mK fridge MCT", 9.0000000000E+2, 5.8395150000E+0, -3.9547040000E+3, -8.8176770000E+3, 5.4031580000E+4, 5.0795730000E+3, -5.7832810000E+2, -1.5006390000E+4, -8.1910000000E-4, -3.0477000000E-1, 2.4811420000E+1, 1.8423860000E-1, 1.5281670000E+1, 0.0000000000E+0, 0.0000000000E+0),
            ("INCORRECT Serial No 32503 top of mK fridge", 1.5000000000E+1, 5.8739060000E+0, -3.8804540000E+3, -1.0734880000E+4, -1.1622760000E+4, 7.0012929000E+1, 7.6188790000E+0, -7.6868540000E+1, 3.6080600000E-2, 0.0000000000E+0, 2.6608790000E+1, 6.8987290000E-1, 1.9650340000E+1, 0.0000000000E+0, 0.0000000000E+0),
            ("Serial No 60476 Brian MCT", 1.0000000000E+3, 5.8533280000E+0, -3.9105350000E+3, -1.0546670000E+4, 0.0000000000E+0, -4.0968460000E+3, 3.9656550000E+2, 1.1620370000E+4, 6.2669000000E-2, 0.0000000000E+0, 2.8353410000E+1, 1.8164560000E+0, 4.3669830000E+1, 1.0937250000E+2, 0.0000000000E+0),
            ("Serial No 86148 Brian sample GHS", 1.0000000000E+3, 5.8594470000E+0, -3.9255150000E+3, -1.0631740000E+4, 0.0000000000E+0, -6.1544250000E+3, 4.1745260000E+2, 1.8197590000E+4, 7.0164000000E-2, 0.0000000000E+0, 3.0072870000E+1, 1.4647360000E+0, 4.7107460000E+1, 1.2907140000E+2, 0.0000000000E+0),
            ("Serial No 101614 Brian new cell gauge", 3.0000000000E+3, 5.7568480000E+0, -3.9955540000E+3, -1.0572700000E+4, 0.0000000000E+0, -1.4437740000E+4, -6.0170570000E+2, 4.3191330000E+4, 4.3944000000E-2, 0.0000000000E+0, 3.0140120000E+1, 4.6720200000E-1, 5.7038810000E+1, 1.6799080000E+2, 0.0000000000E+0),
            ("Serial No 32697 ULT MCT", 9.0000000000E+2, 5.8171920000E+0, -3.9679810000E+3, -8.7458360000E+3, -1.8107050000E+5, 4.8331220000E+3, -6.3796780000E+2, -1.7114720000E+4, 1.5279900000E-2, -4.0725400000E-1, 2.5139870000E+1, 2.2480620000E-1, 1.8004980000E+1, 0.0000000000E+0, 0.0000000000E+0),
            ("Serial No 36252 ULT low P sample", 1.5000000000E+1, 5.8224570000E+0, -3.9448440000E+3, -9.7067960000E+3, 0.0000000000E+0, 8.4236210000E+1, 1.0715790000E+1, -6.3733010000E+1, 4.0823800000E-2, 0.0000000000E+0, 2.4989430000E+1, 4.5040060000E-1, 1.5651930000E+1, 0.0000000000E+0, 0.0000000000E+0),
            ("Serial No 86110 ULT high P sample", 1.0000000000E+3, 5.8613240000E+0, -3.9618540000E+3, -1.0309860000E+4, 0.0000000000E+0, -6.2133590000E+3, 1.5064370000E+1, 1.6577590000E+4, 7.1523000000E-2, 0.0000000000E+0, 3.0318840000E+1, 5.5249300000E-1, 4.2582560000E+1, 1.0886880000E+2, 0.0000000000E+0)
        ]
        
        @staticmethod
        def load_cal(name):
            "load a calibration with a given name"
            for c in ParoLog.Calibration.calibrations:
                if c[0] == name:
                    return ParoLog.Calibration(*c)
            raise ValueError('invalid gauge name')
        
        @staticmethod
        def list_cals(): "list known calibrations by name"; return [c[0] for c in ParoLog.Calibration.calibrations]
        
        #
        # Paroscientific's CD-model:
        # 
        # Temp (in degC) = Y1*U + Y2*U^2 + Y3*U^3
        # 
        # where:
        # U = Temp. period (in usec) - U0
        # -------------
        # P (in psi) = C*x*(1-D*x). with x=1-T0^2/tau^2, tau=pressure period (in usec)
        # 
        # where:
        # C = C1 + C2*U + C3*U^2
        # D = D1 + D2*U
        # T0 = T1 + T2*U + T3*U^2 + T4*U^3  +T5*U^4
        #
        def convert(self, tau_T, tau_P):
            """
            convert temperature and pressure periods [usec] into temperature [degC]
            and pressure [mbar], returned as a tuple. No zero offset correction is made
            """
            
            scalar = numpy.isscalar(tau_T)
            if scalar:
                tau_T = numpy.array([tau_T])
                tau_P = numpy.array([tau_P])
            else:
                tau_T = numpy.asarray(tau_T)
                tau_P = numpy.asarray(tau_P)
            if tau_T.shape != tau_P.shape:
                raise ValueError('different size tau_T and tau_P arrays are given')
            
            # Temp (in degC) = Y1*U + Y2*U^2 + Y3*U^3
            # 
            # where:
            # U = Temp. period (in usec) - U0
            U = tau_T - self.U0
            T = numpy.polyval([self.Y3, self.Y2, self.Y1, 0.0], U)
            
            # P (in psi) = C*x*(1-D*x). with x=1-T0^2/tau^2, tau=pressure period (in usec)
            # 
            # where:
            # C = C1 + C2*U + C3*U^2
            # D = D1 + D2*U
            # T0 = T1 + T2*U + T3*U^2 + T4*U^3  +T5*U^4
            C = numpy.polyval([self.C3, self.C2, self.C1], U)
            D = numpy.polyval([self.D2, self.D1], U)
            T0 = numpy.polyval([self.T5, self.T4, self.T3, self.T2, self.T1], U)
            x = 1 - T0**2 / tau_P**2
            P = C*x*(1-D*x) * ParoLog.psi2mbar()
            
            if scalar:
                T = T[0]
                P = P[0]
            return T, P
