1 Changelog

  • 20230508: Trying this out with a larger dataset, explicitly set wanted_z and wanted_channel for it.
  • 20230328: Reimplemented steps as functions, connected them in one set of calls.
  • 20230317: Initial cellpose implementation.
  • 20230315: Separation of input image into timepoints.
  • 20230310: Used geopandas to trace cells over time by position.
  • 20230307: Setting up my environment to handle ipynb and python markdown.

2 Introduction

Jacques is seeking to segment and follow cells over time.

Steps performed so far:

  1. Python/venv installation of fiji
  2. Figured out some easy interactions between python and actual image data.
  3. Implemented a simple function to find minimum distances between putative cells using the extant methods.

3 Next steps

  1. Note that the czi images are immediately openable via fiji’s bioformat interface.
  2. Load dataset via fiji
  3. Invoke cellpose via the python interface (same as pyimagej)
  4. Save ROIs produced by cellpose, save them to zip output file
  1. The roi interface in fiji can address these
  1. Currently a macro is invoked which performs a set of measurements on every ROI and saves them to a csv. This creates the primary data structure used for all following processes.
  2. Need to create a datastructure which identifies each individual cell over time given these ROIs which x/y/time(frame) along with the measurements of interest (area/mean/stdev/intensities by color).
    1. Small postprocessing details: the intensity values produced must be normalized by cell area.

4 Demonstrate the first couple of steps using an actual dataset

Jacques sent me an image acquired from the microscope and I saved it as ‘test_data/raw.tif’, he also sent me a cellpose model which I saved to the ‘models/’ directory.

Given these as a starting point, let us try to open the image with a fiji instance and split it into a series of smaller files by timepoint.

5 Implementation notes

I have been primarily using this as a way to refresh my brain on python and get current on best practices. Thus, there are decisions I made in this workbook which do not make sense in any other context: e.g. why would anyone have the primary data structure be a dictionary keyed by filename, that is dumb? I chose to do that to refresh myself on playing with dictionaries. By the same token, why would anyone make a dictionary of dataframes only to turn around and concatenate them for usage in geopandas, that is crazy? I did this to get more comfortable with pandas and get out some of my Rish muscle memory.

With that in mind, if we choose to make this a package, the first thing that will need to happen is to rework the base datastructure. I just want anyone who actually reads this code to know that yes, I am a nutter, but a nutter for a reason.

5.1 Load necessary python modules

from cellpose import models, io
from cellpose.io import *
from collections import defaultdict
import geopandas
import glob
import imagej
from jpype import JArray, JInt
import matplotlib.pyplot as plt
import multiprocessing as mp
import numpy as np
import os
import pandas
from pandas import DataFrame
from pathlib import Path
import scyjava
import seaborn
import shutil

5.2 Set up some initial variables

base_dir = Path('/lab/scratch/atb/imaging/mtb_2023').as_posix()
os.chdir(base_dir)
input_file = Path(f"{base_dir}/test_data/Experiment-1568.czi").as_posix()
pandas.set_option('display.max_columns', None)
verbose = True

5.3 Start imagej/fiji

Note that I am using fiji/imagej from within a virtual environment which I symbolically linked to the local directory ‘venv/’.

As a result, when I initialize fiji, I will call the base directory of the downloaded fiji tree within the virtual environment, which I somewhat erroneously put in bin/.

scyjava.config.add_option('-Xmx128g')
start_dir = os.getcwd()
ij = imagej.init(Path('venv/bin/Fiji.app'), mode = 'interactive')
ij.getApp().getInfo(True)
## 'ImageJ2 2.9.0/1.53t; Java 1.8.0_322 [amd64]; 117MB of 116508MB'
ij.ui().showUI()
## Something about this init() function changes the current working directory.
os.chdir(start_dir)
ij.getVersion()
## '2.9.0/1.53t'
showPolygonRoi = scyjava.jimport('ij.gui.PolygonRoi')
Overlay = scyjava.jimport('ij.gui.Overlay')
Regions = scyjava.jimport('net.imglib2.roi.Regions')
LabelRegions = scyjava.jimport('net.imglib2.roi.labeling.LabelRegions')
ZProjector = scyjava.jimport('ij.plugin.ZProjector')()
ov = Overlay()

5.4 Open the input file and split it by time

Open the file, figure out its dimensions, and write portions of it to an output directory.

I wrote this thinking I could parallelize the output writing to 8 cpus. But I think I do not yet understand how python scopes variables in this context and so it did not quite work. I turned that off for the moment and ran it and it finished in about a minute.

The following function may require a test to see if the output directory already exists because scijava will freak out.

def separate_slices(input_file, wanted_x = True, wanted_y = True,
                    wanted_z = 1, wanted_channel = 2, cpus = 8,
                    overwrite = False):
    """ Slice an image in preparation for cellpose.

    Eventually this should be smart enough to handle arbitrary
    x,y,z,channels,times as well as able to use multiple cpus for
    saving the data.  In its current implementation, it only saves 1
    z, 1 channel for every frame of an image into a series of files in
    its output directory.
    """

    input_base = os.path.basename(input_file)
    input_dir = os.path.dirname(input_file)
    input_name = os.path.splitext(input_base)[0]
    output_directory = Path(f"{input_dir}/outputs/{input_name}_z{wanted_z}").as_posix()
    os.makedirs(output_directory, exist_ok = True)
    if verbose:
        print("Starting to open the input file, this takes a moment.")
    raw_dataset = ij.io().open(input_file)
    if verbose:
        print(f"Opened input file, writing images to {output_directory}")

    data_info = {}
    for element in range(len(raw_dataset.dims)):
        name = raw_dataset.dims[element]
        data_info[name] = raw_dataset.shape[element]
    if verbose:
        print(f"This dataset has dimensions: X:{data_info['X']}",
              f"Y:{data_info['Y']} Z:{data_info['Z']} Time:{data_info['Time']}")

    slices = []
    for timepoint in range(data_info['Time']):
        wanted_slice = raw_dataset[:, :, wanted_channel, wanted_z, timepoint]
        slice_data = ij.py.to_dataset(wanted_slice)
        output_filename = Path(f"{output_directory}/frame_{timepoint}.tif").as_posix()
        if (os.path.exists(output_filename)):
            if overwrite:
                print(f"Rewriting {output_filename}")
                os.remove(output_filename)
                saved = ij.io().save(slice_data, output_filename)
            else:
                if verbose:
                    print(f"Skipping {output_filename}, it already exists.")
        else:
            saved = ij.io().save(slice_data, output_filename)
            if verbose:
                print(f"Saving image {input_name}_{timepoint}.")
        slices.append(wanted_slice)

    return raw_dataset, slices, output_directory

5.5 Cellpose

At this point we should have a directory containing files of individual timepoints. Jacques sent me an initial implementation of the usage of cellpose to call individual cells. Let us include that now. I think the previous function should probably also return the directory of the separated input files.

## Relevant options:
## batch_size(increase for more parallelization), channels(two element list of two element
## channels to segment; the first is the segment, second is optional nucleus;
## internal elements are color channels to query, so [[0,0],[2,3]] means do main cells in
## grayscale and a second with cells in blue, nuclei in green.
## channel_axis, z_axis ? invert (T/F flip pixels from b/w I assume),
## normalize(T/F percentile normalize the data), diameter, do_3d,
## anisotropy (rescaling factor for 3d segmentation), net_avg (average models),
## augment ?, tile ?, resample, interp, flow_threshold, cellprob_threshold (interesting),
## min_size (turned off with -1), stitch_threshold ?, rescale ?.
def invoke_cellpose(input_directory, model_file, channels = [[0, 0]], diameter = 160,
                    threshold = 0.4, do_3D = False, batch_size = 64, verbose = True):
    """ Invoke cellpose using individual slices.

    This takes the series of slices from separate_slices() and sends
    them to cellpose with a specific model.  The dictionary it returns
    is the primary datastructure for the various functions which follow.
    """

    ## Relevant options:
    ## model_type(cyto, nuclei, cyto2), net_avg(T/F if load built in networks and average them)
    model = models.CellposeModel(pretrained_model = model_file)
    files = get_image_files(input_directory, '_masks', look_one_level_down = False)
    imgs = []
    output_masks = []
    output_txts = []
    output_files = defaultdict(dict)
    existing_files = 0
    count = 0
    for one_file in files:
        print(f"Reading {one_file}")
        cp_output_directory = Path(f"{input_directory}/cellpose").as_posix()
        os.makedirs(cp_output_directory, exist_ok = True)
        f_name = os.path.basename(one_file)
        f_name = os.path.splitext(f_name)[0]
        start_mask = Path(f"{input_directory}/{f_name}_cp_masks.png").as_posix()
        output_mask = Path(f"{cp_output_directory}/{f_name}_cp_masks.png").as_posix()
        start_txt =  Path(f"{input_directory}/{f_name}_cp_outlines.txt").as_posix()
        output_txt = Path(f"{cp_output_directory}/{f_name}_cp_outlines.txt").as_posix()
        print(f"Adding new txt file: {output_txt}")
        output_files[f_name]['input_file'] = one_file
        output_files[f_name]['start_mask'] = start_mask
        output_files[f_name]['output_mask'] = output_mask
        output_files[f_name]['start_txt'] = start_txt
        output_files[f_name]['output_txt'] = output_txt
        output_files[f_name]['exists'] = False
        if (os.path.exists(output_txt)):
            existing_files = existing_files + 1
            output_files[f_name]['exists'] = True
        else:
            img = imread(one_file)
            imgs.append(img)
        count = count + 1
    nimg = len(imgs)
    if verbose and nimg > 0:
        print(f"Read {nimg} images, starting cellpose.")
        masks, flows, styles = model.eval(
            imgs, diameter = diameter, channels = channels, flow_threshold = threshold,
            do_3D = do_3D, batch_size = batch_size)
        io.save_to_png(imgs, masks, flows, files)
        print(f"Moving cellpose outputs to the cellpose output directory.")
        output_filenames = list(output_files.keys())
        for f_name in output_filenames:
            print(f"Moving {output_files[f_name][start_mask]} to {output_files[f_name][output_mask]}")
            shutil.move(output_files[f_name][start_mask], output_files[f_name][output_mask])
            shutil.move(output_files[f_name][start_txt], output_files[f_name][output_txt])
    else:
        print("Returning the output files.")
    return output_files

5.6 Collapse Z

One possible change is to perform measurements on the sum of Z-stacks instead of a single slice. Thus we would sum the cells, create the ROIs using the single slice grayscale image, then measure the set of all combined.

def collapse_z(raw_dataset, cellpose_result, method = 'sum'):
    """ Stack multiple z slices for each timepoint.

    If I understand Jacques' explanation of the quantification methods
    correctly, they sometimes (often?) perform better on the
    z-integration of pixels at each timepoint.  This function performs
    that and sends the stacked slices to the output directory and adds
    the filenames to the cellpose_result dictionary.
    """
    cellpose_slices = list(cellpose_result.keys())
    slice_number = 0
    collapsed_slices = []
    for slice_name in cellpose_slices:
        output_directory = os.path.dirname(cellpose_result[slice_name]['output_txt'])
        collapsed_directory = os.path.dirname(output_directory)
        collapsed_directory = f"{collapsed_directory}/collapsed"
        os.makedirs(collapsed_directory, exist_ok = True)
        output_filename = Path(f"{collapsed_directory}/frame{slice_number}.tif").as_posix()
        cellpose_result[slice_name]['collapsed_file'] = output_filename
        if (os.path.exists(output_filename)):
            if verbose:
                print(f"Skipping {output_filename}, it already exists.")
        else:
            larger_slice = raw_dataset[:, :, :, :, slice_number]
            imp = ij.py.to_imageplus(larger_slice)
            z_projector_result = ZProjector.run(imp, method)
            ## z_projector_mask = ij.IJ.run(z_projector_result, "Convert to Mask", "method=Otsu background=Light")
            z_collapsed_image = ij.py.from_java(z_projector_result)
            z_collapsed_dataset = ij.py.to_dataset(z_collapsed_image)
            saved = ij.io().save(z_collapsed_dataset, output_filename)
            if verbose:
                print(f"Saving image {output_filename}.")
        slice_number = slice_number + 1
    return cellpose_result

6 Create Regions of interest from cellpose outputs

In Jacques notebook, it looks like he only extracts ROIs from one of the cellpose slices. I am assuming the goal is to extend this across all images?

There is an important caveat that I missed: imagej comes with a python2-based scripting language from which it appears some of his code is coming. As a result I should look carefully before using it, and pay close attention to the examples provided here for the most appropriate ways of interacting with the ROI manager etc:

https://github.com/imagej/pyimagej/blob/main/doc/examples/blob_detection_interactive.py

## The following is from a mix of a couple of implementations I found:
## https://pyimagej.readthedocs.io/en/latest/Classic-Segmentation.html
## an alternative method may be taken from:
## https://pyimagej.readthedocs.io/en/latest/Classic-Segmentation.html#segmentation-workflow-with-imagej2
## My goal is to pass the ROI regions to this function and create a similar df.
def slices_to_roi_measurements(cellpose_result, collapsed = False):
    """ Read the text cellpose output files, generate ROIs, and measure.

    I think there are better ways of accomplishing this task than
    using ij.IJ.run(); but this seems to work...  Upon completion,
    this function should add a series of dataframes to the
    cellpose_result dictionary which comprise the various metrics from
    ImageJ's measurement function of the ROIs detected by cellpose.
    """
    output_dict = cellpose_result
    cellpose_slices = list(cellpose_result.keys())
    slice_number = 0
    for slice_name in cellpose_slices:
        output_dict[slice_name]['slice_number'] = slice_number
        input_tif = ''
        if collapsed:
            input_tif = cellpose_result[slice_name]['collapsed_file']
        else:
            input_tif = cellpose_result[slice_name]['input_file']
        slice_dataset = ij.io().open(input_tif)
        slice_data = ij.py.to_imageplus(slice_dataset)
        input_txt = cellpose_result[slice_name]['output_txt']
        input_mask = cellpose_result[slice_name]['output_mask']
        if verbose:
            print(f"Processing cellpose outline: {input_txt}")
            print(f"Measuring: {input_tif}")
        # convert Dataset to ImagePlus
        imp = ij.py.to_imageplus(slice_data)
        rm = ij.RoiManager.getRoiManager()
        rm.runCommand("Associated", "true")
        rm.runCommand("show All with labels")
        ## The logic for this was taken from:
        ## https://stackoverflow.com/questions/73849418/is-there-any-way-to-switch-imagej-macro-code-to-python3-code
        txt_fh = open(input_txt, 'r')
        set_string = f'Set Measurements...'
        measure_string = f'area mean min centroid median skewness kurtosis integrated stack redirect=None decimal=3'
        ij.IJ.run(set_string, measure_string)
        roi_stats = defaultdict(list)
        for line in txt_fh:
            xy = line.rstrip().split(",")
            xy_coords = [int(element) for element in xy if element not in '']
            x_coords = [int(element) for element in xy[::2] if element not in '']
            y_coords = [int(element) for element in xy[1::2] if element not in '']
            xcoords_jint = JArray(JInt)(x_coords)
            ycoords_jint = JArray(JInt)(y_coords)
            polygon_roi_instance = scyjava.jimport('ij.gui.PolygonRoi')
            roi_instance = scyjava.jimport('ij.gui.Roi')
            imported_polygon = polygon_roi_instance(xcoords_jint, ycoords_jint,
                                                    len(x_coords), int(roi_instance.POLYGON))
            imp.setRoi(imported_polygon)
            rm.addRoi(imported_polygon)
            ij.IJ.run(imp, 'Measure', '')
        rm.runCommand('Update')
        slice_result = ij.ResultsTable.getResultsTable()
        slice_table = ij.convert().convert(slice_result,
                                           scyjava.jimport('org.scijava.table.Table'))
        slice_measurements = ij.py.from_java(slice_table)
        output_dict[slice_name]['measurements'] = slice_measurements
        ij.IJ.run('Clear Results')
        txt_fh.close()
        imp.setOverlay(ov)
        imp.getProcessor().resetMinAndMax()
        slice_number = slice_number + 1
    return output_dict
## Pull out of slices_to_roi_measurements() the parts which actually define
## a cellpose-detected cell with respect to time.
## This should be used in cooperation with functions which apply the
## resulting polygons to other images (to create the ROIs) along with
## functions that perform the final measurements.  If properly
## implemented, this should allow one to mix and match the invocations
## to use them on any set of the raw or sliced data.
## def extract_cellpose(cellpose_result):





## The following is from a mix of a couple of implementations I found:
## https://pyimagej.readthedocs.io/en/latest/Classic-Segmentation.html
## an alternative method may be taken from:
## https://pyimagej.readthedocs.io/en/latest/Classic-Segmentation.html#segmentation-workflow-with-imagej2
## My goal is to pass the ROI regions to this function and create a similar df.
def slices_to_roi_measurements(cellpose_result, collapsed = False):
    """ Read the text cellpose output files, generate ROIs, and measure.

    I think there are better ways of accomplishing this task than
    using ij.IJ.run(); but this seems to work...  Upon completion,
    this function should add a series of dataframes to the
    cellpose_result dictionary which comprise the various metrics from
    ImageJ's measurement function of the ROIs detected by cellpose.
    """
    output_dict = cellpose_result
    cellpose_slices = list(cellpose_result.keys())
    slice_number = 0
    for slice_name in cellpose_slices:
        output_dict[slice_name]['slice_number'] = slice_number
        input_tif = ''
        if collapsed:
            input_tif = cellpose_result[slice_name]['collapsed_file']
        else:
            input_tif = cellpose_result[slice_name]['input_file']
        slice_dataset = ij.io().open(input_tif)
        slice_data = ij.py.to_imageplus(slice_dataset)
        input_txt = cellpose_result[slice_name]['output_txt']
        input_mask = cellpose_result[slice_name]['output_mask']
        if verbose:
            print(f"Processing cellpose outline: {input_txt}")
            print(f"Measuring: {input_tif}")
        # convert Dataset to ImagePlus
        imp = ij.py.to_imageplus(slice_data)
        rm = ij.RoiManager.getRoiManager()
        rm.runCommand("Associated", "true")
        rm.runCommand("show All with labels")
        ## The logic for this was taken from:
        ## https://stackoverflow.com/questions/73849418/is-there-any-way-to-switch-imagej-macro-code-to-python3-code
        txt_fh = open(input_txt, 'r')
        set_string = f'Set Measurements...'
        measure_string = f'area mean min centroid median skewness kurtosis integrated stack redirect=None decimal=3'
        ij.IJ.run(set_string, measure_string)
        roi_stats = defaultdict(list)
        for line in txt_fh:
            xy = line.rstrip().split(",")
            xy_coords = [int(element) for element in xy if element not in '']
            x_coords = [int(element) for element in xy[::2] if element not in '']
            y_coords = [int(element) for element in xy[1::2] if element not in '']
            xcoords_jint = JArray(JInt)(x_coords)
            ycoords_jint = JArray(JInt)(y_coords)
            polygon_roi_instance = scyjava.jimport('ij.gui.PolygonRoi')
            roi_instance = scyjava.jimport('ij.gui.Roi')
            imported_polygon = polygon_roi_instance(xcoords_jint, ycoords_jint,
                                                    len(x_coords), int(roi_instance.POLYGON))
            imp.setRoi(imported_polygon)
            rm.addRoi(imported_polygon)
            ij.IJ.run(imp, 'Measure', '')
        rm.runCommand('Update')
        slice_result = ij.ResultsTable.getResultsTable()
        slice_table = ij.convert().convert(slice_result,
                                           scyjava.jimport('org.scijava.table.Table'))
        slice_measurements = ij.py.from_java(slice_table)
        output_dict[slice_name]['measurements'] = slice_measurements
        ij.IJ.run('Clear Results')
        txt_fh.close()
        imp.setOverlay(ov)
        imp.getProcessor().resetMinAndMax()
        slice_number = slice_number + 1
    return output_dict

7 Convert the slice measurements to pandas df

slices_to_roi_measurements() returns a dictionary with keys which are the filenames of each raw tif file. Each element of that dictionary is in turn a dictionary containing some information about the files along with a df of the measurements provided by imagej.

My little geopandas function assumes a single long df with some columns which tell it which timepoint. So lets make a quick function to give that here. OTOH it may be wiser/better to make some changes to slices_to_roi_measurements() so that it returns that format df; but since I am using this as a learning experience to get more comfortable with python data structures, I will not do it that way.

def convert_slices_to_pandas(slices):
    """ Dump the cellpose_result slice data to a single df.

    There is no good reason for me to store the data as a series of
    dataframes within a dictionary except I want to get more
    comfortable with python datastructures.  Thus, this function
    should be extraneous, but serves as a way to go from my hash to a
    single df.
    """
    concatenated = pandas.DataFrame()
    slice_keys = list(slices.keys())
    slice_counter = 0
    for k in slice_keys:
        slice_counter = slice_counter + 1
        current_slice = slices[k]
        if verbose:
            print(f"The slice is {k}")
        slice_number = current_slice['slice_number']
        slice_data = current_slice['measurements']
        slice_data['Frame'] = slice_number
        if (slice_counter == 1):
            concatenated = slice_data
        else:
            concatenated = pandas.concat([concatenated, slice_data])
    ## This is a little silly, but I couldn't remember that the index attribute
    ## is the numeric rowname for a moment
    ## The reset_index() does what it says on the tine, and changes the 1:19, 1:20, etc
    ## of each individual time Frame to a single range of 1:2000
    concatenated.index = concatenated.reset_index().index
    return concatenated

8 Create cell groups

def nearest_cells_over_time(df, max_dist = 10.0, max_prop = 0.7,
                            x_column = 'X', y_column = 'Y', verbose = True):
    """Trace cells over time

    If I understand Jacques' goals correctly, the tracing of cells
    over time should be a reasonably tractable problem for the various
    geo-statistics tools to handle; their whole purpose is to
    calculate n-dimensional distances.  So, let us pass my df to one
    of them and see what happens!

    Upon completion, we should get an array(dictionary? I forget) of
    arrays where each primary key is the top-level cell ID.  Each
    internal array is the set of IDs from the geopandas dataframe,
    which contains all of the measurements.  Thus, we can easily
    extract the data for individual cells and play with it.
    """
    gdf = geopandas.GeoDataFrame(
        df,
        geometry = geopandas.points_from_xy(df[x_column], df[y_column]))

    final_time = gdf.Frame.max()
    pairwise_distances = []
    for start_time in range(1, final_time):
        i = start_time
        j = i + 1
        ti_idx = gdf.Frame == i
        tj_idx = gdf.Frame == j
        if verbose:
            print(f"Getting distances of dfs {i} and {j}.")
        ti = gdf[ti_idx]
        tj = gdf[tj_idx]
        ti_rows = ti.shape[0]
        tj_rows = tj.shape[0]
        titj = geopandas.sjoin_nearest(ti, tj, distance_col = "pairwise_dist")
        pairwise_distances.append(titj)

    id_counter = 0
    ## Cell IDs pointing to a list of cells
    traced = {}
    ## Endpoints pointing to the cell IDs
    ends = {}
    for i in range(0, final_time - 1):
        query = pairwise_distances[i]
        passed_idx = query.pairwise_dist <= max_dist
        failed_idx = query.pairwise_dist > max_dist
        if (failed_idx.sum() > 0):
            if verbose:
                print(f"Skipped {failed_idx.sum()} elements in segment {i}.")
        query = query[passed_idx]

        prop_change = query.Area_left / query.Area_right
        increased_idx = prop_change > 1.0
        prop_change[increased_idx] = 1.0 / prop_change[increased_idx]
        failed_idx = prop_change < max_prop
        passed_idx = prop_change >= max_prop
        if (failed_idx.sum() > 0):
            if verbose:
                skip_string = (f"Skipped {failed_idx.sum()} elements in segment {i} ",
                               f"because the size changed too much.")
                print(skip_string)
            query = query[passed_idx]

        for row in query.itertuples():
            start_cell = row.Index
            end_cell = row.index_right
            if start_cell in ends.keys():
                cell_id = ends[start_cell]
                current_value = traced[cell_id]
                current_value.append(end_cell)
                traced[cell_id] = current_value
                ends[end_cell] = cell_id
            else:
                id_counter = id_counter + 1
                traced[id_counter] = [start_cell, end_cell]
                ends[end_cell] = id_counter
    return traced

9 Run the functions and plot the results

9.1 Separate slices

Note to self, Jacques’ new dataset uses wanted_z == 2, wanted_channel == 3.

raw_dataset, saved_slices, slice_directory = separate_slices(input_file, wanted_z = 2,
                                                             wanted_channel = 3)
## Starting to open the input file, this takes a moment.
## Opened input file, writing images to /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2
## This dataset has dimensions: X:2048 Y:2048 Z:11 Time:121
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_0.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_1.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_2.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_3.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_4.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_5.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_6.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_7.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_8.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_9.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_10.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_11.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_12.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_13.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_14.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_15.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_16.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_17.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_18.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_19.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_20.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_21.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_22.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_23.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_24.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_25.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_26.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_27.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_28.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_29.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_30.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_31.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_32.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_33.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_34.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_35.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_36.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_37.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_38.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_39.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_40.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_41.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_42.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_43.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_44.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_45.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_46.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_47.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_48.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_49.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_50.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_51.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_52.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_53.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_54.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_55.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_56.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_57.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_58.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_59.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_60.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_61.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_62.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_63.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_64.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_65.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_66.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_67.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_68.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_69.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_70.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_71.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_72.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_73.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_74.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_75.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_76.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_77.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_78.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_79.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_80.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_81.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_82.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_83.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_84.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_85.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_86.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_87.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_88.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_89.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_90.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_91.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_92.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_93.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_94.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_95.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_96.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_97.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_98.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_99.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_100.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_101.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_102.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_103.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_104.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_105.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_106.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_107.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_108.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_109.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_110.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_111.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_112.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_113.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_114.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_115.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_116.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_117.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_118.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_119.tif, it already exists.
## Skipping /lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_120.tif, it already exists.
## 
## [java.lang.Enum.toString] ZeissCZIReader initializing /lab/scratch/atb/imaging/mtb_2023/test_data/Experiment-1568.czi
## [java.lang.Enum.toString] [WARN] Unknown DetectorType value 'GaAsP-PMT' will be stored as "Other"
## [java.lang.Enum.toString] [WARN] Unknown DetectorType value 'GaAsP-PMT' will be stored as "Other"
## [java.lang.Enum.toString] [WARN] Unknown DetectorType value 'GaAsP-PMT' will be stored as "Other"
## [java.lang.Enum.toString] [WARN] Unknown DetectorType value 'Multialkali-PMT' will be stored as "Other"

9.2 Invoke cellpose

cellpose_result = invoke_cellpose(slice_directory, 'models/CP_20220523_104016')
## Error: KeyError: '/lab/scratch/atb/imaging/mtb_2023/test_data/outputs/Experiment-1568_z2/frame_120_cp_masks.png'

9.3 collapse Z

cellpose_result = collapse_z(raw_dataset, cellpose_result)
## Error: NameError: name 'cellpose_result' is not defined

9.4 Measure ROIs

slice_measurements = slices_to_roi_measurements(cellpose_result, collapsed = True)
## Error: NameError: name 'cellpose_result' is not defined

9.5 Convert to pandas

concatenated = convert_slices_to_pandas(slice_measurements)
## Error: NameError: name 'slice_measurements' is not defined

9.6 Find nearest

nearest = nearest_cells_over_time(concatenated, max_dist = 10.0,
                                  x_column = 'X', y_column = 'Y')
## Error: NameError: name 'concatenated' is not defined

9.7 Get information from a group of cells

As a final step, we should be able to extract and play with the information from one or more groups of cells.

cell_id = 129
cell_idx = nearest[cell_id]
## Error: NameError: name 'nearest' is not defined
cell_data = concatenated.loc[cell_idx]
## Error: NameError: name 'concatenated' is not defined
len(cell_data)
## Error: NameError: name 'cell_data' is not defined
cell_data = cell_data.reset_index()
## Error: NameError: name 'cell_data' is not defined
scatter = plt.scatter(cell_data['X'], cell_data['Y'])
## Error: NameError: name 'cell_data' is not defined
final_row = cell_data.index.max()
## Error: NameError: name 'cell_data' is not defined
for start_time in range(0, final_row - 1):
    ti_idx = cell_data.index == start_time
    tj_idx = cell_data.index == start_time + 1
    p1x = cell_data[ti_idx].X
    p2x = cell_data[tj_idx].X
    p1y = cell_data[ti_idx].Y
    p2y = cell_data[tj_idx].Y
    x_points = [p1x, p2x]
    y_points = [p1y, p2y]
    plt.plot(x_points, y_points)
## Error: NameError: name 'final_row' is not defined
finalm1_idx = cell_data.index == final_row - 1
## Error: NameError: name 'cell_data' is not defined
final_idx = cell_data.index == final_row
## Error: NameError: name 'cell_data' is not defined
finalm1_x = cell_data[finalm1_idx].X
## Error: NameError: name 'cell_data' is not defined
final_x = cell_data[final_idx].X
## Error: NameError: name 'cell_data' is not defined
finalm1_y = cell_data[finalm1_idx].Y
## Error: NameError: name 'cell_data' is not defined
final_y = cell_data[final_idx].Y
## Error: NameError: name 'cell_data' is not defined
x_points = [finalm1_x, final_x]
## Error: NameError: name 'finalm1_x' is not defined
y_points = [finalm1_y, final_y]
## Error: NameError: name 'finalm1_y' is not defined
plt.plot(x_points, y_points)
## Error: NameError: name 'x_points' is not defined
plt.show()

seaborn.violinplot(data = cell_data.Area)
## Error: NameError: name 'cell_data' is not defined
plt.show()

pander::pander(sessionInfo())

R version 4.2.0 (2022-04-22)

Platform: x86_64-pc-linux-gnu (64-bit)

locale: LC_CTYPE=en_US.UTF-8, LC_NUMERIC=C, LC_TIME=en_US.UTF-8, LC_COLLATE=en_US.UTF-8, LC_MONETARY=en_US.UTF-8, LC_MESSAGES=en_US.UTF-8, LC_PAPER=en_US.UTF-8, LC_NAME=en_US.UTF-8, LC_ADDRESS=en_US.UTF-8, LC_TELEPHONE=en_US.UTF-8, LC_MEASUREMENT=en_US.UTF-8 and LC_IDENTIFICATION=en_US.UTF-8

attached base packages: stats4, stats, graphics, grDevices, utils, datasets, methods and base

other attached packages: reticulate(v.1.28), spatstat.geom(v.3.2-1), spatstat.data(v.3.0-1), hpgltools(v.1.0), testthat(v.3.1.8), SummarizedExperiment(v.1.28.0), GenomicRanges(v.1.50.2), GenomeInfoDb(v.1.34.9), IRanges(v.2.32.0), S4Vectors(v.0.36.2), MatrixGenerics(v.1.10.0), matrixStats(v.0.63.0), Biobase(v.2.58.0) and BiocGenerics(v.0.44.0)

loaded via a namespace (and not attached): rappdirs(v.0.3.3), rtracklayer(v.1.58.0), tidyr(v.1.3.0), ggplot2(v.3.4.2), clusterGeneration(v.1.3.7), bit64(v.4.0.5), knitr(v.1.42), DelayedArray(v.0.24.0), data.table(v.1.14.8), KEGGREST(v.1.38.0), RCurl(v.1.98-1.12), doParallel(v.1.0.17), generics(v.0.1.3), GenomicFeatures(v.1.50.4), callr(v.3.7.3), RhpcBLASctl(v.0.23-42), cowplot(v.1.1.1), usethis(v.2.1.6), RSQLite(v.2.3.1), shadowtext(v.0.1.2), bit(v.4.0.5), enrichplot(v.1.18.4), xml2(v.1.3.4), httpuv(v.1.6.10), viridis(v.0.6.3), xfun(v.0.39), hms(v.1.1.3), jquerylib(v.0.1.4), evaluate(v.0.21), promises(v.1.2.0.1), fansi(v.1.0.4), restfulr(v.0.0.15), progress(v.1.2.2), caTools(v.1.18.2), dbplyr(v.2.3.2), igraph(v.1.4.2), DBI(v.1.1.3), htmlwidgets(v.1.6.2), purrr(v.1.0.1), ellipsis(v.0.3.2), dplyr(v.1.1.2), backports(v.1.4.1), annotate(v.1.76.0), aod(v.1.3.2), biomaRt(v.2.54.1), deldir(v.1.0-6), vctrs(v.0.6.2), remotes(v.2.4.2), here(v.1.0.1), cachem(v.1.0.8), withr(v.2.5.0), ggforce(v.0.4.1), HDO.db(v.0.99.1), GenomicAlignments(v.1.34.1), treeio(v.1.22.0), prettyunits(v.1.1.1), DOSE(v.3.24.2), ape(v.5.7-1), lazyeval(v.0.2.2), crayon(v.1.5.2), genefilter(v.1.80.3), edgeR(v.3.40.2), pkgconfig(v.2.0.3), tweenr(v.2.0.2), nlme(v.3.1-162), pkgload(v.1.3.2), devtools(v.2.4.5), rlang(v.1.1.1), lifecycle(v.1.0.3), miniUI(v.0.1.1.1), downloader(v.0.4), filelock(v.1.0.2), BiocFileCache(v.2.6.1), rprojroot(v.2.0.3), polyclip(v.1.10-4), graph(v.1.76.0), Matrix(v.1.5-4), aplot(v.0.1.10), boot(v.1.3-28.1), processx(v.3.8.1), png(v.0.1-8), viridisLite(v.0.4.2), rjson(v.0.2.21), bitops(v.1.0-7), gson(v.0.1.0), KernSmooth(v.2.23-21), pander(v.0.6.5), Biostrings(v.2.66.0), blob(v.1.2.4), stringr(v.1.5.0), qvalue(v.2.30.0), remaCor(v.0.0.11), gridGraphics(v.0.5-1), scales(v.1.2.1), memoise(v.2.0.1), GSEABase(v.1.60.0), magrittr(v.2.0.3), plyr(v.1.8.8), gplots(v.3.1.3), zlibbioc(v.1.44.0), compiler(v.4.2.0), scatterpie(v.0.1.9), BiocIO(v.1.8.0), RColorBrewer(v.1.1-3), lme4(v.1.1-33), Rsamtools(v.2.14.0), cli(v.3.6.1), XVector(v.0.38.0), urlchecker(v.1.0.1), patchwork(v.1.1.2), ps(v.1.7.5), MASS(v.7.3-60), mgcv(v.1.8-42), tidyselect(v.1.2.0), stringi(v.1.7.12), highr(v.0.10), yaml(v.2.3.7), GOSemSim(v.2.24.0), locfit(v.1.5-9.7), ggrepel(v.0.9.3), grid(v.4.2.0), sass(v.0.4.6), fastmatch(v.1.1-3), tools(v.4.2.0), parallel(v.4.2.0), rstudioapi(v.0.14), foreach(v.1.5.2), gridExtra(v.2.3), farver(v.2.1.1), ggraph(v.2.1.0), digest(v.0.6.31), shiny(v.1.7.4), Rcpp(v.1.0.10), broom(v.1.0.4), later(v.1.3.1), httr(v.1.4.6), AnnotationDbi(v.1.60.2), Rdpack(v.2.4), colorspace(v.2.1-0), brio(v.1.1.3), XML(v.3.99-0.14), fs(v.1.6.2), splines(v.4.2.0), yulab.utils(v.0.0.6), PROPER(v.1.30.0), tidytree(v.0.4.2), spatstat.utils(v.3.0-3), graphlayouts(v.1.0.0), ggplotify(v.0.1.0), plotly(v.4.10.1), sessioninfo(v.1.2.2), xtable(v.1.8-4), jsonlite(v.1.8.4), nloptr(v.2.0.3), ggtree(v.3.6.2), tidygraph(v.1.2.3), ggfun(v.0.0.9), R6(v.2.5.1), RUnit(v.0.4.32), profvis(v.0.3.8), pillar(v.1.9.0), htmltools(v.0.5.5), mime(v.0.12), glue(v.1.6.2), fastmap(v.1.1.1), minqa(v.1.2.5), clusterProfiler(v.4.6.2), BiocParallel(v.1.32.6), codetools(v.0.2-19), fgsea(v.1.24.0), pkgbuild(v.1.4.0), mvtnorm(v.1.1-3), utf8(v.1.2.3), lattice(v.0.21-8), bslib(v.0.4.2), tibble(v.3.2.1), sva(v.3.46.0), pbkrtest(v.0.5.2), curl(v.5.0.0), gtools(v.3.9.4), GO.db(v.3.16.0), survival(v.3.5-5), limma(v.3.54.2), rmarkdown(v.2.21), desc(v.1.4.2), munsell(v.0.5.0), GenomeInfoDbData(v.1.2.9), iterators(v.1.0.14), variancePartition(v.1.28.9), reshape2(v.1.4.4), gtable(v.0.3.3) and rbibutils(v.2.2.13)

message(paste0("This is hpgltools commit: ", get_git_commit()))
## If you wish to reproduce this exact build of hpgltools, invoke the following:
## > git clone http://github.com/abelew/hpgltools.git
## > git reset 7d590afc508c9049ffc923388a6c0c7ea122a937
## This is hpgltools commit: Thu May 25 14:32:24 2023 -0400: 7d590afc508c9049ffc923388a6c0c7ea122a937
this_save <- paste0(gsub(pattern="\\.Rmd", replace="", x=rmd_file), "-v", ver, ".rda.xz")
message(paste0("Saving to ", this_save))
## Saving to index_functions_big-v20230530.rda.xz
tmp <- sm(saveme(filename=this_save))
LS0tCnRpdGxlOiAiU3BsaXR0aW5nIGEgbGFyZ2UgZGF0YXNldCwgcnVubmluZyBjZWxscG9zZSwgYW5kIG1lYXN1cmluZyB0aGUgcmVzdWx0cy4iCmF1dGhvcjogImF0YiBhYmVsZXdAZ21haWwuY29tIgpkYXRlOiAiYHIgU3lzLkRhdGUoKWAiCm91dHB1dDoKICBodG1sX2RvY3VtZW50OgogICAgY29kZV9kb3dubG9hZDogdHJ1ZQogICAgY29kZV9mb2xkaW5nOiBzaG93CiAgICBmaWdfY2FwdGlvbjogdHJ1ZQogICAgZmlnX2hlaWdodDogNwogICAgZmlnX3dpZHRoOiA3CiAgICBoaWdobGlnaHQ6IHplbmJ1cm4KICAgIGtlZXBfbWQ6IGZhbHNlCiAgICBtb2RlOiBzZWxmY29udGFpbmVkCiAgICBudW1iZXJfc2VjdGlvbnM6IHRydWUKICAgIHNlbGZfY29udGFpbmVkOiB0cnVlCiAgICB0aGVtZTogcmVhZGFibGUKICAgIHRvYzogdHJ1ZQogICAgdG9jX2Zsb2F0OgogICAgICBjb2xsYXBzZWQ6IGZhbHNlCiAgICAgIHNtb290aF9zY3JvbGw6IGZhbHNlCi0tLQoKPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KYm9keSwgdGQgewogIGZvbnQtc2l6ZTogMTZweDsKfQpjb2RlLnJ7CiAgZm9udC1zaXplOiAxNnB4Owp9CnByZSB7CiBmb250LXNpemU6IDE2cHgKfQo8L3N0eWxlPgoKYGBge3Igb3B0aW9ucywgaW5jbHVkZT1GQUxTRX0KbGlicmFyeShocGdsdG9vbHMpCnR0IDwtIGRldnRvb2xzOjpsb2FkX2FsbCgifi9ocGdsdG9vbHMiKQprbml0cjo6b3B0c19rbml0JHNldCgKICB3aWR0aCA9IDEyMCwgcHJvZ3Jlc3MgPSBUUlVFLCB2ZXJib3NlID0gVFJVRSwgZWNobyA9IFRSVUUpCmtuaXRyOjpvcHRzX2NodW5rJHNldChlcnJvciA9IFRSVUUsIGRwaSA9IDk2KQpsdWFfZmlsdGVycyA8LSBybWFya2Rvd246OnBhbmRvY19sdWFfZmlsdGVyX2FyZ3MoInBhbmRvYy16b3R4dC5sdWEiKQpvbGRfb3B0aW9ucyA8LSBvcHRpb25zKAogIGRpZ2l0cyA9IDQsIHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSwga25pdHIuZHVwbGljYXRlLmxhYmVsID0gImFsbG93IikKZ2dwbG90Mjo6dGhlbWVfc2V0KGdncGxvdDI6OnRoZW1lX2J3KGJhc2Vfc2l6ZSA9IDEwKSkKcnVuZGF0ZSA8LSBmb3JtYXQoU3lzLkRhdGUoKSwgZm9ybWF0ID0gIiVZJW0lZCIpCnByZXZpb3VzX2ZpbGUgPC0gIiIKdmVyIDwtIGZvcm1hdChTeXMuRGF0ZSgpLCAiJVklbSVkIikKCiMjdG1wIDwtIHNtKGxvYWRtZShmaWxlbmFtZT1wYXN0ZTAoZ3N1YihwYXR0ZXJuPSJcXC5SbWQiLCByZXBsYWNlPSIiLCB4PXByZXZpb3VzX2ZpbGUpLCAiLXYiLCB2ZXIsICIucmRhLnh6IikpKQpybWRfZmlsZSA8LSAiaW5kZXhfZnVuY3Rpb25zX2JpZy5SbWQiCmxpYnJhcnkoc3BhdHN0YXQuZ2VvbSkKbGlicmFyeShyZXRpY3VsYXRlKQpgYGAKCiMgQ2hhbmdlbG9nCgoqIDIwMjMwNTA4OiBUcnlpbmcgdGhpcyBvdXQgd2l0aCBhIGxhcmdlciBkYXRhc2V0LCBleHBsaWNpdGx5IHNldCB3YW50ZWRfeiBhbmQgd2FudGVkX2NoYW5uZWwgZm9yIGl0LgoqIDIwMjMwMzI4OiBSZWltcGxlbWVudGVkIHN0ZXBzIGFzIGZ1bmN0aW9ucywgY29ubmVjdGVkIHRoZW0gaW4gb25lIHNldCBvZiBjYWxscy4KKiAyMDIzMDMxNzogSW5pdGlhbCBjZWxscG9zZSBpbXBsZW1lbnRhdGlvbi4KKiAyMDIzMDMxNTogU2VwYXJhdGlvbiBvZiBpbnB1dCBpbWFnZSBpbnRvIHRpbWVwb2ludHMuCiogMjAyMzAzMTA6IFVzZWQgZ2VvcGFuZGFzIHRvIHRyYWNlIGNlbGxzIG92ZXIgdGltZSBieSBwb3NpdGlvbi4KKiAyMDIzMDMwNzogU2V0dGluZyB1cCBteSBlbnZpcm9ubWVudCB0byBoYW5kbGUgaXB5bmIgYW5kIHB5dGhvbiBtYXJrZG93bi4KCiMgSW50cm9kdWN0aW9uCgpKYWNxdWVzIGlzIHNlZWtpbmcgdG8gc2VnbWVudCBhbmQgZm9sbG93IGNlbGxzIG92ZXIgdGltZS4KClN0ZXBzIHBlcmZvcm1lZCBzbyBmYXI6CgoxLiAgUHl0aG9uL3ZlbnYgaW5zdGFsbGF0aW9uIG9mIGZpamkKMi4gIEZpZ3VyZWQgb3V0IHNvbWUgZWFzeSBpbnRlcmFjdGlvbnMgYmV0d2VlbiBweXRob24gYW5kIGFjdHVhbCBpbWFnZSBkYXRhLgozLiAgSW1wbGVtZW50ZWQgYSBzaW1wbGUgZnVuY3Rpb24gdG8gZmluZCBtaW5pbXVtIGRpc3RhbmNlcyBiZXR3ZWVuCiAgICBwdXRhdGl2ZSBjZWxscyB1c2luZyB0aGUgZXh0YW50IG1ldGhvZHMuCgojIE5leHQgc3RlcHMKCjEuICBOb3RlIHRoYXQgdGhlIGN6aSBpbWFnZXMgYXJlIGltbWVkaWF0ZWx5IG9wZW5hYmxlIHZpYSBmaWppJ3MgYmlvZm9ybWF0IGludGVyZmFjZS4KMi4gIExvYWQgZGF0YXNldCB2aWEgZmlqaQozLiAgSW52b2tlIGNlbGxwb3NlIHZpYSB0aGUgcHl0aG9uIGludGVyZmFjZSAoc2FtZSBhcyBweWltYWdlaikKNC4gIFNhdmUgUk9JcyBwcm9kdWNlZCBieSBjZWxscG9zZSwgc2F2ZSB0aGVtIHRvIHppcCBvdXRwdXQgZmlsZQogIGEuICBUaGUgcm9pIGludGVyZmFjZSBpbiBmaWppIGNhbiBhZGRyZXNzIHRoZXNlCjUuICBDdXJyZW50bHkgYSBtYWNybyBpcyBpbnZva2VkIHdoaWNoIHBlcmZvcm1zIGEgc2V0IG9mIG1lYXN1cmVtZW50cwogICAgb24gZXZlcnkgUk9JIGFuZCBzYXZlcyB0aGVtIHRvIGEgY3N2LiAgVGhpcyBjcmVhdGVzIHRoZSBwcmltYXJ5CiAgICBkYXRhIHN0cnVjdHVyZSB1c2VkIGZvciBhbGwgZm9sbG93aW5nIHByb2Nlc3Nlcy4KNi4gIE5lZWQgdG8gY3JlYXRlIGEgZGF0YXN0cnVjdHVyZSB3aGljaCBpZGVudGlmaWVzIGVhY2ggaW5kaXZpZHVhbAogICAgY2VsbCBvdmVyIHRpbWUgZ2l2ZW4gdGhlc2UgUk9JcyB3aGljaCB4L3kvdGltZShmcmFtZSkgYWxvbmcgd2l0aAogICAgdGhlIG1lYXN1cmVtZW50cyBvZiBpbnRlcmVzdCAoYXJlYS9tZWFuL3N0ZGV2L2ludGVuc2l0aWVzIGJ5CiAgICBjb2xvcikuCiAgICBhLiAgU21hbGwgcG9zdHByb2Nlc3NpbmcgZGV0YWlsczogdGhlIGludGVuc2l0eSB2YWx1ZXMgcHJvZHVjZWQKICAgIG11c3QgYmUgbm9ybWFsaXplZCBieSBjZWxsIGFyZWEuCgojIERlbW9uc3RyYXRlIHRoZSBmaXJzdCBjb3VwbGUgb2Ygc3RlcHMgdXNpbmcgYW4gYWN0dWFsIGRhdGFzZXQKCkphY3F1ZXMgc2VudCBtZSBhbiBpbWFnZSBhY3F1aXJlZCBmcm9tIHRoZSBtaWNyb3Njb3BlIGFuZCBJIHNhdmVkIGl0CmFzICd0ZXN0X2RhdGEvcmF3LnRpZicsIGhlIGFsc28gc2VudCBtZSBhIGNlbGxwb3NlIG1vZGVsIHdoaWNoIEkgc2F2ZWQKdG8gdGhlICdtb2RlbHMvJyBkaXJlY3RvcnkuCgpHaXZlbiB0aGVzZSBhcyBhIHN0YXJ0aW5nIHBvaW50LCBsZXQgdXMgdHJ5IHRvIG9wZW4gdGhlIGltYWdlIHdpdGggYQpmaWppIGluc3RhbmNlIGFuZCBzcGxpdCBpdCBpbnRvIGEgc2VyaWVzIG9mIHNtYWxsZXIgZmlsZXMgYnkKdGltZXBvaW50LgoKIyBJbXBsZW1lbnRhdGlvbiBub3RlcwoKSSBoYXZlIGJlZW4gcHJpbWFyaWx5IHVzaW5nIHRoaXMgYXMgYSB3YXkgdG8gcmVmcmVzaCBteSBicmFpbiBvbgpweXRob24gYW5kIGdldCBjdXJyZW50IG9uIGJlc3QgcHJhY3RpY2VzLiAgVGh1cywgdGhlcmUgYXJlIGRlY2lzaW9ucyBJCm1hZGUgaW4gdGhpcyB3b3JrYm9vayB3aGljaCBkbyBub3QgbWFrZSBzZW5zZSBpbiBhbnkgb3RoZXIgY29udGV4dDoKZS5nLiB3aHkgd291bGQgYW55b25lIGhhdmUgdGhlIHByaW1hcnkgZGF0YSBzdHJ1Y3R1cmUgYmUgYSBkaWN0aW9uYXJ5CmtleWVkIGJ5IGZpbGVuYW1lLCB0aGF0IGlzIGR1bWI/ICBJIGNob3NlIHRvIGRvIHRoYXQgdG8gcmVmcmVzaCBteXNlbGYKb24gcGxheWluZyB3aXRoIGRpY3Rpb25hcmllcy4gIEJ5IHRoZSBzYW1lIHRva2VuLCB3aHkgd291bGQgYW55b25lCm1ha2UgYSBkaWN0aW9uYXJ5IG9mIGRhdGFmcmFtZXMgb25seSB0byB0dXJuIGFyb3VuZCBhbmQgY29uY2F0ZW5hdGUKdGhlbSBmb3IgdXNhZ2UgaW4gZ2VvcGFuZGFzLCB0aGF0IGlzIGNyYXp5PyAgSSBkaWQgdGhpcyB0byBnZXQgbW9yZQpjb21mb3J0YWJsZSB3aXRoIHBhbmRhcyBhbmQgZ2V0IG91dCBzb21lIG9mIG15IFJpc2ggbXVzY2xlIG1lbW9yeS4KCldpdGggdGhhdCBpbiBtaW5kLCBpZiB3ZSBjaG9vc2UgdG8gbWFrZSB0aGlzIGEgcGFja2FnZSwgdGhlIGZpcnN0CnRoaW5nIHRoYXQgd2lsbCBuZWVkIHRvIGhhcHBlbiBpcyB0byByZXdvcmsgdGhlIGJhc2UgZGF0YXN0cnVjdHVyZS4gIEkKanVzdCB3YW50IGFueW9uZSB3aG8gYWN0dWFsbHkgcmVhZHMgdGhpcyBjb2RlIHRvIGtub3cgdGhhdCB5ZXMsIEkgYW0gYQpudXR0ZXIsIGJ1dCBhIG51dHRlciBmb3IgYSByZWFzb24uCgojIyBMb2FkIG5lY2Vzc2FyeSBweXRob24gbW9kdWxlcwoKYGBge3B5dGhvbiBsb2FkfQpmcm9tIGNlbGxwb3NlIGltcG9ydCBtb2RlbHMsIGlvCmZyb20gY2VsbHBvc2UuaW8gaW1wb3J0ICoKZnJvbSBjb2xsZWN0aW9ucyBpbXBvcnQgZGVmYXVsdGRpY3QKaW1wb3J0IGdlb3BhbmRhcwppbXBvcnQgZ2xvYgppbXBvcnQgaW1hZ2VqCmZyb20ganB5cGUgaW1wb3J0IEpBcnJheSwgSkludAppbXBvcnQgbWF0cGxvdGxpYi5weXBsb3QgYXMgcGx0CmltcG9ydCBtdWx0aXByb2Nlc3NpbmcgYXMgbXAKaW1wb3J0IG51bXB5IGFzIG5wCmltcG9ydCBvcwppbXBvcnQgcGFuZGFzCmZyb20gcGFuZGFzIGltcG9ydCBEYXRhRnJhbWUKZnJvbSBwYXRobGliIGltcG9ydCBQYXRoCmltcG9ydCBzY3lqYXZhCmltcG9ydCBzZWFib3JuCmltcG9ydCBzaHV0aWwKYGBgCgojIyBTZXQgdXAgc29tZSBpbml0aWFsIHZhcmlhYmxlcwoKYGBge3B5dGhvbiBwYXJhbWV0ZXJzfQpiYXNlX2RpciA9IFBhdGgoJy9sYWIvc2NyYXRjaC9hdGIvaW1hZ2luZy9tdGJfMjAyMycpLmFzX3Bvc2l4KCkKb3MuY2hkaXIoYmFzZV9kaXIpCmlucHV0X2ZpbGUgPSBQYXRoKGYie2Jhc2VfZGlyfS90ZXN0X2RhdGEvRXhwZXJpbWVudC0xNTY4LmN6aSIpLmFzX3Bvc2l4KCkKcGFuZGFzLnNldF9vcHRpb24oJ2Rpc3BsYXkubWF4X2NvbHVtbnMnLCBOb25lKQp2ZXJib3NlID0gVHJ1ZQpgYGAKCiMjIFN0YXJ0IGltYWdlai9maWppCgpOb3RlIHRoYXQgSSBhbSB1c2luZyBmaWppL2ltYWdlaiBmcm9tIHdpdGhpbiBhIHZpcnR1YWwgZW52aXJvbm1lbnQKd2hpY2ggSSBzeW1ib2xpY2FsbHkgbGlua2VkIHRvIHRoZSBsb2NhbCBkaXJlY3RvcnkgJ3ZlbnYvJy4KCkFzIGEgcmVzdWx0LCB3aGVuIEkgaW5pdGlhbGl6ZSBmaWppLCBJIHdpbGwgY2FsbCB0aGUgYmFzZSBkaXJlY3Rvcnkgb2YKdGhlIGRvd25sb2FkZWQgZmlqaSB0cmVlIHdpdGhpbiB0aGUgdmlydHVhbCBlbnZpcm9ubWVudCwgd2hpY2ggSQpzb21ld2hhdCBlcnJvbmVvdXNseSBwdXQgaW4gYmluLy4KCmBgYHtweXRob24gc3RhcnRfZmlqaX0Kc2N5amF2YS5jb25maWcuYWRkX29wdGlvbignLVhteDEyOGcnKQpzdGFydF9kaXIgPSBvcy5nZXRjd2QoKQppaiA9IGltYWdlai5pbml0KFBhdGgoJ3ZlbnYvYmluL0ZpamkuYXBwJyksIG1vZGUgPSAnaW50ZXJhY3RpdmUnKQppai5nZXRBcHAoKS5nZXRJbmZvKFRydWUpCmlqLnVpKCkuc2hvd1VJKCkKIyMgU29tZXRoaW5nIGFib3V0IHRoaXMgaW5pdCgpIGZ1bmN0aW9uIGNoYW5nZXMgdGhlIGN1cnJlbnQgd29ya2luZyBkaXJlY3RvcnkuCm9zLmNoZGlyKHN0YXJ0X2RpcikKaWouZ2V0VmVyc2lvbigpCgpzaG93UG9seWdvblJvaSA9IHNjeWphdmEuamltcG9ydCgnaWouZ3VpLlBvbHlnb25Sb2knKQpPdmVybGF5ID0gc2N5amF2YS5qaW1wb3J0KCdpai5ndWkuT3ZlcmxheScpClJlZ2lvbnMgPSBzY3lqYXZhLmppbXBvcnQoJ25ldC5pbWdsaWIyLnJvaS5SZWdpb25zJykKTGFiZWxSZWdpb25zID0gc2N5amF2YS5qaW1wb3J0KCduZXQuaW1nbGliMi5yb2kubGFiZWxpbmcuTGFiZWxSZWdpb25zJykKWlByb2plY3RvciA9IHNjeWphdmEuamltcG9ydCgnaWoucGx1Z2luLlpQcm9qZWN0b3InKSgpCm92ID0gT3ZlcmxheSgpCmBgYAoKIyMgT3BlbiB0aGUgaW5wdXQgZmlsZSBhbmQgc3BsaXQgaXQgYnkgdGltZQoKT3BlbiB0aGUgZmlsZSwgZmlndXJlIG91dCBpdHMgZGltZW5zaW9ucywgYW5kIHdyaXRlIHBvcnRpb25zIG9mIGl0IHRvCmFuIG91dHB1dCBkaXJlY3RvcnkuCgpJIHdyb3RlIHRoaXMgdGhpbmtpbmcgSSBjb3VsZCBwYXJhbGxlbGl6ZSB0aGUgb3V0cHV0IHdyaXRpbmcgdG8gOApjcHVzLiAgQnV0IEkgdGhpbmsgSSBkbyBub3QgeWV0IHVuZGVyc3RhbmQgaG93IHB5dGhvbiBzY29wZXMgdmFyaWFibGVzCmluIHRoaXMgY29udGV4dCBhbmQgc28gaXQgZGlkIG5vdCBxdWl0ZSB3b3JrLiAgSSB0dXJuZWQgdGhhdCBvZmYgZm9yCnRoZSBtb21lbnQgYW5kIHJhbiBpdCBhbmQgaXQgZmluaXNoZWQgaW4gYWJvdXQgYSBtaW51dGUuCgpUaGUgZm9sbG93aW5nIGZ1bmN0aW9uIG1heSByZXF1aXJlIGEgdGVzdCB0byBzZWUgaWYgdGhlIG91dHB1dApkaXJlY3RvcnkgYWxyZWFkeSBleGlzdHMgYmVjYXVzZSBzY2lqYXZhIHdpbGwgZnJlYWsgb3V0LgoKYGBge3B5dGhvbiBzcGxpdF9pbWFnZV9mdW5jdGlvbn0KZGVmIHNlcGFyYXRlX3NsaWNlcyhpbnB1dF9maWxlLCB3YW50ZWRfeCA9IFRydWUsIHdhbnRlZF95ID0gVHJ1ZSwKICAgICAgICAgICAgICAgICAgICB3YW50ZWRfeiA9IDEsIHdhbnRlZF9jaGFubmVsID0gMiwgY3B1cyA9IDgsCiAgICAgICAgICAgICAgICAgICAgb3ZlcndyaXRlID0gRmFsc2UpOgogICAgIiIiIFNsaWNlIGFuIGltYWdlIGluIHByZXBhcmF0aW9uIGZvciBjZWxscG9zZS4KCiAgICBFdmVudHVhbGx5IHRoaXMgc2hvdWxkIGJlIHNtYXJ0IGVub3VnaCB0byBoYW5kbGUgYXJiaXRyYXJ5CiAgICB4LHkseixjaGFubmVscyx0aW1lcyBhcyB3ZWxsIGFzIGFibGUgdG8gdXNlIG11bHRpcGxlIGNwdXMgZm9yCiAgICBzYXZpbmcgdGhlIGRhdGEuICBJbiBpdHMgY3VycmVudCBpbXBsZW1lbnRhdGlvbiwgaXQgb25seSBzYXZlcyAxCiAgICB6LCAxIGNoYW5uZWwgZm9yIGV2ZXJ5IGZyYW1lIG9mIGFuIGltYWdlIGludG8gYSBzZXJpZXMgb2YgZmlsZXMgaW4KICAgIGl0cyBvdXRwdXQgZGlyZWN0b3J5LgogICAgIiIiCgogICAgaW5wdXRfYmFzZSA9IG9zLnBhdGguYmFzZW5hbWUoaW5wdXRfZmlsZSkKICAgIGlucHV0X2RpciA9IG9zLnBhdGguZGlybmFtZShpbnB1dF9maWxlKQogICAgaW5wdXRfbmFtZSA9IG9zLnBhdGguc3BsaXRleHQoaW5wdXRfYmFzZSlbMF0KICAgIG91dHB1dF9kaXJlY3RvcnkgPSBQYXRoKGYie2lucHV0X2Rpcn0vb3V0cHV0cy97aW5wdXRfbmFtZX1fent3YW50ZWRfen0iKS5hc19wb3NpeCgpCiAgICBvcy5tYWtlZGlycyhvdXRwdXRfZGlyZWN0b3J5LCBleGlzdF9vayA9IFRydWUpCiAgICBpZiB2ZXJib3NlOgogICAgICAgIHByaW50KCJTdGFydGluZyB0byBvcGVuIHRoZSBpbnB1dCBmaWxlLCB0aGlzIHRha2VzIGEgbW9tZW50LiIpCiAgICByYXdfZGF0YXNldCA9IGlqLmlvKCkub3BlbihpbnB1dF9maWxlKQogICAgaWYgdmVyYm9zZToKICAgICAgICBwcmludChmIk9wZW5lZCBpbnB1dCBmaWxlLCB3cml0aW5nIGltYWdlcyB0byB7b3V0cHV0X2RpcmVjdG9yeX0iKQoKICAgIGRhdGFfaW5mbyA9IHt9CiAgICBmb3IgZWxlbWVudCBpbiByYW5nZShsZW4ocmF3X2RhdGFzZXQuZGltcykpOgogICAgICAgIG5hbWUgPSByYXdfZGF0YXNldC5kaW1zW2VsZW1lbnRdCiAgICAgICAgZGF0YV9pbmZvW25hbWVdID0gcmF3X2RhdGFzZXQuc2hhcGVbZWxlbWVudF0KICAgIGlmIHZlcmJvc2U6CiAgICAgICAgcHJpbnQoZiJUaGlzIGRhdGFzZXQgaGFzIGRpbWVuc2lvbnM6IFg6e2RhdGFfaW5mb1snWCddfSIsCiAgICAgICAgICAgICAgZiJZOntkYXRhX2luZm9bJ1knXX0gWjp7ZGF0YV9pbmZvWydaJ119IFRpbWU6e2RhdGFfaW5mb1snVGltZSddfSIpCgogICAgc2xpY2VzID0gW10KICAgIGZvciB0aW1lcG9pbnQgaW4gcmFuZ2UoZGF0YV9pbmZvWydUaW1lJ10pOgogICAgICAgIHdhbnRlZF9zbGljZSA9IHJhd19kYXRhc2V0WzosIDosIHdhbnRlZF9jaGFubmVsLCB3YW50ZWRfeiwgdGltZXBvaW50XQogICAgICAgIHNsaWNlX2RhdGEgPSBpai5weS50b19kYXRhc2V0KHdhbnRlZF9zbGljZSkKICAgICAgICBvdXRwdXRfZmlsZW5hbWUgPSBQYXRoKGYie291dHB1dF9kaXJlY3Rvcnl9L2ZyYW1lX3t0aW1lcG9pbnR9LnRpZiIpLmFzX3Bvc2l4KCkKICAgICAgICBpZiAob3MucGF0aC5leGlzdHMob3V0cHV0X2ZpbGVuYW1lKSk6CiAgICAgICAgICAgIGlmIG92ZXJ3cml0ZToKICAgICAgICAgICAgICAgIHByaW50KGYiUmV3cml0aW5nIHtvdXRwdXRfZmlsZW5hbWV9IikKICAgICAgICAgICAgICAgIG9zLnJlbW92ZShvdXRwdXRfZmlsZW5hbWUpCiAgICAgICAgICAgICAgICBzYXZlZCA9IGlqLmlvKCkuc2F2ZShzbGljZV9kYXRhLCBvdXRwdXRfZmlsZW5hbWUpCiAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICBpZiB2ZXJib3NlOgogICAgICAgICAgICAgICAgICAgIHByaW50KGYiU2tpcHBpbmcge291dHB1dF9maWxlbmFtZX0sIGl0IGFscmVhZHkgZXhpc3RzLiIpCiAgICAgICAgZWxzZToKICAgICAgICAgICAgc2F2ZWQgPSBpai5pbygpLnNhdmUoc2xpY2VfZGF0YSwgb3V0cHV0X2ZpbGVuYW1lKQogICAgICAgICAgICBpZiB2ZXJib3NlOgogICAgICAgICAgICAgICAgcHJpbnQoZiJTYXZpbmcgaW1hZ2Uge2lucHV0X25hbWV9X3t0aW1lcG9pbnR9LiIpCiAgICAgICAgc2xpY2VzLmFwcGVuZCh3YW50ZWRfc2xpY2UpCgogICAgcmV0dXJuIHJhd19kYXRhc2V0LCBzbGljZXMsIG91dHB1dF9kaXJlY3RvcnkKYGBgCgojIyBDZWxscG9zZQoKQXQgdGhpcyBwb2ludCB3ZSBzaG91bGQgaGF2ZSBhIGRpcmVjdG9yeSBjb250YWluaW5nIGZpbGVzIG9mCmluZGl2aWR1YWwgdGltZXBvaW50cy4gIEphY3F1ZXMgc2VudCBtZSBhbiBpbml0aWFsIGltcGxlbWVudGF0aW9uIG9mCnRoZSB1c2FnZSBvZiBjZWxscG9zZSB0byBjYWxsIGluZGl2aWR1YWwgY2VsbHMuICBMZXQgdXMgaW5jbHVkZSB0aGF0Cm5vdy4gIEkgdGhpbmsgdGhlIHByZXZpb3VzIGZ1bmN0aW9uIHNob3VsZCBwcm9iYWJseSBhbHNvIHJldHVybiB0aGUKZGlyZWN0b3J5IG9mIHRoZSBzZXBhcmF0ZWQgaW5wdXQgZmlsZXMuCgpgYGB7cHl0aG9uIGNlbGxwb3NlfQojIyBSZWxldmFudCBvcHRpb25zOgojIyBiYXRjaF9zaXplKGluY3JlYXNlIGZvciBtb3JlIHBhcmFsbGVsaXphdGlvbiksIGNoYW5uZWxzKHR3byBlbGVtZW50IGxpc3Qgb2YgdHdvIGVsZW1lbnQKIyMgY2hhbm5lbHMgdG8gc2VnbWVudDsgdGhlIGZpcnN0IGlzIHRoZSBzZWdtZW50LCBzZWNvbmQgaXMgb3B0aW9uYWwgbnVjbGV1czsKIyMgaW50ZXJuYWwgZWxlbWVudHMgYXJlIGNvbG9yIGNoYW5uZWxzIHRvIHF1ZXJ5LCBzbyBbWzAsMF0sWzIsM11dIG1lYW5zIGRvIG1haW4gY2VsbHMgaW4KIyMgZ3JheXNjYWxlIGFuZCBhIHNlY29uZCB3aXRoIGNlbGxzIGluIGJsdWUsIG51Y2xlaSBpbiBncmVlbi4KIyMgY2hhbm5lbF9heGlzLCB6X2F4aXMgPyBpbnZlcnQgKFQvRiBmbGlwIHBpeGVscyBmcm9tIGIvdyBJIGFzc3VtZSksCiMjIG5vcm1hbGl6ZShUL0YgcGVyY2VudGlsZSBub3JtYWxpemUgdGhlIGRhdGEpLCBkaWFtZXRlciwgZG9fM2QsCiMjIGFuaXNvdHJvcHkgKHJlc2NhbGluZyBmYWN0b3IgZm9yIDNkIHNlZ21lbnRhdGlvbiksIG5ldF9hdmcgKGF2ZXJhZ2UgbW9kZWxzKSwKIyMgYXVnbWVudCA/LCB0aWxlID8sIHJlc2FtcGxlLCBpbnRlcnAsIGZsb3dfdGhyZXNob2xkLCBjZWxscHJvYl90aHJlc2hvbGQgKGludGVyZXN0aW5nKSwKIyMgbWluX3NpemUgKHR1cm5lZCBvZmYgd2l0aCAtMSksIHN0aXRjaF90aHJlc2hvbGQgPywgcmVzY2FsZSA/LgpkZWYgaW52b2tlX2NlbGxwb3NlKGlucHV0X2RpcmVjdG9yeSwgbW9kZWxfZmlsZSwgY2hhbm5lbHMgPSBbWzAsIDBdXSwgZGlhbWV0ZXIgPSAxNjAsCiAgICAgICAgICAgICAgICAgICAgdGhyZXNob2xkID0gMC40LCBkb18zRCA9IEZhbHNlLCBiYXRjaF9zaXplID0gNjQsIHZlcmJvc2UgPSBUcnVlKToKICAgICIiIiBJbnZva2UgY2VsbHBvc2UgdXNpbmcgaW5kaXZpZHVhbCBzbGljZXMuCgogICAgVGhpcyB0YWtlcyB0aGUgc2VyaWVzIG9mIHNsaWNlcyBmcm9tIHNlcGFyYXRlX3NsaWNlcygpIGFuZCBzZW5kcwogICAgdGhlbSB0byBjZWxscG9zZSB3aXRoIGEgc3BlY2lmaWMgbW9kZWwuICBUaGUgZGljdGlvbmFyeSBpdCByZXR1cm5zCiAgICBpcyB0aGUgcHJpbWFyeSBkYXRhc3RydWN0dXJlIGZvciB0aGUgdmFyaW91cyBmdW5jdGlvbnMgd2hpY2ggZm9sbG93LgogICAgIiIiCgogICAgIyMgUmVsZXZhbnQgb3B0aW9uczoKICAgICMjIG1vZGVsX3R5cGUoY3l0bywgbnVjbGVpLCBjeXRvMiksIG5ldF9hdmcoVC9GIGlmIGxvYWQgYnVpbHQgaW4gbmV0d29ya3MgYW5kIGF2ZXJhZ2UgdGhlbSkKICAgIG1vZGVsID0gbW9kZWxzLkNlbGxwb3NlTW9kZWwocHJldHJhaW5lZF9tb2RlbCA9IG1vZGVsX2ZpbGUpCiAgICBmaWxlcyA9IGdldF9pbWFnZV9maWxlcyhpbnB1dF9kaXJlY3RvcnksICdfbWFza3MnLCBsb29rX29uZV9sZXZlbF9kb3duID0gRmFsc2UpCiAgICBpbWdzID0gW10KICAgIG91dHB1dF9tYXNrcyA9IFtdCiAgICBvdXRwdXRfdHh0cyA9IFtdCiAgICBvdXRwdXRfZmlsZXMgPSBkZWZhdWx0ZGljdChkaWN0KQogICAgZXhpc3RpbmdfZmlsZXMgPSAwCiAgICBjb3VudCA9IDAKICAgIGZvciBvbmVfZmlsZSBpbiBmaWxlczoKICAgICAgICBwcmludChmIlJlYWRpbmcge29uZV9maWxlfSIpCiAgICAgICAgY3Bfb3V0cHV0X2RpcmVjdG9yeSA9IFBhdGgoZiJ7aW5wdXRfZGlyZWN0b3J5fS9jZWxscG9zZSIpLmFzX3Bvc2l4KCkKICAgICAgICBvcy5tYWtlZGlycyhjcF9vdXRwdXRfZGlyZWN0b3J5LCBleGlzdF9vayA9IFRydWUpCiAgICAgICAgZl9uYW1lID0gb3MucGF0aC5iYXNlbmFtZShvbmVfZmlsZSkKICAgICAgICBmX25hbWUgPSBvcy5wYXRoLnNwbGl0ZXh0KGZfbmFtZSlbMF0KICAgICAgICBzdGFydF9tYXNrID0gUGF0aChmIntpbnB1dF9kaXJlY3Rvcnl9L3tmX25hbWV9X2NwX21hc2tzLnBuZyIpLmFzX3Bvc2l4KCkKICAgICAgICBvdXRwdXRfbWFzayA9IFBhdGgoZiJ7Y3Bfb3V0cHV0X2RpcmVjdG9yeX0ve2ZfbmFtZX1fY3BfbWFza3MucG5nIikuYXNfcG9zaXgoKQogICAgICAgIHN0YXJ0X3R4dCA9ICBQYXRoKGYie2lucHV0X2RpcmVjdG9yeX0ve2ZfbmFtZX1fY3Bfb3V0bGluZXMudHh0IikuYXNfcG9zaXgoKQogICAgICAgIG91dHB1dF90eHQgPSBQYXRoKGYie2NwX291dHB1dF9kaXJlY3Rvcnl9L3tmX25hbWV9X2NwX291dGxpbmVzLnR4dCIpLmFzX3Bvc2l4KCkKICAgICAgICBwcmludChmIkFkZGluZyBuZXcgdHh0IGZpbGU6IHtvdXRwdXRfdHh0fSIpCiAgICAgICAgb3V0cHV0X2ZpbGVzW2ZfbmFtZV1bJ2lucHV0X2ZpbGUnXSA9IG9uZV9maWxlCiAgICAgICAgb3V0cHV0X2ZpbGVzW2ZfbmFtZV1bJ3N0YXJ0X21hc2snXSA9IHN0YXJ0X21hc2sKICAgICAgICBvdXRwdXRfZmlsZXNbZl9uYW1lXVsnb3V0cHV0X21hc2snXSA9IG91dHB1dF9tYXNrCiAgICAgICAgb3V0cHV0X2ZpbGVzW2ZfbmFtZV1bJ3N0YXJ0X3R4dCddID0gc3RhcnRfdHh0CiAgICAgICAgb3V0cHV0X2ZpbGVzW2ZfbmFtZV1bJ291dHB1dF90eHQnXSA9IG91dHB1dF90eHQKICAgICAgICBvdXRwdXRfZmlsZXNbZl9uYW1lXVsnZXhpc3RzJ10gPSBGYWxzZQogICAgICAgIGlmIChvcy5wYXRoLmV4aXN0cyhvdXRwdXRfdHh0KSk6CiAgICAgICAgICAgIGV4aXN0aW5nX2ZpbGVzID0gZXhpc3RpbmdfZmlsZXMgKyAxCiAgICAgICAgICAgIG91dHB1dF9maWxlc1tmX25hbWVdWydleGlzdHMnXSA9IFRydWUKICAgICAgICBlbHNlOgogICAgICAgICAgICBpbWcgPSBpbXJlYWQob25lX2ZpbGUpCiAgICAgICAgICAgIGltZ3MuYXBwZW5kKGltZykKICAgICAgICBjb3VudCA9IGNvdW50ICsgMQogICAgbmltZyA9IGxlbihpbWdzKQogICAgaWYgdmVyYm9zZSBhbmQgbmltZyA+IDA6CiAgICAgICAgcHJpbnQoZiJSZWFkIHtuaW1nfSBpbWFnZXMsIHN0YXJ0aW5nIGNlbGxwb3NlLiIpCiAgICAgICAgbWFza3MsIGZsb3dzLCBzdHlsZXMgPSBtb2RlbC5ldmFsKAogICAgICAgICAgICBpbWdzLCBkaWFtZXRlciA9IGRpYW1ldGVyLCBjaGFubmVscyA9IGNoYW5uZWxzLCBmbG93X3RocmVzaG9sZCA9IHRocmVzaG9sZCwKICAgICAgICAgICAgZG9fM0QgPSBkb18zRCwgYmF0Y2hfc2l6ZSA9IGJhdGNoX3NpemUpCiAgICAgICAgaW8uc2F2ZV90b19wbmcoaW1ncywgbWFza3MsIGZsb3dzLCBmaWxlcykKICAgICAgICBwcmludChmIk1vdmluZyBjZWxscG9zZSBvdXRwdXRzIHRvIHRoZSBjZWxscG9zZSBvdXRwdXQgZGlyZWN0b3J5LiIpCiAgICAgICAgb3V0cHV0X2ZpbGVuYW1lcyA9IGxpc3Qob3V0cHV0X2ZpbGVzLmtleXMoKSkKICAgICAgICBmb3IgZl9uYW1lIGluIG91dHB1dF9maWxlbmFtZXM6CiAgICAgICAgICAgIHByaW50KGYiTW92aW5nIHtvdXRwdXRfZmlsZXNbZl9uYW1lXVtzdGFydF9tYXNrXX0gdG8ge291dHB1dF9maWxlc1tmX25hbWVdW291dHB1dF9tYXNrXX0iKQogICAgICAgICAgICBzaHV0aWwubW92ZShvdXRwdXRfZmlsZXNbZl9uYW1lXVtzdGFydF9tYXNrXSwgb3V0cHV0X2ZpbGVzW2ZfbmFtZV1bb3V0cHV0X21hc2tdKQogICAgICAgICAgICBzaHV0aWwubW92ZShvdXRwdXRfZmlsZXNbZl9uYW1lXVtzdGFydF90eHRdLCBvdXRwdXRfZmlsZXNbZl9uYW1lXVtvdXRwdXRfdHh0XSkKICAgIGVsc2U6CiAgICAgICAgcHJpbnQoIlJldHVybmluZyB0aGUgb3V0cHV0IGZpbGVzLiIpCiAgICByZXR1cm4gb3V0cHV0X2ZpbGVzCmBgYAoKIyMgQ29sbGFwc2UgWgoKT25lIHBvc3NpYmxlIGNoYW5nZSBpcyB0byBwZXJmb3JtIG1lYXN1cmVtZW50cyBvbiB0aGUgc3VtIG9mIFotc3RhY2tzCmluc3RlYWQgb2YgYSBzaW5nbGUgc2xpY2UuICBUaHVzIHdlIHdvdWxkIHN1bSB0aGUgY2VsbHMsIGNyZWF0ZSB0aGUKUk9JcyB1c2luZyB0aGUgc2luZ2xlIHNsaWNlIGdyYXlzY2FsZSBpbWFnZSwgdGhlbiBtZWFzdXJlIHRoZSBzZXQgb2YKYWxsIGNvbWJpbmVkLgoKYGBge3B5dGhvbiBjb2xsYXBzZV96fQpkZWYgY29sbGFwc2VfeihyYXdfZGF0YXNldCwgY2VsbHBvc2VfcmVzdWx0LCBtZXRob2QgPSAnc3VtJyk6CiAgICAiIiIgU3RhY2sgbXVsdGlwbGUgeiBzbGljZXMgZm9yIGVhY2ggdGltZXBvaW50LgoKICAgIElmIEkgdW5kZXJzdGFuZCBKYWNxdWVzJyBleHBsYW5hdGlvbiBvZiB0aGUgcXVhbnRpZmljYXRpb24gbWV0aG9kcwogICAgY29ycmVjdGx5LCB0aGV5IHNvbWV0aW1lcyAob2Z0ZW4/KSBwZXJmb3JtIGJldHRlciBvbiB0aGUKICAgIHotaW50ZWdyYXRpb24gb2YgcGl4ZWxzIGF0IGVhY2ggdGltZXBvaW50LiAgVGhpcyBmdW5jdGlvbiBwZXJmb3JtcwogICAgdGhhdCBhbmQgc2VuZHMgdGhlIHN0YWNrZWQgc2xpY2VzIHRvIHRoZSBvdXRwdXQgZGlyZWN0b3J5IGFuZCBhZGRzCiAgICB0aGUgZmlsZW5hbWVzIHRvIHRoZSBjZWxscG9zZV9yZXN1bHQgZGljdGlvbmFyeS4KICAgICIiIgogICAgY2VsbHBvc2Vfc2xpY2VzID0gbGlzdChjZWxscG9zZV9yZXN1bHQua2V5cygpKQogICAgc2xpY2VfbnVtYmVyID0gMAogICAgY29sbGFwc2VkX3NsaWNlcyA9IFtdCiAgICBmb3Igc2xpY2VfbmFtZSBpbiBjZWxscG9zZV9zbGljZXM6CiAgICAgICAgb3V0cHV0X2RpcmVjdG9yeSA9IG9zLnBhdGguZGlybmFtZShjZWxscG9zZV9yZXN1bHRbc2xpY2VfbmFtZV1bJ291dHB1dF90eHQnXSkKICAgICAgICBjb2xsYXBzZWRfZGlyZWN0b3J5ID0gb3MucGF0aC5kaXJuYW1lKG91dHB1dF9kaXJlY3RvcnkpCiAgICAgICAgY29sbGFwc2VkX2RpcmVjdG9yeSA9IGYie2NvbGxhcHNlZF9kaXJlY3Rvcnl9L2NvbGxhcHNlZCIKICAgICAgICBvcy5tYWtlZGlycyhjb2xsYXBzZWRfZGlyZWN0b3J5LCBleGlzdF9vayA9IFRydWUpCiAgICAgICAgb3V0cHV0X2ZpbGVuYW1lID0gUGF0aChmIntjb2xsYXBzZWRfZGlyZWN0b3J5fS9mcmFtZXtzbGljZV9udW1iZXJ9LnRpZiIpLmFzX3Bvc2l4KCkKICAgICAgICBjZWxscG9zZV9yZXN1bHRbc2xpY2VfbmFtZV1bJ2NvbGxhcHNlZF9maWxlJ10gPSBvdXRwdXRfZmlsZW5hbWUKICAgICAgICBpZiAob3MucGF0aC5leGlzdHMob3V0cHV0X2ZpbGVuYW1lKSk6CiAgICAgICAgICAgIGlmIHZlcmJvc2U6CiAgICAgICAgICAgICAgICBwcmludChmIlNraXBwaW5nIHtvdXRwdXRfZmlsZW5hbWV9LCBpdCBhbHJlYWR5IGV4aXN0cy4iKQogICAgICAgIGVsc2U6CiAgICAgICAgICAgIGxhcmdlcl9zbGljZSA9IHJhd19kYXRhc2V0WzosIDosIDosIDosIHNsaWNlX251bWJlcl0KICAgICAgICAgICAgaW1wID0gaWoucHkudG9faW1hZ2VwbHVzKGxhcmdlcl9zbGljZSkKICAgICAgICAgICAgel9wcm9qZWN0b3JfcmVzdWx0ID0gWlByb2plY3Rvci5ydW4oaW1wLCBtZXRob2QpCiAgICAgICAgICAgICMjIHpfcHJvamVjdG9yX21hc2sgPSBpai5JSi5ydW4oel9wcm9qZWN0b3JfcmVzdWx0LCAiQ29udmVydCB0byBNYXNrIiwgIm1ldGhvZD1PdHN1IGJhY2tncm91bmQ9TGlnaHQiKQogICAgICAgICAgICB6X2NvbGxhcHNlZF9pbWFnZSA9IGlqLnB5LmZyb21famF2YSh6X3Byb2plY3Rvcl9yZXN1bHQpCiAgICAgICAgICAgIHpfY29sbGFwc2VkX2RhdGFzZXQgPSBpai5weS50b19kYXRhc2V0KHpfY29sbGFwc2VkX2ltYWdlKQogICAgICAgICAgICBzYXZlZCA9IGlqLmlvKCkuc2F2ZSh6X2NvbGxhcHNlZF9kYXRhc2V0LCBvdXRwdXRfZmlsZW5hbWUpCiAgICAgICAgICAgIGlmIHZlcmJvc2U6CiAgICAgICAgICAgICAgICBwcmludChmIlNhdmluZyBpbWFnZSB7b3V0cHV0X2ZpbGVuYW1lfS4iKQogICAgICAgIHNsaWNlX251bWJlciA9IHNsaWNlX251bWJlciArIDEKICAgIHJldHVybiBjZWxscG9zZV9yZXN1bHQKYGBgCgojIENyZWF0ZSBSZWdpb25zIG9mIGludGVyZXN0IGZyb20gY2VsbHBvc2Ugb3V0cHV0cwoKSW4gSmFjcXVlcyBub3RlYm9vaywgaXQgbG9va3MgbGlrZSBoZSBvbmx5IGV4dHJhY3RzIFJPSXMgZnJvbSBvbmUgb2YKdGhlIGNlbGxwb3NlIHNsaWNlcy4gIEkgYW0gYXNzdW1pbmcgdGhlIGdvYWwgaXMgdG8gZXh0ZW5kIHRoaXMgYWNyb3NzCmFsbCBpbWFnZXM/CgpUaGVyZSBpcyBhbiBpbXBvcnRhbnQgY2F2ZWF0IHRoYXQgSSBtaXNzZWQ6IGltYWdlaiBjb21lcyB3aXRoIGEKcHl0aG9uMi1iYXNlZCBzY3JpcHRpbmcgbGFuZ3VhZ2UgZnJvbSB3aGljaCBpdCBhcHBlYXJzIHNvbWUgb2YgaGlzCmNvZGUgaXMgY29taW5nLiAgQXMgYSByZXN1bHQgSSBzaG91bGQgbG9vayBjYXJlZnVsbHkgYmVmb3JlIHVzaW5nIGl0LAphbmQgcGF5IGNsb3NlIGF0dGVudGlvbiB0byB0aGUgZXhhbXBsZXMgcHJvdmlkZWQgaGVyZSBmb3IgdGhlIG1vc3QKYXBwcm9wcmlhdGUgd2F5cyBvZiBpbnRlcmFjdGluZyB3aXRoIHRoZSBST0kgbWFuYWdlciBldGM6CgpodHRwczovL2dpdGh1Yi5jb20vaW1hZ2VqL3B5aW1hZ2VqL2Jsb2IvbWFpbi9kb2MvZXhhbXBsZXMvYmxvYl9kZXRlY3Rpb25faW50ZXJhY3RpdmUucHkKCmBgYHtweXRob24gY2VsbHBvc2VfdG9fcm9pfQojIyBUaGUgZm9sbG93aW5nIGlzIGZyb20gYSBtaXggb2YgYSBjb3VwbGUgb2YgaW1wbGVtZW50YXRpb25zIEkgZm91bmQ6CiMjIGh0dHBzOi8vcHlpbWFnZWoucmVhZHRoZWRvY3MuaW8vZW4vbGF0ZXN0L0NsYXNzaWMtU2VnbWVudGF0aW9uLmh0bWwKIyMgYW4gYWx0ZXJuYXRpdmUgbWV0aG9kIG1heSBiZSB0YWtlbiBmcm9tOgojIyBodHRwczovL3B5aW1hZ2VqLnJlYWR0aGVkb2NzLmlvL2VuL2xhdGVzdC9DbGFzc2ljLVNlZ21lbnRhdGlvbi5odG1sI3NlZ21lbnRhdGlvbi13b3JrZmxvdy13aXRoLWltYWdlajIKIyMgTXkgZ29hbCBpcyB0byBwYXNzIHRoZSBST0kgcmVnaW9ucyB0byB0aGlzIGZ1bmN0aW9uIGFuZCBjcmVhdGUgYSBzaW1pbGFyIGRmLgpkZWYgc2xpY2VzX3RvX3JvaV9tZWFzdXJlbWVudHMoY2VsbHBvc2VfcmVzdWx0LCBjb2xsYXBzZWQgPSBGYWxzZSk6CiAgICAiIiIgUmVhZCB0aGUgdGV4dCBjZWxscG9zZSBvdXRwdXQgZmlsZXMsIGdlbmVyYXRlIFJPSXMsIGFuZCBtZWFzdXJlLgoKICAgIEkgdGhpbmsgdGhlcmUgYXJlIGJldHRlciB3YXlzIG9mIGFjY29tcGxpc2hpbmcgdGhpcyB0YXNrIHRoYW4KICAgIHVzaW5nIGlqLklKLnJ1bigpOyBidXQgdGhpcyBzZWVtcyB0byB3b3JrLi4uICBVcG9uIGNvbXBsZXRpb24sCiAgICB0aGlzIGZ1bmN0aW9uIHNob3VsZCBhZGQgYSBzZXJpZXMgb2YgZGF0YWZyYW1lcyB0byB0aGUKICAgIGNlbGxwb3NlX3Jlc3VsdCBkaWN0aW9uYXJ5IHdoaWNoIGNvbXByaXNlIHRoZSB2YXJpb3VzIG1ldHJpY3MgZnJvbQogICAgSW1hZ2VKJ3MgbWVhc3VyZW1lbnQgZnVuY3Rpb24gb2YgdGhlIFJPSXMgZGV0ZWN0ZWQgYnkgY2VsbHBvc2UuCiAgICAiIiIKICAgIG91dHB1dF9kaWN0ID0gY2VsbHBvc2VfcmVzdWx0CiAgICBjZWxscG9zZV9zbGljZXMgPSBsaXN0KGNlbGxwb3NlX3Jlc3VsdC5rZXlzKCkpCiAgICBzbGljZV9udW1iZXIgPSAwCiAgICBmb3Igc2xpY2VfbmFtZSBpbiBjZWxscG9zZV9zbGljZXM6CiAgICAgICAgb3V0cHV0X2RpY3Rbc2xpY2VfbmFtZV1bJ3NsaWNlX251bWJlciddID0gc2xpY2VfbnVtYmVyCiAgICAgICAgaW5wdXRfdGlmID0gJycKICAgICAgICBpZiBjb2xsYXBzZWQ6CiAgICAgICAgICAgIGlucHV0X3RpZiA9IGNlbGxwb3NlX3Jlc3VsdFtzbGljZV9uYW1lXVsnY29sbGFwc2VkX2ZpbGUnXQogICAgICAgIGVsc2U6CiAgICAgICAgICAgIGlucHV0X3RpZiA9IGNlbGxwb3NlX3Jlc3VsdFtzbGljZV9uYW1lXVsnaW5wdXRfZmlsZSddCiAgICAgICAgc2xpY2VfZGF0YXNldCA9IGlqLmlvKCkub3BlbihpbnB1dF90aWYpCiAgICAgICAgc2xpY2VfZGF0YSA9IGlqLnB5LnRvX2ltYWdlcGx1cyhzbGljZV9kYXRhc2V0KQogICAgICAgIGlucHV0X3R4dCA9IGNlbGxwb3NlX3Jlc3VsdFtzbGljZV9uYW1lXVsnb3V0cHV0X3R4dCddCiAgICAgICAgaW5wdXRfbWFzayA9IGNlbGxwb3NlX3Jlc3VsdFtzbGljZV9uYW1lXVsnb3V0cHV0X21hc2snXQogICAgICAgIGlmIHZlcmJvc2U6CiAgICAgICAgICAgIHByaW50KGYiUHJvY2Vzc2luZyBjZWxscG9zZSBvdXRsaW5lOiB7aW5wdXRfdHh0fSIpCiAgICAgICAgICAgIHByaW50KGYiTWVhc3VyaW5nOiB7aW5wdXRfdGlmfSIpCiAgICAgICAgIyBjb252ZXJ0IERhdGFzZXQgdG8gSW1hZ2VQbHVzCiAgICAgICAgaW1wID0gaWoucHkudG9faW1hZ2VwbHVzKHNsaWNlX2RhdGEpCiAgICAgICAgcm0gPSBpai5Sb2lNYW5hZ2VyLmdldFJvaU1hbmFnZXIoKQogICAgICAgIHJtLnJ1bkNvbW1hbmQoIkFzc29jaWF0ZWQiLCAidHJ1ZSIpCiAgICAgICAgcm0ucnVuQ29tbWFuZCgic2hvdyBBbGwgd2l0aCBsYWJlbHMiKQogICAgICAgICMjIFRoZSBsb2dpYyBmb3IgdGhpcyB3YXMgdGFrZW4gZnJvbToKICAgICAgICAjIyBodHRwczovL3N0YWNrb3ZlcmZsb3cuY29tL3F1ZXN0aW9ucy83Mzg0OTQxOC9pcy10aGVyZS1hbnktd2F5LXRvLXN3aXRjaC1pbWFnZWotbWFjcm8tY29kZS10by1weXRob24zLWNvZGUKICAgICAgICB0eHRfZmggPSBvcGVuKGlucHV0X3R4dCwgJ3InKQogICAgICAgIHNldF9zdHJpbmcgPSBmJ1NldCBNZWFzdXJlbWVudHMuLi4nCiAgICAgICAgbWVhc3VyZV9zdHJpbmcgPSBmJ2FyZWEgbWVhbiBtaW4gY2VudHJvaWQgbWVkaWFuIHNrZXduZXNzIGt1cnRvc2lzIGludGVncmF0ZWQgc3RhY2sgcmVkaXJlY3Q9Tm9uZSBkZWNpbWFsPTMnCiAgICAgICAgaWouSUoucnVuKHNldF9zdHJpbmcsIG1lYXN1cmVfc3RyaW5nKQogICAgICAgIHJvaV9zdGF0cyA9IGRlZmF1bHRkaWN0KGxpc3QpCiAgICAgICAgZm9yIGxpbmUgaW4gdHh0X2ZoOgogICAgICAgICAgICB4eSA9IGxpbmUucnN0cmlwKCkuc3BsaXQoIiwiKQogICAgICAgICAgICB4eV9jb29yZHMgPSBbaW50KGVsZW1lbnQpIGZvciBlbGVtZW50IGluIHh5IGlmIGVsZW1lbnQgbm90IGluICcnXQogICAgICAgICAgICB4X2Nvb3JkcyA9IFtpbnQoZWxlbWVudCkgZm9yIGVsZW1lbnQgaW4geHlbOjoyXSBpZiBlbGVtZW50IG5vdCBpbiAnJ10KICAgICAgICAgICAgeV9jb29yZHMgPSBbaW50KGVsZW1lbnQpIGZvciBlbGVtZW50IGluIHh5WzE6OjJdIGlmIGVsZW1lbnQgbm90IGluICcnXQogICAgICAgICAgICB4Y29vcmRzX2ppbnQgPSBKQXJyYXkoSkludCkoeF9jb29yZHMpCiAgICAgICAgICAgIHljb29yZHNfamludCA9IEpBcnJheShKSW50KSh5X2Nvb3JkcykKICAgICAgICAgICAgcG9seWdvbl9yb2lfaW5zdGFuY2UgPSBzY3lqYXZhLmppbXBvcnQoJ2lqLmd1aS5Qb2x5Z29uUm9pJykKICAgICAgICAgICAgcm9pX2luc3RhbmNlID0gc2N5amF2YS5qaW1wb3J0KCdpai5ndWkuUm9pJykKICAgICAgICAgICAgaW1wb3J0ZWRfcG9seWdvbiA9IHBvbHlnb25fcm9pX2luc3RhbmNlKHhjb29yZHNfamludCwgeWNvb3Jkc19qaW50LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGVuKHhfY29vcmRzKSwgaW50KHJvaV9pbnN0YW5jZS5QT0xZR09OKSkKICAgICAgICAgICAgaW1wLnNldFJvaShpbXBvcnRlZF9wb2x5Z29uKQogICAgICAgICAgICBybS5hZGRSb2koaW1wb3J0ZWRfcG9seWdvbikKICAgICAgICAgICAgaWouSUoucnVuKGltcCwgJ01lYXN1cmUnLCAnJykKICAgICAgICBybS5ydW5Db21tYW5kKCdVcGRhdGUnKQogICAgICAgIHNsaWNlX3Jlc3VsdCA9IGlqLlJlc3VsdHNUYWJsZS5nZXRSZXN1bHRzVGFibGUoKQogICAgICAgIHNsaWNlX3RhYmxlID0gaWouY29udmVydCgpLmNvbnZlcnQoc2xpY2VfcmVzdWx0LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2N5amF2YS5qaW1wb3J0KCdvcmcuc2NpamF2YS50YWJsZS5UYWJsZScpKQogICAgICAgIHNsaWNlX21lYXN1cmVtZW50cyA9IGlqLnB5LmZyb21famF2YShzbGljZV90YWJsZSkKICAgICAgICBvdXRwdXRfZGljdFtzbGljZV9uYW1lXVsnbWVhc3VyZW1lbnRzJ10gPSBzbGljZV9tZWFzdXJlbWVudHMKICAgICAgICBpai5JSi5ydW4oJ0NsZWFyIFJlc3VsdHMnKQogICAgICAgIHR4dF9maC5jbG9zZSgpCiAgICAgICAgaW1wLnNldE92ZXJsYXkob3YpCiAgICAgICAgaW1wLmdldFByb2Nlc3NvcigpLnJlc2V0TWluQW5kTWF4KCkKICAgICAgICBzbGljZV9udW1iZXIgPSBzbGljZV9udW1iZXIgKyAxCiAgICByZXR1cm4gb3V0cHV0X2RpY3QKYGBgCgoKYGBge3B5dGhvbiBleHRyYWN0X2NlbGxwb3NlfQojIyBQdWxsIG91dCBvZiBzbGljZXNfdG9fcm9pX21lYXN1cmVtZW50cygpIHRoZSBwYXJ0cyB3aGljaCBhY3R1YWxseSBkZWZpbmUKIyMgYSBjZWxscG9zZS1kZXRlY3RlZCBjZWxsIHdpdGggcmVzcGVjdCB0byB0aW1lLgojIyBUaGlzIHNob3VsZCBiZSB1c2VkIGluIGNvb3BlcmF0aW9uIHdpdGggZnVuY3Rpb25zIHdoaWNoIGFwcGx5IHRoZQojIyByZXN1bHRpbmcgcG9seWdvbnMgdG8gb3RoZXIgaW1hZ2VzICh0byBjcmVhdGUgdGhlIFJPSXMpIGFsb25nIHdpdGgKIyMgZnVuY3Rpb25zIHRoYXQgcGVyZm9ybSB0aGUgZmluYWwgbWVhc3VyZW1lbnRzLiAgSWYgcHJvcGVybHkKIyMgaW1wbGVtZW50ZWQsIHRoaXMgc2hvdWxkIGFsbG93IG9uZSB0byBtaXggYW5kIG1hdGNoIHRoZSBpbnZvY2F0aW9ucwojIyB0byB1c2UgdGhlbSBvbiBhbnkgc2V0IG9mIHRoZSByYXcgb3Igc2xpY2VkIGRhdGEuCiMjIGRlZiBleHRyYWN0X2NlbGxwb3NlKGNlbGxwb3NlX3Jlc3VsdCk6CgoKCgoKIyMgVGhlIGZvbGxvd2luZyBpcyBmcm9tIGEgbWl4IG9mIGEgY291cGxlIG9mIGltcGxlbWVudGF0aW9ucyBJIGZvdW5kOgojIyBodHRwczovL3B5aW1hZ2VqLnJlYWR0aGVkb2NzLmlvL2VuL2xhdGVzdC9DbGFzc2ljLVNlZ21lbnRhdGlvbi5odG1sCiMjIGFuIGFsdGVybmF0aXZlIG1ldGhvZCBtYXkgYmUgdGFrZW4gZnJvbToKIyMgaHR0cHM6Ly9weWltYWdlai5yZWFkdGhlZG9jcy5pby9lbi9sYXRlc3QvQ2xhc3NpYy1TZWdtZW50YXRpb24uaHRtbCNzZWdtZW50YXRpb24td29ya2Zsb3ctd2l0aC1pbWFnZWoyCiMjIE15IGdvYWwgaXMgdG8gcGFzcyB0aGUgUk9JIHJlZ2lvbnMgdG8gdGhpcyBmdW5jdGlvbiBhbmQgY3JlYXRlIGEgc2ltaWxhciBkZi4KZGVmIHNsaWNlc190b19yb2lfbWVhc3VyZW1lbnRzKGNlbGxwb3NlX3Jlc3VsdCwgY29sbGFwc2VkID0gRmFsc2UpOgogICAgIiIiIFJlYWQgdGhlIHRleHQgY2VsbHBvc2Ugb3V0cHV0IGZpbGVzLCBnZW5lcmF0ZSBST0lzLCBhbmQgbWVhc3VyZS4KCiAgICBJIHRoaW5rIHRoZXJlIGFyZSBiZXR0ZXIgd2F5cyBvZiBhY2NvbXBsaXNoaW5nIHRoaXMgdGFzayB0aGFuCiAgICB1c2luZyBpai5JSi5ydW4oKTsgYnV0IHRoaXMgc2VlbXMgdG8gd29yay4uLiAgVXBvbiBjb21wbGV0aW9uLAogICAgdGhpcyBmdW5jdGlvbiBzaG91bGQgYWRkIGEgc2VyaWVzIG9mIGRhdGFmcmFtZXMgdG8gdGhlCiAgICBjZWxscG9zZV9yZXN1bHQgZGljdGlvbmFyeSB3aGljaCBjb21wcmlzZSB0aGUgdmFyaW91cyBtZXRyaWNzIGZyb20KICAgIEltYWdlSidzIG1lYXN1cmVtZW50IGZ1bmN0aW9uIG9mIHRoZSBST0lzIGRldGVjdGVkIGJ5IGNlbGxwb3NlLgogICAgIiIiCiAgICBvdXRwdXRfZGljdCA9IGNlbGxwb3NlX3Jlc3VsdAogICAgY2VsbHBvc2Vfc2xpY2VzID0gbGlzdChjZWxscG9zZV9yZXN1bHQua2V5cygpKQogICAgc2xpY2VfbnVtYmVyID0gMAogICAgZm9yIHNsaWNlX25hbWUgaW4gY2VsbHBvc2Vfc2xpY2VzOgogICAgICAgIG91dHB1dF9kaWN0W3NsaWNlX25hbWVdWydzbGljZV9udW1iZXInXSA9IHNsaWNlX251bWJlcgogICAgICAgIGlucHV0X3RpZiA9ICcnCiAgICAgICAgaWYgY29sbGFwc2VkOgogICAgICAgICAgICBpbnB1dF90aWYgPSBjZWxscG9zZV9yZXN1bHRbc2xpY2VfbmFtZV1bJ2NvbGxhcHNlZF9maWxlJ10KICAgICAgICBlbHNlOgogICAgICAgICAgICBpbnB1dF90aWYgPSBjZWxscG9zZV9yZXN1bHRbc2xpY2VfbmFtZV1bJ2lucHV0X2ZpbGUnXQogICAgICAgIHNsaWNlX2RhdGFzZXQgPSBpai5pbygpLm9wZW4oaW5wdXRfdGlmKQogICAgICAgIHNsaWNlX2RhdGEgPSBpai5weS50b19pbWFnZXBsdXMoc2xpY2VfZGF0YXNldCkKICAgICAgICBpbnB1dF90eHQgPSBjZWxscG9zZV9yZXN1bHRbc2xpY2VfbmFtZV1bJ291dHB1dF90eHQnXQogICAgICAgIGlucHV0X21hc2sgPSBjZWxscG9zZV9yZXN1bHRbc2xpY2VfbmFtZV1bJ291dHB1dF9tYXNrJ10KICAgICAgICBpZiB2ZXJib3NlOgogICAgICAgICAgICBwcmludChmIlByb2Nlc3NpbmcgY2VsbHBvc2Ugb3V0bGluZToge2lucHV0X3R4dH0iKQogICAgICAgICAgICBwcmludChmIk1lYXN1cmluZzoge2lucHV0X3RpZn0iKQogICAgICAgICMgY29udmVydCBEYXRhc2V0IHRvIEltYWdlUGx1cwogICAgICAgIGltcCA9IGlqLnB5LnRvX2ltYWdlcGx1cyhzbGljZV9kYXRhKQogICAgICAgIHJtID0gaWouUm9pTWFuYWdlci5nZXRSb2lNYW5hZ2VyKCkKICAgICAgICBybS5ydW5Db21tYW5kKCJBc3NvY2lhdGVkIiwgInRydWUiKQogICAgICAgIHJtLnJ1bkNvbW1hbmQoInNob3cgQWxsIHdpdGggbGFiZWxzIikKICAgICAgICAjIyBUaGUgbG9naWMgZm9yIHRoaXMgd2FzIHRha2VuIGZyb206CiAgICAgICAgIyMgaHR0cHM6Ly9zdGFja292ZXJmbG93LmNvbS9xdWVzdGlvbnMvNzM4NDk0MTgvaXMtdGhlcmUtYW55LXdheS10by1zd2l0Y2gtaW1hZ2VqLW1hY3JvLWNvZGUtdG8tcHl0aG9uMy1jb2RlCiAgICAgICAgdHh0X2ZoID0gb3BlbihpbnB1dF90eHQsICdyJykKICAgICAgICBzZXRfc3RyaW5nID0gZidTZXQgTWVhc3VyZW1lbnRzLi4uJwogICAgICAgIG1lYXN1cmVfc3RyaW5nID0gZidhcmVhIG1lYW4gbWluIGNlbnRyb2lkIG1lZGlhbiBza2V3bmVzcyBrdXJ0b3NpcyBpbnRlZ3JhdGVkIHN0YWNrIHJlZGlyZWN0PU5vbmUgZGVjaW1hbD0zJwogICAgICAgIGlqLklKLnJ1bihzZXRfc3RyaW5nLCBtZWFzdXJlX3N0cmluZykKICAgICAgICByb2lfc3RhdHMgPSBkZWZhdWx0ZGljdChsaXN0KQogICAgICAgIGZvciBsaW5lIGluIHR4dF9maDoKICAgICAgICAgICAgeHkgPSBsaW5lLnJzdHJpcCgpLnNwbGl0KCIsIikKICAgICAgICAgICAgeHlfY29vcmRzID0gW2ludChlbGVtZW50KSBmb3IgZWxlbWVudCBpbiB4eSBpZiBlbGVtZW50IG5vdCBpbiAnJ10KICAgICAgICAgICAgeF9jb29yZHMgPSBbaW50KGVsZW1lbnQpIGZvciBlbGVtZW50IGluIHh5Wzo6Ml0gaWYgZWxlbWVudCBub3QgaW4gJyddCiAgICAgICAgICAgIHlfY29vcmRzID0gW2ludChlbGVtZW50KSBmb3IgZWxlbWVudCBpbiB4eVsxOjoyXSBpZiBlbGVtZW50IG5vdCBpbiAnJ10KICAgICAgICAgICAgeGNvb3Jkc19qaW50ID0gSkFycmF5KEpJbnQpKHhfY29vcmRzKQogICAgICAgICAgICB5Y29vcmRzX2ppbnQgPSBKQXJyYXkoSkludCkoeV9jb29yZHMpCiAgICAgICAgICAgIHBvbHlnb25fcm9pX2luc3RhbmNlID0gc2N5amF2YS5qaW1wb3J0KCdpai5ndWkuUG9seWdvblJvaScpCiAgICAgICAgICAgIHJvaV9pbnN0YW5jZSA9IHNjeWphdmEuamltcG9ydCgnaWouZ3VpLlJvaScpCiAgICAgICAgICAgIGltcG9ydGVkX3BvbHlnb24gPSBwb2x5Z29uX3JvaV9pbnN0YW5jZSh4Y29vcmRzX2ppbnQsIHljb29yZHNfamludCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxlbih4X2Nvb3JkcyksIGludChyb2lfaW5zdGFuY2UuUE9MWUdPTikpCiAgICAgICAgICAgIGltcC5zZXRSb2koaW1wb3J0ZWRfcG9seWdvbikKICAgICAgICAgICAgcm0uYWRkUm9pKGltcG9ydGVkX3BvbHlnb24pCiAgICAgICAgICAgIGlqLklKLnJ1bihpbXAsICdNZWFzdXJlJywgJycpCiAgICAgICAgcm0ucnVuQ29tbWFuZCgnVXBkYXRlJykKICAgICAgICBzbGljZV9yZXN1bHQgPSBpai5SZXN1bHRzVGFibGUuZ2V0UmVzdWx0c1RhYmxlKCkKICAgICAgICBzbGljZV90YWJsZSA9IGlqLmNvbnZlcnQoKS5jb252ZXJ0KHNsaWNlX3Jlc3VsdCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNjeWphdmEuamltcG9ydCgnb3JnLnNjaWphdmEudGFibGUuVGFibGUnKSkKICAgICAgICBzbGljZV9tZWFzdXJlbWVudHMgPSBpai5weS5mcm9tX2phdmEoc2xpY2VfdGFibGUpCiAgICAgICAgb3V0cHV0X2RpY3Rbc2xpY2VfbmFtZV1bJ21lYXN1cmVtZW50cyddID0gc2xpY2VfbWVhc3VyZW1lbnRzCiAgICAgICAgaWouSUoucnVuKCdDbGVhciBSZXN1bHRzJykKICAgICAgICB0eHRfZmguY2xvc2UoKQogICAgICAgIGltcC5zZXRPdmVybGF5KG92KQogICAgICAgIGltcC5nZXRQcm9jZXNzb3IoKS5yZXNldE1pbkFuZE1heCgpCiAgICAgICAgc2xpY2VfbnVtYmVyID0gc2xpY2VfbnVtYmVyICsgMQogICAgcmV0dXJuIG91dHB1dF9kaWN0CmBgYAoKIyBDb252ZXJ0IHRoZSBzbGljZSBtZWFzdXJlbWVudHMgdG8gcGFuZGFzIGRmCgpzbGljZXNfdG9fcm9pX21lYXN1cmVtZW50cygpIHJldHVybnMgYSBkaWN0aW9uYXJ5IHdpdGgga2V5cyB3aGljaCBhcmUKdGhlIGZpbGVuYW1lcyBvZiBlYWNoIHJhdyB0aWYgZmlsZS4gIEVhY2ggZWxlbWVudCBvZiB0aGF0IGRpY3Rpb25hcnkKaXMgaW4gdHVybiBhIGRpY3Rpb25hcnkgY29udGFpbmluZyBzb21lIGluZm9ybWF0aW9uIGFib3V0IHRoZSBmaWxlcwphbG9uZyB3aXRoIGEgZGYgb2YgdGhlIG1lYXN1cmVtZW50cyBwcm92aWRlZCBieSBpbWFnZWouCgpNeSBsaXR0bGUgZ2VvcGFuZGFzIGZ1bmN0aW9uIGFzc3VtZXMgYSBzaW5nbGUgbG9uZyBkZiB3aXRoIHNvbWUKY29sdW1ucyB3aGljaCB0ZWxsIGl0IHdoaWNoIHRpbWVwb2ludC4gIFNvIGxldHMgbWFrZSBhIHF1aWNrIGZ1bmN0aW9uCnRvIGdpdmUgdGhhdCBoZXJlLiAgT1RPSCBpdCBtYXkgYmUgd2lzZXIvYmV0dGVyIHRvIG1ha2Ugc29tZSBjaGFuZ2VzCnRvIHNsaWNlc190b19yb2lfbWVhc3VyZW1lbnRzKCkgc28gdGhhdCBpdCByZXR1cm5zIHRoYXQgZm9ybWF0IGRmOyBidXQKc2luY2UgSSBhbSB1c2luZyB0aGlzIGFzIGEgbGVhcm5pbmcgZXhwZXJpZW5jZSB0byBnZXQgbW9yZSBjb21mb3J0YWJsZQp3aXRoIHB5dGhvbiBkYXRhIHN0cnVjdHVyZXMsIEkgd2lsbCBub3QgZG8gaXQgdGhhdCB3YXkuCgpgYGB7cHl0aG9uIGNvbnZlcnRfc2xpY2VzX3RvX3BhbmRhc30KZGVmIGNvbnZlcnRfc2xpY2VzX3RvX3BhbmRhcyhzbGljZXMpOgogICAgIiIiIER1bXAgdGhlIGNlbGxwb3NlX3Jlc3VsdCBzbGljZSBkYXRhIHRvIGEgc2luZ2xlIGRmLgoKICAgIFRoZXJlIGlzIG5vIGdvb2QgcmVhc29uIGZvciBtZSB0byBzdG9yZSB0aGUgZGF0YSBhcyBhIHNlcmllcyBvZgogICAgZGF0YWZyYW1lcyB3aXRoaW4gYSBkaWN0aW9uYXJ5IGV4Y2VwdCBJIHdhbnQgdG8gZ2V0IG1vcmUKICAgIGNvbWZvcnRhYmxlIHdpdGggcHl0aG9uIGRhdGFzdHJ1Y3R1cmVzLiAgVGh1cywgdGhpcyBmdW5jdGlvbgogICAgc2hvdWxkIGJlIGV4dHJhbmVvdXMsIGJ1dCBzZXJ2ZXMgYXMgYSB3YXkgdG8gZ28gZnJvbSBteSBoYXNoIHRvIGEKICAgIHNpbmdsZSBkZi4KICAgICIiIgogICAgY29uY2F0ZW5hdGVkID0gcGFuZGFzLkRhdGFGcmFtZSgpCiAgICBzbGljZV9rZXlzID0gbGlzdChzbGljZXMua2V5cygpKQogICAgc2xpY2VfY291bnRlciA9IDAKICAgIGZvciBrIGluIHNsaWNlX2tleXM6CiAgICAgICAgc2xpY2VfY291bnRlciA9IHNsaWNlX2NvdW50ZXIgKyAxCiAgICAgICAgY3VycmVudF9zbGljZSA9IHNsaWNlc1trXQogICAgICAgIGlmIHZlcmJvc2U6CiAgICAgICAgICAgIHByaW50KGYiVGhlIHNsaWNlIGlzIHtrfSIpCiAgICAgICAgc2xpY2VfbnVtYmVyID0gY3VycmVudF9zbGljZVsnc2xpY2VfbnVtYmVyJ10KICAgICAgICBzbGljZV9kYXRhID0gY3VycmVudF9zbGljZVsnbWVhc3VyZW1lbnRzJ10KICAgICAgICBzbGljZV9kYXRhWydGcmFtZSddID0gc2xpY2VfbnVtYmVyCiAgICAgICAgaWYgKHNsaWNlX2NvdW50ZXIgPT0gMSk6CiAgICAgICAgICAgIGNvbmNhdGVuYXRlZCA9IHNsaWNlX2RhdGEKICAgICAgICBlbHNlOgogICAgICAgICAgICBjb25jYXRlbmF0ZWQgPSBwYW5kYXMuY29uY2F0KFtjb25jYXRlbmF0ZWQsIHNsaWNlX2RhdGFdKQogICAgIyMgVGhpcyBpcyBhIGxpdHRsZSBzaWxseSwgYnV0IEkgY291bGRuJ3QgcmVtZW1iZXIgdGhhdCB0aGUgaW5kZXggYXR0cmlidXRlCiAgICAjIyBpcyB0aGUgbnVtZXJpYyByb3duYW1lIGZvciBhIG1vbWVudAogICAgIyMgVGhlIHJlc2V0X2luZGV4KCkgZG9lcyB3aGF0IGl0IHNheXMgb24gdGhlIHRpbmUsIGFuZCBjaGFuZ2VzIHRoZSAxOjE5LCAxOjIwLCBldGMKICAgICMjIG9mIGVhY2ggaW5kaXZpZHVhbCB0aW1lIEZyYW1lIHRvIGEgc2luZ2xlIHJhbmdlIG9mIDE6MjAwMAogICAgY29uY2F0ZW5hdGVkLmluZGV4ID0gY29uY2F0ZW5hdGVkLnJlc2V0X2luZGV4KCkuaW5kZXgKICAgIHJldHVybiBjb25jYXRlbmF0ZWQKYGBgCgojIENyZWF0ZSBjZWxsIGdyb3VwcwoKYGBge3B5dGhvbiBuZWFyZXN0X25laWdoYm9yX2Z1bmN0aW9ufQpkZWYgbmVhcmVzdF9jZWxsc19vdmVyX3RpbWUoZGYsIG1heF9kaXN0ID0gMTAuMCwgbWF4X3Byb3AgPSAwLjcsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB4X2NvbHVtbiA9ICdYJywgeV9jb2x1bW4gPSAnWScsIHZlcmJvc2UgPSBUcnVlKToKICAgICIiIlRyYWNlIGNlbGxzIG92ZXIgdGltZQoKICAgIElmIEkgdW5kZXJzdGFuZCBKYWNxdWVzJyBnb2FscyBjb3JyZWN0bHksIHRoZSB0cmFjaW5nIG9mIGNlbGxzCiAgICBvdmVyIHRpbWUgc2hvdWxkIGJlIGEgcmVhc29uYWJseSB0cmFjdGFibGUgcHJvYmxlbSBmb3IgdGhlIHZhcmlvdXMKICAgIGdlby1zdGF0aXN0aWNzIHRvb2xzIHRvIGhhbmRsZTsgdGhlaXIgd2hvbGUgcHVycG9zZSBpcyB0bwogICAgY2FsY3VsYXRlIG4tZGltZW5zaW9uYWwgZGlzdGFuY2VzLiAgU28sIGxldCB1cyBwYXNzIG15IGRmIHRvIG9uZQogICAgb2YgdGhlbSBhbmQgc2VlIHdoYXQgaGFwcGVucyEKCiAgICBVcG9uIGNvbXBsZXRpb24sIHdlIHNob3VsZCBnZXQgYW4gYXJyYXkoZGljdGlvbmFyeT8gSSBmb3JnZXQpIG9mCiAgICBhcnJheXMgd2hlcmUgZWFjaCBwcmltYXJ5IGtleSBpcyB0aGUgdG9wLWxldmVsIGNlbGwgSUQuICBFYWNoCiAgICBpbnRlcm5hbCBhcnJheSBpcyB0aGUgc2V0IG9mIElEcyBmcm9tIHRoZSBnZW9wYW5kYXMgZGF0YWZyYW1lLAogICAgd2hpY2ggY29udGFpbnMgYWxsIG9mIHRoZSBtZWFzdXJlbWVudHMuICBUaHVzLCB3ZSBjYW4gZWFzaWx5CiAgICBleHRyYWN0IHRoZSBkYXRhIGZvciBpbmRpdmlkdWFsIGNlbGxzIGFuZCBwbGF5IHdpdGggaXQuCiAgICAiIiIKICAgIGdkZiA9IGdlb3BhbmRhcy5HZW9EYXRhRnJhbWUoCiAgICAgICAgZGYsCiAgICAgICAgZ2VvbWV0cnkgPSBnZW9wYW5kYXMucG9pbnRzX2Zyb21feHkoZGZbeF9jb2x1bW5dLCBkZlt5X2NvbHVtbl0pKQoKICAgIGZpbmFsX3RpbWUgPSBnZGYuRnJhbWUubWF4KCkKICAgIHBhaXJ3aXNlX2Rpc3RhbmNlcyA9IFtdCiAgICBmb3Igc3RhcnRfdGltZSBpbiByYW5nZSgxLCBmaW5hbF90aW1lKToKICAgICAgICBpID0gc3RhcnRfdGltZQogICAgICAgIGogPSBpICsgMQogICAgICAgIHRpX2lkeCA9IGdkZi5GcmFtZSA9PSBpCiAgICAgICAgdGpfaWR4ID0gZ2RmLkZyYW1lID09IGoKICAgICAgICBpZiB2ZXJib3NlOgogICAgICAgICAgICBwcmludChmIkdldHRpbmcgZGlzdGFuY2VzIG9mIGRmcyB7aX0gYW5kIHtqfS4iKQogICAgICAgIHRpID0gZ2RmW3RpX2lkeF0KICAgICAgICB0aiA9IGdkZlt0al9pZHhdCiAgICAgICAgdGlfcm93cyA9IHRpLnNoYXBlWzBdCiAgICAgICAgdGpfcm93cyA9IHRqLnNoYXBlWzBdCiAgICAgICAgdGl0aiA9IGdlb3BhbmRhcy5zam9pbl9uZWFyZXN0KHRpLCB0aiwgZGlzdGFuY2VfY29sID0gInBhaXJ3aXNlX2Rpc3QiKQogICAgICAgIHBhaXJ3aXNlX2Rpc3RhbmNlcy5hcHBlbmQodGl0aikKCiAgICBpZF9jb3VudGVyID0gMAogICAgIyMgQ2VsbCBJRHMgcG9pbnRpbmcgdG8gYSBsaXN0IG9mIGNlbGxzCiAgICB0cmFjZWQgPSB7fQogICAgIyMgRW5kcG9pbnRzIHBvaW50aW5nIHRvIHRoZSBjZWxsIElEcwogICAgZW5kcyA9IHt9CiAgICBmb3IgaSBpbiByYW5nZSgwLCBmaW5hbF90aW1lIC0gMSk6CiAgICAgICAgcXVlcnkgPSBwYWlyd2lzZV9kaXN0YW5jZXNbaV0KICAgICAgICBwYXNzZWRfaWR4ID0gcXVlcnkucGFpcndpc2VfZGlzdCA8PSBtYXhfZGlzdAogICAgICAgIGZhaWxlZF9pZHggPSBxdWVyeS5wYWlyd2lzZV9kaXN0ID4gbWF4X2Rpc3QKICAgICAgICBpZiAoZmFpbGVkX2lkeC5zdW0oKSA+IDApOgogICAgICAgICAgICBpZiB2ZXJib3NlOgogICAgICAgICAgICAgICAgcHJpbnQoZiJTa2lwcGVkIHtmYWlsZWRfaWR4LnN1bSgpfSBlbGVtZW50cyBpbiBzZWdtZW50IHtpfS4iKQogICAgICAgIHF1ZXJ5ID0gcXVlcnlbcGFzc2VkX2lkeF0KCiAgICAgICAgcHJvcF9jaGFuZ2UgPSBxdWVyeS5BcmVhX2xlZnQgLyBxdWVyeS5BcmVhX3JpZ2h0CiAgICAgICAgaW5jcmVhc2VkX2lkeCA9IHByb3BfY2hhbmdlID4gMS4wCiAgICAgICAgcHJvcF9jaGFuZ2VbaW5jcmVhc2VkX2lkeF0gPSAxLjAgLyBwcm9wX2NoYW5nZVtpbmNyZWFzZWRfaWR4XQogICAgICAgIGZhaWxlZF9pZHggPSBwcm9wX2NoYW5nZSA8IG1heF9wcm9wCiAgICAgICAgcGFzc2VkX2lkeCA9IHByb3BfY2hhbmdlID49IG1heF9wcm9wCiAgICAgICAgaWYgKGZhaWxlZF9pZHguc3VtKCkgPiAwKToKICAgICAgICAgICAgaWYgdmVyYm9zZToKICAgICAgICAgICAgICAgIHNraXBfc3RyaW5nID0gKGYiU2tpcHBlZCB7ZmFpbGVkX2lkeC5zdW0oKX0gZWxlbWVudHMgaW4gc2VnbWVudCB7aX0gIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGYiYmVjYXVzZSB0aGUgc2l6ZSBjaGFuZ2VkIHRvbyBtdWNoLiIpCiAgICAgICAgICAgICAgICBwcmludChza2lwX3N0cmluZykKICAgICAgICAgICAgcXVlcnkgPSBxdWVyeVtwYXNzZWRfaWR4XQoKICAgICAgICBmb3Igcm93IGluIHF1ZXJ5Lml0ZXJ0dXBsZXMoKToKICAgICAgICAgICAgc3RhcnRfY2VsbCA9IHJvdy5JbmRleAogICAgICAgICAgICBlbmRfY2VsbCA9IHJvdy5pbmRleF9yaWdodAogICAgICAgICAgICBpZiBzdGFydF9jZWxsIGluIGVuZHMua2V5cygpOgogICAgICAgICAgICAgICAgY2VsbF9pZCA9IGVuZHNbc3RhcnRfY2VsbF0KICAgICAgICAgICAgICAgIGN1cnJlbnRfdmFsdWUgPSB0cmFjZWRbY2VsbF9pZF0KICAgICAgICAgICAgICAgIGN1cnJlbnRfdmFsdWUuYXBwZW5kKGVuZF9jZWxsKQogICAgICAgICAgICAgICAgdHJhY2VkW2NlbGxfaWRdID0gY3VycmVudF92YWx1ZQogICAgICAgICAgICAgICAgZW5kc1tlbmRfY2VsbF0gPSBjZWxsX2lkCiAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICBpZF9jb3VudGVyID0gaWRfY291bnRlciArIDEKICAgICAgICAgICAgICAgIHRyYWNlZFtpZF9jb3VudGVyXSA9IFtzdGFydF9jZWxsLCBlbmRfY2VsbF0KICAgICAgICAgICAgICAgIGVuZHNbZW5kX2NlbGxdID0gaWRfY291bnRlcgogICAgcmV0dXJuIHRyYWNlZApgYGAKCiMgUnVuIHRoZSBmdW5jdGlvbnMgYW5kIHBsb3QgdGhlIHJlc3VsdHMKCiMjIFNlcGFyYXRlIHNsaWNlcwoKTm90ZSB0byBzZWxmLCBKYWNxdWVzJyBuZXcgZGF0YXNldCB1c2VzIHdhbnRlZF96ID09IDIsIHdhbnRlZF9jaGFubmVsID09IDMuCgpgYGB7cHl0aG9uIHJ1bl9zZXBhcmF0ZV9zbGljZXN9CnJhd19kYXRhc2V0LCBzYXZlZF9zbGljZXMsIHNsaWNlX2RpcmVjdG9yeSA9IHNlcGFyYXRlX3NsaWNlcyhpbnB1dF9maWxlLCB3YW50ZWRfeiA9IDIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB3YW50ZWRfY2hhbm5lbCA9IDMpCmBgYAoKIyMgSW52b2tlIGNlbGxwb3NlCgpgYGB7cHl0aG9uIHJ1bl9pbnZva2VfY2VsbHBvc2V9CmNlbGxwb3NlX3Jlc3VsdCA9IGludm9rZV9jZWxscG9zZShzbGljZV9kaXJlY3RvcnksICdtb2RlbHMvQ1BfMjAyMjA1MjNfMTA0MDE2JykKYGBgCgojIyBjb2xsYXBzZSBaCgpgYGB7cHl0aG9uIHJ1bl9jb2xsYXBzZV96fQpjZWxscG9zZV9yZXN1bHQgPSBjb2xsYXBzZV96KHJhd19kYXRhc2V0LCBjZWxscG9zZV9yZXN1bHQpCmBgYAoKIyMgTWVhc3VyZSBST0lzCgpgYGB7cHl0aG9uIGludm9rZV9zbGljZV90b19yb2l9CnNsaWNlX21lYXN1cmVtZW50cyA9IHNsaWNlc190b19yb2lfbWVhc3VyZW1lbnRzKGNlbGxwb3NlX3Jlc3VsdCwgY29sbGFwc2VkID0gVHJ1ZSkKYGBgCgojIyBDb252ZXJ0IHRvIHBhbmRhcwoKYGBge3B5dGhvbiBpbnZva2VfY29udmVydF9wYW5kYXN9CmNvbmNhdGVuYXRlZCA9IGNvbnZlcnRfc2xpY2VzX3RvX3BhbmRhcyhzbGljZV9tZWFzdXJlbWVudHMpCmBgYAoKIyMgRmluZCBuZWFyZXN0CgpgYGB7cHl0aG9uIGludm9rZV9uZWFyZXN0X2NlbGxzfQpuZWFyZXN0ID0gbmVhcmVzdF9jZWxsc19vdmVyX3RpbWUoY29uY2F0ZW5hdGVkLCBtYXhfZGlzdCA9IDEwLjAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB4X2NvbHVtbiA9ICdYJywgeV9jb2x1bW4gPSAnWScpCmBgYAoKIyMgR2V0IGluZm9ybWF0aW9uIGZyb20gYSBncm91cCBvZiBjZWxscwoKQXMgYSBmaW5hbCBzdGVwLCB3ZSBzaG91bGQgYmUgYWJsZSB0byBleHRyYWN0IGFuZCBwbGF5IHdpdGggdGhlCmluZm9ybWF0aW9uIGZyb20gb25lIG9yIG1vcmUgZ3JvdXBzIG9mIGNlbGxzLgoKYGBge3B5dGhvbiBnZXRfaW5mb30KY2VsbF9pZCA9IDEyOQpjZWxsX2lkeCA9IG5lYXJlc3RbY2VsbF9pZF0KY2VsbF9kYXRhID0gY29uY2F0ZW5hdGVkLmxvY1tjZWxsX2lkeF0KbGVuKGNlbGxfZGF0YSkKY2VsbF9kYXRhID0gY2VsbF9kYXRhLnJlc2V0X2luZGV4KCkKCnNjYXR0ZXIgPSBwbHQuc2NhdHRlcihjZWxsX2RhdGFbJ1gnXSwgY2VsbF9kYXRhWydZJ10pCmZpbmFsX3JvdyA9IGNlbGxfZGF0YS5pbmRleC5tYXgoKQpmb3Igc3RhcnRfdGltZSBpbiByYW5nZSgwLCBmaW5hbF9yb3cgLSAxKToKICAgIHRpX2lkeCA9IGNlbGxfZGF0YS5pbmRleCA9PSBzdGFydF90aW1lCiAgICB0al9pZHggPSBjZWxsX2RhdGEuaW5kZXggPT0gc3RhcnRfdGltZSArIDEKICAgIHAxeCA9IGNlbGxfZGF0YVt0aV9pZHhdLlgKICAgIHAyeCA9IGNlbGxfZGF0YVt0al9pZHhdLlgKICAgIHAxeSA9IGNlbGxfZGF0YVt0aV9pZHhdLlkKICAgIHAyeSA9IGNlbGxfZGF0YVt0al9pZHhdLlkKICAgIHhfcG9pbnRzID0gW3AxeCwgcDJ4XQogICAgeV9wb2ludHMgPSBbcDF5LCBwMnldCiAgICBwbHQucGxvdCh4X3BvaW50cywgeV9wb2ludHMpCmZpbmFsbTFfaWR4ID0gY2VsbF9kYXRhLmluZGV4ID09IGZpbmFsX3JvdyAtIDEKZmluYWxfaWR4ID0gY2VsbF9kYXRhLmluZGV4ID09IGZpbmFsX3JvdwpmaW5hbG0xX3ggPSBjZWxsX2RhdGFbZmluYWxtMV9pZHhdLlgKZmluYWxfeCA9IGNlbGxfZGF0YVtmaW5hbF9pZHhdLlgKZmluYWxtMV95ID0gY2VsbF9kYXRhW2ZpbmFsbTFfaWR4XS5ZCmZpbmFsX3kgPSBjZWxsX2RhdGFbZmluYWxfaWR4XS5ZCnhfcG9pbnRzID0gW2ZpbmFsbTFfeCwgZmluYWxfeF0KeV9wb2ludHMgPSBbZmluYWxtMV95LCBmaW5hbF95XQpwbHQucGxvdCh4X3BvaW50cywgeV9wb2ludHMpCnBsdC5zaG93KCkKCnNlYWJvcm4udmlvbGlucGxvdChkYXRhID0gY2VsbF9kYXRhLkFyZWEpCnBsdC5zaG93KCkKYGBgCgoKCmBgYHtyIHNhdmVtZX0KcGFuZGVyOjpwYW5kZXIoc2Vzc2lvbkluZm8oKSkKbWVzc2FnZShwYXN0ZTAoIlRoaXMgaXMgaHBnbHRvb2xzIGNvbW1pdDogIiwgZ2V0X2dpdF9jb21taXQoKSkpCnRoaXNfc2F2ZSA8LSBwYXN0ZTAoZ3N1YihwYXR0ZXJuPSJcXC5SbWQiLCByZXBsYWNlPSIiLCB4PXJtZF9maWxlKSwgIi12IiwgdmVyLCAiLnJkYS54eiIpCm1lc3NhZ2UocGFzdGUwKCJTYXZpbmcgdG8gIiwgdGhpc19zYXZlKSkKdG1wIDwtIHNtKHNhdmVtZShmaWxlbmFtZT10aGlzX3NhdmUpKQpgYGAK