def import_mpp_cycle_datum_channel(folder,
                                   cycle_pattern='cycle-<>',
                                   channel_pattern='ch-<>'):
    """
    Imports MPP tracking data from an MPP with JV program, broken in to cycles.

    :param folder: Folder path containing cycles.
    :param cycle_pattern: Pattern for cycle folders. [Default: 'cycle-<>']
    :param channel_pattern: Pattern for channel folders. [Default: 'ch-<>']
    :returns: Tuple of ( voc, jv, mpp ) Pandas DataFrames by cycle.
    """
    # get scan folders
    cycles = os.listdir(folder)

    # get data for each scan
    vocs = []
    jvs = []
    mpps = []

    for cycle in cycles:
        cycle_path = os.path.join(folder, cycle)

        dfs = (voc, jv,
               mpp) = import_mpp_datum_channel(cycle_path)  # import scan data

        # add scan index
        cycle_id = int(
            std.metadata_from_file_name(cycle_pattern,
                                        cycle_path,
                                        full_path=True,
                                        is_numeric=True))

        for df in dfs:
            # channel already in headers
            std.insert_index_levels(df, cycle_id, 'cycle', key_level=1)

        vocs.append(voc)
        jvs.append(jv)
        mpps.append(mpp)

    vocs = std.common_reindex(vocs)
    jvs = std.common_reindex(jvs)
    mpps = std.common_reindex(mpps)

    vocs = pd.concat(vocs, axis=1).sort_index(axis=1)
    jvs = pd.concat(jvs, axis=1).sort_index(axis=1)
    mpps = pd.concat(mpps, axis=1).sort_index(axis=1)

    return (vocs, jvs, mpps)
def import_voc_datum_channel(file,
                             channel_pattern='ch-<>',
                             set_index=True,
                             skiprows=2):
    """
    Imports Voc datum from the given file.

    :param file: File path.
    :param channel_pattern: Add channel from file path as index level.
        Uses value as pattern in standard_functions#metadata_from_file_name.
        None if channel should be excluded.
        [Default: 'ch-<>']
    :param set_index: Sets the index to time. [Default: True]
    :param skiprows: Number of initial data points to drop. [Default: 2]
    :returns: Pandas DataFrame.
    """
    header = ['time', 'voltage']
    df = pd.read_csv(file,
                     names=header,
                     skiprows=(1 + skiprows),
                     engine='python')

    if set_index:
        df.set_index('time', inplace=True)

    df.columns.rename('metrics', inplace=True)

    if channel_pattern is not None:
        ch = channel_from_file_path(file, channel_pattern)
        df = std.insert_index_levels(df, ch, 'channel')

    # remove duplicate axis
    df = df.loc[~df.index.duplicated()]

    return df
def import_mpp_tracking_datum_channel(file,
                                      channel_pattern='ch-<>',
                                      set_index=True,
                                      drop_cycle=True,
                                      skiprows=2):
    """
    Imports MPP tracking datum from the given file.

    :param file: File path.
    :param channel_pattern: Add channel from file path as index level.
        Uses value as pattern in standard_functions#metadata_from_file_name.
        None if channel should be excluded.
        [Default: 'ch-<>']
    :param set_index: Sets the index to time. [Default: True]
    :param drop_cycle: Removes cycle information from the data. [Default: True]
    :param skiprows: Number of initial data points to drop. [Default: 2]
    :returns: Pandas DataFrame.
    """
    header = ['time', 'voltage', 'current', 'power', 'cycle']
    df = pd.read_csv(file, names=header, skiprows=(skiprows + 1))

    if drop_cycle:
        df.drop('cycle', axis=1, inplace=True)

    if set_index:
        df.set_index('time', inplace=True)

    df.columns.rename('metrics', inplace=True)

    if channel_pattern is not None:
        ch = channel_from_file_path(file, channel_pattern)
        df = std.insert_index_levels(df, ch, 'channel')

    return df
def import_mpp_cycle_datum(folder, voc_kwargs={}, jv_kwargs={}, mpp_kwargs={}):
    """
    Import MPP data for a single cycle.

    :param folder: Path to folder containing cycle data.
    :param voc_kwargs: Dictionary of keyword arguments passed to import_voc_datum().
        [Default: {}]
    :param jv_kwargs: Dictionary of keyword arguments passed to import_jv_datum().
        [Default: {}]
    :param mpp_kwargs: Dictionary of keyword arguments passed to import_mpp_datum().
        [Default: {}]
    :returns: Tuple of ( voc, jv, mpp ) DataFrames.
    """
    cycle = cycle_from_file_path(folder)
    dfs = list(import_mpp_datum(folder, voc_kwargs, jv_kwargs, mpp_kwargs))

    for index, df in enumerate(dfs):
        df = std.insert_index_levels(  # add cycle to index, below channel
            df,
            levels=[cycle],
            names=['cycle'],
            key_level=1)

        dfs[index] = df

    return tuple(dfs)
def import_voc_datum(file, channel_index='ch<>', set_index=True):
    """
    Imports Voc datum from the given file.
    
    :param file: File path.
    :param channel_index: Add channel from file path as index level.
        Uses value as pattern in standard_functions#metadata_from_file_name.
        None if channel should be excluded.
        [Default: 'ch<>']
    :param set_index: Sets the index to time. [Default: True]
    :returns: Pandas DataFrame.
    """
    header = ['time', 'voltage']
    df = pd.read_csv(file, names=header, skiprows=1)

    if set_index:
        df.set_index('time', inplace=True)

    df.columns.rename('metrics', inplace=True)

    if channel_index is not None:
        ch = channel_from_file_path(file, channel_index)
        df = std.insert_index_levels(df, ch, 'channel')

    return df
def import_jv_datum(file, channel_index='ch<>', by_scan=True):
    """
    Imports JV datum from the given file.
    
    :param file: File path.
    :param channel_index: Add channel from file path as index level.
        Uses value as pattern in standard_functions#metadata_from_file_name.
        None if channel should be excluded.
        [Default: 'ch<>']
    :param by_scan: Breaks data into forward and reverse scans, and sets the index to voltage. [Default: True]
    :returns: Pandas DataFrame.
    
    :raises ValueError: If multiple sign changes in the scan are detected.
    """
    header = ['voltage', 'current', 'power']
    df = pd.read_csv(file, names=header, skiprows=1)

    if by_scan:
        # detect direction change in votlage scan
        dv = df.voltage.diff()
        change = np.nan_to_num(np.diff(np.sign(dv)))  # calculate sign changes
        change = np.where(change != 0)[0]  # get indices of sign changes
        if change.size > 1:
            # more than one sign change
            raise ValueError('Multiple sign changes detected in scan.')

        change = change[0]

        # break scans apart
        forward_first = (dv[0] > 0)
        df = ([df[:(change + 1)], df[change:]]
              if forward_first else [df[change:], df[:(change + 1)]])

        for index, tdf in enumerate(df):
            # set index
            tdf.set_index('voltage', inplace=True)
            tdf.columns.rename('metrics', inplace=True)

            # create multi-index
            name = 'forward' if (index == 0) else 'reverse'
            tdf = std.add_index_levels(tdf, {name: ['current', 'power']},
                                       names=['direction'])

            df[index] = tdf  # replace with modified

        # reindex for common voltage values
        df = std.common_reindex(df)

        # combine scan directions
        df = pd.concat(df, axis=1)

    if channel_index is not None:
        ch = channel_from_file_path(file, channel_index)
        df = std.insert_index_levels(df, ch, 'channel')

    return df
def align_cycles(df):
    """
    Moves cycles from columns to index, adjusting times.

    :param df: DataFrame with cycles.
    :returns: DataFrame with time aligned in index by scan.
    """
    cycles = []
    time = 0
    for cycle, data in df.groupby(level='cycle', axis=1):
        data.index = data.index + time
        time = data.index.max()

        data = data.dropna()
        data.columns = data.columns.droplevel('cycle')
        data = std.insert_index_levels(data, cycle, 'cycle', axis=0)

        cycles.append(data)

    cycles = pd.concat(cycles, axis=0).sort_index(0)
    return cycles
def import_jv_datum_channel(file,
                            channel_pattern='ch-<>',
                            by_scan=True,
                            skiprows=2,
                            skiprows_tail=0):
    """
    Imports JV datum from the given file.

    :param file: File path.
    :param channel_pattern: Add channel from file path as index level.
        Uses value as pattern in standard_functions#metadata_from_file_name.
        None if channel should be excluded.
        [Default: 'ch-<>']
    :param by_scan: Breaks data into forward and reverse scans, and sets the index to voltage.
        [Default: True]
    :param skiprows: Number of initial data points to drop. [Default: 2]
    :param skiprows_tail: Number of end data points to drop. [Default: 0]
    :returns: Pandas DataFrame.

    :raises ValueError: If multiple sign changes in the scan are detected.
    """
    header = ['voltage', 'current', 'power']
    df = pd.read_csv(file, names=header, skiprows=(1 + skiprows))

    # drop tail points
    if skiprows_tail:
        df = df.iloc[:-skiprows_tail]

    if by_scan:
        df = jvdp.split_jv_scan(df)

    if channel_pattern is not None:
        ch = channel_from_file_path(file, channel_pattern)
        df = std.insert_index_levels(df, ch, 'channel')

    return df
def import_datum( path, **kwargs ):
    """
    Imports a Cary UV Vis Absorption data file into a Pandas DataFrame.

    :param path: Path to the file.
    :returns: Pandas DataFrame.
    """
    data_end = None
    fields   = None
    metrics  = None
    with open( path ) as f:
        for ( index, line ) in enumerate( f ):
            # search for end of data
            # occurs at first blank line

            # every line ends with trailing comma
            if index is 0:
                fields = line.split( ',' )[ :-1 ]

            elif index is 1:
                # get headers
                metrics = line.split( ',' )[ :-1 ]

            elif line is '\n':
                # no data in line
                data_end = index
                break

    # parse metrics for sample width
    # new samples begin with wavelength
    sample_indices = []
    for index, metric in enumerate( metrics ):
        if metric == 'Wavelength (nm)':
            sample_indices.append( index )

    # import samples individually
    df = []
    for ( i, sample_index ) in enumerate( sample_indices ):
        # get sample columns
        next_sample_index = ( # get next sample index, or end if last sample
            sample_indices[ i + 1 ]
            if ( i + 1 ) < len( sample_indices ) else
            len( metrics ) # final sample
        )
        use_cols = range( sample_index, next_sample_index )

        # get names of fields
        # ignore first as it is wavelength
        # will be set to index
        names = map(
            str.lower,
            metrics[ sample_index + 1 : next_sample_index ]
        )


        tdf = pd.read_csv(
            path,
            header = None,
            skiprows = 2,
            nrows = data_end - 2, # account for ignored headers
            usecols = use_cols,
            index_col = 0, # wavelength as index
            names = [ 'wavelength', *names ],
            **kwargs
        ).dropna()

        # add sample name to header
        tdf = std.insert_index_levels( tdf, fields[ sample_index ] )
        df.append( tdf )

    std.common_reindex( df )
    df = pd.concat( df, axis = 1 )
    return df