Instruction:
Given the following Python 3 function, write the documentation
def process_samples_table(samples_table,
                          instruments_table,
                          mef_transform_fxns=None,
                          beads_table=None,
                          base_dir=".",
                          verbose=False,
                          plot=False,
                          plot_dir=None):
    """
    Process flow cytometry samples, as specified by an input table.

    The function processes each entry in `samples_table`, and does the
    following:
        - Load the FCS file specified in the field "File Path".
        - Transform the forward scatter/side scatter to RFI.
        - Transform the fluorescence channels to the units specified in the
          column "<Channel name> Units".
        - Remove the 250 first and 100 last events.
        - Remove saturated events in the forward scatter and side scatter
          channels.
        - Apply density gating on the forward scatter/side scatter
          channels.
        - Plot combined forward/side scatter density plots and fluorescence
          historgrams, if `plot` = True.

    Names of forward/side scatter and fluorescence channels are taken from
    `instruments_table`.

    Parameters
    ----------
    samples_table : DataFrame
        Table specifying samples to be processed. For more information
        about the fields required in this table, please consult the
        module's documentation.
    instruments_table : DataFrame
        Table specifying instruments. For more information about the fields
        required in this table, please consult the module's documentation.
    mef_transform_fxns : dict or OrderedDict, optional
        Dictionary containing MEF transformation functions. If any entry
        in `samples_table` requires transformation to MEF, a key: value
        pair must exist in mef_transform_fxns, with the key being equal to
        the contents of field "Beads ID".
    beads_table : DataFrame, optional
        Table specifying beads samples used to generate
        `mef_transform_fxns`. This is used to check if a beads sample was
        taken at the same acquisition settings as a sample to be
        transformed to MEF. For any beads sample and channel for which a
        MEF transformation function has been generated, the following
        fields should be populated: ``<channel> Amp. Type`` and
        ``<channel> Detector Volt``. If `beads_table` is not specified, no
        checking will be performed.
    base_dir : str, optional
        Directory from where all the other paths are specified.
    verbose : bool, optional
        Whether to print information messages during the execution of this
        function.
    plot : bool, optional
        Whether to generate and save density/histogram plots of each
        sample, and each beads sample.
    plot_dir : str, optional
        Directory relative to `base_dir` into which plots are saved. If
        `plot` is False, this parameter is ignored. If ``plot==True`` and
        ``plot_dir is None``, plot without saving.

    Returns
    -------
    samples : list of FCSData objects
        A list of processed, gated, and transformed samples, as specified
        in `samples_table`, in the order of ``samples_table.index``.

    """
    # Initialize output variable
    samples = []

    # Return empty list if samples table is empty
    if samples_table.empty:
        return samples

    if verbose:
        msg = "Processing Samples table ({} entries)".format(len(samples_table))
        print("")
        print(msg)
        print("="*len(msg))

    # Check that plotting directory exist, create otherwise
    if plot and plot_dir is not None \
            and not os.path.exists(os.path.join(base_dir, plot_dir)):
        os.makedirs(os.path.join(base_dir, plot_dir))

    # Extract header and channel names for which units are specified.
    headers = list(samples_table.columns)
    report_headers_all = [h for h in headers if re_units.match(h)]
    report_channels_all = [re_units.match(h).group(1)
                           for h in report_headers_all]

    # Iterate through table
    # We will look for a ExcelUIException on each iteration. If an exception
    # is caught, it will be stored in beads_samples.
    for sample_id, sample_row in samples_table.iterrows():
        try:
            ###
            # Instrument Data
            ###
            # Get the appropriate row in the instrument table
            instruments_row = instruments_table.loc[sample_row['Instrument ID']]
            # Scatter channels: Foward Scatter, Side Scatter
            sc_channels = [instruments_row['Forward Scatter Channel'],
                           instruments_row['Side Scatter Channel'],
                           ]
            # Fluorescence channels is a comma-separated list
            fl_channels = instruments_row['Fluorescence Channels'].split(',')
            fl_channels = [s.strip() for s in fl_channels]

            ###
            # Sample Data
            ###
            if verbose:
                print("\nSample ID {}...".format(sample_id))
                print("Loading file \"{}\"...".format(sample_row['File Path']))

            # Attempt to open file
            filename = os.path.join(base_dir, sample_row['File Path'])
            try:
                sample = FlowCal.io.FCSData(filename)
            except IOError:
                raise ExcelUIException("file \"{}\" not found".format(
                    sample_row['File Path']))
            # Check that the number of events is greater than 400
            if sample.shape[0] < 400:
                raise ExcelUIException("number of events is lower than 400")

            ###
            # Transform
            ###
            if verbose:
                print("Performing data transformation...")
            # Transform FSC/SSC to linear scale
            sample = FlowCal.transform.to_rfi(sample, sc_channels)

            # Parse fluorescence channels in which to transform
            report_channels = []
            report_units = []
            for fl_channel in fl_channels:
                if fl_channel in report_channels_all:
                    # Get header from channel name
                    report_header = report_headers_all[
                        report_channels_all.index(fl_channel)]
                    # Extract text. If empty, ignore.
                    units_str = sample_row[report_header]
                    if pd.isnull(units_str):
                        continue
                    # Decide what transformation to perform
                    units = units_str.strip()
                    if units.lower() == 'channel':
                        units_label = "Channel Number"

                    elif units.lower() == 'rfi':
                        units_label = "Relative Fluorescence Intensity, RFI"
                        sample = FlowCal.transform.to_rfi(sample, fl_channel)
                    elif units.lower() == 'a.u.' or units.lower() == 'au':
                        units_label = "Arbitrary Units, a.u."
                        sample = FlowCal.transform.to_rfi(sample, fl_channel)

                    elif units.lower() == 'mef':
                        units_label = "Molecules of Equivalent Fluorophore, MEF"
                        # Check if transformation function is available
                        if mef_transform_fxns[sample_row['Beads ID']] is None:
                            raise ExcelUIException("MEF transformation "
                                "function not available")

                        # If beads_table is available, check if the same
                        # settings have been used to acquire the corresponding
                        # beads sample
                        if beads_table is not None:
                            beads_row = beads_table.loc[sample_row['Beads ID']]
                            # Instrument
                            beads_iid = beads_row['Instrument ID']
                            if beads_iid != sample_row['Instrument ID']:
                                raise ExcelUIException("Instruments for "
                                    "acquisition of beads and samples are not "
                                    "the same (beads {}'s instrument: {}, "
                                    "sample's instrument: {})".format(
                                        sample_row['Beads ID'],
                                        beads_iid,
                                        sample_row['Instrument ID']))
                            # Amplification type
                            beads_at = beads_row['{} Amp. Type'. \
                                format(fl_channel)]
                            if sample.amplification_type(fl_channel)[0]:
                                sample_at = "Log"
                            else:
                                sample_at = "Linear"
                            if beads_at != sample_at:
                                raise ExcelUIException("Amplification type for "
                                    "acquisition of beads and samples in "
                                    "channel {} are not the same (beads {}'s "
                                    "amplification: {}, sample's "
                                    "amplification: {})".format(
                                        fl_channel,
                                        sample_row['Beads ID'],
                                        beads_at,
                                        sample_at))
                            # Detector voltage
                            beads_dv = beads_row['{} Detector Volt.'. \
                                format(fl_channel)]
                            if sample.detector_voltage(fl_channel) is not None \
                                    and beads_dv != sample.detector_voltage(
                                        fl_channel):
                                raise ExcelUIException("Detector voltage for "
                                    "acquisition of beads and samples in "
                                    "channel {} are not the same (beads {}'s "
                                    "detector voltage: {}, sample's "
                                    "detector voltage: {})".format(
                                        fl_channel,
                                        sample_row['Beads ID'],
                                        beads_dv,
                                        sample.detector_voltage(fl_channel)))

                        # First, transform to RFI
                        sample = FlowCal.transform.to_rfi(sample, fl_channel)
                        # Attempt to transform to MEF
                        # Transformation function raises a ValueError if a
                        # standard curve does not exist for a channel
                        try:
                            sample = mef_transform_fxns[sample_row['Beads ID']](
                                sample,
                                fl_channel)
                        except ValueError:
                            raise ExcelUIException("no standard curve for "
                                "channel {}".format(fl_channel))
                    else:
                        raise ExcelUIException("units \"{}\" not recognized". \
                            format(units, sample_id))

                    # Register that reporting in this channel must be done
                    report_channels.append(fl_channel)
                    report_units.append(units_label)

            ###
            # Gate
            ###
            if verbose:
                print("Performing gating...")
            # Remove first and last events. Transients in fluidics can make the
            # first few and last events slightly different from the rest.
            sample_gated = FlowCal.gate.start_end(sample,
                                                  num_start=250,
                                                  num_end=100)
            # Remove saturating events in forward/side scatter, and fluorescent
            # channels to report, if the FCS data type is integer. The value of
            # a saturating event is taken automatically from
            # `sample_gated.range`.
            if sample_gated.data_type == 'I':
                sample_gated = FlowCal.gate.high_low(
                    sample_gated,
                    sc_channels + report_channels)
            # Density gating
            try:
                sample_gated, __, gate_contour = FlowCal.gate.density2d(
                    data=sample_gated,
                    channels=sc_channels,
                    gate_fraction=sample_row['Gate Fraction'],
                    xscale='logicle',
                    yscale='logicle',
                    full_output=True)
            except ValueError as ve:
                raise ExcelUIException(ve.message)

            # Plot forward/side scatter density plot and fluorescence histograms
            if plot:
                if verbose:
                    print("Plotting density plot and histogram...")
                # Density plot parameters
                density_params = {}
                density_params['mode'] = 'scatter'
                density_params["title"] = "{} ({:.1f}% retained)".format(
                    sample_id,
                    sample_gated.shape[0] * 100. / sample.shape[0])
                density_params['xscale'] = 'logicle'
                density_params['yscale'] = 'logicle'
                # Histogram plot parameters
                hist_params = []
                for rc, ru in zip(report_channels, report_units):
                    param = {}
                    param['xlabel'] = '{} ({})'.format(rc, ru)
                    # Only channel numbers are plotted in linear scale
                    if (ru == 'Channel Number'):
                        param['xscale'] = 'linear'
                    else:
                        param['xscale'] = 'logicle'
                    hist_params.append(param)

                # Plot
                if plot_dir is not None:
                    figname = os.path.join(
                        base_dir,
                        plot_dir,
                        "{}.png".format(sample_id))
                else:
                    figname = None
                FlowCal.plot.density_and_hist(
                    sample,
                    sample_gated,
                    gate_contour=gate_contour,
                    density_channels=sc_channels,
                    density_params=density_params,
                    hist_channels=report_channels,
                    hist_params=hist_params,
                    savefig=figname)

        except ExcelUIException as e:
            # Print Exception message
            if verbose:
                print("ERROR: {}".format(str(e)))
            # Append exception to samples array
            samples.append(e)

        else:
            # If no errors were found, store results
            samples.append(sample_gated)

    return samples

Response: This function processes the samples table and returns a new flow cytometry tree.


Instruction:
Can you generate the documentation for the following Python 3 function
def add_beads_stats(beads_table, beads_samples, mef_outputs=None):
    # The index name is not preserved if beads_table is empty.
    # Save the index name for later
    beads_table_index_name = beads_table.index.name

    # Add per-row info
    notes = []
    n_events = []
    acq_time = []
    for beads_sample in beads_samples:
        # Check if sample is an exception, otherwise assume it's an FCSData
        if isinstance(beads_sample, ExcelUIException):
            # Print error message
            notes.append("ERROR: {}".format(str(beads_sample)))
            n_events.append(np.nan)
            acq_time.append(np.nan)
        else:
            notes.append('')
            n_events.append(beads_sample.shape[0])
            acq_time.append(beads_sample.acquisition_time)

    beads_table['Analysis Notes'] = notes
    beads_table['Number of Events'] = n_events
    beads_table['Acquisition Time (s)'] = acq_time

    # List of channels that require stats columns
    headers = list(beads_table.columns)
    stats_headers = [h for h in headers if re_mef_values.match(h)]
    stats_channels = [re_mef_values.match(h).group(1) for h in stats_headers]

    # Iterate through channels
    for header, channel in zip(stats_headers, stats_channels):
        # Add empty columns to table
        beads_table[channel + ' Detector Volt.'] = np.nan
        beads_table[channel + ' Amp. Type'] = ""
        if mef_outputs:
            beads_table[channel + ' Beads Model'] = ""
            beads_table[channel + ' Beads Params. Names'] = ""
            beads_table[channel + ' Beads Params. Values'] = ""

        # Iterate
        for i, row_id in enumerate(beads_table.index):
            # If error, skip
            if isinstance(beads_samples[i], ExcelUIException):
                continue
            # If MEF values are specified, calculate stats. If not, leave empty.
            if pd.notnull(beads_table[header][row_id]):

                # Detector voltage
                # Dataframes, such as beads_table, are modified differently
                # depending on pandas' version.
                if packaging.version.parse(pd.__version__) \
                        < packaging.version.parse('0.21'):
                    beads_table.set_value(
                        row_id,
                        channel + ' Detector Volt.',
                        beads_samples[i].detector_voltage(channel))
                else:
                    beads_table.at[row_id, channel + ' Detector Volt.'] = \
                        beads_samples[i].detector_voltage(channel)

                # Amplification type
                if beads_samples[i].amplification_type(channel)[0]:
                    amplification_type = "Log"
                else:
                    amplification_type = "Linear"
                # Dataframes, such as beads_table, are modified differently
                # depending on pandas' version.
                if packaging.version.parse(pd.__version__) \
                        < packaging.version.parse('0.21'):
                    beads_table.set_value(row_id,
                                          channel + ' Amp. Type',
                                          amplification_type)
                else:
                    beads_table.at[row_id, channel + ' Amp. Type'] = \
                        amplification_type

                # Bead model and parameters
                # Only populate if mef_outputs has been provided
                if mef_outputs:
                    # Try to find the current channel among the mef'd channels.
                    # If successful, extract bead fitted parameters.
                    try:
                        mef_channel_index = mef_outputs[i]. \
                            mef_channels.index(channel)
                    except ValueError:
                        pass
                    else:
                        # Bead model
                        beads_model_str = mef_outputs[i]. \
                            fitting['beads_model_str'][mef_channel_index]
                        # Dataframes, such as beads_table, are modified
                        # differently depending on pandas' version.
                        if packaging.version.parse(pd.__version__) \
                                < packaging.version.parse('0.21'):
                            beads_table.set_value(row_id,
                                                  channel + ' Beads Model',
                                                  beads_model_str)
                        else:
                            beads_table.at[row_id, channel + ' Beads Model'] = \
                                beads_model_str

                        # Bead parameter names
                        params_names = mef_outputs[i]. \
                            fitting['beads_params_names'][mef_channel_index]
                        params_names_str = ", ".join([str(p)
                                                      for p in params_names])
                        # Dataframes, such as beads_table, are modified
                        # differently depending on pandas' version.
                        if packaging.version.parse(pd.__version__) \
                                < packaging.version.parse('0.21'):
                            beads_table.set_value(
                                row_id,
                                channel + ' Beads Params. Names',
                                params_names_str)
                        else:
                            beads_table.at[
                                row_id,
                                channel + ' Beads Params. Names'] = \
                                    params_names_str

                        # Bead parameter values
                        params = mef_outputs[i]. \
                            fitting['beads_params'][mef_channel_index]
                        params_str = ", ".join([str(p) for p in params])
                        # Dataframes, such as beads_table, are modified
                        # differently depending on pandas' version.
                        if packaging.version.parse(pd.__version__) \
                                < packaging.version.parse('0.21'):
                            beads_table.set_value(
                                row_id,
                                channel + ' Beads Params. Values',
                                params_str)
                        else:
                            beads_table.at[
                                row_id,
                                channel + ' Beads Params. Values'] = \
                                    params_str

    # Restore index name if table is empty
    if len(beads_table) == 0:
        beads_table.index.name = beads_table_index_name

Response: Add stats fields to beads table.


Instruction:
Write a Python 3 script to
add statistics fields to the samples table.

Response:
def add_samples_stats(samples_table, samples):
    """
    Add stats fields to samples table.

    The following information is added to each row:

        - Notes (warnings, errors) resulting from the analysis
        - Number of Events
        - Acquisition Time (s)

    The following information is added for each row, for each channel in
    which fluorescence units have been specified:

        - Detector voltage (gain)
        - Amplification type
        - Mean
        - Geometric Mean
        - Median
        - Mode
        - Standard Deviation
        - Coefficient of Variation (CV)
        - Geometric Standard Deviation
        - Geometric Coefficient of Variation
        - Inter-Quartile Range
        - Robust Coefficient of Variation (RCV)

    Parameters
    ----------
    samples_table : DataFrame
        Table specifying samples to analyze. For more information about the
        fields required in this table, please consult the module's
        documentation.
    samples : list
        FCSData objects from which to calculate statistics. ``samples[i]``
        should correspond to ``samples_table.iloc[i]``.

    Notes
    -----
    Geometric statistics (geometric mean, standard deviation, and geometric
    coefficient of variation) are defined only for positive data. If there
    are negative events in any relevant channel of any member of `samples`,
    geometric statistics will only be calculated on the positive events,
    and a warning message will be written to the "Analysis Notes" field.

    """
    # The index name is not preserved if samples_table is empty.
    # Save the index name for later
    samples_table_index_name = samples_table.index.name

    # Add per-row info
    notes = []
    n_events = []
    acq_time = []
    for sample in samples:
        # Check if sample is an exception, otherwise assume it's an FCSData
        if isinstance(sample, ExcelUIException):
            # Print error message
            notes.append("ERROR: {}".format(str(sample)))
            n_events.append(np.nan)
            acq_time.append(np.nan)
        else:
            notes.append('')
            n_events.append(sample.shape[0])
            acq_time.append(sample.acquisition_time)

    samples_table['Analysis Notes'] = notes
    samples_table['Number of Events'] = n_events
    samples_table['Acquisition Time (s)'] = acq_time

    # List of channels that require stats columns
    headers = list(samples_table.columns)
    stats_headers = [h for h in headers if re_units.match(h)]
    stats_channels = [re_units.match(h).group(1) for h in stats_headers]

    # Iterate through channels
    for header, channel in zip(stats_headers, stats_channels):
        # Add empty columns to table
        samples_table[channel + ' Detector Volt.'] = np.nan
        samples_table[channel + ' Amp. Type'] = ""
        samples_table[channel + ' Mean'] = np.nan
        samples_table[channel + ' Geom. Mean'] = np.nan
        samples_table[channel + ' Median'] = np.nan
        samples_table[channel + ' Mode'] = np.nan
        samples_table[channel + ' Std'] = np.nan
        samples_table[channel + ' CV'] = np.nan
        samples_table[channel + ' Geom. Std'] = np.nan
        samples_table[channel + ' Geom. CV'] = np.nan
        samples_table[channel + ' IQR'] = np.nan
        samples_table[channel + ' RCV'] = np.nan
        for row_id, sample in zip(samples_table.index, samples):
            # If error, skip
            if isinstance(sample, ExcelUIException):
                continue
            # If units are specified, calculate stats. If not, leave empty.
            if pd.notnull(samples_table[header][row_id]):
                # Acquisition settings

                # Detector voltage
                # Dataframes, such as samples_table, are modified
                # differently depending on pandas' version.
                if packaging.version.parse(pd.__version__) \
                        < packaging.version.parse('0.21'):
                    samples_table.set_value(row_id,
                                            channel + ' Detector Volt.',
                                            sample.detector_voltage(channel))
                else:
                    samples_table.at[row_id, channel + ' Detector Volt.'] = \
                        sample.detector_voltage(channel)

                # Amplification type
                if sample.amplification_type(channel)[0]:
                    amplification_type = "Log"
                else:
                    amplification_type = "Linear"
                # Dataframes, such as samples_table, are modified
                # differently depending on pandas' version.
                if packaging.version.parse(pd.__version__) \
                        < packaging.version.parse('0.21'):
                    samples_table.set_value(row_id,
                                            channel + ' Amp. Type',
                                            amplification_type)
                else:
                    samples_table.at[row_id, channel + ' Amp. Type'] = \
                        amplification_type

                # Statistics from event list
                # Dataframes, such as samples_table, are modified
                # differently depending on pandas' version.
                if packaging.version.parse(pd.__version__) \
                        < packaging.version.parse('0.21'):
                    samples_table.set_value(row_id,
                                            channel + ' Mean',
                                            FlowCal.stats.mean(sample, channel))
                    samples_table.set_value(row_id,
                                            channel + ' Median',
                                            FlowCal.stats.median(sample, channel))
                    samples_table.set_value(row_id,
                                            channel + ' Mode',
                                            FlowCal.stats.mode(sample, channel))
                    samples_table.set_value(row_id,
                                            channel + ' Std',
                                            FlowCal.stats.std(sample, channel))
                    samples_table.set_value(row_id,
                                            channel + ' CV',
                                            FlowCal.stats.cv(sample, channel))
                    samples_table.set_value(row_id,
                                            channel + ' IQR',
                                            FlowCal.stats.iqr(sample, channel))
                    samples_table.set_value(row_id,
                                            channel + ' RCV',
                                            FlowCal.stats.rcv(sample, channel))
                else:
                    samples_table.at[row_id, channel + ' Mean'] = \
                        FlowCal.stats.mean(sample, channel)
                    samples_table.at[row_id, channel + ' Median'] = \
                        FlowCal.stats.median(sample, channel)
                    samples_table.at[row_id, channel + ' Mode'] = \
                        FlowCal.stats.mode(sample, channel)
                    samples_table.at[row_id, channel + ' Std'] = \
                        FlowCal.stats.std(sample, channel)
                    samples_table.at[row_id, channel + ' CV'] = \
                        FlowCal.stats.cv(sample, channel)
                    samples_table.at[row_id, channel + ' IQR'] = \
                        FlowCal.stats.iqr(sample, channel)
                    samples_table.at[row_id, channel + ' RCV'] = \
                        FlowCal.stats.rcv(sample, channel)

                # For geometric statistics, first check for non-positive events.
                # If found, throw a warning and calculate statistics on positive
                # events only.
                if np.any(sample[:, channel] <= 0):
                    # Separate positive events
                    sample_positive = sample[sample[:, channel] > 0]
                    # Throw warning
                    msg = "Geometric statistics for channel" + \
                        " {} calculated on positive events".format(channel) + \
                        " only ({:.1f}%). ".format(
                            100.*sample_positive.shape[0]/sample.shape[0])
                    warnings.warn("On sample {}: {}".format(row_id, msg))
                    # Write warning message to table
                    if samples_table.loc[row_id, 'Analysis Notes']:
                        msg = samples_table.loc[row_id, 'Analysis Notes'] + msg
                    # Dataframes, such as samples_table, are modified
                    # differently depending on pandas' version.
                    if packaging.version.parse(pd.__version__) \
                            < packaging.version.parse('0.21'):
                        samples_table.set_value(row_id, 'Analysis Notes', msg)
                    else:
                        samples_table.at[row_id, 'Analysis Notes'] = msg
                else:
                    sample_positive = sample

                # Calculate and write geometric statistics
                # Dataframes, such as samples_table, are modified
                # differently depending on pandas' version.
                if packaging.version.parse(pd.__version__) \
                        < packaging.version.parse('0.21'):
                    samples_table.set_value(
                        row_id,
                        channel + ' Geom. Mean',
                        FlowCal.stats.gmean(sample_positive, channel))
                    samples_table.set_value(
                        row_id,
                        channel + ' Geom. Std',
                        FlowCal.stats.gstd(sample_positive, channel))
                    samples_table.set_value(
                        row_id,
                        channel + ' Geom. CV',
                        FlowCal.stats.gcv(sample_positive, channel))
                else:
                    samples_table.at[row_id, channel + ' Geom. Mean'] = \
                        FlowCal.stats.gmean(sample_positive, channel)
                    samples_table.at[row_id, channel + ' Geom. Std'] = \
                        FlowCal.stats.gstd(sample_positive, channel)
                    samples_table.at[row_id, channel + ' Geom. CV'] = \
                        FlowCal.stats.gcv(sample_positive, channel)

    # Restore index name if table is empty
    if len(samples_table) == 0:
        samples_table.index.name = samples_table_index_name


Instruction:
Explain what the following Python 3 code does
def generate_histograms_table(samples_table, samples, max_bins=1024):
    # Extract channels that require stats histograms
    headers = list(samples_table.columns)
    hist_headers = [h for h in headers if re_units.match(h)]
    hist_channels = [re_units.match(h).group(1) for h in hist_headers]

    # The number of columns in the DataFrame has to be set to the maximum
    # number of bins of any of the histograms about to be generated.
    # The following iterates through these histograms and finds the
    # largest.
    n_columns = 0
    for sample_id, sample in zip(samples_table.index, samples):
        if isinstance(sample, ExcelUIException):
            continue
        for header, channel in zip(hist_headers, hist_channels):
            if pd.notnull(samples_table[header][sample_id]):
                if n_columns < sample.resolution(channel):
                    n_columns = sample.resolution(channel)
    # Saturate at max_bins
    if n_columns > max_bins:
        n_columns = max_bins

    # Declare multi-indexed DataFrame
    index = pd.MultiIndex.from_arrays([[],[],[]],
                                      names = ['Sample ID', 'Channel', ''])
    columns = ['Bin {}'.format(i + 1) for i in range(n_columns)]
    hist_table = pd.DataFrame([], index=index, columns=columns)

    # Generate histograms
    for sample_id, sample in zip(samples_table.index, samples):
        if isinstance(sample, ExcelUIException):
            continue
        for header, channel in zip(hist_headers, hist_channels):
            if pd.notnull(samples_table[header][sample_id]):
                # Get units in which bins are being reported
                unit = samples_table[header][sample_id]
                # Decide which scale to use
                # Channel units result in linear scale. Otherwise, use logicle.
                if unit == 'Channel':
                    scale = 'linear'
                else:
                    scale = 'logicle'
                # Define number of bins
                nbins = min(sample.resolution(channel), max_bins)
                # Calculate bin edges and centers
                # We generate twice the necessary number of bins. We then take
                # every other value as the proper bin edges, and the remaining
                # values as the bin centers.
                bins_extended = sample.hist_bins(channel, 2*nbins, scale)
                bin_edges = bins_extended[::2]
                bin_centers = bins_extended[1::2]
                # Store bin centers
                hist_table.loc[(sample_id,
                                channel,
                                'Bin Centers ({})'.format(unit)),
                                columns[0:len(bin_centers)]] = bin_centers
                # Calculate and store histogram counts
                hist, __ = np.histogram(sample[:,channel], bins=bin_edges)
                hist_table.loc[(sample_id, channel, 'Counts'),
                               columns[0:len(bin_centers)]] = hist

    return hist_table

Response: Generates a multi - indexed DataFrame containing histograms for each sample and channel in samples_table.


Instruction:
Implement a function in Python 3 to
generate a DataFrame with information about FlowCal and the current analysis.

Response:
def generate_about_table(extra_info={}):
    """
    Make a table with information about FlowCal and the current analysis.

    Parameters
    ----------
    extra_info : dict, optional
        Additional keyword:value pairs to include in the table.

    Returns
    -------
    about_table : DataFrame
        Table with information about FlowCal and the current analysis, as
        keyword:value pairs. The following keywords are included: FlowCal
        version, and date and time of analysis. Keywords and values from
        `extra_info` are also included.

    """
    # Make keyword and value arrays
    keywords = []
    values = []
    # FlowCal version
    keywords.append('FlowCal version')
    values.append(FlowCal.__version__)
    # Analysis date and time
    keywords.append('Date of analysis')
    values.append(time.strftime("%Y/%m/%d"))
    keywords.append('Time of analysis')
    values.append(time.strftime("%I:%M:%S%p"))
    # Add additional keyword:value pairs
    for k, v in six.iteritems(extra_info):
        keywords.append(k)
        values.append(v)

    # Make table as data frame
    about_table = pd.DataFrame(values, index=keywords)

    # Set column names
    about_table.columns = ['Value']
    about_table.index.name = 'Keyword'

    return about_table


Instruction:
Can you generate the documentation for the following Python 3 function
def show_open_file_dialog(filetypes):
    # The following line is used to Tk's main window is not shown
    Tk().withdraw()

    # OSX ONLY: Call bash script to prevent file select window from sticking
    # after use.
    if platform.system() == 'Darwin':
        subprocess.call("defaults write org.python.python " +
                "ApplePersistenceIgnoreState YES", shell=True)
        filename = askopenfilename(filetypes=filetypes)
        subprocess.call("defaults write org.python.python " +
                "ApplePersistenceIgnoreState NO", shell=True)
    else:
        filename = askopenfilename(filetypes=filetypes)

    return filename

Response: Show an open file dialog and return the path of the selected file.


Instruction:
Can you implement a function in Python 3 that
runs the MS Excel User Interface. This function performs the following: 1. If `input_path` is not specified, show a dialog to choose an input Excel file. 2. Extract data from the Instruments, Beads, and Samples tables. 3. Process all the bead samples specified in the Beads table. 4. Generate statistics for each bead sample. 5. Process all the cell samples in the Samples table. 6. Generate statistics for each sample. 7. If requested, generate a histogram table for each fluorescent channel specified for each sample. 8. Generate a table with run time, date, FlowCal version, among others. 9. Save statistics and (if requested) histograms in an output Excel file. Parameters ---------- input_path : str Path to the Excel file to use as input. If None, show a dialog to select an input file. output_path : str Path to which to save the output Excel file. If None, use "<input_path>_output". verbose : bool, optional Whether to print information messages during the execution of this function. plot : bool, optional Whether to generate and save density/histogram plots of each sample, and each beads sample. hist_sheet : bool, optional Whether to generate a sheet in the output Excel file specifying histogram bin information.

Response:
def run(input_path=None,
        output_path=None,
        verbose=True,
        plot=True,
        hist_sheet=False):
    """
    Run the MS Excel User Interface.

    This function performs the following:

     1. If `input_path` is not specified, show a dialog to choose an input
        Excel file.
     2. Extract data from the Instruments, Beads, and Samples tables.
     3. Process all the bead samples specified in the Beads table.
     4. Generate statistics for each bead sample.
     5. Process all the cell samples in the Samples table.
     6. Generate statistics for each sample.
     7. If requested, generate a histogram table for each fluorescent
        channel specified for each sample.
     8. Generate a table with run time, date, FlowCal version, among
        others.
     9. Save statistics and (if requested) histograms in an output Excel
        file.

    Parameters
    ----------
    input_path : str
        Path to the Excel file to use as input. If None, show a dialog to
        select an input file.
    output_path : str
        Path to which to save the output Excel file. If None, use
        "<input_path>_output".
    verbose : bool, optional
        Whether to print information messages during the execution of this
        function.
    plot : bool, optional
        Whether to generate and save density/histogram plots of each
        sample, and each beads sample.
    hist_sheet : bool, optional
        Whether to generate a sheet in the output Excel file specifying
        histogram bin information.

    """

    # If input file has not been specified, show open file dialog
    if input_path is None:
        input_path = show_open_file_dialog(filetypes=[('Excel files',
                                                       '*.xlsx')])
        if not input_path:
            if verbose:
                print("No input file selected.")
            return
    # Extract directory, filename, and filename with no extension from path
    input_dir, input_filename = os.path.split(input_path)
    input_filename_no_ext, __ = os.path.splitext(input_filename)

    # Read relevant tables from workbook
    if verbose:
        print("Reading {}...".format(input_filename))
    instruments_table = read_table(input_path,
                                   sheetname='Instruments',
                                   index_col='ID')
    beads_table = read_table(input_path,
                             sheetname='Beads',
                             index_col='ID')
    samples_table = read_table(input_path,
                               sheetname='Samples',
                               index_col='ID')

    # Process beads samples
    beads_samples, mef_transform_fxns, mef_outputs = process_beads_table(
        beads_table,
        instruments_table,
        base_dir=input_dir,
        verbose=verbose,
        plot=plot,
        plot_dir='plot_beads',
        full_output=True)

    # Add stats to beads table
    if verbose:
        print("")
        print("Calculating statistics for beads...")
    add_beads_stats(beads_table, beads_samples, mef_outputs)

    # Process samples
    samples = process_samples_table(
        samples_table,
        instruments_table,
        mef_transform_fxns=mef_transform_fxns,
        beads_table=beads_table,
        base_dir=input_dir,
        verbose=verbose,
        plot=plot,
        plot_dir='plot_samples')

    # Add stats to samples table
    if verbose:
        print("")
        print("Calculating statistics for all samples...")
    add_samples_stats(samples_table, samples)

    # Generate histograms
    if hist_sheet:
        if verbose:
            print("Generating histograms table...")
        histograms_table = generate_histograms_table(samples_table, samples)

    # Generate about table
    about_table = generate_about_table({'Input file path': input_path})

    # Generate list of tables to save
    table_list = []
    table_list.append(('Instruments', instruments_table))
    table_list.append(('Beads', beads_table))
    table_list.append(('Samples', samples_table))
    if hist_sheet:
        table_list.append(('Histograms', histograms_table))
    table_list.append(('About Analysis', about_table))

    # Write output excel file
    if verbose:
        print("Saving output Excel file...")
    if output_path is None:
        output_filename = "{}_output.xlsx".format(input_filename_no_ext)
        output_path = os.path.join(input_dir, output_filename)
    write_workbook(output_path, table_list)

    if verbose:
        print("\nDone.")


Instruction:
Make a summary of the following Python 3 code
def run_command_line(args=None):
    # Get arguments from ``sys.argv`` if necessary.
    # ``sys.argv`` has the name of the script as its first element. We remove
    # this element because it will break ``parser.parse_args()`` later. In fact,
    # ``parser.parse_args()``, if provided with no arguments, will also use
    # ``sys.argv`` after removing the first element.
    if args is None:
        args = sys.argv[1:]

    import argparse
    # Read command line arguments
    parser = argparse.ArgumentParser(
        description="process flow cytometry files with FlowCal's Excel UI.")
    parser.add_argument(
        "-i",
        "--inputpath",
        type=str,
        nargs='?',
        help="input Excel file name. If not specified, show open file window")
    parser.add_argument(
        "-o",
        "--outputpath",
        type=str,
        nargs='?',
        help="output Excel file name. If not specified, use [INPUTPATH]_output")
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="print information about individual processing steps")
    parser.add_argument(
        "-p",
        "--plot",
        action="store_true",
        help="generate and save density plots/histograms of beads and samples")
    parser.add_argument(
        "-H",
        "--histogram-sheet",
        action="store_true",
        help="generate sheet in output Excel file specifying histogram bins")
    args = parser.parse_args(args=args)

    # Run Excel UI
    run(input_path=args.inputpath,
        output_path=args.outputpath,
        verbose=args.verbose,
        plot=args.plot,
        hist_sheet=args.histogram_sheet)

Response: Entry point for FlowCal and FlowCal console scripts.


Instruction:
Explain what the following Python 3 code does
def read_fcs_header_segment(buf, begin=0):
    fields = [
        'version',
        'text_begin',
        'text_end',
        'data_begin',
        'data_end',
        'analysis_begin',
        'analysis_end']

    FCSHeader = collections.namedtuple('FCSHeader', fields)

    field_values = []

    buf.seek(begin)
    field_values.append(buf.read(10).decode(encoding).rstrip()) # version

    field_values.append(int(buf.read(8)))                       # text_begin
    field_values.append(int(buf.read(8)))                       # text_end
    field_values.append(int(buf.read(8)))                       # data_begin
    field_values.append(int(buf.read(8)))                       # data_end

    fv = buf.read(8).decode(encoding)                           # analysis_begin
    field_values.append(0 if fv == ' '*8 else int(fv))
    fv = buf.read(8).decode(encoding)                           # analysis_end
    field_values.append(0 if fv == ' '*8 else int(fv))

    header = FCSHeader._make(field_values)
    return header

Response: Reads the HEADER segment of the FCS file and returns it as a sequence of namedtuple objects.


Instruction:
Write a Python 3 script for
reading a TEXT segment of FCS file into a tree of lists.

Response:
def read_fcs_text_segment(buf, begin, end, delim=None, supplemental=False):
    """
    Read TEXT segment of FCS file.

    Parameters
    ----------
    buf : file-like object
        Buffer containing data to interpret as TEXT segment.
    begin : int
        Offset (in bytes) to first byte of TEXT segment in `buf`.
    end : int
        Offset (in bytes) to last byte of TEXT segment in `buf`.
    delim : str, optional
        1-byte delimiter character which delimits key-value entries of
        TEXT segment. If None and ``supplemental==False``, will extract
        delimiter as first byte of TEXT segment.
    supplemental : bool, optional
        Flag specifying that segment is a supplemental TEXT segment (see
        FCS3.0 and FCS3.1), in which case a delimiter (``delim``) must be
        specified.

    Returns
    -------
    text : dict
        Dictionary of key-value entries extracted from TEXT segment.
    delim : str or None
        String containing delimiter or None if TEXT segment is empty.

    Raises
    ------
    ValueError
        If supplemental TEXT segment (``supplemental==True``) but ``delim`` is
        not specified.
    ValueError
        If primary TEXT segment (``supplemental==False``) does not start with
        delimiter.
    ValueError
        If first keyword starts with delimiter (e.g. a primary TEXT segment
        with the following contents: ///k1/v1/k2/v2/).
    ValueError
        If odd number of keys + values detected (indicating an unpaired key or
        value).
    ValueError
        If TEXT segment is ill-formed (unable to be parsed according to the
        FCS standards).

    Notes
    -----
    ANALYSIS segments and supplemental TEXT segments are parsed the same way,
    so this function can also be used to parse ANALYSIS segments.

    This function does *not* automatically parse and accumulate additional
    TEXT-like segments (e.g. supplemental TEXT segments or ANALYSIS segments)
    referenced in the originally specified TEXT segment.

    References
    ----------
    .. [1] P.N. Dean, C.B. Bagwell, T. Lindmo, R.F. Murphy, G.C. Salzman,
       "Data file standard for flow cytometry. Data File Standards
       Committee of the Society for Analytical Cytology," Cytometry vol
       11, pp 323-332, 1990, PMID 2340769.

    .. [2] L.C. Seamer, C.B. Bagwell, L. Barden, D. Redelman, G.C. Salzman,
       J.C. Wood, R.F. Murphy, "Proposed new data file standard for flow
       cytometry, version FCS 3.0," Cytometry vol 28, pp 118-122, 1997,
       PMID 9181300.

    .. [3] J. Spidlen, et al, "Data File Standard for Flow Cytometry,
       version FCS 3.1," Cytometry A vol 77A, pp 97-100, 2009, PMID
       19937951.

    """
    if delim is None:
        if supplemental:
            raise ValueError("must specify ``delim`` if reading supplemental"
                             + " TEXT segment")
        else:
            buf.seek(begin)
            delim = buf.read(1).decode(encoding)

    # The offsets are inclusive (meaning they specify first and last byte
    # WITHIN segment) and seeking is inclusive (read() after seek() reads the
    # byte which was seeked to). This means the length of the segment is
    # ((end+1) - begin).
    buf.seek(begin)
    raw = buf.read((end+1)-begin).decode(encoding)

    # If segment is empty, return empty dictionary as text
    if not raw:
        return {}, None

    if not supplemental:
        # Check that the first character of the TEXT segment is equal to the
        # delimiter.
        if raw[0] != delim:
            raise ValueError("primary TEXT segment should start with"
                             + " delimiter")

    # The FCS standards indicate that keyword values must be flanked by the
    # delimiter character, but they do not require that the TEXT segment end
    # with the delimiter. As such, look for the last instance of the delimiter
    # in the segment and retain everything before it (potentially removing
    # TEXT segment characters which occur after the last instance of the
    # delimiter).
    end_index = raw.rfind(delim)
    if supplemental and end_index == -1:
        # Delimiter was not found. This should only be permitted for an empty
        # supplemental TEXT segment (primary TEXT segment should fail above
        # if first character doesn't match delimiter).
        return {}, delim
    else:
        raw = raw[:end_index]

    pairs_list = raw.split(delim)

    ###
    # Reconstruct Keys and Values By Aggregating Escaped Delimiters
    ###
    # According to the FCS standards, delimiter characters are permitted in
    # keywords and keyword values as long as they are escaped by being
    # immediately repeated. Delimiter characters are not permitted as the
    # first character of a keyword or keyword value, however. Null (zero
    # length) keywords or keyword values are also not permitted. According to
    # these restrictions, a delimiter character should manifest itself as an
    # empty element in ``pairs_list``. As such, scan through ``pairs_list``
    # looking for empty elements and reconstruct keywords or keyword values
    # containing delimiters.
    ###

    # Start scanning from the end of the list since the end of the list is
    # well defined (i.e. doesn't depend on whether the TEXT segment is a
    # primary segment, which MUST start with the delimiter, or a supplemental
    # segment, which is not required to start with the delimiter).
    reconstructed_KV_accumulator = []
    idx = len(pairs_list) - 1
    while idx >= 0:
        if pairs_list[idx] == '':
            # Count the number of consecutive empty elements to determine how
            # many escaped delimiters exist and whether or not a true boundary
            # delimiter exists.
            num_empty_elements = 1
            idx = idx - 1
            while idx >=0 and pairs_list[idx] == '':
                num_empty_elements = num_empty_elements + 1
                idx = idx - 1
            # Need to differentiate between rolling off the bottom of the list
            # and hitting a non-empty element. Assessing pairs_list[-1] can
            # still be evaluated (by wrapping around the list, which is not
            # what we want to check) so check if we've finished scanning the
            # list first.
            if idx < 0:
                # We rolled off the bottom of the list.
                if num_empty_elements == 1:
                    # If we only hit 1 empty element before rolling off the
                    # list, then there were no escaped delimiters and the
                    # segment started with a delimiter.
                    break
                elif (num_empty_elements % 2) == 0:
                    # Even number of empty elements.
                    #
                    # If this is a supplemental TEXT segment, this can be
                    # interpreted as *not* starting the TEXT segment with the
                    # delimiter (which is permitted for supplemental TEXT
                    # segments) and starting the first keyword with one or
                    # more delimiters, which is prohibited.
                    if supplemental:
                        raise ValueError("starting a TEXT segment keyword"
                                         + " with a delimiter is prohibited")
                    # If this is a primary TEXT segment, this is an ill-formed
                    # segment. Rationale: 1 empty element will always be
                    # consumed as the initial delimiter which a primary TEXT
                    # segment is required to start with. After that delimiter
                    # is accounted for, you now have either an unescaped
                    # delimiter, which is prohibited, or a boundary delimiter,
                    # which would imply that the entire first keyword was
                    # composed of delimiters, which is prohibited because
                    # keywords are not allowed to start with the delimiter).
                    raise ValueError("ill-formed TEXT segment")
                else:
                    # Odd number of empty elements. This can be interpreted as
                    # starting the segment with a delimiter and then having
                    # one or more delimiters starting the first keyword, which
                    # is prohibited.
                    raise ValueError("starting a TEXT segment keyword with a"
                                     + " delimiter is prohibited")
            else:
                # We encountered a non-empty element. Calculate the number of
                # escaped delimiters and whether or not a true boundary
                # delimiter is present.
                num_delim      = (num_empty_elements+1)//2
                boundary_delim = (num_empty_elements % 2) == 0

                if boundary_delim:
                    # A boundary delimiter exists. We know that the boundary
                    # has to be on the right side (end of the sequence),
                    # because keywords and keyword values are prohibited from
                    # starting with a delimiter, which would be the case if the
                    # boundary delimiter was anywhere BUT the last character.
                    # This means we need to postpend the appropriate number of
                    # delim characters to the end of the non-empty list
                    # element that ``idx`` currently points to.
                    pairs_list[idx] = pairs_list[idx] + (num_delim*delim)

                    # We can now add the reconstructed keyword or keyword
                    # value to the accumulator and move on. It's possible that
                    # this keyword or keyword value is incompletely
                    # reconstructed (e.g. /key//1///value1/
                    # => {'key/1/':'value1'}; there are other delimiters in
                    # this keyword or keyword value), but that case is now
                    # handled independently of this case and just like any
                    # other instance of escaped delimiters *without* a
                    # boundary delimiter.
                    reconstructed_KV_accumulator.append(pairs_list[idx])
                    idx = idx - 1
                else:
                    # No boundary delimiters exist, so we need to glue the
                    # list elements before and after this sequence of
                    # consecutive delimiters together with the appropriate
                    # number of delimiters.
                    if len(reconstructed_KV_accumulator) == 0:
                        # Edge Case: The accumulator should always have at
                        # least 1 element in it at this point. If it doesn't,
                        # the last value ends with the delimiter, which will
                        # result in two consecutive empty elements, which
                        # won't fall into this case. Only 1 empty element
                        # indicates an ill-formed TEXT segment with an
                        # unpaired non-boundary delimiter (e.g. /k1/v1//),
                        # which is not permitted. The ill-formed TEXT segment
                        # implied by 1 empty element is recoverable, though,
                        # and a use case which is known to exist, so throw a
                        # warning and ignore the 2nd copy of the delimiter.
                        warnings.warn("detected ill-formed TEXT segment (ends"
                                      + " with two delimiter characters)."
                                      + " Ignoring last delimiter character")
                        reconstructed_KV_accumulator.append(pairs_list[idx])
                    else:
                        reconstructed_KV_accumulator[-1] = pairs_list[idx] + \
                            (num_delim*delim) + reconstructed_KV_accumulator[-1]
                    idx = idx - 1
        else:
            # Non-empty element, just append
            reconstructed_KV_accumulator.append(pairs_list[idx])
            idx = idx - 1

    pairs_list_reconstructed = list(reversed(reconstructed_KV_accumulator))

    # List length should be even since all key-value entries should be pairs
    if len(pairs_list_reconstructed) % 2 != 0:
        raise ValueError("odd # of (keys + values); unpaired key or value")

    text = dict(zip(pairs_list_reconstructed[0::2],
                    pairs_list_reconstructed[1::2]))

    return text, delim


Instruction:
How would you implement a function in Python 3 that
reads the DATA segment of the FCS file.

Response:
def read_fcs_data_segment(buf,
                          begin,
                          end,
                          datatype,
                          num_events,
                          param_bit_widths,
                          big_endian,
                          param_ranges=None):
    """
    Read DATA segment of FCS file.

    Parameters
    ----------
    buf : file-like object
        Buffer containing data to interpret as DATA segment.
    begin : int
        Offset (in bytes) to first byte of DATA segment in `buf`.
    end : int
        Offset (in bytes) to last byte of DATA segment in `buf`.
    datatype : {'I', 'F', 'D', 'A'}
        String specifying FCS file datatype (see $DATATYPE keyword from
        FCS standards). Supported datatypes include 'I' (unsigned
        binary integer), 'F' (single precision floating point), and 'D'
        (double precision floating point). 'A' (ASCII) is recognized
        but not supported.
    num_events : int
        Total number of events (see $TOT keyword from FCS standards).
    param_bit_widths : array-like
        Array specifying parameter (aka channel) bit width for each
        parameter (see $PnB keywords from FCS standards). The length of
        `param_bit_widths` should match the $PAR keyword value from the
        FCS standards (which indicates the total number of parameters).
        If `datatype` is 'I', data must be byte aligned (i.e. all
        parameter bit widths should be divisible by 8), and data are
        upcast to the nearest uint8, uint16, uint32, or uint64 data
        type. Bit widths larger than 64 bits are not supported.
    big_endian : bool
        Endianness of computer used to acquire data (see $BYTEORD
        keyword from FCS standards). True implies big endian; False
        implies little endian.
    param_ranges : array-like, optional
        Array specifying parameter (aka channel) range for each
        parameter (see $PnR keywords from FCS standards). Used to
        ensure erroneous values are not read from DATA segment by
        applying a bit mask to remove unused bits. The length of
        `param_ranges` should match the $PAR keyword value from the FCS
        standards (which indicates the total number of parameters). If
        None, no masking is performed.

    Returns
    -------
    data : numpy array
        NxD numpy array describing N cytometry events observing D data
        dimensions.

    Raises
    ------
    ValueError
        If lengths of `param_bit_widths` and `param_ranges` don't match.
    ValueError
        If calculated DATA segment size (as determined from the number
        of events, the number of parameters, and the number of bytes per
        data point) does not match size specified by `begin` and `end`.
    ValueError
        If `param_bit_widths` doesn't agree with `datatype` for single
        precision or double precision floating point (i.e. they should
        all be 32 or 64, respectively).
    ValueError
        If `datatype` is unrecognized.
    NotImplementedError
        If `datatype` is 'A'.
    NotImplementedError
        If `datatype` is 'I' but data is not byte aligned.

    References
    ----------
    .. [1] P.N. Dean, C.B. Bagwell, T. Lindmo, R.F. Murphy, G.C. Salzman,
       "Data file standard for flow cytometry. Data File Standards
       Committee of the Society for Analytical Cytology," Cytometry vol
       11, pp 323-332, 1990, PMID 2340769.

    .. [2] L.C. Seamer, C.B. Bagwell, L. Barden, D. Redelman, G.C. Salzman,
       J.C. Wood, R.F. Murphy, "Proposed new data file standard for flow
       cytometry, version FCS 3.0," Cytometry vol 28, pp 118-122, 1997,
       PMID 9181300.

    .. [3] J. Spidlen, et al, "Data File Standard for Flow Cytometry,
       version FCS 3.1," Cytometry A vol 77A, pp 97-100, 2009, PMID
       19937951.

    """
    num_params = len(param_bit_widths)
    if (param_ranges is not None and len(param_ranges) != num_params):
        raise ValueError("param_bit_widths and param_ranges must have same"
            + " length")

    shape = (int(num_events), num_params)

    if datatype == 'I':
        # Check if all parameters fit into preexisting data type
        if (all(bw == 8  for bw in param_bit_widths) or
            all(bw == 16 for bw in param_bit_widths) or
            all(bw == 32 for bw in param_bit_widths) or
            all(bw == 64 for bw in param_bit_widths)):

            num_bits = param_bit_widths[0]

            # Sanity check that the total # of bytes that we're about to
            # interpret is exactly the # of bytes in the DATA segment.
            # In some FCS files, the offset to the last byte (end) actually
            # points to the first byte of the next segment, in which case the #
            # of bytes specified in the header exceeds the # of bytes that we
            # should read by one.
            if (shape[0]*shape[1]*(num_bits//8)) != ((end+1)-begin) and \
                    (shape[0]*shape[1]*(num_bits//8)) != (end-begin):
                raise ValueError("DATA size does not match expected array"
                    + " size (array size ="
                    + " {0} bytes,".format(shape[0]*shape[1]*(num_bits//8))
                    + " DATA segment size = {0} bytes)".format((end+1)-begin))

            dtype = np.dtype('{0}u{1}'.format('>' if big_endian else '<',
                                              num_bits//8))
            data = np.memmap(
                buf,
                dtype=dtype,
                mode='r',
                offset=begin,
                shape=shape,
                order='C')

            # Cast memmap object to regular numpy array stored in memory (as
            # opposed to being backed by disk)
            data = np.array(data)
        else:
            # The FCS standards technically allows for parameters to NOT be
            # byte aligned, but parsing a DATA segment which is not byte
            # aligned requires significantly more computation (and probably an
            # external library which exposes bit level resolution to a block
            # of memory). I don't think this is a common use case, so I'm just
            # going to detect it and raise an error.
            if (not all(bw % 8 == 0 for bw in param_bit_widths) or
                any(bw > 64 for bw in param_bit_widths)):
                raise NotImplementedError("only byte aligned parameter bit"
                    + " widths (bw % 8 = 0) <= 64 are supported"
                    + " (param_bit_widths={0})".format(param_bit_widths))

            # Read data in as a byte array
            byte_shape = (int(num_events),
                          np.sum(np.array(param_bit_widths)//8))

            # Sanity check that the total # of bytes that we're about to
            # interpret is exactly the # of bytes in the DATA segment.
            # In some FCS files, the offset to the last byte (end) actually
            # points to the first byte of the next segment, in which case the #
            # of bytes specified in the header exceeds the # of bytes that we
            # should read by one.
            if (byte_shape[0]*byte_shape[1]) != ((end+1)-begin) and \
                    (byte_shape[0]*byte_shape[1]) != (end-begin):
                raise ValueError("DATA size does not match expected array"
                    + " size (array size ="
                    + " {0} bytes,".format(byte_shape[0]*byte_shape[1])
                    + " DATA segment size = {0} bytes)".format((end+1)-begin))

            byte_data = np.memmap(
                buf,
                dtype='uint8',  # endianness doesn't matter for 1 byte
                mode='r',
                offset=begin,
                shape=byte_shape,
                order='C')

            # Upcast all data to fit nearest supported data type of largest
            # bit width
            upcast_bw = int(2**np.max(np.ceil(np.log2(param_bit_widths))))

            # Create new array of upcast data type and use byte data to
            # populate it. The new array will have endianness native to user's
            # machine; does not preserve endianness of stored FCS data.
            upcast_dtype = 'u{0}'.format(upcast_bw//8)
            data = np.zeros(shape,dtype=upcast_dtype)

            # Array mapping each column of data to first corresponding column
            # in byte_data
            byte_boundaries = np.roll(np.cumsum(param_bit_widths)//8,1)
            byte_boundaries[0] = 0

            # Reconstitute columns of data by bit shifting appropriate columns
            # in byte_data and accumulating them
            for col in range(data.shape[1]):
                num_bytes = param_bit_widths[col]//8
                for b in range(num_bytes):
                    byte_data_col = byte_boundaries[col] + b
                    byteshift = (num_bytes-b-1) if big_endian else b

                    if byteshift > 0:
                        # byte_data must be upcast or else bit shift fails
                        data[:,col] += \
                            byte_data[:,byte_data_col].astype(upcast_dtype) \
                            << (byteshift*8)
                    else:
                        data[:,col] += byte_data[:,byte_data_col]

        if param_ranges is not None:
            # To strictly follow the FCS standards, mask off the unused high bits
            # as specified by param_ranges.
            for col in range(data.shape[1]):
                # bits_used should be related to resolution of cytometer ADC
                bits_used = int(np.ceil(np.log2(param_ranges[col])))

                # Create a bit mask to mask off all but the lowest bits_used bits.
                # bitmask is a native python int type which does not have an
                # underlying size. The int type is effectively left-padded with
                # 0s (infinitely), and the '&' operation preserves the dataype of
                # the array, so this shouldn't be an issue.
                bitmask = ~((~0) << bits_used)
                data[:,col] &= bitmask

    elif datatype in ('F','D'):
        num_bits = 32 if datatype == 'F' else 64

        # Confirm that bit widths are consistent with data type
        if not all(bw == num_bits for bw in param_bit_widths):
            raise ValueError("all param_bit_widths should be"
                + " {0} if datatype =".format(num_bits)
                + " \'{0}\' (param_bit_widths=".format(datatype)
                + "{0})".format(param_bit_widths))

        # Sanity check that the total # of bytes that we're about to interpret
        # is exactly the # of bytes in the DATA segment.
        # In some FCS files, the offset to the last byte (end) actually points
        # to the first byte of the next segment, in which case the # of bytes
        # specified in the header exceeds the # of bytes that we should read by
        # one.
        if (shape[0]*shape[1]*(num_bits//8)) != ((end+1)-begin) and \
            (shape[0]*shape[1]*(num_bits//8)) != (end-begin):
            raise ValueError("DATA size does not match expected array size"
                + " (array size = {0}".format(shape[0]*shape[1]*(num_bits//8))
                + " bytes, DATA segment size ="
                + " {0} bytes)".format((end+1)-begin))

        dtype = np.dtype('{0}f{1}'.format('>' if big_endian else '<',
                                          num_bits//8))
        data = np.memmap(
            buf,
            dtype=dtype,
            mode='r',
            offset=begin,
            shape=shape,
            order='C')

        # Cast memmap object to regular numpy array stored in memory (as
        # opposed to being backed by disk)
        data = np.array(data)
    elif datatype == 'A':
        raise NotImplementedError("only \'I\' (unsigned binary integer),"
            + " \'F\' (single precision floating point), and \'D\' (double"
            + " precision floating point) data types are supported (detected"
            + " datatype=\'{0}\')".format(datatype))
    else:
        raise ValueError("unrecognized datatype (detected datatype="
            + "\'{0}\')".format(datatype))

    return data


Instruction:
Can you generate the documentation for the following Python 3 function
def acquisition_time(self):
        # Get time channels indices
        time_channel_idx = [idx
                            for idx, channel in enumerate(self.channels)
                            if channel.lower() == 'time']
        if len(time_channel_idx) > 1:
            raise KeyError("more than one time channel in data")
        # Check if the time channel is available
        elif len(time_channel_idx) == 1:
            # Use the event list
            time_channel = self.channels[time_channel_idx[0]]
            return (self[-1, time_channel] - self[0, time_channel]) \
                * self.time_step
        elif (self._acquisition_start_time is not None and
                self._acquisition_end_time is not None):
            # Use start_time and end_time:
            dt = (self._acquisition_end_time - self._acquisition_start_time)
            return dt.total_seconds()
        else:
            return None

Response: Returns the acquisition time in seconds.


Instruction:
Write a Python 3 function for
returning the amplification type for the specified channels.

Response:
def amplification_type(self, channels=None):
        """
        Get the amplification type used for the specified channel(s).

        Each channel uses one of two amplification types: linear or
        logarithmic. This function returns, for each channel, a tuple of
        two numbers, in which the first number indicates the number of
        decades covered by the logarithmic amplifier, and the second
        indicates the linear value corresponding to the channel value zero.
        If the first value is zero, the amplifier used is linear

        The amplification type for channel "n" is extracted from the
        required $PnE parameter.

        Parameters
        ----------
        channels : int, str, list of int, list of str
            Channel(s) for which to get the amplification type. If None,
            return a list with the amplification type of all channels, in
            the order of ``FCSData.channels``.

        Return
        ------
        tuple, or list of tuples
            The amplification type of the specified channel(s). This is
            reported as a tuple, in which the first element indicates how
            many decades the logarithmic amplifier covers, and the second
            indicates the linear value that corresponds to a channel value
            of zero. If the first element is zero, the amplification type
            is linear.

        """
        # Check default
        if channels is None:
            channels = self._channels

        # Get numerical indices of channels
        channels = self._name_to_index(channels)

        # Get detector type of the specified channels
        if hasattr(channels, '__iter__') \
                and not isinstance(channels, six.string_types):
            return [self._amplification_type[ch] for ch in channels]
        else:
            return self._amplification_type[channels]


Instruction:
Write a Python 3 script for
getting the detector voltage for the specified channels.

Response:
def detector_voltage(self, channels=None):
        """
        Get the detector voltage used for the specified channel(s).

        The detector voltage for channel "n" is extracted from the $PnV
        parameter, if available.

        Parameters
        ----------
        channels : int, str, list of int, list of str
            Channel(s) for which to get the detector voltage. If None,
            return a list with the detector voltage of all channels, in the
            order of ``FCSData.channels``.

        Return
        ------
        float or list of float
            The detector voltage of the specified channel(s). If no
            information about the detector voltage is found for a channel,
            return None.

        """
        # Check default
        if channels is None:
            channels = self._channels

        # Get numerical indices of channels
        channels = self._name_to_index(channels)

        # Get detector type of the specified channels
        if hasattr(channels, '__iter__') \
                and not isinstance(channels, six.string_types):
            return [self._detector_voltage[ch] for ch in channels]
        else:
            return self._detector_voltage[channels]


Instruction:
Explain what the following Python 3 code does
def amplifier_gain(self, channels=None):
        # Check default
        if channels is None:
            channels = self._channels

        # Get numerical indices of channels
        channels = self._name_to_index(channels)

        # Get detector type of the specified channels
        if hasattr(channels, '__iter__') \
                and not isinstance(channels, six.string_types):
            return [self._amplifier_gain[ch] for ch in channels]
        else:
            return self._amplifier_gain[channels]

Response: Get the amplifier gain used for the specified channels.


Instruction:
Make a summary of the following Python 3 code
def range(self, channels=None):
        # Check default
        if channels is None:
            channels = self._channels

        # Get numerical indices of channels
        channels = self._name_to_index(channels)

        # Get the range of the specified channels
        if hasattr(channels, '__iter__') \
                and not isinstance(channels, six.string_types):
            return [self._range[ch] for ch in channels]
        else:
            return self._range[channels]

Response: Returns the range of the event in the specified channels.


Instruction:
Explain what the following Python 3 code does
def resolution(self, channels=None):
        # Check default
        if channels is None:
            channels = self._channels

        # Get numerical indices of channels
        channels = self._name_to_index(channels)

        # Get resolution of the specified channels
        if hasattr(channels, '__iter__') \
                and not isinstance(channels, six.string_types):
            return [self._resolution[ch] for ch in channels]
        else:
            return self._resolution[channels]

Response: Get the resolution of the specified channels.


Instruction:
Here you have a function in Python 3, explain what it does
def hist_bins(self, channels=None, nbins=None, scale='logicle', **kwargs):
        # Default: all channels
        if channels is None:
            channels = list(self._channels)

        # Get numerical indices of channels
        channels = self._name_to_index(channels)

        # Convert to list if necessary
        channel_list = channels
        if not isinstance(channel_list, list):
            channel_list = [channel_list]
        if not isinstance(nbins, list):
            nbins = [nbins]*len(channel_list)
        if not isinstance(scale, list):
            scale = [scale]*len(channel_list)

        # Iterate
        bins = []
        for channel, nbins_channel, scale_channel in \
                zip(channel_list, nbins, scale):
            # Get channel resolution
            res_channel = self.resolution(channel)
            # Get default nbins
            if nbins_channel is None:
                nbins_channel = res_channel
            # Get range of channel
            range_channel = self.range(channel)
            # Generate bins according to specified scale
            if scale_channel == 'linear':
                # We will now generate ``nbins`` uniformly spaced bins centered
                # at ``linspace(range_channel[0], range_channel[1], nbins)``. To
                # do so, we need to generate ``nbins + 1`` uniformly spaced
                # points.
                delta_res = (range_channel[1] - range_channel[0]) / \
                    (res_channel - 1)
                bins_channel = np.linspace(range_channel[0] - delta_res/2,
                                           range_channel[1] + delta_res/2,
                                           nbins_channel + 1)

            elif scale_channel == 'log':
                # Check if the lower limit is equal or less than zero. If so,
                # change the lower limit to one or some lower value, such that
                # the range covers at least five decades.
                if range_channel[0] <= 0:
                    range_channel[0] = min(1., range_channel[1]/1e5)
                # Log range
                range_channel = [np.log10(range_channel[0]),
                                 np.log10(range_channel[1])]
                # We will now generate ``nbins`` uniformly spaced bins centered
                # at ``linspace(range_channel[0], range_channel[1], nbins)``. To
                # do so, we need to generate ``nbins + 1`` uniformly spaced
                # points.
                delta_res = (range_channel[1] - range_channel[0]) / \
                    (res_channel - 1)
                bins_channel = np.linspace(range_channel[0] - delta_res/2,
                                           range_channel[1] + delta_res/2,
                                           nbins_channel + 1)
                # Exponentiate bins
                bins_channel = 10**(bins_channel)

            elif scale_channel == 'logicle':
                # Create transform class
                # Use the LogicleTransform class from the plot module
                t = FlowCal.plot._LogicleTransform(data=self,
                                                   channel=channel,
                                                   **kwargs)
                # We now generate ``nbins`` uniformly spaced bins centered at
                # ``linspace(0, M, nbins)``. To do so, we need to generate
                # ``nbins + 1`` uniformly spaced points.
                delta_res = float(t.M) / (res_channel - 1)
                s = np.linspace(- delta_res/2.,
                                t.M + delta_res/2.,
                                nbins_channel + 1)
                # Finally, apply the logicle transformation to generate bins
                bins_channel = t.transform_non_affine(s)

            else:
                # Scale not supported
                raise ValueError('scale "{}" not supported'.format(
                    scale_channel))
            # Accumulate
            bins.append(bins_channel)

        # Extract from list if channels was not a list
        if not isinstance(channels, list):
            bins = bins[0]

        return bins

Response: Returns a list of histogram bin edges for the specified channel.


Instruction:
Here you have a function in Python 3, explain what it does
def _parse_time_string(time_str):
        # If input is None, return None
        if time_str is None:
            return None

        time_l = time_str.split(':')
        if len(time_l) == 3:
            # Either 'hh:mm:ss' or 'hh:mm:ss.cc'
            if '.' in time_l[2]:
                # 'hh:mm:ss.cc' format
                time_str = time_str.replace('.', ':')
            else:
                # 'hh:mm:ss' format
                time_str = time_str + ':0'
            # Attempt to parse string, return None if not possible
            try:
                t = datetime.datetime.strptime(time_str, '%H:%M:%S:%f').time()
            except:
                t = None
        elif len(time_l) == 4:
            # 'hh:mm:ss:tt' format
            time_l[3] = '{:06d}'.format(int(float(time_l[3])*1e6/60))
            time_str = ':'.join(time_l)
            # Attempt to parse string, return None if not possible
            try:
                t = datetime.datetime.strptime(time_str, '%H:%M:%S:%f').time()
            except:
                t = None
        else:
            # Unknown format
            t = None

        return t

Response: Parse a string representation of a time into a datetime. time object.


Instruction:
How would you code a function in Python 3 to
parse a string representation of a date into a datetime. date object.

Response:
def _parse_date_string(date_str):
        """
        Get a datetime.date object from a string date representation.

        The FCS standard includes an optional keyword parameter $DATE in
        which the acquistion date is stored. In FCS 2.0, the date is saved
        as 'dd-mmm-yy', whereas in FCS 3.0 and 3.1 the date is saved as
        'dd-mmm-yyyy'.

        This function attempts to parse these formats, along with a couple
        of nonstandard ones, using the datetime module.

        Parameters:
        -----------
        date_str : str, or None
            String representation of date, or None.

        Returns:
        --------
        t : datetime.datetime, or None
            Date parsed from `date_str`. If parsing was not possible,
            return None. If `date_str` is None, return None

        """
        # If input is None, return None
        if date_str is None:
            return None

        # Standard format for FCS2.0
        try:
            return datetime.datetime.strptime(date_str, '%d-%b-%y')
        except ValueError:
            pass
        # Standard format for FCS3.0
        try:
            return datetime.datetime.strptime(date_str, '%d-%b-%Y')
        except ValueError:
            pass
        # Nonstandard format 1
        try:
            return datetime.datetime.strptime(date_str, '%y-%b-%d')
        except ValueError:
            pass
        # Nonstandard format 2
        try:
            return datetime.datetime.strptime(date_str, '%Y-%b-%d')
        except ValueError:
            pass

        # If none of these formats work, return None
        return None


Instruction:
Explain what the following Python 3 code does
def _name_to_index(self, channels):
        # Check if list, then run recursively
        if hasattr(channels, '__iter__') \
                and not isinstance(channels, six.string_types):
            return [self._name_to_index(ch) for ch in channels]

        if isinstance(channels, six.string_types):
            # channels is a string containing a channel name
            if channels in self.channels:
                return self.channels.index(channels)
            else:
                raise ValueError("{} is not a valid channel name."
                    .format(channels))

        if isinstance(channels, int):
            if (channels < len(self.channels)
                    and channels >= -len(self.channels)):
                return channels
            else:
                raise ValueError("index out of range")

        else:
            raise TypeError("input argument should be an integer, string or "
                "list of integers or strings")

Response: Returns the index of the specified channel names.


Instruction:
How would you code a function in Python 3 to
scrape version information from specified file path.

Response:
def find_version(file_path):
    """
    Scrape version information from specified file path.

    """
    with open(file_path, 'r') as f:
        file_contents = f.read()
    version_match = re.search(r"^__version__\s*=\s*['\"]([^'\"]*)['\"]",
                              file_contents, re.M)
    if version_match:
        return version_match.group(1)
    else:
        raise RuntimeError("unable to find version string")


Instruction:
Can you generate the documentation for the following Python 3 function
def start_end(data, num_start=250, num_end=100, full_output=False):

    if num_start < 0:
        num_start = 0
    if num_end < 0:
        num_end = 0

    if data.shape[0] < (num_start + num_end):
        raise ValueError('Number of events to discard greater than total' +
            ' number of events.')

    mask = np.ones(shape=data.shape[0],dtype=bool)
    mask[:num_start] = False
    if num_end > 0:
        # catch the edge case where `num_end=0` causes mask[-num_end:] to mask
        # off all events
        mask[-num_end:] = False
    gated_data = data[mask]

    if full_output:
        StartEndGateOutput = collections.namedtuple(
            'StartEndGateOutput',
            ['gated_data', 'mask'])
        return StartEndGateOutput(gated_data=gated_data, mask=mask)
    else:
        return gated_data

Response: Gate out first and last events from beginning and end of a flow cytometry data.


Instruction:
Here you have a function in Python 3, explain what it does
def high_low(data, channels=None, high=None, low=None, full_output=False):
    # Extract channels in which to gate
    if channels is None:
        data_ch = data
    else:
        data_ch = data[:,channels]
        if data_ch.ndim == 1:
            data_ch = data_ch.reshape((-1,1))

    # Default values for high and low
    if high is None:
        if hasattr(data_ch, 'range'):
            high = [np.Inf if di is None else di[1] for di in data_ch.range()]
            high = np.array(high)
        else:
            high = np.Inf
    if low is None:
        if hasattr(data_ch, 'range'):
            low = [-np.Inf if di is None else di[0] for di in data_ch.range()]
            low = np.array(low)
        else:
            low = -np.Inf

    # Gate
    mask = np.all((data_ch < high) & (data_ch > low), axis = 1)
    gated_data = data[mask]

    if full_output:
        HighLowGateOutput = collections.namedtuple(
            'HighLowGateOutput',
            ['gated_data', 'mask'])
        return HighLowGateOutput(gated_data=gated_data, mask=mask)
    else:
        return gated_data

Response: Gate out high and low values across all specified channels.


Instruction:
Can you generate the documentation for the following Python 3 function
def ellipse(data, channels,
            center, a, b, theta=0,
            log=False, full_output=False):
    """
    Gate that preserves events inside an ellipse-shaped region.

    Events are kept if they satisfy the following relationship::

        (x/a)**2 + (y/b)**2 <= 1

    where `x` and `y` are the coordinates of the event list, after
    substracting `center` and rotating by -`theta`. This is mathematically
    equivalent to maintaining the events inside an ellipse with major
    axis `a`, minor axis `b`, center at `center`, and tilted by `theta`.

    Parameters
    ----------
    data : FCSData or numpy array
        NxD flow cytometry data where N is the number of events and D is
        the number of parameters (aka channels).
    channels : list of int, list of str
        Two channels on which to perform gating.
    center, a, b, theta (optional) : float
        Ellipse parameters. `a` is the major axis, `b` is the minor axis.
    log : bool, optional
        Flag specifying that log10 transformation should be applied to
        `data` before gating.
    full_output : bool, optional
        Flag specifying to return additional outputs. If true, the outputs
        are given as a namedtuple.

    Returns
    -------
    gated_data : FCSData or numpy array
        Gated flow cytometry data of the same format as `data`.
    mask : numpy array of bool, only if ``full_output==True``
        Boolean gate mask used to gate data such that ``gated_data =
        data[mask]``.
    contour : list of 2D numpy arrays, only if ``full_output==True``
        List of 2D numpy array(s) of x-y coordinates tracing out
        the edge of the gated region.

    Raises
    ------
    ValueError
        If more or less than 2 channels are specified.

    """
    # Extract channels in which to gate
    if len(channels) != 2:
        raise ValueError('2 channels should be specified.')
    data_ch = data[:,channels].view(np.ndarray)

    # Log if necessary
    if log:
        data_ch = np.log10(data_ch)

    # Center
    center = np.array(center)
    data_centered = data_ch - center

    # Rotate
    R = np.array([[np.cos(theta), np.sin(theta)],
                    [-np.sin(theta), np.cos(theta)]])
    data_rotated = np.dot(data_centered, R.T)

    # Generate mask
    mask = ((data_rotated[:,0]/a)**2 + (data_rotated[:,1]/b)**2 <= 1)

    # Gate
    data_gated = data[mask]

    if full_output:
        # Calculate contour
        t = np.linspace(0,1,100)*2*np.pi
        ci = np.array([a*np.cos(t), b*np.sin(t)]).T
        ci = np.dot(ci, R) + center
        if log:
            ci = 10**ci
        cntr = [ci]

        # Build output namedtuple
        EllipseGateOutput = collections.namedtuple(
            'EllipseGateOutput',
            ['gated_data', 'mask', 'contour'])
        return EllipseGateOutput(
            gated_data=data_gated, mask=mask, contour=cntr)
    else:
        return data_gated

Response: Returns a new NxD - style grid of events in an ellipse - shaped region.