From ff3545940df4c178abc87e75bd4db290454fcbe4 Mon Sep 17 00:00:00 2001 From: jpmetzca Date: Wed, 3 Feb 2021 14:19:07 -0500 Subject: [PATCH 01/15] Adding two scripts - cell_track_plotter.py, which given a PhysiCell SVG output produces a still of the cells in the SVG plus a history (track) of previous cell locations based on other SVGs in the directory, and cell_tracker_movie.py, which produces a series of stills (based on PhysiCell SVGs) of cells and their positional history (just like cell_track_plotter.py and composes those stills into a movie. --- analysis/cell_track_plotter.py | 287 +++++++++++++++++++++++++ analysis/cell_tracker_movie.py | 373 +++++++++++++++++++++++++++++++++ 2 files changed, 660 insertions(+) create mode 100644 analysis/cell_track_plotter.py create mode 100644 analysis/cell_tracker_movie.py diff --git a/analysis/cell_track_plotter.py b/analysis/cell_track_plotter.py new file mode 100644 index 0000000..0085ddd --- /dev/null +++ b/analysis/cell_track_plotter.py @@ -0,0 +1,287 @@ +# +# cell_tracker.py - plot 2-D cell tracks associated with PhysiCell .svg files +# +# Usage: +# python cell_track_plotter.py <# of samples to include> " +# python cell_track_plotter.py <# of samples to include> +# +# Dependencies include matplotlib and numpy. We recommend installing the Anaconda Python3 distribution. +# +# Examples (run from directory containing the .svg files): +# See below line 239 in "if __name__ == '__main__':" +# +# Author: Randy Heiland, modified by John Metzcar (Twitter - @jmetzcar) +# +import sys +import xml.etree.ElementTree as ET +import numpy as np +import glob +import matplotlib.pyplot as plt +import math + +def plot_cell_tracks(starting_index: int, sample_step_interval: int, number_of_samples: int, output_plot: bool, + show_plot: bool): + + """ + Produces savable image of cell positional history, plotted as arrows (quiver plot) with final cell positions plotted as a cirle. + Slight modification of the function in cell_track_plotter. The modification allows for tracking the index of a series + of inputs such that outputs of this function can be appropriate indexed and compiled into a movie. + + sample_step_interval * number_of_samples - starting_index yields the trail length in time steps. number_of_samples provides + the number of intervals plotted per image. + + Parameters + ---------- + starting_index : + Integer index of the PhysiCell SVG output to begin trackign at + sample_step_interval : + Interval (number of time steps (SVGs)) to sample at. A value of 2 would add a tracking point for every other SVG + number_of_samples : + Number of SVGs to process (total)/Length of cell positional history. Number of samples * sample size step interval provides the index of the final SVG to process + output_plot : + Save plot flag (required to produce a movie from resulting images) + show_plot : + Show plot flag (for processing many images in a loop, this should likely be set to false. Images have to be closed manually) + naming_index : + Unique to this function. Index used in naming output file of plot_cell_tracks function - filename = output + naming_index.png and leading zeros as needed. + Returns + ------- + Null : + Produces a png image from the input PhysiCell SVGs. + """ + + maxCount = starting_index + + d={} # dictionary to hold all (x,y) positions of cells + + """ + --- for example --- + In [141]: d['cell1599'][0:3] + Out[141]: + array([[ 4900. , 4900. ], + [ 4934.17, 4487.91], + [ 4960.75, 4148.02]]) + """ + + #################################################################################################################### + #################################### Generate list of file indices to load ######################## + #################################################################################################################### + + endpoint = starting_index + sample_step_interval*number_of_samples + file_indices = np.linspace(starting_index, endpoint, num=number_of_samples, endpoint=False) + print(file_indices) + + ####### Uncomment for statement below to generate a random list of file names versus the prespecifed list. ######## + ####### Leaving for historical record. If used, the inputs would need to be a single integer, ######## + ####### versus the three integers required to generate the prespecified list. Also, remove the other for statement. ######## + # count = 0 + # + # for fname in glob.glob('snapshot*.svg'): + # print(fname) + # # for fname in['snapshot00000000.svg', 'snapshot00000001.svg']: + # # for fname in['snapshot00000000.svg']: + # # print(fname) + # count += 1 + # if count > maxCount: + # break + + + #################################################################################################################### + #################################### Main loading and processing loop ######################## + #################################################################################################################### + + for file_index in file_indices: + fname = "%0.8d" % file_index + fname = 'snapshot' + fname + '.svg'# https://realpython.com/python-f-strings/ + print(fname) + + ##### Parse XML tree into a dictionary called 'tree" and get root + # print('\n---- ' + fname + ':') + tree=ET.parse(fname) + + # print('--- root.tag, root.attrib ---') + root=tree.getroot() + # print('--- root.tag ---') + # print(root.tag) + # print('--- root.attrib ---') + # print(root.attrib) + # print('--- child.tag, child.attrib ---') + + numChildren = 0 + + ### Find branches coming from root - tissue parents + for child in root: + # print(child.tag, child.attrib) + # print('attrib=',child.attrib) + # if (child.attrib['id'] == 'tissue'): + + if 'width' in child.attrib.keys(): + #### Assumes square domains + plot_spatial_length = float(child.attrib['width']) + # print(plot_spatial_length) + + ##### Find the tissue tag and make it child + if 'id' in child.attrib.keys(): + # print('-------- found tissue!!') + tissue_parent = child + break + + # print('------ search tissue') + + ### find the branch with the cells "id=cells" among all the branches in the XML root + for child in tissue_parent: + # print('attrib=',child.attrib) + if (child.attrib['id'] == 'cells'): + # print('-------- found cells!!') + cells_parent = child + break + numChildren += 1 + + ### Search within the cells branch for all indiviual cells. Get their locations + num_cells = 0 + # print('------ search cells') + for child in cells_parent: + # print(child.tag, child.attrib) + # print('attrib=',child.attrib) + + # Find the locations of the cells within the cell tags + for circle in child: + # print(' --- cx,cy=',circle.attrib['cx'],circle.attrib['cy']) + xval = float(circle.attrib['cx']) + + # should we test for bogus x,y locations?? + if (math.fabs(xval) > 10000.): + print("xval=",xval) + break + yval = float(circle.attrib['cy']) + if (math.fabs(yval) > 10000.): + print("xval=",xval) + break + + # Pull out the cell's location. If ID not already in stack to track, put in new cell in dictionary + if (child.attrib['id'] in d.keys()): + d[child.attrib['id']] = np.vstack((d[child.attrib['id']], [ float(circle.attrib['cx']), float(circle.attrib['cy']) ])) + else: + d[child.attrib['id']] = np.array( [ float(circle.attrib['cx']), float(circle.attrib['cy']) ]) + break + + # if (child.attrib['id'] == 'cells'): + # print('-------- found cells!!') + # tissue_child = child + + #### num_cells becomes total number of cells per frame/sample + num_cells += 1 + print(fname,': num_cells= ',num_cells) + + #################################################################################################################### + #################################### Plot cell tracks and other options ######################## + #################################################################################################################### + + fig = plt.figure(figsize=(8,8)) + ax = fig.gca() + ax.set_aspect("equal") + #ax.set_xticks([]) + #ax.set_yticks([]); + #ax.set_xlim(0, 8); ax.set_ylim(0, 8) + + #print 'dir(fig)=',dir(fig) + #fig.set_figwidth(8) + #fig.set_figheight(8) + + count = 0 + + # weighting = np.linspace(0.0001, 3.5, num=number_of_samples) + # + # weighting = np.log10(weighting) + + ##### Extract and plot position data for each cell found + for key in d.keys(): + if (len(d[key].shape) == 2): + x = d[key][:,0] + y = d[key][:,1] + + # plt.plot(x, y,'-') # plot doesn't seem to allow weighting or size variation at all in teh connections ... # https://matplotlib.org/api/_as_gen/matplotlib.pyplot.arrow.html or https://stackoverflow.com/questions/7519467/line-plot-with-arrows-in-matplotlib + # plt.scatter(x, y, s = weighting) - scatter allows weighting but doens't connect ... + # plt.scatter(x, y, s=weighting) # could try a non-linear weighting ... + + #### Plot cell track as a directed, weighted (by length) path + plt.quiver(x[:-1], y[:-1], x[1:] - x[:-1], y[1:] - y[:-1], scale_units='xy', angles='xy', scale=1, minlength = 0.001, headwidth=1.5, headlength=4) + + #### Plot final cell position + plt.scatter(x[-1],y[-1], s = 3.5) + + elif (len(d[key].shape) == 1): + x = d[key][0] + y = d[key][1] + + plt.scatter(x, y, s=3.5) + + else: + print(key, " has no x,y points") + + #### Build plot frame, titles, and save data + plt.ylim(0, plot_spatial_length) + plt.xlim(0, plot_spatial_length) + + title_str = "Starting at frame {}, sample interval of {} for {} total samples".format(starting_index, sample_step_interval, number_of_samples) + plt.title(title_str) + + # could change to the custom in the movie output or some other more better output if desired. + output_folder = '' + snapshot = str(starting_index) + '_' + str(sample_step_interval) + '_' + str(number_of_samples) + + # Plot output + if output_plot is True: + plt.savefig(output_folder + snapshot + '.svg') + if show_plot is True: + plt.show() + # plt.close() + +if __name__ == '__main__': + + #################################################################################################################### + #################################### Usage example and input loading ######################## + #################################################################################################################### + + if (len(sys.argv) == 6): + usage_str = "Usage: %s <# of samples to include> " % ( + sys.argv[0]) + # print(usage_str) + starting_index = int(sys.argv[1]) + sample_step_interval = int(sys.argv[2]) + number_of_samples = int(sys.argv[3]) + save_plot = bool(sys.argv[4]) + show_plot = bool(sys.argv[5]) + + # print("e.g.,") + # eg_str = "%s 0 1 10 indicates start at 0, go up by ones, and stop when you 10 samples" % (sys.argv[0]) + # print(eg_str) + + plot_cell_tracks(starting_index, sample_step_interval, number_of_samples, save_plot, show_plot) + + + elif (len(sys.argv) == 4): + usage_str = "Usage: %s <# of samples to include>" % ( + sys.argv[0]) + # print(usage_str) + starting_index = int(sys.argv[1]) + sample_step_interval = int(sys.argv[2]) + number_of_samples = int(sys.argv[3]) + + # print("e.g.,") + # eg_str = "%s 0 1 10 indicates start at 0, go up by ones, and stop when you 10 samples" % (sys.argv[0]) + # print(eg_str) + + plot_cell_tracks(starting_index, sample_step_interval, number_of_samples, False, True) + + else: + print('\nInput 3 arguments to produce and show plot only') + usage_str = "Usage: %s <# of samples to include> \n" % ( + sys.argv[0]) + print(usage_str) + print('Input 5 arguments to directly control saving and showing the plots') + usage_str = "Usage: %s <# of samples to include> \n" % ( + sys.argv[0]) + print(usage_str) + exit(1) + diff --git a/analysis/cell_tracker_movie.py b/analysis/cell_tracker_movie.py new file mode 100644 index 0000000..b08b80e --- /dev/null +++ b/analysis/cell_tracker_movie.py @@ -0,0 +1,373 @@ +import sys +import xml.etree.ElementTree as ET +import numpy as np +import glob +import matplotlib.pyplot as plt +import math, os, sys, re +import distutils.util + +def plot_cell_tracks_for_movie(starting_index: int, sample_step_interval: int, number_of_samples: int, output_plot: bool, + show_plot: bool, naming_index: int): + """ + Produces savable image of cell positional history, plotted as arrows (quiver plot) with final cell positions plotted as a cirle. + Slight modification of the function in cell_track_plotter. The modification allows for tracking the index of a series + of inputs such that outputs of this function can be appropriate indexed and compiled into a movie. + + sample_step_interval * number_of_samples - starting_index yields the trail length in time steps. number_of_samples provides + the number of intervals plotted per image. + + Parameters + ---------- + starting_index : + Integer index of the PhysiCell SVG output to begin trackign at + sample_step_interval : + Interval (number of time steps (SVGs)) to sample at. A value of 2 would add a tracking point for every other SVG. For this special + function, it is currently (01.27.21) assumed that will be 1. + number_of_samples : + Number of SVGs to process (total)/Length of cell positional history. Number of samples * sample size step interval provides the index of the final SVG to process + output_plot : + Save plot flag (required to produce a movie from resulting images) + show_plot : + Show plot flag (for processing many images in a loop, this should likely be set to false. Images have to be closed manually) + naming_index : + Unique to this function. Index used in naming output file of plot_cell_tracks function - filename = output + naming_index.png and leading zeros as needed. + Returns + ------- + Null : + Produces a png image from the input PhysiCell SVGs. + """ + + d = {} # dictionary to hold all (x,y) positions of cells + + """ + --- for example --- + In [141]: d['cell1599'][0:3] + Out[141]: + array([[ 4900. , 4900. ], + [ 4934.17, 4487.91], + [ 4960.75, 4148.02]]) + """ + + #################################################################################################################### + #################################### Generate list of file indices to load ######################## + #################################################################################################################### + + endpoint = starting_index + sample_step_interval * number_of_samples + file_indices = np.linspace(starting_index, endpoint, num=number_of_samples, endpoint=False) + print(file_indices) + + #################################################################################################################### + #################################### Main loading and processing loop ######################## + #################################################################################################################### + + for file_index in file_indices: + fname = "%0.8d" % file_index + fname = 'snapshot' + fname + '.svg' # https://realpython.com/python-f-strings/ + # print(fname) + + ##### Parse XML tree into a dictionary called 'tree" and get root + # print('\n---- ' + fname + ':') + tree = ET.parse(fname) + + # print('--- root.tag, root.attrib ---') + root = tree.getroot() + # print('--- root.tag ---') + # print(root.tag) + # print('--- root.attrib ---') + # print(root.attrib) + # print('--- child.tag, child.attrib ---') + + numChildren = 0 + + ### Find branches coming from root - tissue parents + for child in root: + # print(child.tag, child.attrib) + # print('attrib=',child.attrib) + # if (child.attrib['id'] == 'tissue'): + ##### Find the tissue tag and make it child + + if ('width' in child.attrib.keys()): + #### Assumes square domains + plot_spatial_length = float(child.attrib['width']) + # print(plot_spatial_length) + + if ('id' in child.attrib.keys()): + # print('-------- found tissue!!') + tissue_parent = child + break + + # print('------ search tissue') + + ### find the branch with the cells "id=cells" among all the branches in the XML root + for child in tissue_parent: + # print('attrib=',child.attrib) + if (child.attrib['id'] == 'cells'): + # print('-------- found cells!!') + cells_parent = child + break + numChildren += 1 + + ### Search within the cells branch for all indiviual cells. Get their locations + num_cells = 0 + # print('------ search cells') + for child in cells_parent: + # print(child.tag, child.attrib) + # print('attrib=',child.attrib) + + # Find the locations of the cells within the cell tags + for circle in child: + # print(' --- cx,cy=',circle.attrib['cx'],circle.attrib['cy']) + xval = float(circle.attrib['cx']) + + # should we test for bogus x,y locations?? + if (math.fabs(xval) > 10000.): + print("xval=", xval) + break + yval = float(circle.attrib['cy']) + if (math.fabs(yval) > 10000.): + print("xval=", xval) + break + + # Pull out the cell's location. If ID not already in stack to track, put in new cell in dictionary + if (child.attrib['id'] in d.keys()): + d[child.attrib['id']] = np.vstack( + (d[child.attrib['id']], [float(circle.attrib['cx']), float(circle.attrib['cy'])])) + else: + d[child.attrib['id']] = np.array([float(circle.attrib['cx']), float(circle.attrib['cy'])]) + break + + # if (child.attrib['id'] == 'cells'): + # print('-------- found cells!!') + # tissue_child = child + + #### num_cells becomes total number of cells per frame/sample + num_cells += 1 + print(fname, ': num_cells= ', num_cells) + + #################################################################################################################### + #################################### Plot cell tracks and other options ######################## + #################################################################################################################### + + fig = plt.figure(figsize=(8, 8)) + ax = fig.gca() + ax.set_aspect("equal") + # ax.set_xticks([]) + # ax.set_yticks([]); + # ax.set_xlim(0, 8); ax.set_ylim(0, 8) + + # print 'dir(fig)=',dir(fig) + # fig.set_figwidth(8) + # fig.set_figheight(8) + + count = 0 + + # weighting = np.linspace(0.0001, 3.5, num=number_of_samples) + # + # weighting = np.log10(weighting) + + ##### Extract and plot position data for each cell found + for key in d.keys(): + if (len(d[key].shape) == 2): + x = d[key][:, 0] + y = d[key][:, 1] + + # plt.plot(x, y,'-') # plot doesn't seem to allow weighting or size variation at all in teh connections ... # https://matplotlib.org/api/_as_gen/matplotlib.pyplot.arrow.html or https://stackoverflow.com/questions/7519467/line-plot-with-arrows-in-matplotlib + # plt.scatter(x, y, s = weighting) - scatter allows weighting but doens't connect ... + # plt.scatter(x, y, s=weighting) # could try a non-linear weighting ... + + #### Plot cell track as a directed, weighted (by length) path + plt.quiver(x[:-1], y[:-1], x[1:] - x[:-1], y[1:] - y[:-1], scale_units='xy', angles='xy', scale=1, + minlength=0.001, headwidth=1.5, headlength=4) + + #### Plot final cell position + plt.scatter(x[-1], y[-1], s=3.5) + + elif (len(d[key].shape) == 1): + x = d[key][0] + y = d[key][1] + + plt.scatter(x, y, s=3.5) + + else: + print(key, " has no x,y points") + + #### Build plot frame, titles, and save data + + + plt.ylim(0, plot_spatial_length) + plt.xlim(0, plot_spatial_length) + + title_str = "Starting at frame {}, sample interval of {} for {} total samples".format(starting_index, + sample_step_interval, + number_of_samples) + plt.title(title_str) + + output_folder = '' + snapshot = str(starting_index) + '_' + str(sample_step_interval) + '_' + str(number_of_samples) + snapshot = 'output' + f'{naming_index:08}' + + #### Flags for output + # output_plot = True + # show_plot = True + + # Plot output + if output_plot is True: + plt.savefig(output_folder + snapshot + '.png') + if show_plot is True: + plt.show() + plt.close() # cell_tracker_movie.py:151: RuntimeWarning: More than 20 figures have been opened. Figures created through the pyplot interface (`matplotlib.pyplot.figure`) are retained until explicitly closed and may consume too much memory. (To control this warning, see the rcParam `figure.max_open_warning`). + + +def create_tracks_movie(data_path: str, save_path: str, save_name: str, start_file_index: int, end_file_index: int, + trail_length: int, INCLUDE_ALL_SVGs: bool, INCLUDE_FULL_HISTORY: bool): + + """ + Generates the list of files in data_path, finds the relevant SVGs, makes plots from them, then outputs an + ffmpeg generated movie to save_path, naming the movie save_name. + + This function requires ffmpeg be installed at the command line. + + + :param data_path: Path to directory containing data + :param save_path: Path to save generated image(s) and movie to + :param save_name: Save name for movie + :param start_file_index: For the plotting call - Integer index of the PhysiCell SVG output to begin tracking at + :param end_file_index: For the plotting call - Integer index of last PhysiCell SVG output to include in movie + :param trail_length: For the plotting call - Length (in output steps) of cell positional history to include in movie + :param INCLUDE_ALL_SVGs: If true, all findable PhysiCell SVGs are processed and included in movie + :param INCLUDE_FULL_HISTORY: If true, the entire available cell history is included, regardless of the value of trail length. + :return: Null. Produces a series of images from PhysiCell SVGs and movie from said images. + """ + + #### Get list of all file names in directory + files = os.listdir(data_path) + + list_of_svgs = [] + + #### examine all file names in directory and add ones, via string matching, as needed to list of names of files of interest + for i in range(len(files)): + if not re.search('snapshot(.*)\.svg', files[i]): + continue + + # I feel like a dictionary could be used here, but I really need some ordering. A dict might be faster, but I don't + # expect huge file lists. So I will just sort as I know how to do that ... + + list_of_svgs.append(files[i]) + + #### Sort file name list + list_of_svgs.sort() + + truncated_list_of_svgs = [] + + #### Reduce file list to times of interst only + for i in range(len(list_of_svgs)): + + if i < start_file_index: + continue + + if i >= end_file_index: + continue + + truncated_list_of_svgs.append(list_of_svgs[i]) + + # print(list_of_svgs) + print(truncated_list_of_svgs) + + if INCLUDE_ALL_SVGs: + print('Including all SVGs') + truncated_list_of_svgs = list_of_svgs + + max_number_of_samples = trail_length + + if INCLUDE_FULL_HISTORY: + print('Including full positional history of cells') + max_number_of_samples = len(truncated_list_of_svgs) + + print('Processing {} SVGs'.format(len(truncated_list_of_svgs))) + + # Also, as written it isn't very flexible + # would certainly be ideal to not call plot_cell_tracks every time, but instead store what is available. Could add a function that just + # extracts the data from one SVG then appends it to exsisting data structure. could read all the desired data into Pandas DF + # then write out images. Etc. But as is, this is definitely reading the SVGs much to frequently. + + for i in range(len(truncated_list_of_svgs)): + j = i + 1 # this offsets the index so that we don't report that 0 samples have been taken, while stil producing an image. + starting_index = j - max_number_of_samples + + #### Goes with "trail closing" block - not currently being used. + projected_upper_sample_index = max_number_of_samples + starting_index + max_samples_left = len(truncated_list_of_svgs) - j + + if i >= max_number_of_samples: + plot_cell_tracks_for_movie(starting_index, 1, max_number_of_samples, True, False, i) + # print('middle') + + #### If one wanted to make the trails collapse into the last available location of the cell you would use something + #### like this elif block + # elif projected_upper_sample_index > len(list_of_svgs)-1: + # plot_cell_tracks(starting_index, 1, max_samples_left, True, True, i) + # print(max_samples_left) + # print('late') + else: + plot_cell_tracks_for_movie(0, 1, j, True, False, i) + # print('early') + + #### Total frames to include in moview + number_frames = end_file_index - start_file_index + + if INCLUDE_ALL_SVGs: + number_frames = len(list_of_svgs) + start_file_index = 0 + + # string_of_interest = 'ffmpeg -start_number ' + str( + # start_file_index) + ' -y -framerate 12 -i ' + save_path + 'output%08d.png' + ' -frames:v ' + str( + # number_frames) + ' -pix_fmt yuv420p -vf pad="width=ceil(iw/2)*2:height=ceil(ih/2)*2" "' + save_name + '.mp4"' + # print(string_of_interest) + os.system( + 'ffmpeg -start_number ' + str( + start_file_index) + ' -y -framerate 12 -i ' + save_path + 'output%08d.png' + ' -frames:v ' + str( + number_frames) + ' -pix_fmt yuv420p -vf pad="width=ceil(iw/2)*2:height=ceil(ih/2)*2" "' + save_name + '.mp4"') + + # https://superuser.com/questions/666860/clarification-for-ffmpeg-input-option-with-image-files-as-input + # https://superuser.com/questions/734976/ffmpeg-limit-number-of-images-converted-to-video + + +if __name__ == '__main__': + # Execute only if run as a script + + if len(sys.argv) == 1: + # Running with no arguments will make the script run every SVG with not stop to trail length + + create_tracks_movie('.', '', 'cell_tracks', 0, 10, 1, True, True) + + elif len(sys.argv) == 2: + # Running with 1 argument sets the movie name and nothign else + + movie_name = sys.argv[1] + create_tracks_movie('.', '', movie_name, 0, 10, 1, True, True) + + elif len(sys.argv) == 7: + starting_file_index = int(sys.argv[1]) + end_file_index = int(sys.argv[2]) + cell_trail_length = int(sys.argv[3]) # length in time steps + movie_name = sys.argv[4] + INCLUDE_ALL_SVGs = bool(distutils.util.strtobool(sys.argv[5]))# bool(sys.argv[5]) + INCLUDE_FULL_HISTORY = bool(distutils.util.strtobool(sys.argv[6])) # bool(sys.argv[6]) + + create_tracks_movie('.', '', movie_name, starting_file_index, end_file_index, cell_trail_length, INCLUDE_ALL_SVGs, INCLUDE_FULL_HISTORY) + + else: + print('\nInput 0 arguments to process every available full and include full history and out put movie with ' + 'default name of cell_tracks.mp4') + usage_str = "Usage: %s \n" % (sys.argv[0]) + print(usage_str) + print('Input 1 argument (a string) to set movie name and process all files and full history') + usage_str = "Usage: %s this_is_great_data\n" % (sys.argv[0]) + print(usage_str) + + print('Input 6 arguments to gain the most control') + usage_str = "Usage: %s " \ + " \n" % (sys.argv[0]) + print(usage_str) + + exit(1) From 09fd5150cae16f077183f2764e792730de8cc715 Mon Sep 17 00:00:00 2001 From: jpmetzca Date: Mon, 8 Feb 2021 16:50:27 -0500 Subject: [PATCH 02/15] Moved flags to top of functions, changed dpi of output to high quality (256 dpi), added extraction of color from SVGs, commented new code portions, and altered example and usage sectiosn slightly. 95% of the way there - stopping for now.l --- analysis/cell_track_plotter.py | 149 ++++++++++++++++++++++----------- analysis/cell_tracker_movie.py | 105 ++++++++++++++++------- 2 files changed, 174 insertions(+), 80 deletions(-) diff --git a/analysis/cell_track_plotter.py b/analysis/cell_track_plotter.py index 0085ddd..a7e49ad 100644 --- a/analysis/cell_track_plotter.py +++ b/analysis/cell_track_plotter.py @@ -2,25 +2,29 @@ # cell_tracker.py - plot 2-D cell tracks associated with PhysiCell .svg files # # Usage: -# python cell_track_plotter.py <# of samples to include> " -# python cell_track_plotter.py <# of samples to include> -# +# python cell_tracks.py <# of samples to include> +# +# Also takes 6 arguments. python cell_tracks.py <# of samples to +# include> +# # Dependencies include matplotlib and numpy. We recommend installing the Anaconda Python3 distribution. # # Examples (run from directory containing the .svg files): -# See below line 239 in "if __name__ == '__main__':" +# python cell_tracks.py 0 1 100 # -# Author: Randy Heiland, modified by John Metzcar (Twitter - @jmetzcar) +# Author: Randy Heiland, modified by John Metzcar. See also anim_svg_opac.py in PhysiCell tools for coloring functionality # import sys import xml.etree.ElementTree as ET import numpy as np import glob import matplotlib.pyplot as plt +import matplotlib.colors as mplc import math +import distutils.util def plot_cell_tracks(starting_index: int, sample_step_interval: int, number_of_samples: int, output_plot: bool, - show_plot: bool): + show_plot: bool, produce_for_panel: bool): """ Produces savable image of cell positional history, plotted as arrows (quiver plot) with final cell positions plotted as a cirle. @@ -42,17 +46,20 @@ def plot_cell_tracks(starting_index: int, sample_step_interval: int, number_of_s Save plot flag (required to produce a movie from resulting images) show_plot : Show plot flag (for processing many images in a loop, this should likely be set to false. Images have to be closed manually) - naming_index : - Unique to this function. Index used in naming output file of plot_cell_tracks function - filename = output + naming_index.png and leading zeros as needed. + produce_for_panel : + Flag - calls tight_layout, increases axes font sizes, and plots without title. For using in panels of images where there will be captions. Returns ------- Null : Produces a png image from the input PhysiCell SVGs. """ - - maxCount = starting_index + + output_plot = output_plot + show_plot = show_plot d={} # dictionary to hold all (x,y) positions of cells + d_attributes = {} #dictionary to hold other attributes, like color (a data frame might be nice here in the long run ... ) \ + # currently only being read once only as cell dictionary is populated - so only use for static values! """ --- for example --- @@ -71,6 +78,8 @@ def plot_cell_tracks(starting_index: int, sample_step_interval: int, number_of_s file_indices = np.linspace(starting_index, endpoint, num=number_of_samples, endpoint=False) print(file_indices) + maxCount = starting_index + ####### Uncomment for statement below to generate a random list of file names versus the prespecifed list. ######## ####### Leaving for historical record. If used, the inputs would need to be a single integer, ######## ####### versus the three integers required to generate the prespecified list. Also, remove the other for statement. ######## @@ -111,14 +120,26 @@ def plot_cell_tracks(starting_index: int, sample_step_interval: int, number_of_s ### Find branches coming from root - tissue parents for child in root: - # print(child.tag, child.attrib) + # print(child.tag, child.attrib) # print('attrib=',child.attrib) # if (child.attrib['id'] == 'tissue'): + if child.text and "Current time" in child.text: + svals = child.text.split() + title_str = "Current time: " + svals[2] + "d, " + svals[4] + "h, " + svals[ + 7] + "m" + if 'width' in child.attrib.keys(): - #### Assumes square domains - plot_spatial_length = float(child.attrib['width']) - # print(plot_spatial_length) + #### Assumes a 70 length unit offsite inthe the Y dimension of the SVG!!!!!! + plot_x_extend = float(child.attrib['width']) + plot_y_extend = float(child.attrib['height']) + + #### Remove the padding placed into the SVG to determine the true y extend + plot_y_extend = plot_y_extend-70 + + #### Find the coordinate transform amounts + y_coordinate_transform = plot_y_extend/2 + x_coordinate_transform = plot_x_extend/2 ##### Find the tissue tag and make it child if 'id' in child.attrib.keys(): @@ -147,23 +168,40 @@ def plot_cell_tracks(starting_index: int, sample_step_interval: int, number_of_s # Find the locations of the cells within the cell tags for circle in child: # print(' --- cx,cy=',circle.attrib['cx'],circle.attrib['cy']) - xval = float(circle.attrib['cx']) - - # should we test for bogus x,y locations?? - if (math.fabs(xval) > 10000.): - print("xval=",xval) - break - yval = float(circle.attrib['cy']) - if (math.fabs(yval) > 10000.): - print("xval=",xval) - break - - # Pull out the cell's location. If ID not already in stack to track, put in new cell in dictionary - if (child.attrib['id'] in d.keys()): - d[child.attrib['id']] = np.vstack((d[child.attrib['id']], [ float(circle.attrib['cx']), float(circle.attrib['cy']) ])) - else: - d[child.attrib['id']] = np.array( [ float(circle.attrib['cx']), float(circle.attrib['cy']) ]) - break + xval = float(circle.attrib['cx']) + + # should we test for bogus x,y locations?? + if (math.fabs(xval) > 10000.): + print("xval=",xval) + break + yval = float(circle.attrib['cy']) #- y_coordinate_transform + if (math.fabs(yval) > 10000.): + print("yval=",yval) + break + + # Pull out the cell's location. If ID not already in stack to track, put in new cell in dictionary while applying coordinate transform. + if (child.attrib['id'] in d.keys()): + d[child.attrib['id']] = np.vstack((d[child.attrib['id']], [ float(circle.attrib['cx'])-x_coordinate_transform, float(circle.attrib['cy'])-y_coordinate_transform ])) + else: + d[child.attrib['id']] = np.array( [ float(circle.attrib['cx'])-x_coordinate_transform, float(circle.attrib['cy'])-y_coordinate_transform]) + d_attributes[child.attrib['id']] = circle.attrib['fill'] + ##### This 'break' statement is required to skip the nucleus circle. There are two circle attributes. \ + ##### If both nuclear and cell boundary attributes are needed, this break NEEDS REMOVED!!!! + break + + ### Code to translate string based coloring to rgb coloring. Use as needed. + # s = circle.attrib['fill'] + # print("s=",s) + # print("type(s)=",type(s)) + # if (s[0:3] == "rgb"): # if an rgb string, e.g. "rgb(175,175,80)" + # # circle.attrib={'cx': '1085.59','cy': '1225.24','fill': 'rgb(159,159,96)','r': '6.67717','stroke': 'rgb(159,159,96)','stroke-width': '0.5'} + # rgb = list(map(int, s[4:-1].split(","))) + # rgb[:] = [x / 255. for x in rgb] + # else: # otherwise, must be a color name + # rgb_tuple = mplc.to_rgb(mplc.cnames[s]) # a tuple + # print(rgb_tuple) + # rgb = [x for x in rgb_tuple] + # print(rgb) # if (child.attrib['id'] == 'cells'): # print('-------- found cells!!') @@ -177,7 +215,7 @@ def plot_cell_tracks(starting_index: int, sample_step_interval: int, number_of_s #################################### Plot cell tracks and other options ######################## #################################################################################################################### - fig = plt.figure(figsize=(8,8)) + fig = plt.figure(figsize=(7,7)) ax = fig.gca() ax.set_aspect("equal") #ax.set_xticks([]) @@ -207,32 +245,41 @@ def plot_cell_tracks(starting_index: int, sample_step_interval: int, number_of_s #### Plot cell track as a directed, weighted (by length) path plt.quiver(x[:-1], y[:-1], x[1:] - x[:-1], y[1:] - y[:-1], scale_units='xy', angles='xy', scale=1, minlength = 0.001, headwidth=1.5, headlength=4) - #### Plot final cell position - plt.scatter(x[-1],y[-1], s = 3.5) + #### Plot final cell position. MAY NOT TAKE RGB VALUES!!! + plt.scatter(x[-1],y[-1], s = 4.5, c = d_attributes[key]) + #### used if history lenght is set to 0 and if in first frame of sequnece (there is no history) elif (len(d[key].shape) == 1): x = d[key][0] y = d[key][1] - - plt.scatter(x, y, s=3.5) + #### Plot final cell position. MAY NOT TAKE RGB VALUES!!! + plt.scatter(x, y, s = 4.5, c = d_attributes[key]) + # plt.scatter(x, y, s=3.5, c=) else: print(key, " has no x,y points") #### Build plot frame, titles, and save data - plt.ylim(0, plot_spatial_length) - plt.xlim(0, plot_spatial_length) + plt.ylim(-plot_y_extend/2, plot_y_extend/2) + plt.xlim(-plot_x_extend/2, plot_x_extend/2) - title_str = "Starting at frame {}, sample interval of {} for {} total samples".format(starting_index, sample_step_interval, number_of_samples) - plt.title(title_str) + if produce_for_panel == False: + title_str = "History from image " + str(starting_index) + " to image " + str(endpoint) + "; " + title_str + # %"Starting at frame {}, sample interval of {} for {} total samples".format(number_of_samples, sample_step_interval, number_of_samples) + plt.title(title_str) + else: + fig.tight_layout() + plt.xticks(fontsize=20) + plt.yticks(fontsize=20) # could change to the custom in the movie output or some other more better output if desired. output_folder = '' snapshot = str(starting_index) + '_' + str(sample_step_interval) + '_' + str(number_of_samples) - # Plot output + # Produce plot following the available options. + if output_plot is True: - plt.savefig(output_folder + snapshot + '.svg') + plt.savefig(output_folder + snapshot + '.png', dpi=256) if show_plot is True: plt.show() # plt.close() @@ -243,21 +290,21 @@ def plot_cell_tracks(starting_index: int, sample_step_interval: int, number_of_s #################################### Usage example and input loading ######################## #################################################################################################################### - if (len(sys.argv) == 6): - usage_str = "Usage: %s <# of samples to include> " % ( + if (len(sys.argv) == 7): + usage_str = "Usage: %s <# of samples to include> " % ( sys.argv[0]) # print(usage_str) starting_index = int(sys.argv[1]) sample_step_interval = int(sys.argv[2]) number_of_samples = int(sys.argv[3]) - save_plot = bool(sys.argv[4]) - show_plot = bool(sys.argv[5]) - + save_plot = bool(distutils.util.strtobool(sys.argv[4])) + show_plot = bool(distutils.util.strtobool(sys.argv[5])) + produce_for_panel = bool(distutils.util.strtobool(sys.argv[6])) # print("e.g.,") # eg_str = "%s 0 1 10 indicates start at 0, go up by ones, and stop when you 10 samples" % (sys.argv[0]) # print(eg_str) - plot_cell_tracks(starting_index, sample_step_interval, number_of_samples, save_plot, show_plot) + plot_cell_tracks(starting_index, sample_step_interval, number_of_samples, save_plot, show_plot, produce_for_panel) elif (len(sys.argv) == 4): @@ -272,15 +319,15 @@ def plot_cell_tracks(starting_index: int, sample_step_interval: int, number_of_s # eg_str = "%s 0 1 10 indicates start at 0, go up by ones, and stop when you 10 samples" % (sys.argv[0]) # print(eg_str) - plot_cell_tracks(starting_index, sample_step_interval, number_of_samples, False, True) + plot_cell_tracks(starting_index, sample_step_interval, number_of_samples, True, True, False) else: print('\nInput 3 arguments to produce and show plot only') usage_str = "Usage: %s <# of samples to include> \n" % ( sys.argv[0]) print(usage_str) - print('Input 5 arguments to directly control saving and showing the plots') - usage_str = "Usage: %s <# of samples to include> \n" % ( + print('Input 6 arguments to directly control saving and showing the plots') + usage_str = "Usage: %s <# of samples to include> \n" % ( sys.argv[0]) print(usage_str) exit(1) diff --git a/analysis/cell_tracker_movie.py b/analysis/cell_tracker_movie.py index b08b80e..036a90e 100644 --- a/analysis/cell_tracker_movie.py +++ b/analysis/cell_tracker_movie.py @@ -1,3 +1,17 @@ +# +# cell_tracker.py - plot 2-D cell tracks associated with PhysiCell .svg files +# +# Usage: +# Takes 0, 1, or 7 arguments. See below line 239 in "if __name__ == '__main__':" for usage. +# +# Dependencies include matplotlib and numpy. We recommend installing the Anaconda Python3 distribution. +# +# Examples (run from directory containing the .svg files): +# See below line 239 in "if __name__ == '__main__':" +# +# Author: function plot_cell_tracks_for_movie - Randy Heiland, modified by John Metzcar (see cell_track_plotter.py and cell_tracks.py as well for original functions) +# This script cell_tracker_movie.py - John Metzcar (Twitter - @jmetzcar). See also anim_svg_opac.py in PhysiCell tools for coloring functionality + import sys import xml.etree.ElementTree as ET import numpy as np @@ -7,7 +21,7 @@ import distutils.util def plot_cell_tracks_for_movie(starting_index: int, sample_step_interval: int, number_of_samples: int, output_plot: bool, - show_plot: bool, naming_index: int): + show_plot: bool, naming_index: int, produce_for_panel: bool): """ Produces savable image of cell positional history, plotted as arrows (quiver plot) with final cell positions plotted as a cirle. Slight modification of the function in cell_track_plotter. The modification allows for tracking the index of a series @@ -31,13 +45,24 @@ def plot_cell_tracks_for_movie(starting_index: int, sample_step_interval: int, n Show plot flag (for processing many images in a loop, this should likely be set to false. Images have to be closed manually) naming_index : Unique to this function. Index used in naming output file of plot_cell_tracks function - filename = output + naming_index.png and leading zeros as needed. + produce_for_panel : + Flag - calls tight_layout, increases axes font sizes, and plots without title. For using in panels of images where there will be captions. Returns ------- Null : Produces a png image from the input PhysiCell SVGs. """ + #### Flags + + output_plot = output_plot + show_plot = show_plot + naming_index = naming_index + produce_for_panel = produce_for_panel + d = {} # dictionary to hold all (x,y) positions of cells + d_attributes = {} #dictionary to hold other attributes, like color (a data frame might be nice here in the long run ... )\ + # currently only being read once only as cell dictionary is populated - so only use for static values! """ --- for example --- @@ -86,10 +111,22 @@ def plot_cell_tracks_for_movie(starting_index: int, sample_step_interval: int, n # if (child.attrib['id'] == 'tissue'): ##### Find the tissue tag and make it child + if child.text and "Current time" in child.text: + svals = child.text.split() + title_str = "Current time: " + svals[2] + "d, " + svals[4] + "h, " + svals[ + 7] + "m" + if ('width' in child.attrib.keys()): - #### Assumes square domains - plot_spatial_length = float(child.attrib['width']) - # print(plot_spatial_length) + #### Assumes a 70 length unit offsite inthe the Y dimension of the SVG!!!!!! + plot_x_extend = float(child.attrib['width']) + plot_y_extend = float(child.attrib['height']) + + #### Remove the padding placed into the SVG to determine the true y extend + plot_y_extend = plot_y_extend-70 + + #### Find the coordinate transform amounts + y_coordinate_transform = plot_y_extend/2 + x_coordinate_transform = plot_x_extend/2 if ('id' in child.attrib.keys()): # print('-------- found tissue!!') @@ -130,10 +167,15 @@ def plot_cell_tracks_for_movie(starting_index: int, sample_step_interval: int, n # Pull out the cell's location. If ID not already in stack to track, put in new cell in dictionary if (child.attrib['id'] in d.keys()): - d[child.attrib['id']] = np.vstack( - (d[child.attrib['id']], [float(circle.attrib['cx']), float(circle.attrib['cy'])])) + d[child.attrib['id']] = np.vstack((d[child.attrib['id']], + [float(circle.attrib['cx']) - x_coordinate_transform, + float(circle.attrib['cy']) - y_coordinate_transform])) else: - d[child.attrib['id']] = np.array([float(circle.attrib['cx']), float(circle.attrib['cy'])]) + d[child.attrib['id']] = np.array([float(circle.attrib['cx']) - x_coordinate_transform, + float(circle.attrib['cy']) - y_coordinate_transform]) + d_attributes[child.attrib['id']] = circle.attrib['fill'] + ##### This 'break' statement is required to skip the nucleus circle. There are two circle attributes. \ + ##### If both nuclear and cell boundary attributes are needed, this break NEEDS REMOVED!!!! break # if (child.attrib['id'] == 'cells'): @@ -179,40 +221,39 @@ def plot_cell_tracks_for_movie(starting_index: int, sample_step_interval: int, n plt.quiver(x[:-1], y[:-1], x[1:] - x[:-1], y[1:] - y[:-1], scale_units='xy', angles='xy', scale=1, minlength=0.001, headwidth=1.5, headlength=4) - #### Plot final cell position - plt.scatter(x[-1], y[-1], s=3.5) + #### Plot final cell position. MAY NOT TAKE RGB VALUES!!! + plt.scatter(x[-1], y[-1], s = 5.0, c = d_attributes[key]) + #### used if history lenght is set to 0 and if in first frame of sequnece (there is no history) elif (len(d[key].shape) == 1): x = d[key][0] y = d[key][1] - - plt.scatter(x, y, s=3.5) + #### Plot final cell position. MAY NOT TAKE RGB VALUES!!! + plt.scatter(x, y, s = 5.0, c = d_attributes[key]) else: print(key, " has no x,y points") #### Build plot frame, titles, and save data - - - plt.ylim(0, plot_spatial_length) - plt.xlim(0, plot_spatial_length) - - title_str = "Starting at frame {}, sample interval of {} for {} total samples".format(starting_index, - sample_step_interval, - number_of_samples) - plt.title(title_str) + plt.ylim(-plot_y_extend/2, plot_y_extend/2) + plt.xlim(-plot_x_extend/2, plot_x_extend/2) output_folder = '' snapshot = str(starting_index) + '_' + str(sample_step_interval) + '_' + str(number_of_samples) snapshot = 'output' + f'{naming_index:08}' - #### Flags for output - # output_plot = True - # show_plot = True + # Produce plot following the available options. + if produce_for_panel == False: + title_str = "History from image " + str(starting_index) + " to image " + str(endpoint) + "; " + title_str + # %"Starting at frame {}, sample interval of {} for {} total samples".format(number_of_samples, sample_step_interval, number_of_samples) + plt.title(title_str) + else: + fig.tight_layout() + plt.xticks(fontsize=20) + plt.yticks(fontsize=20) - # Plot output if output_plot is True: - plt.savefig(output_folder + snapshot + '.png') + plt.savefig(output_folder + snapshot + '.png', dpi=256) if show_plot is True: plt.show() plt.close() # cell_tracker_movie.py:151: RuntimeWarning: More than 20 figures have been opened. Figures created through the pyplot interface (`matplotlib.pyplot.figure`) are retained until explicitly closed and may consume too much memory. (To control this warning, see the rcParam `figure.max_open_warning`). @@ -239,6 +280,12 @@ def create_tracks_movie(data_path: str, save_path: str, save_name: str, start_fi :return: Null. Produces a series of images from PhysiCell SVGs and movie from said images. """ + #### Flags (for cell track plotter calls) + + output_plot = True + show_plot = False + produce_for_panel = False + #### Get list of all file names in directory files = os.listdir(data_path) @@ -299,7 +346,7 @@ def create_tracks_movie(data_path: str, save_path: str, save_name: str, start_fi max_samples_left = len(truncated_list_of_svgs) - j if i >= max_number_of_samples: - plot_cell_tracks_for_movie(starting_index, 1, max_number_of_samples, True, False, i) + plot_cell_tracks_for_movie(starting_index, 1, max_number_of_samples, output_plot, show_plot, i, produce_for_panel) # print('middle') #### If one wanted to make the trails collapse into the last available location of the cell you would use something @@ -309,7 +356,7 @@ def create_tracks_movie(data_path: str, save_path: str, save_name: str, start_fi # print(max_samples_left) # print('late') else: - plot_cell_tracks_for_movie(0, 1, j, True, False, i) + plot_cell_tracks_for_movie(0, 1, j, output_plot, show_plot, i, produce_for_panel) # print('early') #### Total frames to include in moview @@ -357,7 +404,7 @@ def create_tracks_movie(data_path: str, save_path: str, save_name: str, start_fi create_tracks_movie('.', '', movie_name, starting_file_index, end_file_index, cell_trail_length, INCLUDE_ALL_SVGs, INCLUDE_FULL_HISTORY) else: - print('\nInput 0 arguments to process every available full and include full history and out put movie with ' + print('\nInput 0 arguments to process every available full and include full history and output movie with ' 'default name of cell_tracks.mp4') usage_str = "Usage: %s \n" % (sys.argv[0]) print(usage_str) @@ -365,7 +412,7 @@ def create_tracks_movie(data_path: str, save_path: str, save_name: str, start_fi usage_str = "Usage: %s this_is_great_data\n" % (sys.argv[0]) print(usage_str) - print('Input 6 arguments to gain the most control') + print('Input 7 arguments to gain the most control') usage_str = "Usage: %s " \ " \n" % (sys.argv[0]) print(usage_str) From 6823b0d1adadd94958d73c04d4a732a41c0a7a9c Mon Sep 17 00:00:00 2001 From: Randy Heiland Date: Mon, 29 Mar 2021 09:54:09 -0400 Subject: [PATCH 03/15] init --- analysis/covid19/mean_stddev_errorbars_epi.py | 299 ++++++++++++ .../mean_stddev_errorbars_immunecells.py | 443 ++++++++++++++++++ 2 files changed, 742 insertions(+) create mode 100644 analysis/covid19/mean_stddev_errorbars_epi.py create mode 100644 analysis/covid19/mean_stddev_errorbars_immunecells.py diff --git a/analysis/covid19/mean_stddev_errorbars_epi.py b/analysis/covid19/mean_stddev_errorbars_epi.py new file mode 100644 index 0000000..864f76e --- /dev/null +++ b/analysis/covid19/mean_stddev_errorbars_epi.py @@ -0,0 +1,299 @@ +import numpy as np +import matplotlib.pyplot as plt + +live_list = list() +infect_list = list() +dead_list = list() + +tval= [ 0., 180., 360., 540., 720., 900., 1080., 1260., 1440., + 1620., 1800., 1980., 2160., 2340., 2520., 2700., 2880., 3060., + 3240., 3420., 3600., 3780., 3960., 4140., 4320., 4500., 4680., + 4860., 5040., 5220., 5400., 5580., 5760., 5940., 6120., 6300., + 6480., 6660., 6840., 7020., 7200., 7380., 7560., 7740., 7920., + 8100., 8280., 8460., 8640., 8820., 9000., 9180., 9360., 9540., + 9720., 9900., 10080., 10260., 10440., 10620., 10800., 10980., 11160., + 11340., 11520., 11700., 11880., 12060., 12240., 12420., 12600., 12780., + 12960., 13140., 13320., 13500., 13680., 13860., 14040., 14220., 14400.] +y_live= [2793, 2793, 2793, 2793, 2793, 2792, 2791, 2789, 2787, 2782, 2775, 2773, + 2765, 2749, 2740, 2727, 2715, 2709, 2695, 2688, 2674, 2658, 2646, 2630, + 2616, 2609, 2599, 2579, 2568, 2557, 2539, 2521, 2507, 2500, 2482, 2464, + 2443, 2429, 2415, 2397, 2374, 2350, 2335, 2319, 2302, 2283, 2257, 2231, + 2214, 2189, 2159, 2139, 2107, 2083, 2053, 2013, 1973, 1939, 1901, 1851, + 1784, 1731, 1668, 1592, 1505, 1409, 1287, 1173, 1053, 942, 851, 788, + 695, 629, 554, 485, 419, 351, 313, 274, 242] +y_infected= [ 0, 0, 0, 0, 0, 0, 14, 219, 436, 572, 585, 584, 580, 567, 559, + 551, 548, 550, 556, 557, 571, 577, 594, 595, 609, 621, 634, 652, 676, 698, + 705, 711, 721, 723, 730, 740, 745, 760, 777, 789, 802, 808, 824, 849, 843, + 836, 826, 831, 839, 847, 854, 869, 866, 857, 873, 878, 876, 868, 855, 844, + 841, 802, 775, 755, 720, 683, 652, 611, 555, 480, 410, 399, 358, 330, 304, + 281, 247, 194, 160, 130, 111] +y_dead= [ 0, 0, 0, 0, 0, 0, 0, 2, 0, 1, 4, 2, 6, 9, 10, + 13, 14, 15, 19, 10, 14, 15, 16, 13, 17, 16, 15, 18, 19, 18, + 18, 22, 26, 24, 26, 24, 25, 24, 26, 26, 35, 38, 33, 38, 25, + 26, 33, 44, 40, 44, 47, 51, 53, 45, 51, 60, 72, 70, 67, 81, + 116, 105, 113, 128, 152, 175, 217, 244, 258, 249, 214, 188, 180, 152, 147, + 137, 126, 112, 88, 73, 59] + +live_list.append(y_live) +infect_list.append(y_infected) +dead_list.append(y_dead) +#-------------------------------------------------- +y_live= [2793, 2793, 2793, 2793, 2792, 2791, 2790, 2789, 2788, 2783, 2777, 2775, + 2770, 2759, 2750, 2741, 2730, 2714, 2695, 2683, 2671, 2663, 2656, 2630, + 2613, 2597, 2582, 2575, 2562, 2550, 2535, 2519, 2498, 2478, 2464, 2449, + 2437, 2423, 2409, 2395, 2377, 2359, 2337, 2313, 2294, 2279, 2259, 2236, + 2218, 2203, 2183, 2162, 2134, 2116, 2091, 2070, 2036, 2011, 1975, 1935, + 1904, 1870, 1826, 1778, 1730, 1676, 1617, 1529, 1439, 1332, 1221, 1092, + 965, 836, 744, 693, 635, 564, 494, 438, 380] +y_infected= [ 0, 0, 0, 0, 0, 0, 14, 229, 469, 599, 621, 621, 618, 612, 602, + 596, 594, 590, 583, 588, 590, 612, 628, 646, 650, 662, 672, 669, 667, 668, + 681, 690, 692, 684, 689, 685, 698, 704, 732, 760, 782, 789, 799, 801, 798, + 804, 805, 809, 814, 842, 857, 876, 887, 892, 892, 888, 895, 902, 890, 880, + 875, 874, 858, 856, 850, 837, 817, 788, 769, 749, 683, 608, 526, 468, 398, + 345, 301, 276, 270, 261, 222] +y_dead= [ 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 3, 3, 5, 10, 9, + 10, 14, 19, 22, 19, 14, 14, 10, 23, 26, 29, 24, 16, 14, 14, + 21, 20, 31, 26, 28, 23, 20, 16, 19, 25, 26, 30, 32, 40, 37, + 35, 27, 28, 23, 31, 25, 32, 45, 41, 42, 36, 48, 45, 51, 54, + 58, 61, 68, 77, 85, 90, 98, 128, 160, 201, 217, 245, 259, 276, 237, + 179, 125, 112, 124, 123, 105] + +live_list.append(y_live) +infect_list.append(y_infected) +dead_list.append(y_dead) +#-------------------------------------------------- +y_live= [2793, 2793, 2793, 2793, 2793, 2792, 2792, 2790, 2787, 2784, 2780, 2775, + 2770, 2757, 2747, 2734, 2721, 2714, 2702, 2689, 2677, 2665, 2654, 2636, + 2621, 2604, 2586, 2573, 2557, 2548, 2528, 2516, 2494, 2483, 2466, 2453, + 2436, 2417, 2402, 2395, 2383, 2365, 2344, 2322, 2301, 2274, 2247, 2230, + 2214, 2185, 2160, 2134, 2099, 2068, 2025, 1981, 1940, 1893, 1846, 1796, + 1741, 1676, 1594, 1520, 1426, 1332, 1226, 1115, 985, 887, 802, 730, + 660, 594, 517, 455, 390, 339, 285, 259, 228] +y_infected= [ 0, 0, 0, 0, 0, 0, 14, 222, 446, 591, 613, 610, 608, 601, 597, + 589, 583, 588, 599, 602, 620, 633, 636, 640, 634, 641, 646, 646, 652, 672, + 692, 709, 729, 751, 768, 766, 791, 812, 826, 831, 843, 841, 857, 864, 876, + 874, 865, 863, 852, 858, 870, 872, 875, 879, 883, 875, 870, 847, 823, 795, + 782, 782, 770, 758, 730, 688, 639, 580, 522, 460, 396, 356, 334, 300, 279, + 262, 225, 192, 155, 117, 78] +y_dead= [ 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 1, 2, 3, 9, 15, + 17, 13, 13, 17, 14, 15, 16, 16, 19, 18, 28, 29, 27, 25, 18, + 25, 23, 33, 30, 28, 19, 23, 30, 29, 22, 14, 12, 24, 33, 41, + 44, 43, 37, 30, 36, 41, 43, 49, 57, 74, 80, 85, 86, 86, 81, + 94, 112, 136, 146, 171, 185, 202, 217, 255, 242, 208, 176, 154, 130, 137, + 128, 118, 98, 93, 64, 44] + +live_list.append(y_live) +infect_list.append(y_infected) +dead_list.append(y_dead) +#-------------------------------------------------- +y_live= [2793, 2793, 2793, 2793, 2793, 2792, 2792, 2791, 2789, 2786, 2781, 2774, + 2768, 2760, 2744, 2736, 2721, 2716, 2706, 2688, 2677, 2666, 2654, 2645, + 2631, 2622, 2613, 2600, 2586, 2573, 2556, 2535, 2518, 2503, 2490, 2480, + 2461, 2453, 2437, 2429, 2413, 2396, 2378, 2363, 2348, 2331, 2319, 2303, + 2287, 2275, 2253, 2238, 2214, 2189, 2168, 2138, 2117, 2096, 2075, 2052, + 2018, 1991, 1952, 1926, 1883, 1856, 1828, 1785, 1740, 1695, 1652, 1611, + 1559, 1503, 1430, 1355, 1274, 1193, 1061, 957, 853] +y_infected= [ 0, 0, 0, 0, 0, 0, 13, 220, 442, 573, 595, 591, + 588, 582, 573, 562, 560, 562, 570, 585, 600, 604, 611, 619, + 619, 636, 639, 656, 682, 703, 720, 727, 736, 734, 735, 745, + 742, 751, 771, 790, 803, 818, 823, 846, 858, 850, 846, 843, + 859, 884, 903, 909, 927, 947, 953, 950, 939, 936, 949, 965, + 986, 1003, 1017, 1009, 1001, 992, 979, 972, 974, 959, 941, 919, + 888, 852, 819, 800, 762, 703, 647, 589, 528] +y_dead= [ 0, 0, 0, 0, 0, 1, 0, 0, 2, 3, 4, 4, 7, 9, 16, + 11, 18, 14, 15, 22, 26, 19, 16, 18, 19, 17, 10, 13, 16, 18, + 18, 24, 28, 22, 18, 19, 18, 19, 23, 24, 22, 23, 26, 28, 29, + 29, 26, 24, 28, 24, 33, 27, 35, 41, 43, 47, 40, 35, 37, 38, + 47, 54, 66, 59, 67, 67, 62, 67, 78, 83, 86, 78, 83, 86, 109, + 134, 152, 158, 207, 229, 240] + +live_list.append(y_live) +infect_list.append(y_infected) +dead_list.append(y_dead) +#-------------------------------------------------- +y_live= [2793, 2793, 2793, 2793, 2793, 2792, 2791, 2790, 2789, 2785, 2775, 2767, + 2759, 2743, 2734, 2721, 2712, 2695, 2683, 2666, 2653, 2645, 2635, 2620, + 2602, 2591, 2579, 2564, 2551, 2540, 2526, 2511, 2491, 2470, 2459, 2446, + 2426, 2405, 2378, 2366, 2349, 2330, 2315, 2294, 2269, 2249, 2234, 2212, + 2190, 2171, 2147, 2125, 2098, 2071, 2042, 2019, 1980, 1940, 1909, 1866, + 1821, 1768, 1703, 1630, 1562, 1477, 1370, 1258, 1148, 1042, 950, 846, + 763, 680, 603, 529, 444, 362, 297, 257, 218] +y_infected= [ 0, 0, 0, 0, 0, 0, 14, 213, 474, 612, 630, 624, 614, 607, 600, + 593, 591, 597, 606, 602, 612, 626, 643, 651, 660, 672, 682, 685, 687, 699, + 715, 731, 757, 756, 750, 750, 754, 758, 777, 785, 795, 797, 804, 813, 834, + 846, 854, 851, 845, 848, 856, 867, 877, 884, 894, 899, 896, 887, 891, 879, + 873, 875, 856, 844, 804, 749, 703, 681, 623, 567, 497, 455, 416, 397, 366, + 337, 306, 261, 218, 166, 121] +y_dead= [ 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 5, 7, 5, 14, 16, + 18, 11, 15, 20, 20, 22, 13, 14, 16, 25, 25, 19, 19, 18, 17, + 17, 14, 21, 29, 22, 21, 24, 28, 41, 36, 29, 21, 22, 24, 37, + 38, 35, 33, 33, 32, 29, 32, 42, 47, 52, 43, 49, 61, 73, 78, + 89, 103, 120, 141, 147, 154, 179, 225, 237, 239, 218, 213, 197, 181, 163, + 154, 156, 153, 140, 107, 77] + +live_list.append(y_live) +infect_list.append(y_infected) +dead_list.append(y_dead) +#-------------------------------------------------- +y_live= [2793, 2793, 2793, 2793, 2793, 2793, 2791, 2788, 2785, 2781, 2778, 2771, + 2760, 2747, 2734, 2721, 2716, 2703, 2693, 2679, 2669, 2655, 2635, 2619, + 2605, 2596, 2574, 2563, 2546, 2532, 2525, 2511, 2498, 2474, 2465, 2447, + 2430, 2418, 2398, 2383, 2363, 2348, 2327, 2305, 2282, 2260, 2247, 2231, + 2209, 2184, 2161, 2138, 2124, 2107, 2094, 2065, 2049, 2034, 2007, 1990, + 1967, 1950, 1917, 1883, 1850, 1810, 1782, 1744, 1706, 1659, 1613, 1564, + 1500, 1442, 1360, 1272, 1187, 1066, 929, 800, 696] +y_infected= [ 0, 0, 0, 0, 0, 0, 14, 224, 469, 600, 616, 618, 613, 605, 594, + 589, 578, 572, 573, 582, 615, 642, 644, 644, 637, 638, 637, 645, 657, 661, + 682, 706, 716, 725, 731, 762, 774, 780, 778, 785, 789, 800, 819, 826, 821, + 817, 833, 838, 834, 837, 846, 853, 859, 861, 868, 883, 900, 918, 936, 946, + 932, 930, 920, 908, 910, 904, 914, 918, 899, 895, 877, 860, 839, 814, 780, + 740, 696, 646, 589, 507, 433] +y_dead= [ 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 5, 10, 15, 17, + 24, 15, 16, 13, 13, 15, 24, 28, 34, 25, 18, 20, 18, 20, 24, + 23, 25, 19, 29, 23, 29, 31, 31, 27, 26, 30, 27, 31, 33, 37, + 33, 28, 27, 32, 35, 39, 40, 34, 32, 32, 44, 38, 33, 35, 34, + 31, 31, 39, 41, 53, 54, 54, 69, 62, 81, 83, 92, 109, 116, 134, + 156, 174, 214, 251, 259, 240] + +live_list.append(y_live) +infect_list.append(y_infected) +dead_list.append(y_dead) +#-------------------------------------------------- +y_live= [2793, 2793, 2793, 2792, 2792, 2792, 2792, 2791, 2790, 2784, 2779, 2769, + 2756, 2749, 2743, 2729, 2720, 2710, 2700, 2693, 2680, 2670, 2657, 2635, + 2618, 2604, 2591, 2575, 2558, 2546, 2535, 2523, 2510, 2494, 2483, 2476, + 2464, 2447, 2432, 2416, 2401, 2384, 2367, 2350, 2336, 2316, 2299, 2281, + 2258, 2231, 2217, 2193, 2177, 2154, 2131, 2105, 2086, 2067, 2032, 1994, + 1964, 1919, 1878, 1836, 1780, 1722, 1662, 1582, 1504, 1415, 1317, 1211, + 1104, 985, 883, 785, 715, 660, 594, 529, 450] +y_infected= [ 0, 0, 0, 0, 0, 0, 14, 228, 451, 572, 582, 580, 575, 565, 559, + 549, 542, 557, 567, 582, 580, 585, 602, 599, 599, 604, 603, 608, 612, 618, + 631, 635, 651, 665, 674, 681, 694, 703, 716, 734, 740, 742, 748, 752, 770, + 792, 819, 830, 829, 841, 854, 859, 856, 859, 879, 892, 899, 894, 881, 887, + 884, 879, 863, 857, 845, 835, 817, 774, 732, 693, 648, 597, 545, 507, 451, + 385, 349, 300, 293, 269, 240] +y_dead= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 5, 12, 9, 8, + 11, 9, 15, 17, 16, 13, 12, 13, 17, 19, 24, 22, 27, 29, 24, + 20, 17, 21, 24, 17, 10, 11, 20, 28, 34, 30, 28, 28, 28, 23, + 27, 31, 28, 29, 37, 34, 40, 32, 36, 42, 49, 39, 41, 49, 64, + 64, 76, 79, 83, 96, 106, 122, 139, 150, 171, 179, 197, 208, 237, 234, + 212, 177, 139, 116, 99, 115] + +live_list.append(y_live) +infect_list.append(y_infected) +dead_list.append(y_dead) +#-------------------------------------------------- +y_live= [2793, 2793, 2793, 2793, 2793, 2793, 2793, 2792, 2788, 2785, 2781, 2777, + 2769, 2760, 2749, 2740, 2728, 2713, 2705, 2694, 2684, 2671, 2656, 2645, + 2632, 2613, 2597, 2584, 2574, 2566, 2548, 2533, 2523, 2509, 2492, 2479, + 2466, 2451, 2434, 2417, 2398, 2386, 2374, 2352, 2331, 2312, 2295, 2286, + 2265, 2242, 2220, 2201, 2171, 2155, 2133, 2110, 2076, 2056, 2027, 1999, + 1968, 1936, 1893, 1844, 1805, 1747, 1688, 1640, 1573, 1503, 1427, 1326, + 1235, 1132, 1006, 888, 809, 741, 674, 634, 569] +y_infected= [ 0, 0, 0, 0, 0, 0, 12, 222, 447, 586, 594, 592, 588, 583, 579, + 573, 569, 569, 575, 581, 588, 600, 602, 623, 637, 635, 632, 636, 646, 656, + 661, 661, 672, 689, 702, 714, 714, 714, 718, 737, 752, 759, 751, 753, 763, + 777, 800, 807, 839, 844, 856, 858, 848, 843, 841, 846, 860, 884, 910, 902, + 908, 897, 888, 879, 862, 838, 806, 786, 749, 725, 699, 675, 626, 574, 485, + 435, 367, 306, 257, 267, 281] +y_dead= [ 0, 0, 0, 0, 0, 0, 0, 1, 1, 3, 0, 0, 4, 8, 14, + 12, 16, 21, 16, 11, 8, 14, 14, 15, 17, 24, 25, 21, 18, 14, + 17, 17, 15, 20, 21, 25, 23, 20, 19, 20, 29, 26, 18, 25, 35, + 37, 32, 21, 27, 28, 33, 33, 35, 29, 32, 31, 42, 41, 47, 39, + 43, 49, 61, 76, 81, 97, 103, 108, 111, 124, 143, 177, 186, 209, 223, + 242, 205, 160, 118, 102, 101] + +live_list.append(y_live) +infect_list.append(y_infected) +dead_list.append(y_dead) +#-------------------------------------------------- +y_live= [2793, 2793, 2793, 2793, 2793, 2793, 2792, 2790, 2789, 2786, 2780, 2772, + 2764, 2753, 2746, 2736, 2720, 2703, 2695, 2683, 2670, 2654, 2639, 2627, + 2617, 2605, 2589, 2575, 2560, 2550, 2532, 2520, 2508, 2492, 2478, 2461, + 2447, 2427, 2408, 2399, 2381, 2364, 2347, 2330, 2314, 2293, 2273, 2251, + 2227, 2208, 2185, 2164, 2140, 2115, 2097, 2079, 2049, 2017, 1994, 1966, + 1943, 1914, 1883, 1855, 1816, 1784, 1748, 1693, 1652, 1598, 1547, 1474, + 1407, 1334, 1241, 1147, 1045, 923, 798, 697, 616] +y_infected= [ 0, 0, 0, 0, 0, 0, 15, 218, 446, 574, 589, 580, 573, 569, 563, + 561, 554, 553, 560, 562, 571, 591, 598, 617, 623, 629, 617, 624, 640, 653, + 660, 668, 672, 677, 691, 704, 716, 720, 727, 740, 742, 743, 755, 773, 796, + 819, 825, 831, 840, 852, 848, 848, 854, 854, 857, 870, 867, 887, 897, 899, + 900, 912, 924, 944, 938, 937, 916, 895, 881, 860, 844, 823, 795, 761, 729, + 681, 630, 567, 482, 402, 340] +y_dead= [ 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 2, 1, 1, 8, 9, + 12, 14, 22, 20, 19, 17, 23, 20, 24, 21, 21, 19, 21, 21, 19, + 19, 21, 23, 21, 25, 25, 25, 25, 27, 27, 27, 28, 27, 27, 29, + 35, 32, 37, 41, 42, 36, 39, 43, 45, 40, 38, 36, 47, 48, 51, + 39, 45, 53, 62, 62, 64, 58, 70, 80, 91, 92, 117, 131, 138, 167, + 181, 207, 229, 241, 228, 197] + +live_list.append(y_live) +infect_list.append(y_infected) +dead_list.append(y_dead) +#-------------------------------------------------- +y_live= [2793, 2793, 2793, 2793, 2793, 2793, 2792, 2792, 2789, 2787, 2786, 2778, + 2773, 2767, 2754, 2742, 2726, 2718, 2708, 2698, 2686, 2676, 2658, 2639, + 2627, 2620, 2610, 2593, 2579, 2563, 2551, 2540, 2528, 2512, 2497, 2483, + 2464, 2444, 2429, 2412, 2396, 2384, 2365, 2346, 2332, 2315, 2296, 2270, + 2245, 2226, 2199, 2176, 2158, 2129, 2093, 2070, 2045, 2014, 1988, 1952, + 1921, 1897, 1867, 1832, 1805, 1770, 1731, 1696, 1653, 1612, 1570, 1514, + 1462, 1399, 1336, 1249, 1154, 1040, 918, 793, 697] +y_infected= [ 0, 0, 0, 0, 0, 1, 15, 212, 447, 585, 604, 602, 598, 592, 586, + 574, 571, 567, 575, 571, 579, 595, 609, 620, 626, 631, 633, 640, 656, 674, + 695, 709, 723, 738, 754, 764, 779, 786, 793, 790, 800, 808, 824, 836, 852, + 866, 864, 873, 873, 866, 862, 857, 867, 864, 881, 903, 922, 911, 911, 905, + 904, 898, 890, 872, 867, 865, 853, 854, 838, 827, 813, 794, 776, 751, 739, + 714, 674, 627, 567, 505, 423] +y_dead= [ 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 5, 5, 5, 12, + 10, 18, 15, 18, 10, 10, 13, 17, 21, 21, 14, 9, 15, 20, 22, + 21, 19, 16, 15, 22, 23, 27, 30, 27, 29, 23, 21, 29, 26, 26, + 28, 28, 41, 49, 46, 48, 37, 36, 33, 46, 50, 51, 44, 47, 56, + 52, 51, 52, 53, 47, 57, 62, 67, 70, 72, 81, 89, 93, 103, 113, + 139, 167, 212, 232, 264, 239] + +live_list.append(y_live) +infect_list.append(y_infected) +dead_list.append(y_dead) +#-------------------------------------------------- +live = np.array(live_list) # live.shape = (10, 81) +infect = np.array(infect_list) +dead = np.array(dead_list) + +#------------------------------------------------------- +live_sum = live.sum(axis=0) +live_avg = live_sum / 10. +print('live_avg=',np.array2string(live_avg,separator=',')) + +live_std = live.std(axis=0) +print('live_std=',np.array2string(live_std,separator=',')) + +#-------------------------- +infect_sum = infect.sum(axis=0) +infect_avg = infect_sum / 10. +infect_std = infect.std(axis=0) +#-------------------------- +dead_sum = dead.sum(axis=0) +dead_avg = dead_sum / 10. +dead_std = dead.std(axis=0) + +#------------------------------------------------- +fig = plt.figure(figsize=(8, 5)) +ax = fig.add_subplot(111) +# Set the axis lables +fontsize = 14 +#ax.set_xlabel('Time (mins)', fontsize = fontsize) +ax.set_xlabel('Time (days)', fontsize = fontsize) +ax.set_xticks(np.arange(0,14401,step=1440)) +ax.set_xticklabels(['0','1','2','3','4','5','6','7','8','9','10']) +#ax.set_ylabel('Macrophage counts: mean and std dev', fontsize = fontsize) +ax.set_ylabel('Epithelial cell states', fontsize = fontsize) + +ax.errorbar(tval, live_avg, yerr=live_std, color='blue', label='live') +ax.errorbar(tval, infect_avg, yerr=infect_std, color='gray', label='infected') +ax.errorbar(tval, dead_avg, yerr=dead_std, color='black', label='dead') + +plt.legend(loc='upper left', prop={'size': 10}) + +plt.title('Results from 10 runs: error bars with mean and std dev') +#plt.savefig(data_dir + '.png') +plt.show() diff --git a/analysis/covid19/mean_stddev_errorbars_immunecells.py b/analysis/covid19/mean_stddev_errorbars_immunecells.py new file mode 100644 index 0000000..fd0362c --- /dev/null +++ b/analysis/covid19/mean_stddev_errorbars_immunecells.py @@ -0,0 +1,443 @@ +import numpy as np +import matplotlib.pyplot as plt + +mac_list = list() +neut_list = list() +dc_list = list() +cd8_list = list() + +tval= [ 0., 180., 360., 540., 720., 900., 1080., 1260., 1440., + 1620., 1800., 1980., 2160., 2340., 2520., 2700., 2880., 3060., + 3240., 3420., 3600., 3780., 3960., 4140., 4320., 4500., 4680., + 4860., 5040., 5220., 5400., 5580., 5760., 5940., 6120., 6300., + 6480., 6660., 6840., 7020., 7200., 7380., 7560., 7740., 7920., + 8100., 8280., 8460., 8640., 8820., 9000., 9180., 9360., 9540., + 9720., 9900., 10080., 10260., 10440., 10620., 10800., 10980., 11160., + 11340., 11520., 11700., 11880., 12060., 12240., 12420., 12600., 12780., + 12960., 13140., 13320., 13500., 13680., 13860., 14040., 14220., 14400.] +mac= [ 50, 50, 52, 62, 74, 79, 86, 97, 105, 111, 121, 130, 134, 139, 147, + 149, 146, 150, 153, 158, 163, 170, 174, 176, 175, 176, 174, 179, 172, 179, + 176, 184, 185, 182, 187, 191, 197, 202, 199, 196, 196, 194, 188, 191, 197, + 200, 204, 204, 197, 194, 192, 199, 205, 206, 209, 213, 214, 219, 221, 217, + 218, 222, 223, 227, 226, 231, 222, 221, 223, 228, 230, 224, 223, 227, 227, + 224, 225, 222, 228, 228, 225] +neut= [ 0, 0, 2, 10, 18, 24, 28, 36, 43, 48, 52, 47, 44, 49, 47, 51, 50, 49, + 45, 50, 47, 56, 57, 56, 61, 64, 62, 62, 62, 53, 54, 53, 51, 54, 53, 52, + 51, 51, 46, 47, 53, 55, 59, 51, 50, 46, 44, 40, 39, 40, 40, 39, 42, 43, + 44, 42, 49, 50, 47, 49, 53, 53, 49, 54, 54, 50, 54, 59, 64, 65, 58, 57, + 58, 60, 66, 64, 63, 61, 55, 51, 53] +cd8= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, + 3, 3, 4, 4, 4, 5, 7, 8, 7, 8, 11, 12, 15, 18, 21, + 25, 30, 37, 41, 50, 56, 67, 79, 94, 118, 139, 150, 163, 199, 232, + 273, 323, 379, 444, 515, 597] +dc= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, 1] +cd4= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, 1] +fib= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 2, 2, 2, 2, 4, 8, 12, 16, 18, 23, + 31, 37, 45, 51, 53, 64, 71, 75, 79, 81, 88, 94, 99, 99, 105, + 107, 112, 114, 116, 115, 119] + +mac_list.append(mac) +neut_list.append(neut) +dc_list.append(dc) +cd8_list.append(cd8) +#-------------------------------------------------- +mac= [ 50, 50, 53, 58, 69, 77, 82, 92, 98, 103, 103, 107, 114, 120, 125, + 131, 135, 137, 138, 140, 145, 146, 149, 155, 162, 169, 174, 174, 182, 184, + 185, 186, 187, 192, 193, 200, 205, 210, 208, 205, 204, 199, 205, 207, 212, + 217, 221, 219, 224, 228, 229, 233, 235, 239, 240, 244, 235, 229, 232, 236, + 241, 235, 242, 234, 235, 243, 245, 245, 240, 239, 242, 238, 240, 241, 246, + 244, 245, 245, 245, 249, 250] +neut= [ 0, 0, 3, 14, 25, 27, 34, 37, 39, 42, 46, 44, 46, 50, 50, 54, 51, 60, + 64, 64, 69, 62, 64, 62, 61, 60, 63, 62, 62, 57, 54, 50, 51, 47, 48, 52, + 48, 45, 44, 44, 50, 52, 53, 57, 58, 59, 55, 58, 58, 59, 63, 64, 62, 61, + 59, 62, 62, 64, 63, 68, 66, 67, 62, 66, 62, 57, 56, 52, 50, 50, 53, 59, + 61, 61, 63, 67, 63, 65, 64, 63, 61] +cd8= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 2, 2, 2, 3, 3, 4, 6, 6, 7, 9, 12, 14, + 15, 13, 19, 19, 24, 31, 36, 44, 51, 61, 69, 80, 92, 108, 126, + 147, 170, 205, 246, 291, 341] +dc= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 1] +cd4= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 1] +fib= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 2, 4, 8, 11, 13, 15, 17, 19, 24, 31, 35, 42, 46, 53, 57, 57, 61, + 66, 69, 76, 81, 86, 89, 94, 95, 98] + +mac_list.append(mac) +neut_list.append(neut) +dc_list.append(dc) +cd8_list.append(cd8) +#-------------------------------------------------- +mac= [ 50, 50, 50, 61, 66, 78, 90, 95, 99, 104, 111, 116, 124, 134, 131, + 137, 143, 154, 159, 159, 158, 160, 167, 172, 169, 165, 169, 166, 167, 172, + 170, 171, 172, 173, 180, 182, 183, 186, 191, 192, 198, 197, 190, 191, 192, + 194, 198, 204, 205, 210, 204, 206, 209, 211, 209, 212, 214, 215, 217, 218, + 217, 214, 223, 226, 231, 233, 228, 223, 222, 221, 229, 228, 228, 233, 241, + 244, 247, 249, 249, 246, 243] +neut= [ 0, 0, 4, 10, 20, 25, 28, 33, 40, 44, 50, 48, 46, 46, 52, 50, 52, 51, + 57, 55, 57, 56, 50, 55, 56, 51, 56, 61, 64, 59, 57, 64, 63, 58, 58, 60, + 60, 56, 59, 50, 53, 54, 49, 48, 48, 46, 46, 44, 44, 52, 58, 58, 57, 62, + 59, 53, 58, 55, 60, 66, 67, 70, 71, 65, 54, 57, 59, 62, 60, 63, 60, 57, + 56, 56, 62, 58, 54, 59, 56, 58, 57] +cd8= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, + 2, 3, 3, 4, 6, 6, 9, 8, 10, 11, 11, 14, 16, 20, 24, + 27, 37, 39, 43, 49, 58, 69, 84, 98, 107, 130, 153, 179, 208, 238, + 265, 315, 364, 429, 509, 606] +dc= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1] +cd4= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1] +fib= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 2, 2, 3, 5, 7, 8, 11, 16, 20, 25, 31, + 36, 43, 46, 58, 62, 71, 76, 83, 87, 93, 99, 104, 108, 111, 111, + 112, 113, 118, 123, 126, 117] + +mac_list.append(mac) +neut_list.append(neut) +dc_list.append(dc) +cd8_list.append(cd8) +#-------------------------------------------------- +mac= [ 50, 50, 50, 56, 64, 73, 80, 88, 92, 99, 107, 110, 114, 121, 126, + 131, 137, 138, 142, 148, 155, 160, 166, 175, 170, 167, 172, 173, 170, 172, + 180, 183, 186, 185, 188, 191, 189, 191, 192, 195, 195, 199, 210, 208, 205, + 199, 200, 209, 213, 212, 216, 221, 224, 220, 218, 221, 219, 222, 221, 226, + 226, 224, 226, 234, 237, 233, 237, 237, 241, 245, 247, 245, 251, 250, 249, + 249, 255, 252, 252, 251, 250] +neut= [ 0, 0, 1, 9, 17, 19, 28, 32, 34, 39, 42, 38, 48, 49, 50, 52, 54, 54, + 58, 58, 60, 59, 56, 59, 58, 52, 55, 55, 59, 50, 51, 54, 54, 57, 57, 55, + 56, 58, 62, 60, 61, 65, 70, 74, 72, 72, 75, 69, 57, 55, 47, 44, 49, 52, + 57, 65, 60, 57, 59, 54, 54, 55, 55, 57, 55, 60, 61, 59, 62, 64, 64, 65, + 70, 64, 69, 68, 70, 68, 63, 61, 54] +cd8= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 10, 10, 11, 12, 14, 18, 20, 20, + 22, 27, 33, 39, 44, 51, 56, 65, 74] +dc= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0] +cd4= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0] +fib= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 2, 2, 3, 3, 4, 9, 16, 17, 21, 27, 29, 33, + 39, 44, 46, 54, 67, 69, 75, 76, 83] + +mac_list.append(mac) +neut_list.append(neut) +dc_list.append(dc) +cd8_list.append(cd8) +#-------------------------------------------------- +mac= [ 50, 50, 53, 55, 62, 71, 79, 84, 87, 95, 103, 111, 121, 128, 128, + 130, 133, 137, 145, 152, 153, 154, 161, 165, 173, 177, 184, 190, 194, 197, + 203, 201, 210, 214, 214, 219, 221, 222, 226, 229, 226, 230, 233, 237, 233, + 237, 241, 233, 236, 235, 237, 240, 236, 234, 237, 238, 243, 244, 240, 237, + 232, 221, 219, 221, 216, 217, 222, 217, 214, 212, 213, 217, 220, 212, 209, + 212, 216, 213, 212, 208, 210] +neut= [ 0, 0, 0, 8, 14, 21, 25, 26, 30, 32, 35, 39, 43, 45, 46, 50, 49, 52, + 54, 61, 62, 66, 65, 62, 58, 56, 57, 58, 59, 59, 54, 54, 59, 57, 52, 52, + 55, 56, 61, 64, 65, 70, 65, 71, 69, 68, 65, 68, 67, 66, 64, 65, 68, 65, + 64, 60, 58, 58, 58, 61, 68, 78, 76, 72, 69, 64, 65, 61, 59, 60, 57, 56, + 56, 57, 61, 60, 63, 69, 68, 64, 67] +cd8= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, + 0, 1, 1, 2, 2, 3, 4, 5, 6, 8, 9, 12, 12, 14, 15, + 22, 25, 29, 31, 38, 47, 54, 62, 75, 91, 111, 130, 153, 188, 215, + 264, 327, 396, 475, 562, 681] +dc= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1] +cd4= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1] +fib= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 10, 12, 16, 20, + 25, 30, 40, 50, 58, 65, 72, 76, 83, 86, 91, 99, 99, 100, 100, + 104, 110, 112, 116, 115, 115] + +mac_list.append(mac) +neut_list.append(neut) +dc_list.append(dc) +cd8_list.append(cd8) +#-------------------------------------------------- +mac= [ 50, 50, 50, 59, 65, 74, 83, 87, 94, 104, 108, 112, 114, 118, 127, + 128, 132, 136, 141, 145, 145, 147, 150, 150, 153, 157, 167, 169, 175, 174, + 175, 177, 182, 188, 194, 190, 196, 194, 193, 194, 199, 203, 206, 206, 209, + 218, 214, 219, 222, 223, 229, 234, 236, 229, 217, 216, 226, 227, 229, 228, + 235, 233, 235, 233, 237, 242, 235, 233, 235, 232, 231, 228, 230, 239, 233, + 236, 236, 241, 247, 249, 247] +neut= [ 0, 0, 1, 11, 17, 23, 27, 33, 38, 43, 48, 51, 51, 47, 51, 47, 47, 46, + 51, 54, 53, 58, 56, 54, 51, 51, 54, 57, 58, 64, 56, 55, 53, 64, 62, 66, + 66, 66, 65, 64, 69, 63, 61, 61, 54, 61, 59, 56, 52, 56, 55, 52, 52, 53, + 48, 52, 55, 59, 51, 56, 54, 53, 51, 47, 46, 49, 50, 52, 51, 52, 58, 62, + 64, 58, 61, 57, 60, 63, 59, 58, 57] +cd8= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 3, + 4, 5, 7, 8, 9, 11, 14, 15, 14, 16, 20, 23, 28, 35, 38, + 47, 61, 70, 82, 98, 112] +dc= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0] +cd4= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0] +fib= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 1, 2, 4, 5, 6, 5, 10, 14, 18, 26, 33, + 38, 45, 51, 57, 62, 74, 82, 84, 90] + +mac_list.append(mac) +neut_list.append(neut) +dc_list.append(dc) +cd8_list.append(cd8) +#-------------------------------------------------- +mac= [ 50, 50, 53, 56, 64, 74, 80, 88, 94, 101, 109, 116, 123, 133, 131, + 136, 139, 143, 154, 153, 155, 159, 161, 164, 165, 166, 166, 170, 174, 180, + 184, 189, 195, 203, 205, 207, 207, 199, 195, 197, 201, 201, 195, 196, 193, + 198, 201, 206, 213, 210, 213, 209, 217, 210, 203, 202, 200, 202, 199, 199, + 197, 204, 206, 213, 215, 215, 218, 218, 221, 223, 229, 230, 233, 231, 236, + 244, 243, 244, 250, 248, 242] +neut= [ 0, 0, 2, 10, 17, 24, 25, 27, 33, 32, 33, 37, 37, 32, 39, 38, 43, 43, + 45, 48, 50, 52, 57, 58, 60, 60, 62, 68, 61, 66, 63, 60, 59, 61, 62, 62, + 63, 62, 62, 50, 48, 53, 55, 56, 54, 56, 56, 56, 51, 55, 51, 46, 48, 48, + 50, 55, 57, 57, 62, 60, 58, 61, 56, 60, 66, 65, 63, 62, 59, 58, 58, 63, + 62, 61, 65, 61, 58, 53, 53, 52, 51] +cd8= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 2, 2, 3, 4, 4, 6, 7, 8, 9, 11, 12, + 13, 14, 15, 20, 23, 28, 34, 37, 44, 50, 57, 66, 77, 90, 103, + 116, 136, 151, 170, 202, 243] +dc= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 1] +cd4= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 1] +fib= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 6, + 10, 14, 17, 22, 31, 30, 40, 41, 46, 50, 63, 72, 76, 82, 79, + 80, 80, 85, 91, 95, 101] + +mac_list.append(mac) +neut_list.append(neut) +dc_list.append(dc) +cd8_list.append(cd8) +#-------------------------------------------------- +mac= [ 50, 49, 55, 64, 70, 77, 85, 95, 103, 112, 119, 125, 129, 134, 141, + 143, 148, 150, 156, 159, 163, 169, 170, 168, 177, 185, 182, 189, 190, 195, + 197, 199, 206, 204, 203, 201, 203, 201, 203, 206, 210, 212, 215, 215, 220, + 224, 224, 226, 227, 223, 222, 225, 231, 230, 234, 235, 235, 239, 239, 239, + 241, 244, 247, 245, 245, 247, 249, 244, 243, 244, 247, 245, 242, 244, 246, + 247, 252, 253, 259, 257, 254] +neut= [ 0, 0, 2, 9, 19, 28, 33, 37, 44, 47, 45, 51, 48, 50, 51, 48, 49, 50, + 52, 52, 51, 51, 50, 51, 55, 55, 59, 60, 63, 60, 62, 61, 63, 65, 56, 56, + 55, 46, 37, 39, 46, 48, 53, 55, 57, 56, 57, 59, 60, 61, 58, 62, 66, 61, + 59, 57, 52, 54, 52, 53, 52, 59, 52, 55, 52, 52, 52, 52, 49, 47, 44, 48, + 50, 54, 57, 56, 56, 58, 60, 56, 62] +cd8= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, + 1, 0, 0, 1, 1, 1, 1, 1, 2, 3, 4, 4, 7, 7, 6, + 8, 10, 13, 15, 18, 20, 24, 29, 36, 39, 47, 51, 59, 70, 81, + 99, 122, 139, 166, 200, 239] +dc= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 1] +cd4= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 1, 1, 1] +fib= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 2, 5, + 4, 7, 9, 10, 15, 20, 24, 32, 38, 43, 52, 59, 60, 64, 74, + 75, 83, 88, 91, 102, 107] + +mac_list.append(mac) +neut_list.append(neut) +dc_list.append(dc) +cd8_list.append(cd8) +#-------------------------------------------------- +mac= [ 50, 49, 51, 60, 69, 81, 89, 96, 106, 111, 120, 125, 133, 140, 142, + 141, 148, 153, 158, 161, 168, 173, 178, 180, 180, 181, 183, 185, 192, 198, + 200, 200, 202, 204, 201, 196, 198, 199, 200, 196, 201, 206, 209, 209, 207, + 206, 211, 212, 215, 217, 215, 214, 205, 209, 209, 212, 208, 210, 211, 214, + 221, 218, 212, 213, 215, 219, 219, 225, 225, 223, 223, 226, 236, 227, 230, + 236, 240, 241, 242, 241, 237] +neut= [ 0, 0, 2, 10, 15, 16, 22, 30, 36, 40, 49, 48, 50, 49, 53, 46, 51, 52, + 48, 50, 53, 59, 53, 58, 58, 58, 63, 59, 53, 56, 53, 59, 60, 56, 61, 59, + 62, 62, 65, 64, 71, 69, 64, 67, 68, 65, 66, 70, 62, 62, 65, 66, 57, 61, + 53, 57, 56, 56, 54, 56, 54, 53, 54, 60, 62, 61, 58, 59, 60, 63, 60, 60, + 59, 61, 61, 64, 72, 73, 75, 71, 67] +cd8= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 0, 0, 0, 1, 1, 1, 2, 2, 3, 4, 4, 4, + 5, 7, 9, 9, 10, 12, 15, 18, 21, 25, 26, 31, 34, 42, 48, + 55, 66, 81, 100, 111, 125] +dc= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1] +cd4= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1] +fib= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 3, 4, 4, 6, 7, 11, 14, 21, 24, 29, 33, 36, 40, + 48, 52, 57, 64, 73, 79, 85, 89, 92] + +mac_list.append(mac) +neut_list.append(neut) +dc_list.append(dc) +cd8_list.append(cd8) +#-------------------------------------------------- +mac= [ 50, 50, 53, 61, 70, 82, 92, 95, 104, 106, 110, 110, 120, 128, 134, + 142, 145, 157, 163, 167, 171, 175, 178, 183, 179, 187, 195, 190, 194, 192, + 197, 201, 200, 203, 198, 200, 199, 207, 211, 216, 213, 220, 222, 228, 227, + 232, 227, 230, 231, 235, 238, 235, 228, 230, 235, 231, 234, 234, 239, 240, + 231, 230, 232, 231, 233, 228, 230, 230, 234, 234, 231, 235, 238, 236, 235, + 241, 244, 236, 239, 238, 240] +neut= [ 0, 0, 0, 6, 13, 23, 29, 38, 44, 46, 44, 50, 50, 49, 44, 52, 56, 55, + 55, 57, 62, 64, 66, 64, 60, 59, 59, 50, 56, 58, 58, 61, 59, 59, 60, 62, + 59, 60, 60, 61, 67, 61, 57, 57, 49, 49, 56, 55, 54, 57, 57, 62, 59, 58, + 62, 63, 65, 62, 63, 62, 59, 58, 65, 65, 65, 61, 61, 59, 57, 52, 55, 55, + 51, 49, 49, 51, 55, 52, 56, 49, 48] +cd8= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 3, + 3, 3, 4, 5, 6, 6, 6, 9, 6, 7, 8, 9, 9, 11, 13, 19, 21, 24, + 29, 37, 43, 45, 54, 62, 71, 78, 92] +dc= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0] +cd4= [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0] +fib= [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + 3, 3, 4, 4, 4, 6, 8, 9, 11, 14, 13, 14, 18, 20, 24, 28, 31, 33, + 41, 53, 60, 69, 71, 82, 87, 91, 97] + +mac_list.append(mac) +macs = np.array(mac_list) + +neut_list.append(neut) +neuts = np.array(neut_list) + +dc_list.append(dc) +dcs = np.array(dc_list) + +cd8_list.append(cd8) +cd8s = np.array(cd8_list) +#------------------------------------------------------- +# macs.shape = (10,81) +macs_sum = macs.sum(axis=0) +macs_avg = macs_sum / 10. +# array([ 500, 498, 520, 592, 673, 766, 846, 917, 982, 1046, 1111, +# 1162, 1226, 1295, 1332, 1368, 1406, 1455, 1509, 1542, 1576, 1613, +# 1654, 1688, 1703, 1730, 1766, 1785, 1810, 1843, 1867, 1891, 1925, +# 1948, 1963, 1977, 1998, 2011, 2018, 2026, 2043, 2061, 2073, 2088, +# 2095, 2125, 2141, 2162, 2183, 2187, 2195, 2216, 2226, 2218, 2211, +# 2224, 2228, 2241, 2248, 2254, 2259, 2245, 2265, 2277, 2290, 2308, +# 2305, 2293, 2298, 2301, 2322, 2316, 2341, 2340, 2352, 2377, 2403, +# 2396, 2423, 2415, 2398]) +print('macs_avg=',np.array2string(macs_avg,separator=',')) + +macs_std = macs.std(axis=0) +print('macs_std=',np.array2string(macs_std,separator=',')) + +#-------------------------- +neuts_sum = neuts.sum(axis=0) +neuts_avg = neuts_sum / 10. +neuts_std = neuts.std(axis=0) +#-------------------------- +dcs_sum = dcs.sum(axis=0) +dcs_avg = dcs_sum / 10. +dcs_std = dcs.std(axis=0) +#-------------------------- +cd8s_sum = cd8s.sum(axis=0) +cd8s_avg = cd8s_sum / 10. +cd8s_std = cd8s.std(axis=0) + +#------------------------------------------------- +fig = plt.figure(figsize=(8, 5)) +ax = fig.add_subplot(111) +# Set the axis lables +fontsize = 14 +#ax.set_xlabel('Time (mins)', fontsize = fontsize) +ax.set_xlabel('Time (days)', fontsize = fontsize) +ax.set_xticks(np.arange(0,14401,step=1440)) +ax.set_xticklabels(['0','1','2','3','4','5','6','7','8','9','10']) +#ax.set_ylabel('Macrophage counts: mean and std dev', fontsize = fontsize) +ax.set_ylabel('Immune cell counts', fontsize = fontsize) + +ax.errorbar(tval, macs_avg, yerr=macs_std, color='lime', label='macrophages') +ax.errorbar(tval, neuts_avg, yerr=neuts_std, color='cyan', label='neutrophils') + +# ax.errorbar(tval, cd8s_avg, yerr=cd8s_std, color='red', label='CD8+ T cells') +# ax.errorbar(tval, dcs_avg, yerr=dcs_std, color='fuchsia', label='DC') + +#plt.plot(tval, macs_std, linestyle='dashed', label='Fib', linewidth=1, color='orange') +#plt.plot(tval, yval9, linestyle='dashed', label='Fib', linewidth=1, color='orange') + +#plt.legend(loc='center left', prop={'size': 15}) +plt.legend(loc='upper left', prop={'size': 10}) + +plt.title('Results from 10 runs: error bars with mean and std dev') +#plt.savefig(data_dir + '.png') +plt.show() \ No newline at end of file From 0dfdc4b566dd64c2994a2fbd53ddb8097cb67ae5 Mon Sep 17 00:00:00 2001 From: Randy Heiland Date: Tue, 30 Mar 2021 10:26:09 -0400 Subject: [PATCH 04/15] repr print for commas --- analysis/covid19/plot_immune_cells.py | 32 ++++++++++++++++++++------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/analysis/covid19/plot_immune_cells.py b/analysis/covid19/plot_immune_cells.py index 1d04bbe..5317c79 100644 --- a/analysis/covid19/plot_immune_cells.py +++ b/analysis/covid19/plot_immune_cells.py @@ -16,7 +16,7 @@ kdx = 1 data_dir = sys.argv[kdx] -print('data_dir = ',data_dir) +print('# data_dir = ',data_dir) os.chdir(data_dir) xml_files = glob.glob('output*.xml') os.chdir('..') @@ -24,32 +24,39 @@ #print('xml_files = ',xml_files) ds_count = len(xml_files) -print("----- ds_count = ",ds_count) +print("# ----- ds_count = ",ds_count) mcds = [pyMCDS_cells(xml_files[i], data_dir) for i in range(ds_count)] tval = np.linspace(0, mcds[-1].get_time(), ds_count) -print('tval= ',tval) +print('type(tval)= ',type(tval)) +print('tval= ',repr(tval)) -y_load = np.array( [np.floor(mcds[idx].data['discrete_cells']['assembled_virion']).sum() for idx in range(ds_count)] ).astype(int) -print(y_load) +# y_load = np.array( [np.floor(mcds[idx].data['discrete_cells']['assembled_virion']).sum() for idx in range(ds_count)] ).astype(int) +# print(y_load) # # mac,neut,cd8,DC,cd4,Fib yval4 = np.array( [(np.count_nonzero((mcds[idx].data['discrete_cells']['cell_type'] == 4) & (mcds[idx].data['discrete_cells']['cycle_model'] < 100.) == True)) for idx in range(ds_count)] ) +print('macs=',repr(yval4)) # count Neuts yval5 = np.array( [(np.count_nonzero((mcds[idx].data['discrete_cells']['cell_type'] == 5) & (mcds[idx].data['discrete_cells']['cycle_model'] < 100.) == True)) for idx in range(ds_count)] ) +print('neuts=',repr(yval5)) # count CD8 yval6 = np.array( [(np.count_nonzero((mcds[idx].data['discrete_cells']['cell_type'] == 3) & (mcds[idx].data['discrete_cells']['cycle_model'] < 100.) == True)) for idx in range(ds_count)] ) +print('cd8=',repr(yval6)) # count DC yval7 = np.array( [(np.count_nonzero((mcds[idx].data['discrete_cells']['cell_type'] == 6) & (mcds[idx].data['discrete_cells']['cycle_model'] < 100.) == True)) for idx in range(ds_count)] ) +print('DC=',repr(yval7)) # count CD4 yval8 = np.array( [(np.count_nonzero((mcds[idx].data['discrete_cells']['cell_type'] == 7) & (mcds[idx].data['discrete_cells']['cycle_model'] < 100.) == True)) for idx in range(ds_count)] ) +print('cd4=',repr(yval8)) # count Fibroblasts yval9 = np.array( [(np.count_nonzero((mcds[idx].data['discrete_cells']['cell_type'] == 8) & (mcds[idx].data['discrete_cells']['cycle_model'] < 100.) == True)) for idx in range(ds_count)] ) +print('fib=',repr(yval9)) plt.plot(tval, yval4, label='Mac', linewidth=1, color='lime') plt.plot(tval, yval5, linestyle='dashed', label='Neut', linewidth=1, color='cyan') @@ -59,8 +66,17 @@ plt.plot(tval, yval9, linestyle='dashed', label='Fib', linewidth=1, color='orange') #plt.legend(loc='center left', prop={'size': 15}) -plt.legend(loc='top left', prop={'size': 10}) +plt.legend(loc='upper left', prop={'size': 10}) +#plt.legend(loc='upper right', prop={'size': 10}) -plt.title(data_dir) -plt.savefig(data_dir + '.png') +plt.xticks([1440,2*1440, 3*1440, 4*1440, 5*1440, 6*1440, 7*1440, 8*1440, 9*1440, 10*1440], ('1', '2','3','4','5','6','7','8','9','10')) + +# plt.xticks([1440,2*1440, 3*1440, 4*1440, 5*1440, 6*1440, 7*1440, 8*1440,9*1440,10*1440,11*1440,12*1440,13*1440,14*1440,15*1440,16*1440,17*1440,18*1440,19*1440,20*1440,21*1440], ('1', '2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20','21')) + +plt.xlabel('Time (days)') +plt.ylabel('Number of cells') + +plt.title(data_dir + ": immune cell counts") +outfile = data_dir + '_immune.png' +plt.savefig(outfile) plt.show() From 8ecfb12383374cfcbcc3a7a450147f91e86cb72f Mon Sep 17 00:00:00 2001 From: jpmetzca Date: Wed, 31 Mar 2021 13:38:44 -0400 Subject: [PATCH 05/15] Plots cells and their physical environment. Can also plot chemical environment. --- analysis/cell_plus_environment_plotter.py | 219 ++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 analysis/cell_plus_environment_plotter.py diff --git a/analysis/cell_plus_environment_plotter.py b/analysis/cell_plus_environment_plotter.py new file mode 100644 index 0000000..1b59b01 --- /dev/null +++ b/analysis/cell_plus_environment_plotter.py @@ -0,0 +1,219 @@ +from pyMCDS_ECM import * +import numpy as np + +# Script REQUIRES ffmpeg to make movei!!!!!!! + +######## If using on remote system, uncomment this line below to load correct matplotlib backend ################ +# matplotlib.use('Agg') + +import matplotlib.pyplot as plt +from matplotlib.patches import Circle +import math, os, sys, re +import scipy +import distutils.util + + +def print_stats(arr): + """ + Produces relevant statistical output to screen given an array of any dimension. It flattens the in row-major style, + the default np.flatten behavior. + + :param arr: any dimensional array, but it probably makes the most sense to be a 2-d array + :return: Prints to termminal the array mean, quartiles, min, and max. + """ + + print("Mean: ", np.mean(arr.flatten())) + print("Q2 quantile of arr : ", np.quantile(arr, .50)) + print("Q1 quantile of arr : ", np.quantile(arr, .25)) + print("Q3 quantile of arr : ", np.quantile(arr, .75)) + print("Min : ", arr.min()) + print("Max : ", arr.max()) + + +def create_plot(snapshot, folder, output_folder='.', output_plot=True, show_plot=False): + """ + Creates a plot as per instructions inside the function. As of 10.13.20 this was a plot of ECM-organoid simulations: + a base layer of a contour plot of either the anisotropy or the oxygen, the cells in the smulation as a scatter plot, + and finally the ECM orientation overlaid with a quiver plot. + + Parameters + ---------- + snapshot : + Base name of PhysiCell output files - eg 'output00000275' --> 'output' + '%08d' + folder : str + Path to input data + output_folder : str + Path for image output + output_plot : bool + True = image file will be made. Image output is required for movie production. + show_plot : bool + True = plot is displayed. Expected to be false for large batches. + Returns + ------- + Nothing : + Produces a png image from the input PhysiCell data. + """ + + ###### Flags ###### + + produce_for_panel = False + + #################################################################################################################### + #################################### Load data ######################## + #################################################################################################################### + # load cell and microenvironment data + mcds = pyMCDS(snapshot + '.xml', folder) + + # loads and reads ECM data + mcds.load_ecm(snapshot + '_ECM.mat', folder) + + # Get cell positions and attributes, microenvironment, and ECM data for plotting. + + # Cells + cell_df = mcds.get_cell_df() + + #### Diffusion microenvironment + xx, yy = mcds.get_2D_mesh() # Mesh + plane_oxy = mcds.get_concentrations('oxygen', 0.0) # Oxyen (used for contour plot) + + #### ECM microenvironment + xx_ecm, yy_ecm = mcds.get_2D_ECM_mesh() # Mesh + plane_anisotropy = mcds.get_ECM_field('anisotropy', 0.0) # Anistropy (used for scaling and contour plot) + # plane_anisotropy = micro # Used for contour plot + + #################################################################################################################### + #################################### Preprocessing ######################## + #################################################################################################################### + + #### Helper varialbes and functions ###### + + # Number of contours (could include as a parameter) + num_levels = 10 # 25 works well for ECM, 38 works well for oxygen + + # Make levels for contours + levels_o2 = np.linspace(1e-14, 38, num_levels) + # levels_ecm = np.linspace(1e-14, 1.0, num_levels) + levels_ecm = np.linspace(0.90, 0.93, num_levels) # for the march environment - need to especially highlight small changes in anistoropy. + + # Old function and scripting to scale and threshold anisotorpy values for later use in scaling lenght of ECM fibers + # for visualization purposes. + + # micro = plane_anisotropy + # micro_scaled = micro + # + # def curve(x): + # #return (V_max * x) / (K_M + x) + # return 0.5 if x > 0.5 else x + + # for i in range(len(micro)): + # for j in range(len(micro[i])): + # #micro_scaled[i][j] = 10 * math.log10(micro[i][j] + 1) / math.log10(2) + # micro_scaled[i][j] = curve(micro[i][j]) + + ##### Process data for plotting - weight fibers by anisotropy, mask out 0 anisotropy ECM units, get cell radii and types + + # Anisotropy strictly runs between 0 and 1. Element by element mulitplication produces weighted lengths between 0 - 1 + # for vizualization + + scaled_ECM_x = np.multiply(mcds.data['ecm']['ECM_fields']['x_fiber_orientation'][:, :, 0], plane_anisotropy) + scaled_ECM_y = np.multiply(mcds.data['ecm']['ECM_fields']['y_fiber_orientation'][:, :, 0], plane_anisotropy) + + # if we want the arrows the same length instead + ECM_x = mcds.data['ecm']['ECM_fields']['x_fiber_orientation'][:, :, 0] + ECM_y = mcds.data['ecm']['ECM_fields']['y_fiber_orientation'][:, :, 0] + + # mask out zero vectors + mask = plane_anisotropy > 0.0001 + + # get unique cell types and radii + cell_df['radius'] = (cell_df['total_volume'].values * 3 / (4 * np.pi)) ** (1 / 3) + types = cell_df['cell_type'].unique() + colors = ['yellow', 'blue'] + + #################################################################################################################### + #################################### Plotting ######################## + #################################################################################################################### + + # start plot and make correct size + fig = plt.figure(figsize=(8, 8)) + ax = fig.gca() + ax.set_aspect("equal") + plt.ylim(-500, 500) + plt.xlim(-500, 500) + + # add contour layer + # cs = plt.contourf(xx, yy, plane_oxy, cmap="Greens_r", levels=levels_o2) + cs = plt.contourf(xx_ecm, yy_ecm, plane_anisotropy, cmap="YlGnBu", levels=levels_ecm) + + # Add cells layer + for i, ct in enumerate(types): + plot_df = cell_df[cell_df['cell_type'] == ct] + for j in plot_df.index: + circ = Circle((plot_df.loc[j, 'position_x'], plot_df.loc[j, 'position_y']), + radius=plot_df.loc[j, 'radius'], color='blue', alpha=0.7, edgecolor='black') + # for a blue circle with a black edge + # circ = Circle((plot_df.loc[j, 'position_x'], plot_df.loc[j, 'position_y']), + # radius=plot_df.loc[j, 'radius'], alpha=0.7, edgecolor='black') + ax.add_artist(circ) + + # add quiver layer with scaled arrows ### + # q = ax.quiver(xx_ecm[mask], yy_ecm[mask], scaled_ECM_x[mask], scaled_ECM_y[mask], pivot='middle', angles='xy', scale_units='inches', scale=2.0, headwidth=0, + # width=0.0015) ## What is the deal with the line segment lengths shifting as the plots progress when I don't ue teh scaling?? + + # add ECM orientation vectors unscaled by anistorpy ### + plt.quiver(xx, yy, ECM_x, ECM_y, + pivot='middle', angles='xy', scale_units='inches', scale=4.75, headwidth=0, alpha = 0.3) ### was at 3.0 before changing the mat size from 12 to 8 to match my otherr images AND get the font for the ticks large + + # ax.axis('scaled') #used to be 'equal' https://stackoverflow.com/questions/45057647/difference-between-axisequal-and-axisscaled-in-matplotlib + # This changes teh axis from -750,750 to ~-710,730. It looks better with scaled compared to axix, but either way it changes the plot limits + + # Labels and title (will need removed for journal - they will be added manually) + + plt.ylim(-500, 500) + plt.xlim(-500, 500) + # ax.axis('scaled') + + if produce_for_panel == False: + + ax.set_xlabel('x [micron]') + ax.set_ylabel('y [micron]') + fig.colorbar(cs, ax=ax) + plt.title(snapshot) + # Carefully place the command to make the plot square AFTER the color bar has been added. + ax.axis('scaled') + else: + + plt.xticks(fontsize=20) + plt.yticks(fontsize=20) + ax.set_xlabel('microns', fontsize=20) + ax.set_ylabel('microns', fontsize=20) + fig.tight_layout() + + # Plot output + if output_plot is True: + plt.savefig(output_folder + snapshot + '.png', dpi=256) + if show_plot is True: + plt.show() + # plt.close() + +if __name__ == '__main__': + # def create_plot(snapshot, folder, output_folder='.', output_plot=True, show_plot=False) + snapshot = sys.argv[1] + output_plot = bool(distutils.util.strtobool(sys.argv[2])) + show_plot = bool(distutils.util.strtobool(sys.argv[3])) + + # elif (len(sys.argv) == 4): + # usage_str = "Usage: %s <# of samples to include>" % ( + # sys.argv[0]) + # # print(usage_str) + # starting_index = int(sys.argv[1]) + # sample_step_interval = int(sys.argv[2]) + # number_of_samples = int(sys.argv[3]) + + # # print("e.g.,") + # # eg_str = "%s 0 1 10 indicates start at 0, go up by ones, and stop when you 10 samples" % (sys.argv[0]) + # # print(eg_str) + + # Sample call with meaningful variables: + # create_plot('output00000275', output_folder='21_03_leader_follower_model_3_test/',output_plot=False, show_plot=False) + create_plot(snapshot, '.', '', output_plot=True, show_plot=False) \ No newline at end of file From 9e07d214b03d431df6d9b2862935755f56344dae Mon Sep 17 00:00:00 2001 From: jpmetzca Date: Wed, 31 Mar 2021 14:24:58 -0400 Subject: [PATCH 06/15] Reorganized scripts that depend on the ECM pyMCDS to their own location. Added additional processing scripts. --- .../cell_plus_environment_movie_maker.py | 224 +++++ .../cell_plus_environment_plotter.py | 0 .../environment_visualizer.py | 220 +++++ .../pyMCDS_ECM.py | 794 ++++++++++++++++++ 4 files changed, 1238 insertions(+) create mode 100644 analysis/extracellular_matrx_plotting/cell_plus_environment_movie_maker.py rename analysis/{ => extracellular_matrx_plotting}/cell_plus_environment_plotter.py (100%) create mode 100644 analysis/extracellular_matrx_plotting/environment_visualizer.py create mode 100644 analysis/extracellular_matrx_plotting/pyMCDS_ECM.py diff --git a/analysis/extracellular_matrx_plotting/cell_plus_environment_movie_maker.py b/analysis/extracellular_matrx_plotting/cell_plus_environment_movie_maker.py new file mode 100644 index 0000000..063de31 --- /dev/null +++ b/analysis/extracellular_matrx_plotting/cell_plus_environment_movie_maker.py @@ -0,0 +1,224 @@ +from pyMCDS_ECM import * +import numpy as np + +# Script REQUIRES ffmpeg to make movei!!!!!!! + +######## If using on remote system, uncomment this line below to load correct matplotlib backend ################ +# matplotlib.use('Agg') + +import matplotlib.pyplot as plt +from matplotlib.patches import Circle +import math, os, sys, re +import scipy +import distutils.util + + +def print_stats(arr): + """ + Produces relevant statistical output to screen given an array of any dimension. It flattens the in row-major style, + the default np.flatten behavior. + + :param arr: any dimensional array, but it probably makes the most sense to be a 2-d array + :return: Prints to termminal the array mean, quartiles, min, and max. + """ + + print("Mean: ", np.mean(arr.flatten())) + print("Q2 quantile of arr : ", np.quantile(arr, .50)) + print("Q1 quantile of arr : ", np.quantile(arr, .25)) + print("Q3 quantile of arr : ", np.quantile(arr, .75)) + print("Min : ", arr.min()) + print("Max : ", arr.max()) + + +def create_plot(snapshot, folder, output_folder='.', output_plot=True, show_plot=False): + """ + Creates a plot as per instructions inside the function. As of 10.13.20 this was a plot of ECM-organoid simulations: + a base layer of a contour plot of either the anisotropy or the oxygen, the cells in the smulation as a scatter plot, + and finally the ECM orientation overlaid with a quiver plot. + + Parameters + ---------- + snapshot : + Base name of PhysiCell output files - eg 'output00000275' --> 'output' + '%08d' + folder : str + Path to input data + output_folder : str + Path for image output + output_plot : bool + True = image file will be made. Image output is required for movie production. + show_plot : bool + True = plot is displayed. Expected to be false for large batches. + Returns + ------- + Nothing : + Produces a png image from the input PhysiCell data. + """ + #################################################################################################################### + #################################### Load data ######################## + #################################################################################################################### + # load cell and microenvironment data + mcds = pyMCDS(snapshot + '.xml', folder) + + # loads and reads ECM data + mcds.load_ecm(snapshot + '_ECM.mat', folder) + + # Get cell positions and attributes, microenvironment, and ECM data for plotting. + + # Cells + cell_df = mcds.get_cell_df() + + #### Diffusion microenvironment + xx, yy = mcds.get_2D_mesh() # Mesh + plane_oxy = mcds.get_concentrations('oxygen', 0.0) # Oxyen (used for contour plot) + + #### ECM microenvironment + xx_ecm, yy_ecm = mcds.get_2D_ECM_mesh() # Mesh + plane_anisotropy = mcds.get_ECM_field('anisotropy', 0.0) # Anistropy (used for scaling and contour plot) + # plane_anisotropy = micro # Used for contour plot + + #################################################################################################################### + #################################### Preprocessing ######################## + #################################################################################################################### + + #### Helper varialbes and functions ###### + + # Number of contours (could include as a parameter) + num_levels = 10 # 25 works well for ECM, 38 works well for oxygen + + # Make levels for contours + levels_o2 = np.linspace(1e-14, 38, num_levels) + # levels_ecm = np.linspace(1e-14, 1.0, num_levels) + levels_ecm = np.linspace(0.90, 0.93, num_levels) # for the march environment - need to especially highlight small changes in anistoropy. + + # Old function and scripting to scale and threshold anisotorpy values for later use in scaling lenght of ECM fibers + # for visualization purposes. + + # micro = plane_anisotropy + # micro_scaled = micro + # + # def curve(x): + # #return (V_max * x) / (K_M + x) + # return 0.5 if x > 0.5 else x + + # for i in range(len(micro)): + # for j in range(len(micro[i])): + # #micro_scaled[i][j] = 10 * math.log10(micro[i][j] + 1) / math.log10(2) + # micro_scaled[i][j] = curve(micro[i][j]) + + ##### Process data for plotting - weight fibers by anisotropy, mask out 0 anisotropy ECM units, get cell radii and types + + # Anisotropy strictly runs between 0 and 1. Element by element mulitplication produces weighted lengths between 0 - 1 + # for vizualization + + scaled_ECM_x = np.multiply(mcds.data['ecm']['ECM_fields']['x_fiber_orientation'][:, :, 0], plane_anisotropy) + scaled_ECM_y = np.multiply(mcds.data['ecm']['ECM_fields']['y_fiber_orientation'][:, :, 0], plane_anisotropy) + + # if we want the arrows the same length instead + ECM_x = mcds.data['ecm']['ECM_fields']['x_fiber_orientation'][:, :, 0] + ECM_y = mcds.data['ecm']['ECM_fields']['y_fiber_orientation'][:, :, 0] + + # mask out zero vectors + mask = plane_anisotropy > 0.0001 + + # get unique cell types and radii + cell_df['radius'] = (cell_df['total_volume'].values * 3 / (4 * np.pi)) ** (1 / 3) + types = cell_df['cell_type'].unique() + colors = ['yellow', 'blue'] + + #################################################################################################################### + #################################### Plotting ######################## + #################################################################################################################### + + # start plot and make correct size + fig, ax = plt.subplots(figsize=(12, 12)) + plt.ylim(-500, 500) + plt.xlim(-500, 500) + + # add contour layer + # cs = plt.contourf(xx, yy, plane_oxy, cmap="Greens_r", levels=levels_o2) + cs = plt.contourf(xx_ecm, yy_ecm, plane_anisotropy, cmap="YlGnBu", levels=levels_ecm) + + # Add cells layer + for i, ct in enumerate(types): + plot_df = cell_df[cell_df['cell_type'] == ct] + for j in plot_df.index: + circ = Circle((plot_df.loc[j, 'position_x'], plot_df.loc[j, 'position_y']), + radius=plot_df.loc[j, 'radius'], alpha=0.7, edgecolor='black') + ax.add_artist(circ) + + # add quiver layer with scaled arrows ### + # q = ax.quiver(xx_ecm[mask], yy_ecm[mask], scaled_ECM_x[mask], scaled_ECM_y[mask], pivot='middle', angles='xy', scale_units='inches', scale=2.0, headwidth=0, + # width=0.0015) ## What is the deal with the line segment lengths shifting as the plots progress when I don't ue teh scaling?? + + # add ECM orientation vectors unscaled by anistorpy ### + plt.quiver(xx, yy, ECM_x, ECM_y, + pivot='middle', angles='xy', scale_units='inches', scale=3.0, headwidth=0) + + # ax.axis('scaled') #used to be 'equal' https://stackoverflow.com/questions/45057647/difference-between-axisequal-and-axisscaled-in-matplotlib + # This changes teh axis from -750,750 to ~-710,730. It looks better with scaled compared to axix, but either way it changes the plot limits + + # Labels and title (will need removed for journal - they will be added manually) + ax.set_xlabel('x [micron]') + ax.set_ylabel('y [micron]') + fig.colorbar(cs, ax=ax) + plt.title(snapshot) + + # Carefully place the command to make the plot square AFTER the color bar has been added. + ax.axis('scaled') + fig.tight_layout() + plt.xticks(fontsize=20) + plt.yticks(fontsize=20) + plt.ylim(-500, 500) + plt.xlim(-500, 500) + + # Plot output + if output_plot is True: + plt.savefig(output_folder + snapshot + '.png') + if show_plot is True: + plt.show() + # plt.close() + +def create_movie(data_path: str, save_path: str, save_name: str): + """ + Generates the list of files in data_path, finds the ones with ECM data, makes plots from them, then outputs an + ffmpeg generated movie to save_path, naming the movie save_name. + + This function requires ffmpeg be installed at the command line. + + :param data_path: Path to direcotry containing data + :param save_path: Path to save generated image and movie to + :param save_name: Save name for movie + :return: + """ + + # generate list of files in the directory + files = os.listdir(data_path) + # files = list(filter(re.compile(r'output*ECM\.mat').search, files)) + + # For all files in the directory, process only those with with 'ECM.mat' in the names. I am not sure why there is a + # period at the beginning of the search pattern. + for i in range(len(files)): + if not re.search('.ECM\.mat', files[i]): + continue + + # Sample call with meaningful variables: + # create_plot('output00000275', output_folder='21_03_leader_follower_model_3_test/',output_plot=False, show_plot=False) + create_plot(files[i].split('_')[0], data_path, output_folder=save_path, output_plot=True, show_plot=False) + + # make the movie - see ffmpeg documentation for more information + + # consider saving as jpegs - https://blender.stackexchange.com/questions/148231/what-image-format-encodes-the-fastest-or-at-least-faster-png-is-too-slow + # consider compiling as movie instead of saving the files (all to increase processing speed) (then again, it was teh same speed) + + # consider not loading the unneeded data - and be sure to get rid of the unneeded fields!!! + + os.system( + 'ffmpeg -y -framerate 24 -i ' + save_path + 'output%08d.png -pix_fmt yuv420p -vf pad="width=ceil(iw/2)*2:height=ceil(ih/2)*2" "' + save_name + '.mp4"') + + +if __name__ == '__main__': + # auto call the create movive function using the current directory as the data path and save path, and with teh given name. + + name_of_movie = sys.argv[1] + + create_movie('.', '', name_of_movie) \ No newline at end of file diff --git a/analysis/cell_plus_environment_plotter.py b/analysis/extracellular_matrx_plotting/cell_plus_environment_plotter.py similarity index 100% rename from analysis/cell_plus_environment_plotter.py rename to analysis/extracellular_matrx_plotting/cell_plus_environment_plotter.py diff --git a/analysis/extracellular_matrx_plotting/environment_visualizer.py b/analysis/extracellular_matrx_plotting/environment_visualizer.py new file mode 100644 index 0000000..dba3f6a --- /dev/null +++ b/analysis/extracellular_matrx_plotting/environment_visualizer.py @@ -0,0 +1,220 @@ +from pyMCDS_ECM import * +import numpy as np + +# Script REQUIRES ffmpeg to make movei!!!!!!! + +######## If using on remote system, uncomment this line below to load correct matplotlib backend ################ +# matplotlib.use('Agg') + +import matplotlib.pyplot as plt +from matplotlib.patches import Circle +import math, os, sys, re +import scipy + + +def print_stats(arr): + """ + Produces relevant statistical output to screen given an array of any dimension. It flattens the in row-major style, + the default np.flatten behavior. + + :param arr: any dimensional array, but it probably makes the most sense to be a 2-d array + :return: Prints to termminal the array mean, quartiles, min, and max. + """ + + print("Mean: ", np.mean(arr.flatten())) + print("Q2 quantile of arr : ", np.quantile(arr, .50)) + print("Q1 quantile of arr : ", np.quantile(arr, .25)) + print("Q3 quantile of arr : ", np.quantile(arr, .75)) + print("Min : ", arr.min()) + print("Max : ", arr.max()) + + +def create_plot(snapshot, folder, output_folder='.', output_plot=True, show_plot=False): + """ + Creates a plot as per instructions inside the function. As of 10.13.20 this was a plot of ECM-organoid simulations: + a base layer of a contour plot of either the anisotropy or the oxygen, the cells in the smulation as a scatter plot, + and finally the ECM orientation overlaid with a quiver plot. + + Parameters + ---------- + snapshot : + Base name of PhysiCell output files - eg 'output00000275' --> 'output' + '%08d' + folder : str + Path to input data + output_folder : str + Path for image output + output_plot : bool + True = image file will be made. Image output is required for movie production. + show_plot : bool + True = plot is displayed. Expected to be false for large batches. + Returns + ------- + Nothing : + Produces a png image from the input PhysiCell data. + """ + #################################################################################################################### + #################################### Load data ######################## + #################################################################################################################### + # load cell and microenvironment data + mcds = pyMCDS(snapshot + '.xml', folder) + + # loads and reads ECM data + mcds.load_ecm(snapshot + '_ECM.mat', folder) + + # Get cell positions and attributes, microenvironment, and ECM data for plotting. + + # Cells + cell_df = mcds.get_cell_df() + + #### Diffusion microenvironment + xx, yy = mcds.get_2D_mesh() # Mesh + plane_oxy = mcds.get_concentrations('oxygen', 0.0) # Oxyen (used for contour plot) + + #### ECM microenvironment + xx_ecm, yy_ecm = mcds.get_2D_ECM_mesh() # Mesh + plane_anisotropy = mcds.get_ECM_field('anisotropy', 0.0) # Anistropy (used for scaling and contour plot) + # plane_anisotropy = micro # Used for contour plot + + #################################################################################################################### + #################################### Preprocessing ######################## + #################################################################################################################### + + #### Helper varialbes and functions ###### + + # Number of contours (could include as a parameter) + num_levels = 25 # 25 works well for ECM, 38 works well for oxygen + + # Make levels for contours + levels_o2 = np.linspace(1e-14, 38, num_levels) + levels_ecm = np.linspace(1e-14, 1.0, num_levels) + + # Old function and scripting to scale and threshold anisotorpy values for later use in scaling lenght of ECM fibers + # for visualization purposes. + + # micro = plane_anisotropy + # micro_scaled = micro + # + # def curve(x): + # #return (V_max * x) / (K_M + x) + # return 0.5 if x > 0.5 else x + + # for i in range(len(micro)): + # for j in range(len(micro[i])): + # #micro_scaled[i][j] = 10 * math.log10(micro[i][j] + 1) / math.log10(2) + # micro_scaled[i][j] = curve(micro[i][j]) + + ##### Process data for plotting - weight fibers by anisotropy, mask out 0 anisotropy ECM units, get cell radii and types + + # Anisotropy strictly runs between 0 and 1. Element by element mulitplication produces weighted lengths between 0 - 1 + # for vizualization + + scaled_ECM_x = np.multiply(mcds.data['ecm']['ECM_fields']['x_fiber_orientation'][:, :, 0], plane_anisotropy) + scaled_ECM_y = np.multiply(mcds.data['ecm']['ECM_fields']['y_fiber_orientation'][:, :, 0], plane_anisotropy) + + # if we want the arrows the same length instead + ECM_x = mcds.data['ecm']['ECM_fields']['x_fiber_orientation'][:, :, 0] + ECM_y = mcds.data['ecm']['ECM_fields']['y_fiber_orientation'][:, :, 0] + + # mask out zero vectors + mask = plane_anisotropy > 0.0001 + + # get unique cell types and radii + cell_df['radius'] = (cell_df['total_volume'].values * 3 / (4 * np.pi)) ** (1 / 3) + types = cell_df['cell_type'].unique() + colors = ['yellow', 'blue'] + + #################################################################################################################### + #################################### Plotting ######################## + #################################################################################################################### + + # start plot and make correct size + fig, ax = plt.subplots(figsize=(12, 12)) + plt.ylim(-500, 500) + plt.xlim(-500, 500) + + # add contour layer + # cs = plt.contourf(xx, yy, plane_oxy, cmap="Greens_r", levels=levels_o2) + # cs = plt.contourf(xx_ecm, yy_ecm, plane_anisotropy, cmap="Reds", levels=levels_ecm) + + # Add cells layer + # for i, ct in enumerate(types): + # plot_df = cell_df[cell_df['cell_type'] == ct] + # for j in plot_df.index: + # circ = Circle((plot_df.loc[j, 'position_x'], plot_df.loc[j, 'position_y']), + # color=colors[i], radius=plot_df.loc[j, 'radius'], alpha=0.7) + # ax.add_artist(circ) + + # add quiver layer with scaled arrows ### + # q = ax.quiver(xx_ecm[mask], yy_ecm[mask], scaled_ECM_x[mask], scaled_ECM_y[mask], pivot='middle', angles='xy', scale_units='inches', scale=2.0, headwidth=0, + # width=0.0015) ## What is the deal with the line segment lengths shifting as the plots progress when I don't ue teh scaling?? + + # add unscaled arrows ### + plt.quiver(xx[mask], yy[mask], ECM_x[mask], ECM_y[mask], + pivot='middle', angles='xy', scale_units='inches', scale=3.0, headwidth=0) + + # ax.axis('scaled') #used to be 'equal' https://stackoverflow.com/questions/45057647/difference-between-axisequal-and-axisscaled-in-matplotlib + # This changes teh axis from -750,750 to ~-710,730. It looks better with scaled compared to axix, but either way it changes the plot limits + + # Labels and title + # ax.set_xlabel('x [micron]') + # ax.set_ylabel('y [micron]') + # fig.colorbar(cs, ax=ax) + # plt.title(snapshot) + + # Carefully place the command to make the plot square AFTER the color bar has been added. + ax.axis('scaled') + fig.tight_layout() + plt.xticks(fontsize=20) + plt.yticks(fontsize=20) + plt.ylim(-500, 500) + plt.xlim(-500, 500) + + # Plot output + if output_plot is True: + plt.savefig(output_folder + snapshot + '.png') + if show_plot is True: + plt.show() + plt.close() + + +def create_movie(data_path: str, save_path: str, save_name: str): + """ + Generates the list of files in data_path, finds the ones with ECM data, makes plots from them, then outputs an + ffmpeg generated movie to save_path, naming the movie save_name. + + This function requires ffmpeg be installed at the command line. + + :param data_path: Path to direcotry containing data + :param save_path: Path to save generated image and movie to + :param save_name: Save name for movie + :return: + """ + + # generate list of files in the directory + files = os.listdir(data_path) + # files = list(filter(re.compile(r'output*ECM\.mat').search, files)) + + # For all files in the directory, process only those with with 'ECM.mat' in the names. I am not sure why there is a + # period at the beginning of the search pattern. + for i in range(len(files)): + if not re.search('.ECM\.mat', files[i]): + continue + + # Sample call with meaningful variables: + # create_plot('output00000275', output_folder='21_03_leader_follower_model_3_test/',output_plot=False, show_plot=False) + create_plot(files[i].split('_')[0], data_path, output_folder=save_path, output_plot=True, show_plot=False) + + # make the movie - see ffmpeg documentation for more information + + # consider saving as jpegs - https://blender.stackexchange.com/questions/148231/what-image-format-encodes-the-fastest-or-at-least-faster-png-is-too-slow + # consider compiling as movie instead of saving the files (all to increase processing speed) (then again, it was teh same speed) + + # consider not loading the unneeded data - and be sure to get rid of the unneeded fields!!! + + os.system( + 'ffmpeg -y -framerate 24 -i ' + save_path + 'output%08d.png -pix_fmt yuv420p -vf pad="width=ceil(iw/2)*2:height=ceil(ih/2)*2" "' + save_name + '.mp4"') + + +if __name__ == '__main__': + # auto call the create movive function using the current directory as the data path and save path, and with teh given name. + create_movie('.', '', 'anisotropy_3') diff --git a/analysis/extracellular_matrx_plotting/pyMCDS_ECM.py b/analysis/extracellular_matrx_plotting/pyMCDS_ECM.py new file mode 100644 index 0000000..d4733e2 --- /dev/null +++ b/analysis/extracellular_matrx_plotting/pyMCDS_ECM.py @@ -0,0 +1,794 @@ +import xml.etree.ElementTree as ET +import numpy as np +import pandas as pd +import scipy.io as sio +import sys +import warnings +from pathlib import Path + + +class pyMCDS: + """ + This class contains a dictionary of dictionaries that contains all of the + output from a single time step of a PhysiCell Model. This class assumes that + all output files are stored in the same directory. Data is loaded by reading + the .xml file for a particular timestep. + + Parameters + ---------- + xml_name: str + String containing the name of the xml file without the path + output_path: str, optional + String containing the path (relative or absolute) to the directory + where PhysiCell output files are stored (default= ".") + + Attributes + ---------- + data : dict + Hierarchical container for all of the data retrieved by parsing the xml + file and the files referenced therein. + """ + def __init__(self, xml_file, output_path='.'): + self.data = self._read_xml(xml_file, output_path) + + # METADATA RELATED FUNCTIONS + + def get_time(self): + return self.data['metadata']['current_time'] + + # MESH RELATED FUNCTIONS + + def get_mesh(self, flat=False): + """ + Return a meshgrid of the computational domain. Can return either full + 3D or a 2D plane for contour plots. + + Parameters + ---------- + flat : bool + If flat is set to true, we return only the x and y meshgrid. + Otherwise we return x, y, and z + + Returns + ------- + splitting : list length=2 if flat=True, else length=3 + Contains arrays of voxel center coordinates as meshgrid with shape + [nx_voxel, ny_voxel, nz_voxel] or [nx_voxel, ny_voxel] if flat=True. + """ + if flat == True: + xx = self.data['mesh']['x_coordinates'][:, :, 0] + yy = self.data['mesh']['y_coordinates'][:, :, 0] + + + + return [xx, yy] + + # if we dont want a plane just return appropriate values + else: + xx = self.data['mesh']['x_coordinates'] + yy = self.data['mesh']['y_coordinates'] + zz = self.data['mesh']['z_coordinates'] + + return [xx, yy, zz] + + def get_2D_mesh(self): + """ + This function returns the x, y meshgrid as two numpy arrays. It is + identical to get_mesh with the option flat=True + + Returns + ------- + splitting : list length=2 + Contains arrays of voxel center coordinates in x and y dimensions + as meshgrid with shape [nx_voxel, ny_voxel] + """ + + xx = self.data['mesh']['x_coordinates'][:, :, 0] + yy = self.data['mesh']['y_coordinates'][:, :, 0] + + return [xx, yy] + + def get_linear_voxels(self): + """ + Helper function to quickly grab voxel centers array stored linearly as + opposed to meshgrid-style. + """ + return self.data['mesh']['voxels']['centers'] + + def get_mesh_spacing(self): + """ + Returns the space in between voxel centers for the mesh in terms of the + mesh's spatial units. Assumes that voxel centers fall on integer values. + + Returns + ------- + dx : float + Distance between voxel centers in the same units as the other + spatial measurements + """ + centers = self.get_linear_voxels() + X = np.unique(centers[0, :]) + Y = np.unique(centers[1, :]) + Z = np.unique(centers[2, :]) + + dx = (X.max() - X.min()) / X.shape[0] + dy = (Y.max() - Y.min()) / Y.shape[0] + dz = (Z.max() - Z.min()) / Z.shape[0] + + if np.abs(dx - dy) > 1e-10 or np.abs(dy - dz) > 1e-10 \ + or np.abs(dx - dz) > 1e-10: + print('Warning: grid spacing may be axis dependent.') + + return round(dx) + + def get_containing_voxel_ijk(self, x, y, z): + """ + Internal function to get the meshgrid indices for the center of a voxel + that contains the given position. + + Note that pyMCDS stores meshgrids as 'cartesian' + (indexing='xy' in np.meshgrid) which means that we will have + to use these indices as [j, i, k] on the actual meshgrid objects + + Parameters + ---------- + x : float + x-coordinate for the position + y : float + y-coordinate for the position + z : float + z-coordinate for the position + + Returns + ------- + ijk : list length=3 + contains the i, j, and k indices for the containing voxel's center + """ + xx, yy, zz = self.get_mesh() + ds = self.get_mesh_spacing() + + if x > xx.max(): + warnings.warn('Position out of bounds: x out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting x = x_max!'.format(x, y, z)) + x = xx.max() + elif x < xx.min(): + warnings.warn('Position out of bounds: x out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting x = x_min!'.format(x, y, z)) + x = xx.min() + elif y > yy.max(): + warnings.warn('Position out of bounds: y out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting y = y_max!'.format(x, y, z)) + y = yy.max() + elif y < yy.min(): + warnings.warn('Position out of bounds: y out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting y = y_min!'.format(x, y, z)) + y = yy.min() + elif z > zz.max(): + warnings.warn('Position out of bounds: z out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting z = z_max!'.format(x, y, z)) + z = zz.max() + elif z < zz.min(): + warnings.warn('Position out of bounds: z out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting z = z_min!'.format(x, y, z)) + z = zz.min() + + i = np.round((x - xx.min()) / ds) + j = np.round((y - yy.min()) / ds) + k = np.round((z - zz.min()) / ds) + + ii, jj, kk = int(i), int(j), int(k) + + return [ii, jj, kk] + + ## MICROENVIRONMENT RELATED FUNCTIONS + + def get_substrate_names(self): + """ + Returns list of chemical species in microenvironment + + Returns + ------- + species_list : array (str), shape=[n_species,] + Contains names of chemical species in microenvironment + """ + species_list = [] + for name in self.data['continuum_variables']: + species_list.append(name) + + return species_list + + def get_concentrations(self, species_name, z_slice=None): + """ + Returns the concentration array for the specified chemical species + in the microenvironment. Can return either the whole 3D picture, or + a 2D plane of concentrations. + + Parameters + ---------- + species_name : str + Name of the chemical species for which to get concentrations + + z_slice : float + z-axis position to use as plane for 2D output. This value must match + a plane of voxel centers in the z-axis. + Returns + ------- + conc_arr : array (np.float) shape=[nx_voxels, ny_voxels, nz_voxels] + Contains the concentration of the specified chemical in each voxel. + The array spatially maps to a meshgrid of the voxel centers. + """ + if z_slice is not None: + # check to see that z_slice is a valid plane + zz = self.data['mesh']['z_coordinates'] + assert z_slice in zz, 'Specified z_slice {} not in z_coordinates'.format(z_slice) + + # do the processing if its ok + mask = zz == z_slice + full_conc = self.data['continuum_variables'][species_name]['data'] + conc_arr = full_conc[mask].reshape((zz.shape[0], zz.shape[1])) + else: + conc_arr = self.data['continuum_variables'][species_name]['data'] + + return conc_arr + + def get_concentrations_at(self, x, y, z): + """ + Return concentrations of each chemical species inside a particular voxel + that contains the point described in the arguments. + + Parameters + ---------- + x : float + x-position for the point of interest + y : float + y_position for the point of interest + z : float + z_position for the point of interest + + Returns + ------- + concs : array, shape=[n_substrates,] + array of concentrations in the order given by get_substrate_names() + """ + i, j, k = self.get_containing_voxel_ijk(x, y, z) + sub_name_list = self.get_substrate_names() + concs = np.zeros(len(sub_name_list)) + + for ix in range(len(sub_name_list)): + concs[ix] = self.get_concentrations(sub_name_list[ix])[j, i, k] + + return concs + + + ## CELL RELATED FUNCTIONS + + def get_cell_df(self): + """ + Builds DataFrame from data['discrete_cells'] + + Returns + ------- + cells_df : pd.Dataframe, shape=[n_cells, n_variables] + Dataframe containing the cell data for all cells at this time step + """ + cells_df = pd.DataFrame(self.data['discrete_cells']) + return cells_df + + def get_cell_variables(self): + """ + Returns the names of all of the cell variables tracked in ['discrete cells'] + dictionary + + Returns + ------- + var_list : list, shape=[n_variables] + Contains the names of the cell variables + """ + var_list = [] + for name in self.data['discrete_cells']: + var_list.append(name) + return var_list + + def get_cell_df_at(self, x, y, z): + """ + Returns a dataframe for cells in the same voxel as the position given by + x, y, and z. + + Parameters + ---------- + x : float + x-position for the point of interest + y : float + y_position for the point of interest + z : float + z_position for the point of interest + + Returns + ------- + vox_df : pd.DataFrame, shape=[n_cell_in_voxel, n_variables] + cell dataframe containing only cells in the same voxel as the point + specified by x, y, and z. + """ + ds = self.get_mesh_spacing() + xx, yy, zz = self.get_mesh() + i, j, k = self.get_containing_voxel_ijk(x, y, z) + x_vox = xx[j, i, k] + y_vox = yy[j, i, k] + z_vox = zz[j, i, k] + + cell_df = self.get_cell_df() + inside_voxel = ( (cell_df['position_x'] < x_vox + ds/2.) & + (cell_df['position_x'] > x_vox - ds/2.) & + (cell_df['position_y'] < y_vox + ds/2.) & + (cell_df['position_y'] > y_vox - ds/2.) & + (cell_df['position_z'] < z_vox + ds/2.) & + (cell_df['position_z'] > z_vox - ds/2.) ) + vox_df = cell_df[inside_voxel] + return vox_df + + #### ADDITIONAL LOADING: ECM data. Call load_ecm to call the individual methods en bloc and load the ECM data. + #### The individual functions procede load_ecm in this file. load_ecm is followed by the more "public" methods used + # ## to call up the pre-loaded data. + + def make_ECM_mesh(self, ecm_arr): + """ + Creates the ECM mesh from the original ECM data exported in custom ECM script to a .mat file. In theory, + this should only need called once, as ECM mesh does not change with time. + + REQUIRES .mat file loading prior + to calling. --> done in load_ecm. + + REQUIRES that ecm dictionary has already been added to self.data --> done in load_ecm. + + Parameters + ---------- + ecm_arr : Ndarray + loaded from .mat file. + + Returns + ------- + Nothing : + Makes the ECM mesh (grid) and loads it in as specific x, y, and z coordinates into dictionaries under + 'ecm'/'mesh'/'x_coordinates', and 'y_coordinates', and 'z_coordinates' + """ + + # Make mesh dict + self.data['ecm']['mesh'] = {} + + # Generate and store unique coordinates from the ECM mesh coordinates + x_coords, y_coords, z_coords = np.unique(ecm_arr[0,:]), np.unique(ecm_arr[1,:]), np.unique(ecm_arr[2,:])#, np.unique(zz) + + self.data['ecm']['mesh']['x_coordinates_vec'] = x_coords + self.data['ecm']['mesh']['y_coordinates_vec'] = y_coords + self.data['ecm']['mesh']['z_coordinates_vec'] = z_coords + + # Generate and store coordinates as meshgrid arrays + xx, yy, zz = np.meshgrid(x_coords, y_coords, z_coords) + + self.data['ecm']['mesh']['x_coordinates_mesh'] = xx + self.data['ecm']['mesh']['y_coordinates_mesh'] = yy + self.data['ecm']['mesh']['z_coordinates_mesh'] = zz + + def load_ECM_centers(self, ecm_arr): + """ + Loads ECM unit/voxel center from the original ECM data exported in custom ECM script to a .mat file. + requires .mat file loading prior to calling. In theory load_ECM_centers should only need called once, + as ECM mesh does not change with time. + + REQUIRES that ECM mesh dictionary already created (call 'make_ECM_mesh' to do this) + + + Parameters + ---------- + ecm_arr : 'Ndarray' + loaded from .mat file. + + Returns + ------- + Nothing : + Loads the ECM centers into dictionary under 'ecm'/'mesh'/'centers' + """ + + self.data['ecm']['mesh']['centers'] = {} + + self.data['ecm']['mesh']['centers'] = ecm_arr[:3, :] + + + def load_ECM_volumes(self, ecm_arr): + """ + NOT CURRENTLY IMPLEMENTED - not currently writing out ECM unit volumes. If it is decided to export volumes and one wants + them, follow the same pattern as the function 'load_ECM_centers' + + Would loads ECM unit/voxel volume from the original ECM data exported in custom ECM script to a .mat file. REQUIRES .mat file + loading prior to calling. In theory, this should only need called once, as ECM mesh does not change with time. + + Parameters + ---------- + ecm_arr : 'Ndarray' + loaded from .mat file. + + Returns + ------- + Nothing : + Loads the ECM centers into dictionary under 'ecm'/'mesh'/'volumes' + """ + + + def load_ECM_data_as_vectors(self, ecm_arr): + + """ + Loads actual ECM data - the anisotropy, density, and orientation vectors. This function stores them as + straight vectors, versus meshgrid arrays. REQUIRES that 'ecm' dictionary already be made. Call load_ecm to do this. + + :param ecm_arr: + loaded from .mat file. + :return: Nothing + Loads ECM data into the dictionary 'ECM_field_vectors' keyed under each field name. ECM orientation is stored + as 3 sets of scalar fields. + """ + + # Make dictionary names + self.data['ecm']['ECM_field_vectors'] = {} + self.data['ecm']['ECM_field_vectors']['anisotropy'] = {} + self.data['ecm']['ECM_field_vectors']['density'] = {} + self.data['ecm']['ECM_field_vectors']['x_fiber_orientation'] = {} + self.data['ecm']['ECM_field_vectors']['y_fiber_orientation'] = {} + self.data['ecm']['ECM_field_vectors']['z_fiber_orientation'] = {} + + self.data['ecm']['ECM_field_vectors']['anisotropy'] = ecm_arr[3,:] + self.data['ecm']['ECM_field_vectors']['density'] = ecm_arr[4,:] + self.data['ecm']['ECM_field_vectors']['x_fiber_orientation'] = ecm_arr[5,:] + self.data['ecm']['ECM_field_vectors']['y_fiber_orientation'] = ecm_arr[6,:] + self.data['ecm']['ECM_field_vectors']['z_fiber_orientation'] = ecm_arr[7,:] + + def load_ECM_data_as_meshgrid(self, ecm_arr): + """ + Loads ECM data as meshgrdi arrays. + + REQUIRES the fields be loaded as vectors - that is where the key names come + from. See 'load_ECM_data_as_vectors'. + + REQUIRES that the ECM coordinates/mesh is loaded. See 'make_ECM_mesh' + + REQUIRES that teh ECM centers are loaded. See 'load_ECM_centers' + + :param ecm_arr: + loaded from .mat file. + :return: Nothing + Loads ECM data into the dictionary 'ECM_fields' keyed under each field name. ECM orientation is stored + as 3 sets of scalar fields. All fields are loaded as mesh grids. + """ + + # Set up storage + self.data['ecm']['ECM_fields'] = {} + + # the first three fields are the x, y, and z coordinates respectively so they need jumped over + ecm_field_number = 3 + + # iterate over each data field + for field in self.data['ecm']['ECM_field_vectors']: + + #Set up data structure + self.data['ecm']['ECM_fields'][field] = np.zeros(self.data['ecm']['mesh']['x_coordinates_mesh'].shape) + + # iterate over each voxel + for vox_idx in range(self.data['ecm']['mesh']['centers'].shape[1]): + + # find the center + center = self.data['ecm']['mesh']['centers'][:, vox_idx] + + # use the center to find the cartesian indices of the voxel + i = np.where(np.abs(center[0] - self.data['ecm']['mesh']['x_coordinates_vec']) < 1e-10)[0][0] + j = np.where(np.abs(center[1] - self.data['ecm']['mesh']['y_coordinates_vec']) < 1e-10)[0][0] + k = np.where(np.abs(center[2] - self.data['ecm']['mesh']['z_coordinates_vec']) < 1e-10)[0][0] + + # Use this to make a dictionary with the Cartesian indices as keys to a dictionary containing the values + # if you declare the field to be a dictionary. Otherwise, as written and declared as a np array, it gives one a meshgric + # Note that pyMCDS stores meshgrids as 'cartesian'(indexing='xy' in np.meshgrid) which means that we + # will have to use these indices as [j, i, k] on the actual meshgrid objects + + self.data['ecm']['ECM_fields'][field][j, i, k] \ + = ecm_arr[ecm_field_number, vox_idx] + + ecm_field_number = ecm_field_number + 1 + + def load_ecm(self, ecm_file, output_path='.'): + """ + Does the actual work of initializing and loading the ECM data by starting the ecm data (data['ecm']) dictionary + and calling various functions to load into the *_ecm.mat file into that dictionary. + + When executed, all ECM information - the ECM attributes and mesh data - will be loaded into memory. + + Parameters + ---------- + ecm_file : string + ecm file name as a string + output_path : string + Path to ecm data file. + + Returns + ------- + Nothing : + Produces ECM data through several function calls. + """ + self.data['ecm'] = {} + read_file = Path(output_path) / ecm_file + ecm_arr = sio.loadmat(read_file)['ECM_Data'] + + self.make_ECM_mesh(ecm_arr) + self.load_ECM_centers(ecm_arr) + self.load_ECM_data_as_vectors(ecm_arr) + self.load_ECM_data_as_meshgrid(ecm_arr) + + def get_ECM_field(self, field_name, z_slice=None): + """ + Returns the ECM array for the specified chemical species + in the microenvironment. Can return either the whole 3D picture, or + a 2D plane of concentrations. + + Parameters + ---------- + species_name : str + Name of the ECM field of interest + + z_slice : float + z-axis position to use as plane for 2D output. This value must match + a plane of voxel centers in the z-axis. + Returns + ------- + conc_arr : array (np.float) shape=[nx_voxels, ny_voxels, nz_voxels] + Contains the quantitity of interest at each voxel. + The array spatially maps to a meshgrid of the voxel centers. + """ + if z_slice is not None: + # check to see that z_slice is a valid plane + zz = self.data['ecm']['mesh']['z_coordinates_mesh'] + assert z_slice in zz, 'Specified z_slice {} not in z_coordinates'.format(z_slice) + + # do the processing if its ok + mask = zz == z_slice + full_field = self.data['ecm']['ECM_fields'][field_name] + field_arr = full_field[mask].reshape((zz.shape[0], zz.shape[1])) + else: + field_arr = self.data['ecm']['ECM_fields'][field_name] + + return field_arr + + def get_2D_ECM_mesh(self): + """ + This function returns the x, y meshgrid as two numpy arrays. It is + identical to get_mesh with the option flat=True + + Returns + ------- + splitting : list length=2 + Contains arrays of voxel center coordinates in x and y dimensions + as meshgrid with shape [nx_voxel, ny_voxel] + """ + + xx = self.data['ecm']['mesh']['x_coordinates_mesh'][:, :, 0] + yy = self.data['ecm']['mesh']['y_coordinates_mesh'][:, :, 0] + + return [xx, yy] + + def _read_xml(self, xml_file, output_path='.'): + """ + Does the actual work of initializing MultiCellDS by parsing the xml + """ + + output_path = Path(output_path) + xml_file = output_path / xml_file + tree = ET.parse(xml_file) + + print('Reading {}'.format(xml_file)) + + root = tree.getroot() + MCDS = {} + + # Get current simulated time + metadata_node = root.find('metadata') + time_node = metadata_node.find('current_time') + MCDS['metadata'] = {} + MCDS['metadata']['current_time'] = float(time_node.text) + MCDS['metadata']['time_units'] = time_node.get('units') + + # Get current runtime + time_node = metadata_node.find('current_runtime') + MCDS['metadata']['current_runtime'] = float(time_node.text) + MCDS['metadata']['runtime_units'] = time_node.get('units') + + # find the microenvironment node + me_node = root.find('microenvironment') + me_node = me_node.find('domain') + + # find the mesh node + mesh_node = me_node.find('mesh') + MCDS['metadata']['spatial_units'] = mesh_node.get('units') + MCDS['mesh'] = {} + + # while we're at it, find the mesh + coord_str = mesh_node.find('x_coordinates').text + delimiter = mesh_node.find('x_coordinates').get('delimiter') + x_coords = np.array(coord_str.split(delimiter), dtype=np.float) + + coord_str = mesh_node.find('y_coordinates').text + delimiter = mesh_node.find('y_coordinates').get('delimiter') + y_coords = np.array(coord_str.split(delimiter), dtype=np.float) + + coord_str = mesh_node.find('z_coordinates').text + delimiter = mesh_node.find('z_coordinates').get('delimiter') + z_coords = np.array(coord_str.split(delimiter), dtype=np.float) + + # reshape into a mesh grid + xx, yy, zz = np.meshgrid(x_coords, y_coords, z_coords) + + MCDS['mesh']['x_coordinates'] = xx + MCDS['mesh']['y_coordinates'] = yy + MCDS['mesh']['z_coordinates'] = zz + + # Voxel data must be loaded from .mat file + voxel_file = mesh_node.find('voxels').find('filename').text + voxel_path = output_path / voxel_file + try: + initial_mesh = sio.loadmat(voxel_path)['mesh'] + except: + raise FileNotFoundError( + "No such file or directory:\n'{}' referenced in '{}'".format(voxel_path, xml_file)) + sys.exit(1) + + print('Reading {}'.format(voxel_path)) + + # center of voxel specified by first three rows [ x, y, z ] + # volume specified by fourth row + MCDS['mesh']['voxels'] = {} + MCDS['mesh']['voxels']['centers'] = initial_mesh[:3, :] + MCDS['mesh']['voxels']['volumes'] = initial_mesh[3, :] + + # Continuum_variables, unlike in the matlab version the individual chemical + # species will be primarily accessed through their names e.g. + # MCDS['continuum_variables']['oxygen']['units'] + # MCDS['continuum_variables']['glucose']['data'] + MCDS['continuum_variables'] = {} + variables_node = me_node.find('variables') + file_node = me_node.find('data').find('filename') + + # micro environment data is shape [4+n, len(voxels)] where n is the number + # of species being tracked. the first 3 rows represent (x, y, z) of voxel + # centers. The fourth row contains the voxel volume. The 5th row and up will + # contain values for that species in that voxel. + me_file = file_node.text + me_path = output_path / me_file + # Changes here + try: + me_data = sio.loadmat(me_path)['multiscale_microenvironment'] + except: + raise FileNotFoundError( + "No such file or directory:\n'{}' referenced in '{}'".format(me_path, xml_file)) + sys.exit(1) + + print('Reading {}'.format(me_path)) + + var_children = variables_node.findall('variable') + + # we're going to need the linear x, y, and z coordinates later + # but we dont need to get them in the loop + X, Y, Z = np.unique(xx), np.unique(yy), np.unique(zz) + + for si, species in enumerate(var_children): + species_name = species.get('name') + MCDS['continuum_variables'][species_name] = {} + MCDS['continuum_variables'][species_name]['units'] = species.get( + 'units') + + print('Parsing {:s} data'.format(species_name)) + + # initialize array for concentration data + MCDS['continuum_variables'][species_name]['data'] = np.zeros(xx.shape) + + # travel down one level on tree + species = species.find('physical_parameter_set') + + # diffusion data for each species + MCDS['continuum_variables'][species_name]['diffusion_coefficient'] = {} + MCDS['continuum_variables'][species_name]['diffusion_coefficient']['value'] \ + = float(species.find('diffusion_coefficient').text) + MCDS['continuum_variables'][species_name]['diffusion_coefficient']['units'] \ + = species.find('diffusion_coefficient').get('units') + + # decay data for each species + MCDS['continuum_variables'][species_name]['decay_rate'] = {} + MCDS['continuum_variables'][species_name]['decay_rate']['value'] \ + = float(species.find('decay_rate').text) + MCDS['continuum_variables'][species_name]['decay_rate']['units'] \ + = species.find('decay_rate').get('units') + + # store data from microenvironment file as numpy array + # iterate over each voxel + for vox_idx in range(MCDS['mesh']['voxels']['centers'].shape[1]): + # find the center + center = MCDS['mesh']['voxels']['centers'][:, vox_idx] + i_helper = np.where(np.abs(center[0] - X) < 1e-10)[0][0] + i = np.where(np.abs(center[0] - X) < 1e-10)[0][0] + j = np.where(np.abs(center[1] - Y) < 1e-10)[0][0] + k = np.where(np.abs(center[2] - Z) < 1e-10)[0][0] + + MCDS['continuum_variables'][species_name]['data'][j, i, k] \ + = me_data[4+si, vox_idx] + + # in order to get to the good stuff we have to pass through a few different + # hierarchal levels + cell_node = root.find('cellular_information') + cell_node = cell_node.find('cell_populations') + cell_node = cell_node.find('cell_population') + cell_node = cell_node.find('custom') + # we want the PhysiCell data, there is more of it + for child in cell_node.findall('simplified_data'): + if child.get('source') == 'PhysiCell': + cell_node = child + break + + MCDS['discrete_cells'] = {} + data_labels = [] + # iterate over 'label's which are children of 'labels' these will be used to + # label data arrays + for label in cell_node.find('labels').findall('label'): + # I don't like spaces in my dictionary keys + fixed_label = label.text.replace(' ', '_') + if int(label.get('size')) > 1: + # tags to differentiate repeated labels (usually space related) + dir_label = ['_x', '_y', '_z'] + for i in range(int(label.get('size'))): + data_labels.append(fixed_label + dir_label[i]) + else: + data_labels.append(fixed_label) + + # load the file + cell_file = cell_node.find('filename').text + cell_path = output_path / cell_file + try: + cell_data = sio.loadmat(cell_path)['cells'] + except: + raise FileNotFoundError( + "No such file or directory:\n'{}' referenced in '{}'".format(cell_path, xml_file)) + sys.exit(1) + + print('Reading {}'.format(cell_path)) + + for col in range(len(data_labels)): + MCDS['discrete_cells'][data_labels[col]] = cell_data[col, :] + + return MCDS + + +# scratch code + +#from make_ECM_mesh + + # add in centers and volumes + + # print('X coordiantes, Y coordinates, Z coordinetes shape') + # print(xx.shape, yy.shape, zz.shape) + # print(self.data['ecm']['mesh']['x_coordinates']) + # print(self.data['ecm']['mesh']['y_coordinates']) + # print(self.data['ecm']['mesh']['z_coordinates']) + # if flat == False: + # # x_coords = np.array(ecm_arr[0,:], dtype=np.float) #I need something like what is in load_ecm - I wish I oculd just explore the structure ... + # # y_coords = np.array(ecm_arr[1,:], dtype=np.float) + # x_coords, y_coords = np.unique(ecm_arr[0,:]), np.unique(ecm_arr[1,:])#, np.unique(zz) + # # ecm_arr[1,:] + # print('Shape of x_coords') + # print(x_coords.shape) + # xx, yy = np.meshgrid(x_coords, y_coords) + + # return [xx, yy] + + # else: + # xx = ecm_arr[0,:] #I need something like what is in load_ecm - I wish I oculd just explore the structure ... + # yy = ecm_arr[1,:] + # zz = ecm_arr[2,:] + + # return [xx, yy, zz] + # xx = self.data['ecm'][:, :, 0] #I need something like what is in load_ecm - I wish I oculd just explore the structure ... + # yy = self.data['ecm'][:, :, 0] + + # return [xx, yy] + + # # if we dont want a plane just return appropriate values + # else: + # xx = self.data['ecm']['x_coordinates'] + # yy = self.data['ecm']['y_coordinates'] + # zz = self.data['ecm']['z_coordinates'] From 59fc75e1d7651774e541975bf0cc9509bc508406 Mon Sep 17 00:00:00 2001 From: jpmetzca Date: Wed, 31 Mar 2021 16:54:10 -0400 Subject: [PATCH 07/15] Found bugs and fixed them. --- analysis/cell_track_plotter.py | 25 +++++++++++++++---- .../cell_plus_environment_plotter.py | 6 ++--- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/analysis/cell_track_plotter.py b/analysis/cell_track_plotter.py index a7e49ad..66f7818 100644 --- a/analysis/cell_track_plotter.py +++ b/analysis/cell_track_plotter.py @@ -46,16 +46,18 @@ def plot_cell_tracks(starting_index: int, sample_step_interval: int, number_of_s Save plot flag (required to produce a movie from resulting images) show_plot : Show plot flag (for processing many images in a loop, this should likely be set to false. Images have to be closed manually) - produce_for_panel : + produce_for_panel : Flag - calls tight_layout, increases axes font sizes, and plots without title. For using in panels of images where there will be captions. Returns ------- Null : Produces a png image from the input PhysiCell SVGs. + """ output_plot = output_plot show_plot = show_plot + produce_for_panel = True d={} # dictionary to hold all (x,y) positions of cells d_attributes = {} #dictionary to hold other attributes, like color (a data frame might be nice here in the long run ... ) \ @@ -182,9 +184,18 @@ def plot_cell_tracks(starting_index: int, sample_step_interval: int, number_of_s # Pull out the cell's location. If ID not already in stack to track, put in new cell in dictionary while applying coordinate transform. if (child.attrib['id'] in d.keys()): d[child.attrib['id']] = np.vstack((d[child.attrib['id']], [ float(circle.attrib['cx'])-x_coordinate_transform, float(circle.attrib['cy'])-y_coordinate_transform ])) + #### Comment out this else to produce single cell tracks else: d[child.attrib['id']] = np.array( [ float(circle.attrib['cx'])-x_coordinate_transform, float(circle.attrib['cy'])-y_coordinate_transform]) d_attributes[child.attrib['id']] = circle.attrib['fill'] + + ###### Uncomment this elif and else to produce single cell tracks + # elif (child.attrib['id'] == 'cell24'): + # d[child.attrib['id']] = np.array( [ float(circle.attrib['cx'])-x_coordinate_transform, float(circle.attrib['cy'])-y_coordinate_transform]) + # d_attributes[child.attrib['id']] = circle.attrib['fill'] + # else: + # break + ##### This 'break' statement is required to skip the nucleus circle. There are two circle attributes. \ ##### If both nuclear and cell boundary attributes are needed, this break NEEDS REMOVED!!!! break @@ -246,14 +257,14 @@ def plot_cell_tracks(starting_index: int, sample_step_interval: int, number_of_s plt.quiver(x[:-1], y[:-1], x[1:] - x[:-1], y[1:] - y[:-1], scale_units='xy', angles='xy', scale=1, minlength = 0.001, headwidth=1.5, headlength=4) #### Plot final cell position. MAY NOT TAKE RGB VALUES!!! - plt.scatter(x[-1],y[-1], s = 4.5, c = d_attributes[key]) + plt.scatter(x[-1], y[-1], s=85.0, c=d_attributes[key], alpha=0.7) #### used if history lenght is set to 0 and if in first frame of sequnece (there is no history) elif (len(d[key].shape) == 1): x = d[key][0] y = d[key][1] #### Plot final cell position. MAY NOT TAKE RGB VALUES!!! - plt.scatter(x, y, s = 4.5, c = d_attributes[key]) + plt.scatter(x, y, s=85.0, c=d_attributes[key], alpha=0.7) # plt.scatter(x, y, s=3.5, c=) else: @@ -266,12 +277,16 @@ def plot_cell_tracks(starting_index: int, sample_step_interval: int, number_of_s if produce_for_panel == False: title_str = "History from image " + str(starting_index) + " to image " + str(endpoint) + "; " + title_str # %"Starting at frame {}, sample interval of {} for {} total samples".format(number_of_samples, sample_step_interval, number_of_samples) + ax.set_xlabel('microns') + ax.set_ylabel('microns') plt.title(title_str) else: - fig.tight_layout() + plt.xticks(fontsize=20) plt.yticks(fontsize=20) - + ax.set_xlabel('microns', fontsize=20) + ax.set_ylabel('microns', fontsize=20) + fig.tight_layout() # could change to the custom in the movie output or some other more better output if desired. output_folder = '' snapshot = str(starting_index) + '_' + str(sample_step_interval) + '_' + str(number_of_samples) diff --git a/analysis/extracellular_matrx_plotting/cell_plus_environment_plotter.py b/analysis/extracellular_matrx_plotting/cell_plus_environment_plotter.py index 1b59b01..2322dcd 100644 --- a/analysis/extracellular_matrx_plotting/cell_plus_environment_plotter.py +++ b/analysis/extracellular_matrx_plotting/cell_plus_environment_plotter.py @@ -56,7 +56,7 @@ def create_plot(snapshot, folder, output_folder='.', output_plot=True, show_plot ###### Flags ###### - produce_for_panel = False + produce_for_panel = True #################################################################################################################### #################################### Load data ######################## @@ -175,8 +175,8 @@ def create_plot(snapshot, folder, output_folder='.', output_plot=True, show_plot if produce_for_panel == False: - ax.set_xlabel('x [micron]') - ax.set_ylabel('y [micron]') + ax.set_xlabel('microns') + ax.set_ylabel('microns') fig.colorbar(cs, ax=ax) plt.title(snapshot) # Carefully place the command to make the plot square AFTER the color bar has been added. From a10c5eaf488ffbc59fc210dbc6daefd5e7a67ea9 Mon Sep 17 00:00:00 2001 From: jpmetzca Date: Fri, 28 May 2021 13:08:18 -0400 Subject: [PATCH 08/15] Adding two new files - an image processing module and a script that uses it. This should let some one specify many different image outputs (using the options). The function generic_plotter is set up to produce images that include up to all these components: cells positions, cell positional history (tracks), a contour plot of oxygen, and a vector field rendered as a quiver plot. There are additional options that are ECM specific as well. The options in the attached file are setup to produce cell positions, cell tracks, and oxygen contours. Documentation and comments for the image proessing modules (as well as a setup.py file) are forthcoming developments on this project. --- .../image_processing_for_physicell_module.py | 1333 +++++++++++++++++ analysis/image_processing_script.py | 70 + 2 files changed, 1403 insertions(+) create mode 100644 analysis/image_processing_for_physicell_module.py create mode 100644 analysis/image_processing_script.py diff --git a/analysis/image_processing_for_physicell_module.py b/analysis/image_processing_for_physicell_module.py new file mode 100644 index 0000000..8242bec --- /dev/null +++ b/analysis/image_processing_for_physicell_module.py @@ -0,0 +1,1333 @@ +import math, os, sys, re +import xml.etree.ElementTree as ET +import numpy as np + +import matplotlib.pyplot as plt + +######## If using on remote system, uncomment this line below to load correct matplotlib backend ################ +# matplotlib.use('Agg') + +import matplotlib.colors as mplc +import matplotlib.colorbar as colorbar +import matplotlib as mpl +from mpl_toolkits.axes_grid1 import make_axes_locatable +from matplotlib.patches import Circle + +import distutils.util + +# from pyMCDS_ECM import * +try: + from pyMCDS_ECM import * +except ImportError: + from pyMCDS import * + +# Features for PhysiImage module +# Plots cells, tracks (as vectors???), and at least one microenvironment feature (ECM or otherwise) +# Allows for fine grain control of rate of plotting of tracks - start, end and interval +# Allows for fine grain control of outputs - quality, for insets, for videos +# has scale bar (ideally) +# preserves correctly scaled cell diamteers - DONE! working with SVG loader if cells are constant size. Must use other one otherwise. +# preserves cell colors (only in SVGs!!!!!!!!) and also allows for that to be overridden if needed +# Gets mat size and time/slide number from images +# Allows you to specify a title and add time/slide number to it +# plots color bar that isn't stupidly large (can I get something that returns a figure and then lets me change it in a script????) Ormaybe I can just write a bunch of different ones or use flags. Something to make it easier than it currently is - which it is currently assine. +# Be able to specify an output directory (might want to check that it is exsists (or not - does Python give an error?)) +# Add in module catch that says - ECM functionality will fail - load pyMCDS_ECM to use with ECM, otherwise your are fine + +class PhysiCellPlotter(): + + # # https://realpython.com/documenting-python-code/ + # https://stackoverflow.com/questions/37019744/is-there-a-consensus-what-should-be-documented-in-the-classes-and-init-docst + + def __init__(self, parent = None): + + """ + Initializes a plot using matplotlib pyplot subplot routine, returning a figure and axis handle to self. Provides a default figure size, title, and all + default options (self.default_options) required to make a plot that displays ONLY cell positions (and tracks if the generic_plotter is called with the appropriate variables). + self.default_options is used by generic_plotter to fill in any option values not set when pass the options to generic_plotter + + """ + self.figsize_width_svg = 7.0 + self.figsize_height_svg = 7.0 + self.title = "title" + self.fig, self.ax = plt.subplots(figsize=(self.figsize_width_svg, self.figsize_height_svg)) + self.default_options = {"output_plot": True, + "show_plot": True, + "produce_for_panel": False, + "load_SVG_data" : True, # cell color and positions + "load_full_physicell_data" : False, # The runs py_MCDS_ECM (ECM could be split out later if pyMCDS changes??) + "retrieve_first_chemical_field_data" : False, # Gets first chemical field from pyMCDS object. Eventually will probably want multiple sets of options - like "load this field" etc - maybe need an options class?? + "retrieve_ECM_data": False, # Gets ECM data from pyMCDS object + "plot_ECM_anisotropy" : False, # Calls contour plotter with anisotropy as input + 'plot_chemical_field': False, # Calls contour plotter with chemical field as input + "plot_ECM_orientation" : False, # calls quiver plotter with orientation as input + "plot_cells_from_SVG" : True, # plots cell positions and colors using data from SVGs + "plot_cells_from_physicell_data": False, # plots cell positions from pyMCDS --> will need more options if I want to specify colors ... currently set up to read color from SVG data + ####### Cell tracks are always plotted when calling plot_cells_from_svg - to not plot tracks - make the number of samples = 1 ... + "produce_for_movie" : False, + "contour_options": None, + "quiver_options": None} + + def generic_plotter(self, starting_index: int = 0, sample_step_interval: int = 1, number_of_samples: int = 120, + file_name: str = None, input_path: str= '.', output_path: str= '', naming_index: int=0, options=None): + + """ + Produces multlilayer image: allows for one cell layer, a contour layer (with colorbar), vector field, + and cell positional history, plotted as arrows (quiver plot) with final cell positions plotted as a cirle. + Options passed through a dictionary (see class consctructor for example). + + sample_step_interval * number_of_samples - starting_index yields the trail length in time steps. number_of_samples provides + the number of intervals plotted per image. + + Example: starting_index of 0, sample intervale of 1, and number of samples of 120 will produce a cell track 120 steps long, sampled at whatever rate the SVGs were produced, starting at + snapshot 0 going until snapshot 119. + + Parameters + ---------- + starting_index : + Integer index of the PhysiCell SVG output to begin trackign at. Default is 0. + sample_step_interval : + Interval (number of time steps (SVGs)) to sample at. A value of 2 would add a tracking point for every other SVG. Default is 1. + number_of_samples : + Total Number of SVGs to process. Length of cell positional history. Number_of_samples * sample_step_interval provides the index of the final SVG to process. Default is 120. + file_name : + Use to specify a non-default image output name. "produce_for_movie" option=True overrides both the default and given (if given) file name to allow for + required image names to make movie. Default is None, producing the default naming scheme. Example: for the default arguements: 0_1_120 (starting index, sample interval, number of samples). + input_path : + Sets input directory for .mat, xml, and SVG files. All data assumed to be in the same directory. Default values is the current/working directory. + NOT CURRENTLY IMMPLEMENTED FOR SVGs!!!!!!!!!! In future versions, plan to use os.chdir, but want to set up logic to help with this. + output_path : + Sets image output location. Default is current/working directory. + naming_index : + Special use variable to specify expected and ordered file names required to make movie from multiple output images. Default is 0. + options : + Diectinoary containing all options required to specify image to be produced. Default is None. Since the dictionary is requied, the default trigeers copying of the default_options, + specified in the PhysiCellPlotter default constructor. Basically, the defaults make an image with cells and cell histories only plotted. + + Returns + ------- + Null : + Produces a png image using specified PhysiCell inputs etc as specified in the options dictionary. + + """ + + self.fig, self.ax = plt.subplots(figsize=(self.figsize_width_svg, self.figsize_height_svg)) + + if options is None: + options = {"output_plot": True, + "show_plot": True, + "produce_for_panel": False, + "load_SVG_data" : True, # cell color and positions + "load_full_physicell_data" : False, # The runs py_MCDS_ECM (ECM could be split out later if pyMCDS changes??) + "retrieve_first_chemical_field_data" : False, # Gets first chemical field from pyMCDS object. Eventually will probably want multiple sets of options - like "load this field" etc - maybe need an options class?? + "retrieve_ECM_data": False, # Gets ECM data from pyMCDS object + "plot_ECM_anisotropy" : False, # Calls contour plotter with anisotropy as input + 'plot_chemical_field' : False, + "plot_ECM_orientation" : False, # calls quiver plotter with orientation as input + "plot_cells_from_SVG" : True, # plots cell positions and colors using data from SVGs + "plot_cells_from_physicell_data": False, # plots cell positions from pyMCDS --> will need more options if I want to specify colors ... currently set up to read color from SVG data + ####### Cell tracks are always plotted when calling plot_cells_from_svg - to not plot tracks - make the number of samples = 1 ... + "produce_for_movie" : False, + "contour_options": None, + "quiver_options": None} + else: + for key in self.default_options.keys(): + if key in options.keys(): + pass + else: + options[key] = self.default_options[key] + print(options[key]) ##### Add in something saying that defaults were used for this key value???. Then is there someway to get it to only do that once per call??? + print(key) + + # print("Current Working Directory " , os.getcwd()) + # os.chdir("/home/varun/temp") + + if options["load_SVG_data"] is True: + cell_positions, cell_attributes, title_str, plot_x_extend, plot_y_extend = self.load_cell_positions_from_SVG( + starting_index, sample_step_interval, number_of_samples) + print('Stil need to get input_path for SVGs working!!!') + + if options["load_SVG_data"] is False: + endpoint = starting_index + sample_step_interval * number_of_samples - 1 + final_snapshot_name = 'output' + f'{endpoint:08}' + print(final_snapshot_name) + title_str = 'some one should add extracting the file name from teh .mat files or similar to the code!!!' + plot_x_extend = 1000 + plot_y_extend = 1000 + print("WARNING!!!!!!!!!!! Plot extent is not dynamic!!!!!!!!!!!!!! Load from SVG to get dynamic changes OR change pyMCDS to get bounding box then change Load Physicell Data method!!!!!") + + else: + endpoint = starting_index + sample_step_interval * number_of_samples - 1 + final_snapshot_name = 'output' + f'{endpoint:08}' + print(final_snapshot_name) + title_str = "History from image " + str(starting_index) + " to image " + str(endpoint) + "; " + title_str + + if file_name is None: + file_name = str(starting_index) + '_' + str(sample_step_interval) + '_' + str(number_of_samples) + + if options["produce_for_movie"] is True: + file_name = snapshot = 'output' + f'{naming_index:08}' + print('Output file name forced to indexable name to produce movie') + + if options['load_full_physicell_data'] is True: + self.load_full_physicell_data(final_snapshot_name, folder=input_path) + print('test input path option!!!! (for loading physicell data...)') + + if options['retrieve_first_chemical_field_data'] is True: + xx, yy, plane_oxy = self.load_chemical_field('oxygen') + print('this call needs updated to use an option for putting in the chemical field name then defaulting ot oxygen perhaps for generic???') + if options['retrieve_ECM_data'] is True: + xx_ecm, yy_ecm, ECM_anisotropy, ECM_density, ECM_x_orientation, ECM_y_orientation = self.retreive_ECM_data() + # 1e-14, 1.0 + + # contour_spacing = np.linspace(contour_options['lowest_contour'], contour_options['upper_contour'], + # contour_options['number_of_levels']) + # + # cs = self.ax.contourf(x_mesh, y_mesh, data_to_contour, cmap=contour_options['color_map_name'], + # levels=contour_spacing) + + # if contour_options['color_bar'] is True: + if options['plot_chemical_field'] is True: + self.create_contour_plot(x_mesh=xx, y_mesh=yy, data_to_contour=plane_oxy, + contour_options=options["contour_options"], options=options) + if options['plot_ECM_anisotropy'] is True: + self.create_contour_plot(x_mesh=xx_ecm, y_mesh=yy_ecm, data_to_contour=ECM_anisotropy, contour_options=options["contour_options"], options=options) + + if options['plot_ECM_orientation'] is True: + self.create_quiver_plot(scaling_values=ECM_anisotropy, x_mesh=xx_ecm, y_mesh=yy_ecm, x_orientation=ECM_x_orientation, y_orientation=ECM_y_orientation, quiver_options=options['quiver_options']) + # Would be greato to pass kwargs here to teh plotting function, but can do that later ... I think maybe I can do some default behavior here?? + # And have a scaling inconsistency - but can deal with that later ... + # https://stackoverflow.com/questions/49887526/rescaling-quiver-arrows-in-physical-units-consistent-to-the-aspect-ratio-of-the/49891134 + if options['plot_cells_from_physicell_data'] is True: + self.plot_cells_from_physicell_data() + + if options['plot_cells_from_SVG'] is True: + self.create_cell_layer_from_SVG(cell_positions, cell_attributes) + + self.plot_figure(title_str, plot_x_extend, plot_y_extend, file_name, output_path, options) + + def plot_cells_from_physicell_data(self): + cell_df = self.mcds.get_cell_df() + cell_df['radius'] = (cell_df['total_volume'].values * 3 / (4 * np.pi)) ** (1 / 3) + types = cell_df['cell_type'].unique() + colors = ['yellow', 'blue'] + print('WARNING!!!!!! WARNING!!!!!!!!!! These colors are hard coded AND WONT WORK ON NON-ECM SIMS!!!!!') + # Add cells layer + for i, ct in enumerate(types): + plot_df = cell_df[cell_df['cell_type'] == ct] + for j in plot_df.index: + circ = Circle((plot_df.loc[j, 'position_x'], plot_df.loc[j, 'position_y']), + radius=plot_df.loc[j, 'radius'], color=colors[i], alpha=0.7) + # for a blue circle with a black edge + # circ = Circle((plot_df.loc[j, 'position_x'], plot_df.loc[j, 'position_y']), + # radius=plot_df.loc[j, 'radius'], alpha=0.7, edgecolor='black') + self.ax.add_artist(circ) + + def load_full_physicell_data (self, snapshot: str='output000000000', folder: str='.'): + # load cell and microenvironment data + self.mcds = pyMCDS(snapshot + '.xml', folder) + + # loads and reads ECM data + self.mcds.load_ecm(snapshot + '_ECM.mat', folder) + + def create_contour_plot(self, x_mesh: dict, y_mesh: dict, data_to_contour: dict, contour_options=None, options: dict=None): + ### best options are probably to just allow defaults, search for max and min for limits, or maybe insist on limits ... + ### another obvious option - and this coudl be a global to reset ... you could even change it with function calls + ### countour color maps ... + + if contour_options is None: + cs = self.ax.contourf(x_mesh, y_mesh, data_to_contour, cmap="Reds") + self.fig.colorbar(cs, ax=self.ax) + # self.fig.show() + else: + + # Make levels for contours + contour_spacing = np.linspace(contour_options['lowest_contour'], contour_options['upper_contour'], contour_options['number_of_levels']) + + cs = self.ax.contourf(x_mesh, y_mesh, data_to_contour, cmap=contour_options['color_map_name'], levels=contour_spacing) + + if contour_options['color_bar'] is True: + divider = make_axes_locatable(self.ax) + cax = divider.append_axes("right", size="5%", pad=0.10) + # other fancy things you can do with colorbars - https://stackoverflow.com/questions/16595138/standalone-colorbar-matplotlib + if options is None: + cb = self.fig.colorbar(cs, cax=cax, format='%.3f') + elif options['produce_for_panel'] is False: + cb = self.fig.colorbar(cs, cax=cax, format='%.3f') + else: + tick_spacing = np.linspace(contour_options['lowest_contour'], contour_options['upper_contour'], 5) + cb = self.fig.colorbar(cs, cax=cax, format='%.2f', ticks=tick_spacing) + cb.ax.tick_params(labelsize=20) + + def create_separate_colorbar(self, file_name='just_colorbar', contour_options: dict=None): + print('Working - gives continous colorbar instead of discrete - could fix possibly but not sure how to match N') + + if contour_options is not None: + contour_spacing = np.linspace(contour_options['lowest_contour'], contour_options['upper_contour'], + contour_options['number_of_levels']) + fig, ax = plt.subplots(figsize=(1, 8)) + cmap_str = 'mpl.cm.' + contour_options['color_map_name'] + + cmap = eval(cmap_str) + norm = mpl.colors.Normalize(vmin=contour_options['lowest_contour'], vmax=contour_options['upper_contour']) + cb = colorbar.ColorbarBase(ax, orientation='vertical', + cmap=cmap, norm=norm) + + plt.savefig(output_folder + file_name, bbox_inches='tight', dpi=256) + plt.show() + else: + print("you need to put in something for the color bar options. Supply \"contour_options\" to me!!!!") + + def create_quiver_plot(self, scaling_values: dict, x_mesh: dict, y_mesh: dict, x_orientation: dict, y_orientation: dict, quiver_options: dict=None): + + if quiver_options is None: + mask = scaling_values > 0.0001 + ECM_x = np.multiply(x_orientation, scaling_values) + ECM_y = np.multiply(y_orientation, scaling_values) + self.ax.quiver(x_mesh[mask], y_mesh[mask], ECM_x[mask], ECM_y[mask], + pivot='middle', angles='xy', scale_units='inches', scale=4.75, headwidth=0, alpha = 0.3) + else: + if quiver_options["scale_quiver"] is True: + ECM_x = np.multiply(x_orientation, scaling_values) + ECM_y = np.multiply(y_orientation, scaling_values) + else: + ECM_x = x_orientation + ECM_y = y_orientation + + # q = ax.quiver(xx_ecm[mask], yy_ecm[mask], scaled_ECM_x[mask], scaled_ECM_y[mask], pivot='middle', angles='xy', scale_units='inches', scale=2.0, headwidth=0, + # width=0.0015) ## What is the deal with the line segment lengths shifting as the plots progress when I don't ue teh scaling?? + + # mask out zero vectors + mask = scaling_values > 0.0001 + if quiver_options["mask_quiver"] is True: + self.ax.quiver(x_mesh[mask], y_mesh[mask], ECM_x[mask], ECM_y[mask], + pivot='middle', angles='xy', scale_units='inches', scale=4.75, headwidth=0, alpha = 0.3) + else: + self.ax.quiver(x_mesh, y_mesh, ECM_x, ECM_y, + pivot='middle', angles='xy', scale_units='inches', scale=4.75, headwidth=0, alpha = 0.3) + + def load_chemical_field(self, field_name: str=None): + + #### Diffusion microenvironment + xx, yy = self.mcds.get_2D_mesh() # Mesh + + if field_name is not None: + scalar_field_at_z_equals_zero = self.mcds.get_concentrations(field_name, 0.0) # Oxyen (used for contour plot) + else: + print('Must supply field name as a string to use \'load_chemical_field\' function.') + + + return xx, yy, scalar_field_at_z_equals_zero + + def retreive_ECM_data(self): + + #### ECM microenvironment + xx_ecm, yy_ecm = self.mcds.get_2D_ECM_mesh() # Mesh + anisotropy_at_z_equals_zero = self.mcds.get_ECM_field('anisotropy', 0.0) # Anistropy (used for scaling and contour plot) + density_at_z_equals_zero = self.mcds.get_ECM_field('density', 0.0) + x_orientation_at_z_equals_zero = self.mcds.data['ecm']['ECM_fields']['x_fiber_orientation'][:, :, 0] + y_orientation_at_z_equals_zero = self.mcds.data['ecm']['ECM_fields']['y_fiber_orientation'][:, :, 0] + + return xx_ecm, yy_ecm, anisotropy_at_z_equals_zero, density_at_z_equals_zero, x_orientation_at_z_equals_zero, y_orientation_at_z_equals_zero + + def plot_figure(self, title_str: str, plot_x_extend: float, plot_y_extend: float, file_name: str, output_directory: str='', options: dict=None): ###### This should probably have to have options??????? Why though??? + if options is None: + options= {"output_plot": True, + "show_plot": True, + "produce_for_panel": False + } + output_plot = options['output_plot'] + show_plot = options['show_plot'] + produce_for_panel = options['produce_for_panel'] + output_folder = '' + # print(output_folder.type) + # print(file_name.type) + # fig.figure(figsize=(7, 7)) + + self.ax.set_aspect("equal") + # endpoint = starting_index + sample_step_interval*number_of_samples + #### Build plot frame, titles, and save data + self.ax.set_ylim(-plot_y_extend/2, plot_y_extend/2) + self.ax.set_xlim(-plot_x_extend/2, plot_x_extend/2) + + if produce_for_panel == False: + # title_str = "History from image " + str(starting_index) + " to image " + str(endpoint) + "; " + title_str + # %"Starting at frame {}, sample interval of {} for {} total samples".format(number_of_samples, sample_step_interval, number_of_samples) + self.ax.set_title(title_str) + else: + self.ax.xaxis.set_tick_params(labelsize=20) + self.ax.yaxis.set_tick_params(labelsize=20) + self.ax.set_xlabel('microns', fontsize=20) + self.ax.set_ylabel('microns', fontsize=20) + self.ax.set_xticks([ -plot_x_extend/2, -plot_x_extend/4, 0, plot_x_extend/4 ,plot_x_extend/2]) + self.ax.set_yticks([ -plot_y_extend/2, -plot_y_extend/4, 0, plot_y_extend/4 ,plot_y_extend/2]) + self.fig.tight_layout() + # could change to the custom in the movie output or some other more better output if desired. + output_folder = output_directory + # if file_name is None: + # file_name = str(starting_index) + '_' + str(sample_step_interval) + '_' + str(number_of_samples) + + # Produce plot following the available options. + + if output_plot is True: + plt.savefig(output_folder + file_name + '.png', dpi=256) + if show_plot is True: + plt.show() + # self.fig.clf() + + def load_cell_positions_from_SVG(self, starting_index: int, sample_step_interval: int, number_of_samples: int): + """ + Produces savable image of cell positional history, plotted as arrows (quiver plot) with final cell positions plotted as a cirle. + Slight modification of the function in cell_track_plotter. The modification allows for tracking the index of a series + of inputs such that outputs of this function can be appropriate indexed and compiled into a movie. + + sample_step_interval * number_of_samples - starting_index yields the trail length in time steps. number_of_samples provides + the number of intervals plotted per image. + + Parameters + ---------- + starting_index : + Integer index of the PhysiCell SVG output to begin trackign at + sample_step_interval : + Interval (number of time steps (SVGs)) to sample at. A value of 2 would add a tracking point for every other SVG + number_of_samples : + Number of SVGs to process (total)/Length of cell positional history. Number of samples * sample size step interval provides the index of the final SVG to process + output_plot : + Save plot flag (required to produce a movie from resulting images) + show_plot : + Show plot flag (for processing many images in a loop, this should likely be set to false. Images have to be closed manually) + produce_for_panel : + Flag - calls tight_layout, increases axes font sizes, and plots without title. For using in panels of images where there will be captions. + Returns + ------- + Null : + Produces a png image from the input PhysiCell SVGs. + + """ + + # if options is None: + # options = {"output_plot": True, + # "show_plot": True, + # "produce_for_panel": False + # } + # output_plot = options['output_plot'] + # show_plot = options['show_plot'] + # produce_for_panel = options['produce_for_panel'] + + d = {} # dictionary to hold all (x,y) positions of cells + d_attributes = {} # dictionary to hold other attributes, like color (a data frame might be nice here in the long run ... ) \ + # currently only being read once only as cell dictionary is populated - so only use for static values! + + """ + --- for example --- + In [141]: d['cell1599'][0:3] + Out[141]: + array([[ 4900. , 4900. ], + [ 4934.17, 4487.91], + [ 4960.75, 4148.02]]) + """ + + #################################################################################################################### + #################################### Generate list of file indices to load ######################## + #################################################################################################################### + + endpoint = starting_index + sample_step_interval * number_of_samples + file_indices = np.linspace(starting_index, endpoint, num=number_of_samples, endpoint=False) + print(file_indices) + + maxCount = starting_index + + ####### Uncomment for statement below to generate a random list of file names versus the prespecifed list. ######## + ####### Leaving for historical record. If used, the inputs would need to be a single integer, ######## + ####### versus the three integers required to generate the prespecified list. Also, remove the other for statement. ######## + # count = 0 + # + # for fname in glob.glob('snapshot*.svg'): + # print(fname) + # # for fname in['snapshot00000000.svg', 'snapshot00000001.svg']: + # # for fname in['snapshot00000000.svg']: + # # print(fname) + # count += 1 + # if count > maxCount: + # break + + #################################################################################################################### + #################################### Main loading and processing loop ######################## + #################################################################################################################### + + for file_index in file_indices: + print(os.getcwd()) + fname = "%0.8d" % file_index + fname = 'snapshot' + fname + '.svg' # https://realpython.com/python-f-strings/ + print(fname) + + ##### Parse XML tree into a dictionary called 'tree" and get root + # print('\n---- ' + fname + ':') + tree = ET.parse(fname) + + # print('--- root.tag, root.attrib ---') + root = tree.getroot() + # print('--- root.tag ---') + # print(root.tag) + # print('--- root.attrib ---') + # print(root.attrib) + # print('--- child.tag, child.attrib ---') + + numChildren = 0 + + ### Find branches coming from root - tissue parents + for child in root: + # print(child.tag, child.attrib) + # print('attrib=',child.attrib) + # if (child.attrib['id'] == 'tissue'): + + if child.text and "Current time" in child.text: + svals = child.text.split() + title_str = "Current time: " + svals[2] + "d, " + svals[4] + "h, " + svals[ + 7] + "m" + + if 'width' in child.attrib.keys(): + #### Assumes 100% of difference in SVG width and height is due to top margin of the SVG!!!!!! + # print('Reading SVG - Assumes 100% of difference in SVG width and height is due to top margin of the SVG!!!!!!') + plot_x_extend = float(child.attrib['width']) + top_margin_size = abs(float(child.attrib['height']) - float(child.attrib['width'])) + + #### Remove the padding placed into the SVG to determine the true y extend + plot_y_extend = float(child.attrib['height']) - top_margin_size + + #### Find the coordinate transform amounts + y_coordinate_transform = plot_y_extend / 2 + x_coordinate_transform = plot_x_extend / 2 + + ##### Find the tissue tag and make it child + if 'id' in child.attrib.keys(): + # print('-------- found tissue!!') + tissue_parent = child + break + + # print('------ search tissue') + + ### find the branch with the cells "id=cells" among all the branches in the XML root + for child in tissue_parent: + # print('attrib=',child.attrib) + if (child.attrib['id'] == 'cells'): + # print('-------- found cells!!') + cells_parent = child + break + numChildren += 1 + + ### Search within the cells branch for all indiviual cells. Get their locations + num_cells = 0 + # print('------ search cells') + for child in cells_parent: + # print(child.tag, child.attrib) + # print('attrib=',child.attrib) + + # Find the locations of the cells within the cell tags + for circle in child: + # print(' --- cx,cy=',circle.attrib['cx'],circle.attrib['cy']) + xval = float(circle.attrib['cx']) + + # should we test for bogus x,y locations?? + if (math.fabs(xval) > 10000.): + print("xval=", xval) + break + yval = float(circle.attrib['cy']) # - y_coordinate_transform + if (math.fabs(yval) > 10000.): + print("yval=", yval) + break + + # Pull out the cell's location. If ID not already in stack to track, put in new cell in dictionary while applying coordinate transform. + if (child.attrib['id'] in d.keys()): + d[child.attrib['id']] = np.vstack((d[child.attrib['id']], + [float(circle.attrib['cx']) - x_coordinate_transform, + float(circle.attrib['cy']) - y_coordinate_transform])) + #### Comment out this else to produce single cell tracks + else: + d[child.attrib['id']] = np.array([float(circle.attrib['cx']) - x_coordinate_transform, + float(circle.attrib['cy']) - y_coordinate_transform]) + d_attributes[child.attrib['id']] = circle.attrib['fill'] + + ###### Uncomment this elif and else to produce single cell tracks + # elif (child.attrib['id'] == 'cell24'): + # d[child.attrib['id']] = np.array( [ float(circle.attrib['cx'])-x_coordinate_transform, float(circle.attrib['cy'])-y_coordinate_transform]) + # d_attributes[child.attrib['id']] = circle.attrib['fill'] + # else: + # break + + ##### This 'break' statement is required to skip the nucleus circle. There are two circle attributes. \ + ##### If both nuclear and cell boundary attributes are needed, this break NEEDS REMOVED!!!! + break + + ### Code to translate string based coloring to rgb coloring. Use as needed. + # s = circle.attrib['fill'] + # print("s=",s) + # print("type(s)=",type(s)) + # if (s[0:3] == "rgb"): # if an rgb string, e.g. "rgb(175,175,80)" + # # circle.attrib={'cx': '1085.59','cy': '1225.24','fill': 'rgb(159,159,96)','r': '6.67717','stroke': 'rgb(159,159,96)','stroke-width': '0.5'} + # rgb = list(map(int, s[4:-1].split(","))) + # rgb[:] = [x / 255. for x in rgb] + # else: # otherwise, must be a color name + # rgb_tuple = mplc.to_rgb(mplc.cnames[s]) # a tuple + # print(rgb_tuple) + # rgb = [x for x in rgb_tuple] + # print(rgb) + + # if (child.attrib['id'] == 'cells'): + # print('-------- found cells!!') + # tissue_child = child + + #### num_cells becomes total number of cells per frame/sample + num_cells += 1 + print(fname, ': num_cells= ', num_cells) + + return d, d_attributes, title_str, plot_x_extend, plot_y_extend + + def create_cell_layer_from_SVG(self, cell_positions: dict, cell_attributes: dict): + d = cell_positions + d_attributes = cell_attributes + + #################################################################################################################### + #################################### Plot cell tracks and other options ######################## + #################################################################################################################### + + # ax.set_xticks([]) + # ax.set_yticks([]); + # ax.set_xlim(0, 8); ax.set_ylim(0, 8) + + # print 'dir(fig)=',dir(fig) + # fig.set_figwidth(8) + # fig.set_figheight(8) + + count = 0 + + # weighting = np.linspace(0.0001, 3.5, num=number_of_samples) + # + # weighting = np.log10(weighting) + + ##### Extract and plot position data for each cell found + for key in d.keys(): + if (len(d[key].shape) == 2): + x = d[key][:, 0] + y = d[key][:, 1] + + # plt.plot(x, y,'-') # plot doesn't seem to allow weighting or size variation at all in teh connections ... # https://matplotlib.org/api/_as_gen/matplotlib.pyplot.arrow.html or https://stackoverflow.com/questions/7519467/line-plot-with-arrows-in-matplotlib + # plt.scatter(x, y, s = weighting) - scatter allows weighting but doens't connect ... + # plt.scatter(x, y, s=weighting) # could try a non-linear weighting ... + + #### Plot cell track as a directed, weighted (by length) path + self.ax.quiver(x[:-1], y[:-1], x[1:] - x[:-1], y[1:] - y[:-1], scale_units='xy', angles='xy', scale=1, + minlength=0.001, headwidth=1.5, headlength=4) + + #### Plot final cell position. MAY NOT TAKE RGB VALUES!!! + # self.ax.scatter(x[-1], y[-1], s=85.0, c=d_attributes[key], alpha=0.7) + + # Add cells layer + # for i, ct in enumerate(types): + # plot_df = cell_df[cell_df['cell_type'] == ct] + # for j in plot_df.index: + circ = Circle((x[-1], y[-1]), + radius=8.41271, color=d_attributes[key], alpha=0.7) + # for a blue circle with a black edge + # circ = Circle((plot_df.loc[j, 'position_x'], plot_df.loc[j, 'position_y']), + # radius=plot_df.loc[j, 'radius'], alpha=0.7, edgecolor='black') + self.ax.add_artist(circ) + + #### used if history lenght is set to 0 and if in first frame of sequnece (there is no history) + elif (len(d[key].shape) == 1): + x = d[key][0] + y = d[key][1] + #### Plot final cell position. MAY NOT TAKE RGB VALUES!!! + circ = Circle((x, y), + radius=8.41271, color=d_attributes[key], alpha=0.7) + self.ax.add_artist(circ) + # self.ax.scatter(x, y, s=85.0, c=d_attributes[key], alpha=0.7) + # plt.scatter(x, y, s=3.5, c=) + + else: + print(key, " has no x,y points") + + def create_figure_from_SVG (self, cell_positions: dict, cell_attributes: dict): + d = cell_positions + d_attributes = cell_attributes + + + #################################################################################################################### + #################################### Plot cell tracks and other options ######################## + #################################################################################################################### + + # ax.set_xticks([]) + # ax.set_yticks([]); + # ax.set_xlim(0, 8); ax.set_ylim(0, 8) + + # print 'dir(fig)=',dir(fig) + # fig.set_figwidth(8) + # fig.set_figheight(8) + + count = 0 + + # weighting = np.linspace(0.0001, 3.5, num=number_of_samples) + # + # weighting = np.log10(weighting) + + ##### Extract and plot position data for each cell found + for key in d.keys(): + if (len(d[key].shape) == 2): + x = d[key][:, 0] + y = d[key][:, 1] + + # plt.plot(x, y,'-') # plot doesn't seem to allow weighting or size variation at all in teh connections ... # https://matplotlib.org/api/_as_gen/matplotlib.pyplot.arrow.html or https://stackoverflow.com/questions/7519467/line-plot-with-arrows-in-matplotlib + # plt.scatter(x, y, s = weighting) - scatter allows weighting but doens't connect ... + # plt.scatter(x, y, s=weighting) # could try a non-linear weighting ... + + #### Plot cell track as a directed, weighted (by length) path + self.ax.quiver(x[:-1], y[:-1], x[1:] - x[:-1], y[1:] - y[:-1], scale_units='xy', angles='xy', scale=1, + minlength=0.001, headwidth=1.5, headlength=4) + + #### Plot final cell position. MAY NOT TAKE RGB VALUES!!! + self.ax.scatter(x[-1], y[-1], s=85.0, c=d_attributes[key], alpha=0.7) + + #### used if history lenght is set to 0 and if in first frame of sequnece (there is no history) + elif (len(d[key].shape) == 1): + x = d[key][0] + y = d[key][1] + #### Plot final cell position. MAY NOT TAKE RGB VALUES!!! + self.ax.scatter(x, y, s=85.0, c=d_attributes[key], alpha=0.7) + # plt.scatter(x, y, s=3.5, c=) + + else: + print(key, " has no x,y points") + + def produce_movie(self, data_path: str= '.', save_path: str= '', start_file_index: int = 0, sample_step_interval: int = 1, + end_file_index: int=120, trail_length: int=10, movie_options: dict=None, image_options: dict=None): + if movie_options is None: + movie_options = {'INCLUDE_ALL_SVGs': True, + 'INCLUDE_FULL_HISTORY': True + } + + if image_options is None: + image_options = {"produce_for_movie" : True, + "show_plot": False} + # movie_options['INCLUDE_ALL_SVGs'] = True + # movie_options['INCLUDE_FULL_HISTORY'] = True + + #### Get list of all file names in directory + + # data_path: str, save_path: str, save_name: str, start_file_index: int, end_file_index: int, + # trail_length: int, INCLUDE_ALL_SVGs: bool, INCLUDE_FULL_HISTORY: bool) + + # def generic_plotter(self, starting_index: int = 0, sample_step_interval: int = 1, number_of_samples: int = 120, + # file_name: str = None, input_path: str= '.', output_path: str= '', naming_index: int=0, options=None): + + files = os.listdir(data_path) + + list_of_svgs = [] + + #### examine all file names in directory and add ones, via string matching, as needed to list of names of files of interest + for i in range(len(files)): + if not re.search('snapshot(.*)\.svg', files[i]): + continue + + # I feel like a dictionary could be used here, but I really need some ordering. A dict might be faster, but I don't + # expect huge file lists. So I will just sort as I know how to do that ... + + list_of_svgs.append(files[i]) + + #### Sort file name list + list_of_svgs.sort() + + truncated_list_of_svgs = [] + + #### Reduce file list to times of interst only + for i in range(len(list_of_svgs)): + + if i < start_file_index: + continue + + if i >= end_file_index: + continue + + truncated_list_of_svgs.append(list_of_svgs[i]) + + # print(list_of_svgs) + print(truncated_list_of_svgs) + + if movie_options['INCLUDE_ALL_SVGs'] : + print('Including all SVGs') + truncated_list_of_svgs = list_of_svgs + + max_number_of_samples = trail_length + + if movie_options['INCLUDE_FULL_HISTORY']: + print('Including full positional history of cells') + max_number_of_samples = len(truncated_list_of_svgs) + + print('Processing {} SVGs'.format(len(truncated_list_of_svgs))) + + # Also, as written it isn't very flexible + # would certainly be ideal to not call plot_cell_tracks every time, but instead store what is available. Could add a function that just + # extracts the data from one SVG then appends it to exsisting data structure. could read all the desired data into Pandas DF + # then write out images. Etc. But as is, this is definitely reading the SVGs much to frequently. + + for i in range(len(truncated_list_of_svgs)): + j = i + 1 # this offsets the index so that we don't report that 0 samples have been taken, while stil producing an image. + starting_index = j - max_number_of_samples + + #### Goes with "trail closing" block - not currently being used. + projected_upper_sample_index = max_number_of_samples + starting_index + max_samples_left = len(truncated_list_of_svgs) - j + + # def generic_plotter(self, starting_index: int = 0, sample_step_interval: int = 1, number_of_samples: int = 120, + # file_name: str = None, input_path: str= '.', output_path: str= '', naming_index: int=0, options=None): + + if i >= max_number_of_samples: + self.generic_plotter(starting_index, 1, max_number_of_samples, naming_index=i, options=image_options) + # print('middle') + + #### If one wanted to make the trails collapse into the last available location of the cell you would use something + #### like this elif block + # elif projected_upper_sample_index > len(list_of_svgs)-1: + # plot_cell_tracks(starting_index, 1, max_samples_left, True, True, i) + # print(max_samples_left) + # print('late') + else: + self.generic_plotter(0, 1, j, naming_index=i, options=image_options) + # print('early') + + #### Total frames to include in moview + number_frames = end_file_index - start_file_index + + if movie_options['INCLUDE_ALL_SVGs']: + number_frames = len(list_of_svgs) + start_file_index = 0 + + # string_of_interest = 'ffmpeg -start_number ' + str( + # start_file_index) + ' -y -framerate 12 -i ' + save_path + 'output%08d.png' + ' -frames:v ' + str( + # number_frames) + ' -pix_fmt yuv420p -vf pad="width=ceil(iw/2)*2:height=ceil(ih/2)*2" "' + save_name + '.mp4"' + # print(string_of_interest) + os.system( + 'ffmpeg -start_number ' + str( + start_file_index) + ' -y -framerate 12 -i ' + save_path + 'output%08d.png' + ' -frames:v ' + str( + number_frames) + ' -pix_fmt yuv420p -vf pad="width=ceil(iw/2)*2:height=ceil(ih/2)*2" "' + save_name + '.mp4"') + + # https://superuser.com/questions/666860/clarification-for-ffmpeg-input-option-with-image-files-as-input + # https://superuser.com/questions/734976/ffmpeg-limit-number-of-images-converted-to-video + + + def general_image_plotter (filename: str=None, folder: str='.', output_folder='', cell_df: dict=None, cell_positions_from_SVG: dict=None, cell_attributes_from_SVG: dict=None, chemical_mesh: dict=None, ECM_mesh: dict=None, options=None): + if options is None: + options = {"output_plot": True, + "show_plot": True, + "produce_for_panel": False + } + output_plot = options['output_plot'] + show_plot = options['show_plot'] + produce_for_panel = options['produce_for_panel'] + + # Testing this + + d = cell_positions_from_SVG + d_attributes = cell_attributes_from_SVG + + #################################################################################################################### + #################################### Plot cell tracks and other options ######################## + #################################################################################################################### + + fig = plt.figure(figsize=(7, 7)) + ax = fig.gca() + ax.set_aspect("equal") + # ax.set_xticks([]) + # ax.set_yticks([]); + # ax.set_xlim(0, 8); ax.set_ylim(0, 8) + + # print 'dir(fig)=',dir(fig) + # fig.set_figwidth(8) + # fig.set_figheight(8) + + count = 0 + + # weighting = np.linspace(0.0001, 3.5, num=number_of_samples) + # + # weighting = np.log10(weighting) + + ##### Extract and plot position data for each cell found + for key in d.keys(): + if (len(d[key].shape) == 2): + x = d[key][:, 0] + y = d[key][:, 1] + + # plt.plot(x, y,'-') # plot doesn't seem to allow weighting or size variation at all in teh connections ... # https://matplotlib.org/api/_as_gen/matplotlib.pyplot.arrow.html or https://stackoverflow.com/questions/7519467/line-plot-with-arrows-in-matplotlib + # plt.scatter(x, y, s = weighting) - scatter allows weighting but doens't connect ... + # plt.scatter(x, y, s=weighting) # could try a non-linear weighting ... + + #### Plot cell track as a directed, weighted (by length) path + plt.quiver(x[:-1], y[:-1], x[1:] - x[:-1], y[1:] - y[:-1], scale_units='xy', angles='xy', scale=1, + minlength=0.001, headwidth=1.5, headlength=4) + + #### Plot final cell position. MAY NOT TAKE RGB VALUES!!! + plt.scatter(x[-1], y[-1], s=85.0, c=d_attributes[key], alpha=0.7) + + #### used if history lenght is set to 0 and if in first frame of sequnece (there is no history) + elif (len(d[key].shape) == 1): + x = d[key][0] + y = d[key][1] + #### Plot final cell position. MAY NOT TAKE RGB VALUES!!! + plt.scatter(x, y, s=85.0, c=d_attributes[key], alpha=0.7) + # plt.scatter(x, y, s=3.5, c=) + + else: + print(key, " has no x,y points") + + #### Build plot frame, titles, and save data + plt.ylim(-1000 / 2, 1000 / 2) + plt.xlim(-1000 / 2, 1000 / 2) + + if produce_for_panel == False: + title_str = "History from image " + str(starting_index) + " to image " + str(endpoint) + "; " + title_str + # %"Starting at frame {}, sample interval of {} for {} total samples".format(number_of_samples, sample_step_interval, number_of_samples) + plt.title(title_str) + else: + plt.xticks(fontsize=20) + plt.yticks(fontsize=20) + ax.set_xlabel('microns', fontsize=20) + ax.set_ylabel('microns', fontsize=20) + fig.tight_layout() + # could change to the custom in the movie output or some other more better output if desired. + output_folder = '' + if file_name is None: + file_name = str(starting_index) + '_' + str(sample_step_interval) + '_' + str(number_of_samples) + + # Produce plot following the available options. + + if output_plot is True: + plt.savefig(output_folder + file_name + '.png', dpi=256) + if show_plot is True: + plt.show() + # plt.close() + return fig + + ######################################################################################################################## + ######################################################################################################################## + ######################################################################################################################## + + def plot_cells_and_uE_for_movie (starting_index: int, sample_step_interval: int, number_of_samples: int, naming_index: int, options=None ): + if options is None: + options= {"output_plot": True, + "show_plot": False, + "produce_for_panel": True + } + + ### Now add in place to call the regular one, but with a string ... + file_name = 'output' + f'{naming_index:08}' + print(options) + plot_cell_tracks_from_svg (starting_index, sample_step_interval, number_of_samples, file_name, options) + # or a dictionary - and then I just modify the dictionary for the options or even have several of them + + + # 0 + # 1 + # 417 + # True + # True + # True + + # test_of_args_and_kwargs(0, 0, 0, output_plot='False') + + def plot_cell_tracks_from_svg(starting_index: int, sample_step_interval: int, number_of_samples: int, file_name: str=None, options=None ): + """ + Produces savable image of cell positional history, plotted as arrows (quiver plot) with final cell positions plotted as a cirle. + Slight modification of the function in cell_track_plotter. The modification allows for tracking the index of a series + of inputs such that outputs of this function can be appropriate indexed and compiled into a movie. + + sample_step_interval * number_of_samples - starting_index yields the trail length in time steps. number_of_samples provides + the number of intervals plotted per image. + + Parameters + ---------- + starting_index : + Integer index of the PhysiCell SVG output to begin trackign at + sample_step_interval : + Interval (number of time steps (SVGs)) to sample at. A value of 2 would add a tracking point for every other SVG + number_of_samples : + Number of SVGs to process (total)/Length of cell positional history. Number of samples * sample size step interval provides the index of the final SVG to process + output_plot : + Save plot flag (required to produce a movie from resulting images) + show_plot : + Show plot flag (for processing many images in a loop, this should likely be set to false. Images have to be closed manually) + produce_for_panel : + Flag - calls tight_layout, increases axes font sizes, and plots without title. For using in panels of images where there will be captions. + Returns + ------- + Null : + Produces a png image from the input PhysiCell SVGs. + + """ + + if options is None: + options= {"output_plot": True, + "show_plot": True, + "produce_for_panel": False + } + output_plot = options['output_plot'] + show_plot = options['show_plot'] + produce_for_panel = options['produce_for_panel'] + + d={} # dictionary to hold all (x,y) positions of cells + d_attributes = {} #dictionary to hold other attributes, like color (a data frame might be nice here in the long run ... ) \ + # currently only being read once only as cell dictionary is populated - so only use for static values! + + """ + --- for example --- + In [141]: d['cell1599'][0:3] + Out[141]: + array([[ 4900. , 4900. ], + [ 4934.17, 4487.91], + [ 4960.75, 4148.02]]) + """ + + #################################################################################################################### + #################################### Generate list of file indices to load ######################## + #################################################################################################################### + + endpoint = starting_index + sample_step_interval*number_of_samples + file_indices = np.linspace(starting_index, endpoint, num=number_of_samples, endpoint=False) + print(file_indices) + + maxCount = starting_index + + ####### Uncomment for statement below to generate a random list of file names versus the prespecifed list. ######## + ####### Leaving for historical record. If used, the inputs would need to be a single integer, ######## + ####### versus the three integers required to generate the prespecified list. Also, remove the other for statement. ######## + # count = 0 + # + # for fname in glob.glob('snapshot*.svg'): + # print(fname) + # # for fname in['snapshot00000000.svg', 'snapshot00000001.svg']: + # # for fname in['snapshot00000000.svg']: + # # print(fname) + # count += 1 + # if count > maxCount: + # break + + + #################################################################################################################### + #################################### Main loading and processing loop ######################## + #################################################################################################################### + + for file_index in file_indices: + fname = "%0.8d" % file_index + fname = 'snapshot' + fname + '.svg'# https://realpython.com/python-f-strings/ + print(fname) + + ##### Parse XML tree into a dictionary called 'tree" and get root + # print('\n---- ' + fname + ':') + tree=ET.parse(fname) + + # print('--- root.tag, root.attrib ---') + root=tree.getroot() + # print('--- root.tag ---') + # print(root.tag) + # print('--- root.attrib ---') + # print(root.attrib) + # print('--- child.tag, child.attrib ---') + + numChildren = 0 + + ### Find branches coming from root - tissue parents + for child in root: + # print(child.tag, child.attrib) + # print('attrib=',child.attrib) + # if (child.attrib['id'] == 'tissue'): + + if child.text and "Current time" in child.text: + svals = child.text.split() + title_str = "Current time: " + svals[2] + "d, " + svals[4] + "h, " + svals[ + 7] + "m" + + if 'width' in child.attrib.keys(): + #### Assumes a 70 length unit offsite inthe the Y dimension of the SVG!!!!!! + plot_x_extend = float(child.attrib['width']) + plot_y_extend = float(child.attrib['height']) + + #### Remove the padding placed into the SVG to determine the true y extend + plot_y_extend = plot_y_extend-70 + + #### Find the coordinate transform amounts + y_coordinate_transform = plot_y_extend/2 + x_coordinate_transform = plot_x_extend/2 + + ##### Find the tissue tag and make it child + if 'id' in child.attrib.keys(): + # print('-------- found tissue!!') + tissue_parent = child + break + + # print('------ search tissue') + + ### find the branch with the cells "id=cells" among all the branches in the XML root + for child in tissue_parent: + # print('attrib=',child.attrib) + if (child.attrib['id'] == 'cells'): + # print('-------- found cells!!') + cells_parent = child + break + numChildren += 1 + + ### Search within the cells branch for all indiviual cells. Get their locations + num_cells = 0 + # print('------ search cells') + for child in cells_parent: + # print(child.tag, child.attrib) + # print('attrib=',child.attrib) + + # Find the locations of the cells within the cell tags + for circle in child: + # print(' --- cx,cy=',circle.attrib['cx'],circle.attrib['cy']) + xval = float(circle.attrib['cx']) + + # should we test for bogus x,y locations?? + if (math.fabs(xval) > 10000.): + print("xval=",xval) + break + yval = float(circle.attrib['cy']) #- y_coordinate_transform + if (math.fabs(yval) > 10000.): + print("yval=",yval) + break + + # Pull out the cell's location. If ID not already in stack to track, put in new cell in dictionary while applying coordinate transform. + if (child.attrib['id'] in d.keys()): + d[child.attrib['id']] = np.vstack((d[child.attrib['id']], [ float(circle.attrib['cx'])-x_coordinate_transform, float(circle.attrib['cy'])-y_coordinate_transform ])) + #### Comment out this else to produce single cell tracks + else: + d[child.attrib['id']] = np.array( [ float(circle.attrib['cx'])-x_coordinate_transform, float(circle.attrib['cy'])-y_coordinate_transform]) + d_attributes[child.attrib['id']] = circle.attrib['fill'] + + ###### Uncomment this elif and else to produce single cell tracks + # elif (child.attrib['id'] == 'cell24'): + # d[child.attrib['id']] = np.array( [ float(circle.attrib['cx'])-x_coordinate_transform, float(circle.attrib['cy'])-y_coordinate_transform]) + # d_attributes[child.attrib['id']] = circle.attrib['fill'] + # else: + # break + + ##### This 'break' statement is required to skip the nucleus circle. There are two circle attributes. \ + ##### If both nuclear and cell boundary attributes are needed, this break NEEDS REMOVED!!!! + break + + ### Code to translate string based coloring to rgb coloring. Use as needed. + # s = circle.attrib['fill'] + # print("s=",s) + # print("type(s)=",type(s)) + # if (s[0:3] == "rgb"): # if an rgb string, e.g. "rgb(175,175,80)" + # # circle.attrib={'cx': '1085.59','cy': '1225.24','fill': 'rgb(159,159,96)','r': '6.67717','stroke': 'rgb(159,159,96)','stroke-width': '0.5'} + # rgb = list(map(int, s[4:-1].split(","))) + # rgb[:] = [x / 255. for x in rgb] + # else: # otherwise, must be a color name + # rgb_tuple = mplc.to_rgb(mplc.cnames[s]) # a tuple + # print(rgb_tuple) + # rgb = [x for x in rgb_tuple] + # print(rgb) + + # if (child.attrib['id'] == 'cells'): + # print('-------- found cells!!') + # tissue_child = child + + #### num_cells becomes total number of cells per frame/sample + num_cells += 1 + print(fname,': num_cells= ',num_cells) + + #################################################################################################################### + #################################### Plot cell tracks and other options ######################## + #################################################################################################################### + + fig = plt.figure(figsize=(7,7)) + ax = fig.gca() + ax.set_aspect("equal") + #ax.set_xticks([]) + #ax.set_yticks([]); + #ax.set_xlim(0, 8); ax.set_ylim(0, 8) + + #print 'dir(fig)=',dir(fig) + #fig.set_figwidth(8) + #fig.set_figheight(8) + + count = 0 + + # weighting = np.linspace(0.0001, 3.5, num=number_of_samples) + # + # weighting = np.log10(weighting) + + ##### Extract and plot position data for each cell found + for key in d.keys(): + if (len(d[key].shape) == 2): + x = d[key][:,0] + y = d[key][:,1] + + # plt.plot(x, y,'-') # plot doesn't seem to allow weighting or size variation at all in teh connections ... # https://matplotlib.org/api/_as_gen/matplotlib.pyplot.arrow.html or https://stackoverflow.com/questions/7519467/line-plot-with-arrows-in-matplotlib + # plt.scatter(x, y, s = weighting) - scatter allows weighting but doens't connect ... + # plt.scatter(x, y, s=weighting) # could try a non-linear weighting ... + + #### Plot cell track as a directed, weighted (by length) path + plt.quiver(x[:-1], y[:-1], x[1:] - x[:-1], y[1:] - y[:-1], scale_units='xy', angles='xy', scale=1, minlength = 0.001, headwidth=1.5, headlength=4) + + #### Plot final cell position. MAY NOT TAKE RGB VALUES!!! + plt.scatter(x[-1], y[-1], s=85.0, c=d_attributes[key], alpha=0.7) + + #### used if history lenght is set to 0 and if in first frame of sequnece (there is no history) + elif (len(d[key].shape) == 1): + x = d[key][0] + y = d[key][1] + #### Plot final cell position. MAY NOT TAKE RGB VALUES!!! + plt.scatter(x, y, s=85.0, c=d_attributes[key], alpha=0.7) + # plt.scatter(x, y, s=3.5, c=) + + else: + print(key, " has no x,y points") + + #### Build plot frame, titles, and save data + plt.ylim(-plot_y_extend/2, plot_y_extend/2) + plt.xlim(-plot_x_extend/2, plot_x_extend/2) + + if produce_for_panel == False: + title_str = "History from image " + str(starting_index) + " to image " + str(endpoint) + "; " + title_str + # %"Starting at frame {}, sample interval of {} for {} total samples".format(number_of_samples, sample_step_interval, number_of_samples) + plt.title(title_str) + else: + plt.xticks(fontsize=20) + plt.yticks(fontsize=20) + ax.set_xlabel('microns', fontsize=20) + ax.set_ylabel('microns', fontsize=20) + fig.tight_layout() + # could change to the custom in the movie output or some other more better output if desired. + output_folder = '' + if file_name is None: + file_name = str(starting_index) + '_' + str(sample_step_interval) + '_' + str(number_of_samples) + + # Produce plot following the available options. + + if output_plot is True: + plt.savefig(output_folder + file_name + '.png', dpi=256) + if show_plot is True: + plt.show() + # plt.close() + + def cell_history_movie_generator_from_SVG(data_path: str, save_path: str, save_name: str, start_file_index: int, end_file_index: int, + trail_length: int, INCLUDE_ALL_SVGs: bool, INCLUDE_FULL_HISTORY: bool): + + """ + Generates the list of files in data_path, finds the relevant SVGs, makes plots from them, then outputs an + ffmpeg generated movie to save_path, naming the movie save_name. + + This function requires ffmpeg be installed at the command line. + + + :param data_path: Path to directory containing data + :param save_path: Path to save generated image(s) and movie to + :param save_name: Save name for movie + :param start_file_index: For the plotting call - Integer index of the PhysiCell SVG output to begin tracking at + :param end_file_index: For the plotting call - Integer index of last PhysiCell SVG output to include in movie + :param trail_length: For the plotting call - Length (in output steps) of cell positional history to include in movie + :param INCLUDE_ALL_SVGs: If true, all findable PhysiCell SVGs are processed and included in movie + :param INCLUDE_FULL_HISTORY: If true, the entire available cell history is included, regardless of the value of trail length. + :return: Null. Produces a series of images from PhysiCell SVGs and movie from said images. + """ + + #### Flags (for cell track plotter calls) + + output_plot = True + show_plot = False + produce_for_panel = False + + #### Get list of all file names in directory + files = os.listdir(data_path) + + list_of_svgs = [] + + #### examine all file names in directory and add ones, via string matching, as needed to list of names of files of interest + for i in range(len(files)): + if not re.search('snapshot(.*)\.svg', files[i]): + continue + + # I feel like a dictionary could be used here, but I really need some ordering. A dict might be faster, but I don't + # expect huge file lists. So I will just sort as I know how to do that ... + + list_of_svgs.append(files[i]) + + #### Sort file name list + list_of_svgs.sort() + + truncated_list_of_svgs = [] + + #### Reduce file list to times of interst only + for i in range(len(list_of_svgs)): + + if i < start_file_index: + continue + + if i >= end_file_index: + continue + + truncated_list_of_svgs.append(list_of_svgs[i]) + + # print(list_of_svgs) + print(truncated_list_of_svgs) + + if INCLUDE_ALL_SVGs: + print('Including all SVGs') + truncated_list_of_svgs = list_of_svgs + + max_number_of_samples = trail_length + + if INCLUDE_FULL_HISTORY: + print('Including full positional history of cells') + max_number_of_samples = len(truncated_list_of_svgs) + + print('Processing {} SVGs'.format(len(truncated_list_of_svgs))) + + # Also, as written it isn't very flexible + # would certainly be ideal to not call plot_cell_tracks every time, but instead store what is available. Could add a function that just + # extracts the data from one SVG then appends it to exsisting data structure. could read all the desired data into Pandas DF + # then write out images. Etc. But as is, this is definitely reading the SVGs much to frequently. + + for i in range(len(truncated_list_of_svgs)): + j = i + 1 # this offsets the index so that we don't report that 0 samples have been taken, while stil producing an image. + starting_index = j - max_number_of_samples + + #### Goes with "trail closing" block - not currently being used. + projected_upper_sample_index = max_number_of_samples + starting_index + max_samples_left = len(truncated_list_of_svgs) - j + + if i >= max_number_of_samples: + plot_cell_tracks_for_movie(starting_index, 1, max_number_of_samples, output_plot, show_plot, i, + produce_for_panel) + # print('middle') + + #### If one wanted to make the trails collapse into the last available location of the cell you would use something + #### like this elif block + # elif projected_upper_sample_index > len(list_of_svgs)-1: + # plot_cell_tracks(starting_index, 1, max_samples_left, True, True, i) + # print(max_samples_left) + # print('late') + else: + plot_cell_tracks_for_movie(0, 1, j, output_plot, show_plot, i, produce_for_panel) + # print('early') + + #### Total frames to include in moview + number_frames = end_file_index - start_file_index + + if INCLUDE_ALL_SVGs: + number_frames = len(list_of_svgs) + start_file_index = 0 + + # string_of_interest = 'ffmpeg -start_number ' + str( + # start_file_index) + ' -y -framerate 12 -i ' + save_path + 'output%08d.png' + ' -frames:v ' + str( + # number_frames) + ' -pix_fmt yuv420p -vf pad="width=ceil(iw/2)*2:height=ceil(ih/2)*2" "' + save_name + '.mp4"' + # print(string_of_interest) + os.system( + 'ffmpeg -start_number ' + str( + start_file_index) + ' -y -framerate 12 -i ' + save_path + 'output%08d.png' + ' -frames:v ' + str( + number_frames) + ' -pix_fmt yuv420p -vf pad="width=ceil(iw/2)*2:height=ceil(ih/2)*2" "' + save_name + '.mp4"') + + # https://superuser.com/questions/666860/clarification-for-ffmpeg-input-option-with-image-files-as-input + # https://superuser.com/questions/734976/ffmpeg-limit-number-of-images-converted-to-video + + diff --git a/analysis/image_processing_script.py b/analysis/image_processing_script.py new file mode 100644 index 0000000..dde1ef5 --- /dev/null +++ b/analysis/image_processing_script.py @@ -0,0 +1,70 @@ +import sys +import matplotlib.pyplot as plt + +# To use the imaging module wihtout having to put it in every directory for analysis, put in the absolute path below. That directory +# will also need either 'pyMCDS.py' or 'pyMCDS_ECM.py'. Otherwise, place 'imaging_processing_for_phyiscell.py' and either MCDS file +# in the current working directory. + +# sys.path.append(r'') + +from image_processing_for_physicell import * + +options_for_figure = {} + +options_for_figure = {"output_plot" : True, + "show_plot" : True, + "produce_for_panel" : False, + "plot_ECM_anisotropy" : False, + "plot_ECM_orientation" : False, + "retrieve_ECM_data": False, + "retrieve_first_chemical_field_data" : True, + 'plot_chemical_field' : True, + "load_full_physicell_data" : True, + "plot_cells_from_SVG" : True, + "contour_options" : {'lowest_contour': 0.0, ### I woud like this to be cleaner - but it does work!!! + 'upper_contour': 38, + 'number_of_levels': 38, + 'color_map_name': 'summer', + 'color_bar': True + }, + "quiver_options" : None + } + +#### Right now, if you don't have None or the full contour and quiver options, it will break in the plotting ... I wonder if there +#### is a better/more robust way to do it (kwargs???, lots of "trapping"??) but this will be handled later ... and I can ask Randy etc +### What is up with scaling - hum ... + +# oof - I got different results on the two runs when I did and didn't scale by anistoropy ... yikes! How do I manage that!! + +mf = PhysiCellPlotter() + +# options[''] + +# plot_cells_and_uE_for_movie (0, 1, 10, 1999) +# +# +# plot_cell_tracks_from_svg(0, 1, 10) +# general_image_plotter (filename: str, folder: str='.', output_folder='', cell_df: dict=None, cell_positions_from_SVG: dict=None, chemical_mesh: dict=None, ECM_mesh: dict=None, options=None): + +image_list_for_figure = [] + +image_list_for_figure = [100] + +for number in image_list_for_figure: + mf.generic_plotter(starting_index=0, number_of_samples=number, options=options_for_figure) +# generic_plotter (start, intervnal, finish, save_filename, data_path, save_path, options) +# +# All based on options/logic- function +# load_cell_positiondata +# load_uE_data_chemical +# load_uE_data_ECM +# +# process data into plots - functions +# - cell tracks (might be loaded by just be plotted???) +# - cell positions +# - ECM layer +# - chemical layer +# +# complete plot presentaiont and save (maybe functions) +# - title +# - axes \ No newline at end of file From 11cf547dc96730d751669de40d7a9910cb7e6828 Mon Sep 17 00:00:00 2001 From: Randy Heiland Date: Wed, 30 Jun 2021 06:36:50 -0400 Subject: [PATCH 09/15] ID attrib --- params_biorobots.txt | 8 ++++ params_run.py | 97 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 params_biorobots.txt create mode 100644 params_run.py diff --git a/params_biorobots.txt b/params_biorobots.txt new file mode 100644 index 0000000..c5908bd --- /dev/null +++ b/params_biorobots.txt @@ -0,0 +1,8 @@ +# File to be used with params_run.py +# Allows for changing parameters in .xml, running sim, and writing results to different folders. +# pairs, where is the first unique node name found in the xml. +folder out_bots_attrib +max_time 30 +microenvironment_setup.variable[@ID='1'].decay_rate 0.442 +cell_definition[@ID='3'].migration_bias 0.542 +run_it dummy \ No newline at end of file diff --git a/params_run.py b/params_run.py new file mode 100644 index 0000000..2100d1e --- /dev/null +++ b/params_run.py @@ -0,0 +1,97 @@ +# params_run.py: +# +# This Python script provides simple parameter exploration functionality. The script creates +# a new folder (subdirectory) for each set of parameters, makes changes to a default +# configuration (.xml) file using specified parameter values (in an accompanying .txt file), +# copies the new config file into the new folder, then +# runs the simulation (optionally, in the background) which writes simulation results into the new folder. +# +# Author: Randy Heiland + +import xml.etree.ElementTree as ET +from shutil import copyfile +import os +import sys +import subprocess + +# print(len(sys.argv)) +if (len(sys.argv) < 3): + usage_str = "Usage: %s " % (sys.argv[0]) + print(usage_str) + print("e.g.: python params_run.py biorobots params_biorobots.txt") + exit(1) +else: + exec_pgm = sys.argv[1] + params_file = sys.argv[2] + +sequential_flag = 1 # if =1, do runs sequentially, i.e., not in background + +# background_str = " &" # works on Unix +# if sys.platform == 'win32': +# background_str = "" + + +xml_file_in = 'config/PhysiCell_settings.xml' +xml_file_out = 'config/tmp.xml' +copyfile(xml_file_in, xml_file_out) +tree = ET.parse(xml_file_out) +xml_root = tree.getroot() +first_time = True +output_dirs = [] +with open(params_file) as f: + for line in f: + # print(len(line),line) + print(line, end="") + if (line[0] == '#'): + continue + (key, val) = line.split() + if (key == 'run_it'): + # write the config file to the previous folder (output) dir and start a simulation + print('\n\n---> write config file (and start sim): ', xml_file_out) + tree.write(xml_file_out) # will create folder_name/config.xml + log_file = folder_name + ".log" + if sequential_flag > 0: + # cmd = exec_pgm + " " + xml_file_out + " > " + log_file + " " + background_str + cmd = exec_pgm + " " + xml_file_out + " > " + log_file + print("Doing sequential run...") + print("cmd = ",cmd) + os.system(cmd) # <------ Execute the simulation + else: # put (multiple) runs in the background + with open(log_file,"w") as outf: + subprocess.Popen([exec_pgm, xml_file_out],stdout=outf) + elif ('.' in key): + k = key.split('.') + # print("---- found keys path: ",k) + # print("---- val: ",val) + uep = xml_root + full_path = '.' # we build up a (unique) "full_path" to a param that will have its value changed. + for idx in range(len(k)): + # uep = uep.find('.//' + k[idx]) # unique entry point (uep) into xml + # if "@" in k[idx]: + # print("---------- found @") + full_path += '//' + k[idx] # unique entry point (uep) into xml +# print(k[idx]) + # print("---- full_path: ",full_path) + + # uep = xml_root.find(".//microenvironment_setup//variable[@ID='1']//physical_parameter_set//decay_rate") + # uep = xml_root.find(".//microenvironment_setup//variable[@ID='1']//decay_rate") + uep = xml_root.find(full_path) # uep: unique entry point + # print("uep = ",uep) + uep.text = val # unique entry point (uep) into xml + else: + if (key == 'folder'): + folder_name = val + output_dirs.append(folder_name) + if (not os.path.exists(folder_name)): + print("--- parsed 'folder', makedir " + folder_name) + os.makedirs(folder_name) + xml_file_out = os.path.join(folder_name, 'config.xml') # copy config file into the output dir + + try: + xml_root.find('.//' + key).text = val + except: + print("--- Error: could not find ",key," in .xml\n") + sys.exit(1) + +print("\n ------\n Your output results will appear in these directories:\n ",output_dirs) +print("and check for a .log file of each name for your terminal output from each simulation.\n") From 18889c5128cde849d7cdb0577581478706354d0a Mon Sep 17 00:00:00 2001 From: Randy Heiland Date: Wed, 30 Jun 2021 06:38:59 -0400 Subject: [PATCH 10/15] ID attrib --- .../params_biorobots.txt | 0 analysis/params_run.py | 110 ++++++++++-------- params_run.py | 97 --------------- 3 files changed, 64 insertions(+), 143 deletions(-) rename params_biorobots.txt => analysis/params_biorobots.txt (100%) delete mode 100644 params_run.py diff --git a/params_biorobots.txt b/analysis/params_biorobots.txt similarity index 100% rename from params_biorobots.txt rename to analysis/params_biorobots.txt diff --git a/analysis/params_run.py b/analysis/params_run.py index 5acccae..2100d1e 100644 --- a/analysis/params_run.py +++ b/analysis/params_run.py @@ -1,8 +1,10 @@ -# This script provides a simple approach to running multiple simulations with different -# parameter values. The script creates a new folder (subdirectory) for each set of parameters, -# makes changes to a default configuration (.xml) file using specified parameter values (in an -# accompanying .txt file), copies the new config file into the new folder, then -# runs the simulation (in the background) which writes results into the new folder. +# params_run.py: +# +# This Python script provides simple parameter exploration functionality. The script creates +# a new folder (subdirectory) for each set of parameters, makes changes to a default +# configuration (.xml) file using specified parameter values (in an accompanying .txt file), +# copies the new config file into the new folder, then +# runs the simulation (optionally, in the background) which writes simulation results into the new folder. # # Author: Randy Heiland @@ -10,70 +12,86 @@ from shutil import copyfile import os import sys +import subprocess -current_dir = 'foo' -print(len(sys.argv)) +# print(len(sys.argv)) if (len(sys.argv) < 3): - usage_str = "Usage: %s " % (sys.argv[0]) + usage_str = "Usage: %s " % (sys.argv[0]) print(usage_str) - print("e.g.: python params_run.py cancer_biorobots params_run.txt") + print("e.g.: python params_run.py biorobots params_biorobots.txt") exit(1) else: - pgm = sys.argv[1] + exec_pgm = sys.argv[1] params_file = sys.argv[2] +sequential_flag = 1 # if =1, do runs sequentially, i.e., not in background + +# background_str = " &" # works on Unix +# if sys.platform == 'win32': +# background_str = "" + xml_file_in = 'config/PhysiCell_settings.xml' xml_file_out = 'config/tmp.xml' -copyfile(xml_file_in,xml_file_out) +copyfile(xml_file_in, xml_file_out) tree = ET.parse(xml_file_out) xml_root = tree.getroot() first_time = True output_dirs = [] with open(params_file) as f: for line in f: - print(len(line),line) + # print(len(line),line) + print(line, end="") if (line[0] == '#'): continue (key, val) = line.split() - if (key == 'folder'): - if first_time: # we've read the 1st 'folder' - current_dir = val - first_time = False - else: # we've read additional 'folder's - # write the config file to the previous folder (output) dir and start a simulation - print('---write (previous) config file and start its sim') - tree.write(xml_file_out) - # cmd = pgm + " " + xml_file_out + " &" - cmd = pgm + " " + xml_file_out + " > " + current_dir + ".log" - print(cmd) - current_dir = val - os.system(cmd) - - xml_file_out = val + '/config.xml' # copy config file into the output dir - output_dirs.append(val) - if ('.' in key): + if (key == 'run_it'): + # write the config file to the previous folder (output) dir and start a simulation + print('\n\n---> write config file (and start sim): ', xml_file_out) + tree.write(xml_file_out) # will create folder_name/config.xml + log_file = folder_name + ".log" + if sequential_flag > 0: + # cmd = exec_pgm + " " + xml_file_out + " > " + log_file + " " + background_str + cmd = exec_pgm + " " + xml_file_out + " > " + log_file + print("Doing sequential run...") + print("cmd = ",cmd) + os.system(cmd) # <------ Execute the simulation + else: # put (multiple) runs in the background + with open(log_file,"w") as outf: + subprocess.Popen([exec_pgm, xml_file_out],stdout=outf) + elif ('.' in key): k = key.split('.') + # print("---- found keys path: ",k) + # print("---- val: ",val) uep = xml_root + full_path = '.' # we build up a (unique) "full_path" to a param that will have its value changed. for idx in range(len(k)): - uep = uep.find('.//' + k[idx]) # unique entry point (uep) into xml + # uep = uep.find('.//' + k[idx]) # unique entry point (uep) into xml + # if "@" in k[idx]: + # print("---------- found @") + full_path += '//' + k[idx] # unique entry point (uep) into xml # print(k[idx]) - uep.text = val -# d[key] = val - else: - if (key == 'folder' and not os.path.exists(val)): - print('creating ' + val) - os.makedirs(val) - # current_dir = val - - xml_root.find('.//' + key).text = val + # print("---- full_path: ",full_path) -tree.write(xml_file_out) + # uep = xml_root.find(".//microenvironment_setup//variable[@ID='1']//physical_parameter_set//decay_rate") + # uep = xml_root.find(".//microenvironment_setup//variable[@ID='1']//decay_rate") + uep = xml_root.find(full_path) # uep: unique entry point + # print("uep = ",uep) + uep.text = val # unique entry point (uep) into xml + else: + if (key == 'folder'): + folder_name = val + output_dirs.append(folder_name) + if (not os.path.exists(folder_name)): + print("--- parsed 'folder', makedir " + folder_name) + os.makedirs(folder_name) + xml_file_out = os.path.join(folder_name, 'config.xml') # copy config file into the output dir -#cwd = os.getcwd() -#cmd = pgm + " " + xml_file_out + " &" -cmd = pgm + " " + xml_file_out + " > " + current_dir + ".log" -print(cmd) -os.system(cmd) + try: + xml_root.find('.//' + key).text = val + except: + print("--- Error: could not find ",key," in .xml\n") + sys.exit(1) -print(output_dirs) +print("\n ------\n Your output results will appear in these directories:\n ",output_dirs) +print("and check for a .log file of each name for your terminal output from each simulation.\n") diff --git a/params_run.py b/params_run.py deleted file mode 100644 index 2100d1e..0000000 --- a/params_run.py +++ /dev/null @@ -1,97 +0,0 @@ -# params_run.py: -# -# This Python script provides simple parameter exploration functionality. The script creates -# a new folder (subdirectory) for each set of parameters, makes changes to a default -# configuration (.xml) file using specified parameter values (in an accompanying .txt file), -# copies the new config file into the new folder, then -# runs the simulation (optionally, in the background) which writes simulation results into the new folder. -# -# Author: Randy Heiland - -import xml.etree.ElementTree as ET -from shutil import copyfile -import os -import sys -import subprocess - -# print(len(sys.argv)) -if (len(sys.argv) < 3): - usage_str = "Usage: %s " % (sys.argv[0]) - print(usage_str) - print("e.g.: python params_run.py biorobots params_biorobots.txt") - exit(1) -else: - exec_pgm = sys.argv[1] - params_file = sys.argv[2] - -sequential_flag = 1 # if =1, do runs sequentially, i.e., not in background - -# background_str = " &" # works on Unix -# if sys.platform == 'win32': -# background_str = "" - - -xml_file_in = 'config/PhysiCell_settings.xml' -xml_file_out = 'config/tmp.xml' -copyfile(xml_file_in, xml_file_out) -tree = ET.parse(xml_file_out) -xml_root = tree.getroot() -first_time = True -output_dirs = [] -with open(params_file) as f: - for line in f: - # print(len(line),line) - print(line, end="") - if (line[0] == '#'): - continue - (key, val) = line.split() - if (key == 'run_it'): - # write the config file to the previous folder (output) dir and start a simulation - print('\n\n---> write config file (and start sim): ', xml_file_out) - tree.write(xml_file_out) # will create folder_name/config.xml - log_file = folder_name + ".log" - if sequential_flag > 0: - # cmd = exec_pgm + " " + xml_file_out + " > " + log_file + " " + background_str - cmd = exec_pgm + " " + xml_file_out + " > " + log_file - print("Doing sequential run...") - print("cmd = ",cmd) - os.system(cmd) # <------ Execute the simulation - else: # put (multiple) runs in the background - with open(log_file,"w") as outf: - subprocess.Popen([exec_pgm, xml_file_out],stdout=outf) - elif ('.' in key): - k = key.split('.') - # print("---- found keys path: ",k) - # print("---- val: ",val) - uep = xml_root - full_path = '.' # we build up a (unique) "full_path" to a param that will have its value changed. - for idx in range(len(k)): - # uep = uep.find('.//' + k[idx]) # unique entry point (uep) into xml - # if "@" in k[idx]: - # print("---------- found @") - full_path += '//' + k[idx] # unique entry point (uep) into xml -# print(k[idx]) - # print("---- full_path: ",full_path) - - # uep = xml_root.find(".//microenvironment_setup//variable[@ID='1']//physical_parameter_set//decay_rate") - # uep = xml_root.find(".//microenvironment_setup//variable[@ID='1']//decay_rate") - uep = xml_root.find(full_path) # uep: unique entry point - # print("uep = ",uep) - uep.text = val # unique entry point (uep) into xml - else: - if (key == 'folder'): - folder_name = val - output_dirs.append(folder_name) - if (not os.path.exists(folder_name)): - print("--- parsed 'folder', makedir " + folder_name) - os.makedirs(folder_name) - xml_file_out = os.path.join(folder_name, 'config.xml') # copy config file into the output dir - - try: - xml_root.find('.//' + key).text = val - except: - print("--- Error: could not find ",key," in .xml\n") - sys.exit(1) - -print("\n ------\n Your output results will appear in these directories:\n ",output_dirs) -print("and check for a .log file of each name for your terminal output from each simulation.\n") From c86c6bbc1cd964574db64ea292048fee952974fc Mon Sep 17 00:00:00 2001 From: Randy Heiland Date: Wed, 30 Jun 2021 15:29:14 -0400 Subject: [PATCH 11/15] sequential key --- analysis/params_run.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/analysis/params_run.py b/analysis/params_run.py index 2100d1e..4c61d2d 100644 --- a/analysis/params_run.py +++ b/analysis/params_run.py @@ -45,7 +45,10 @@ if (line[0] == '#'): continue (key, val) = line.split() - if (key == 'run_it'): + + if (key == 'sequential'): + sequential_flag = int(val) # if =1, do runs sequentially, i.e., not in background + elif (key == 'run_it'): # write the config file to the previous folder (output) dir and start a simulation print('\n\n---> write config file (and start sim): ', xml_file_out) tree.write(xml_file_out) # will create folder_name/config.xml From c78863cfd40197a377a9a68c4424a7253cc8d3b2 Mon Sep 17 00:00:00 2001 From: Randy Heiland Date: Sat, 21 Aug 2021 18:14:28 -0400 Subject: [PATCH 12/15] hex cells --- hex_pack3d.py | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 hex_pack3d.py diff --git a/hex_pack3d.py b/hex_pack3d.py new file mode 100644 index 0000000..7c6e7b3 --- /dev/null +++ b/hex_pack3d.py @@ -0,0 +1,97 @@ +# Generate hex close packed cells (centers) in a sphere, containing a smaller spheroid (embolism) +# +# Output: text file containing 4-tuples: +# x y z cell_type +# + +import numpy as np +#from fury import window, actor, ui +#import itertools +import math + +#cell_radius = 1.0 +cell_radius = 8.412710547954228 # PhysiCell default +xc = yc = zc = -1.0 + +# setup hex packing constants +x_spacing = cell_radius*2 +y_spacing = cell_radius*np.sqrt(3) +z_spacing = (cell_radius*2.0/3.0)*np.sqrt(6) +print("z_spacing=",z_spacing) + +box_radius = 500.0 +box_radius = 250.0 +box_radius = 50.0 +box_radius = 400.0 +sphere_radius2 = box_radius * box_radius +eq_tri_yctr = math.tan(math.radians(30)) * cell_radius + +# We'll append into these arrays +xyz = np.empty((0,3)) +colors = np.empty((0,4)) + +# spheroid axes +a = 15 +a2 = a*a +c = 9 +c2 = c*c + +# centroid of the emboli +x0_e = 200 +y0_e = 50 +z0_e = 0 + +fp = open("cells.dat","w") + +# for z in np.arange(0.0, 2*cell_radius, z_spacing): +for z in np.arange(-box_radius, box_radius, z_spacing): + zc += 1 + z_xoffset = (zc % 2) * cell_radius + z_yoffset = (zc % 2) * eq_tri_yctr # 0.5773502691896256 + zsq = z * z + term3 = (z - z0_e) * (z - z0_e) + # print("z_xoffset=",z_xoffset) + # print("z_yoffset=",z_yoffset) + for y in np.arange(-box_radius, box_radius, y_spacing): + yc += 1 + y2 = y + z_yoffset + ysq = y2 * y2 + term2 = (y2 - y0_e) * (y2 - y0_e) + # print('--------') + for x in np.arange(-box_radius, box_radius, x_spacing): + x2 = x + (yc%2) * cell_radius + z_xoffset + xsq = x2 * x2 + term1 = (x2 - x0_e) * (x2 - x0_e) + # print(x2,y2,z) + # if ( (z<0.0) and (xsq + ysq + zsq) < sphere_radius2): # assume centered about origin + if ( (xsq + ysq + zsq) < sphere_radius2): # assume centered about origin + xyz = np.append(xyz, np.array([[x2,y2,z]]), axis=0) + # val = (xsq + ysq)/a2 + zsq/c2 + val = (term1 + term2)/a2 + term3/c2 + # print(val) + if val < 19.0: + # colors = np.append(colors, np.array([[1,0,0, 1]]), axis=0) + print('cell type 1') + cell_type = 1 + else: + # colors = np.append(colors, np.array([[0,1,1, 1]]), axis=0) + # colors = np.append(colors, np.array([[0,1,1,0.5]]), axis=0) + cell_type = 0 + fp.write("%f,%f,%f,%d\n" % (x2,y2,z, cell_type)) +# if (zc > 0): +# break + +# ncells = len(xyz) +#colors = np.ones((ncells,4)) # white [0-2]; opaque [3] +#colors[:,0] = 0 # make them cyan + +# scene = window.Scene() +# sphere_actor = actor.sphere(centers=xyz, +# colors=colors, +# radii=cell_radius) +# scene.add(sphere_actor) +# showm = window.ShowManager(scene, +# size=(900, 768), reset_camera=False, +# order_transparent=False) +# showm.initialize() +# showm.start() \ No newline at end of file From 1727da808e1e06700365bdecabe2089f8fb0d73a Mon Sep 17 00:00:00 2001 From: Randy Heiland Date: Thu, 25 Nov 2021 18:17:34 -0500 Subject: [PATCH 13/15] upload file --- analysis/covid19/plot_Ig_field.py | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 analysis/covid19/plot_Ig_field.py diff --git a/analysis/covid19/plot_Ig_field.py b/analysis/covid19/plot_Ig_field.py new file mode 100644 index 0000000..f26bd1d --- /dev/null +++ b/analysis/covid19/plot_Ig_field.py @@ -0,0 +1,43 @@ +import sys +import os +import glob +import numpy as np +from pyMCDS import pyMCDS +import matplotlib.pyplot as plt + +argc = len(sys.argv)-1 +print("# args=",argc) + +if (argc < 1): +# data_dir = int(sys.argv[kdx]) + print("Usage: provide output subdir") + sys.exit(-1) + +#data_dir = 'output' +kdx = 1 +data_dir = sys.argv[kdx] +print('data_dir = ',data_dir) +os.chdir(data_dir) +#xml_files = glob.glob('output/output*.xml') +xml_files = glob.glob('output*.xml') +os.chdir('..') +xml_files.sort() +#print('xml_files = ',xml_files) + +ds_count = len(xml_files) +print("----- ds_count = ",ds_count) +mcds = [pyMCDS(xml_files[i], data_dir) for i in range(ds_count)] # spews lots of prints + +tval = np.linspace(0, mcds[-1].get_time(), ds_count) +print('tval= ',tval) + +#print(mcds[0].get_substrate_names()) +# ['virion', 'assembled virion', 'interferon 1', 'pro-inflammatory cytokine', 'chemokine', 'debris', 'pro-pyroptosis cytokine', 'anti-inflammatory cytokine', 'collagen'] + +f = np.array([ (mcds[idx].get_concentrations('Ig')).sum() for idx in range(ds_count)] ) + +plt.plot(tval,f) +plt.title(data_dir + ": Ig (sum)") +plt.xlabel("time (mins)") +#plt.savefig(data_dir + '.png') +plt.show() From ced3592c02a91bfab20f7973594144a2c5c961cb Mon Sep 17 00:00:00 2001 From: Randy Heiland Date: Fri, 15 Jul 2022 17:42:51 -0400 Subject: [PATCH 14/15] init --- cells_range.py | 20 ++ pyMCDS_cells_new.py | 516 ++++++++++++++++++++++++++++++++++++++++++++ pyMCDS_new.py | 516 ++++++++++++++++++++++++++++++++++++++++++++ pyMCDS_orig.py | 505 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1557 insertions(+) create mode 100644 cells_range.py create mode 100644 pyMCDS_cells_new.py create mode 100644 pyMCDS_new.py create mode 100644 pyMCDS_orig.py diff --git a/cells_range.py b/cells_range.py new file mode 100644 index 0000000..db6ba9a --- /dev/null +++ b/cells_range.py @@ -0,0 +1,20 @@ +#from pyMCDS_new import pyMCDS +from pyMCDS_cells_new import pyMCDS_cells +import numpy as np + +#mcds = pyMCDS('output00000000.xml','.') +mcds = pyMCDS_cells('output00000000.xml','.') +tmins = mcds.get_time() +print('time (mins)=',tmins) + +print(mcds.data['discrete_cells'].keys()) +#print(mcds.data['discrete_cells']['ID']) +ncells = len(mcds.data['discrete_cells']['ID']) +print('num cells= ',ncells) + +xvals = mcds.data['discrete_cells']['position_x'] +yvals = mcds.data['discrete_cells']['position_y'] +zvals = mcds.data['discrete_cells']['position_z'] +print("x range: ",xvals.min(), xvals.max()) +print("y range: ",yvals.min(), yvals.max()) +print("z range: ",zvals.min(), zvals.max()) diff --git a/pyMCDS_cells_new.py b/pyMCDS_cells_new.py new file mode 100644 index 0000000..8d1b3ec --- /dev/null +++ b/pyMCDS_cells_new.py @@ -0,0 +1,516 @@ +import xml.etree.ElementTree as ET +import numpy as np +import pandas as pd +import scipy.io as sio +import sys +import warnings +from pathlib import Path + +class pyMCDS_cells: + """ + This class contains a dictionary of dictionaries that contains all of the + output from a single time step of a PhysiCell Model. This class assumes that + all output files are stored in the same directory. Data is loaded by reading + the .xml file for a particular timestep. + + Parameters + ---------- + xml_name: str + String containing the name of the xml file without the path + output_path: str, optional + String containing the path (relative or absolute) to the directory + where PhysiCell output files are stored (default= ".") + + Attributes + ---------- + data : dict + Hierarchical container for all of the data retrieved by parsing the xml + file and the files referenced therein. + """ + def __init__(self, xml_file, output_path='.'): + self.data = self._read_xml(xml_file, output_path) + + ## METADATA RELATED FUNCTIONS + + def get_time(self): + return self.data['metadata']['current_time'] + + ## MESH RELATED FUNCTIONS + + def get_mesh(self, flat=False): + """ + Return a meshgrid of the computational domain. Can return either full + 3D or a 2D plane for contour plots. + + Parameters + ---------- + flat : bool + If flat is set to true, we return only the x and y meshgrid. + Otherwise we return x, y, and z + + Returns + ------- + splitting : list length=2 if flat=True, else length=3 + Contains arrays of voxel center coordinates as meshgrid with shape + [nx_voxel, ny_voxel, nz_voxel] or [nx_voxel, ny_voxel] if flat=True. + """ + if flat == True: + xx = self.data['mesh']['x_coordinates'][:, :, 0] + yy = self.data['mesh']['y_coordinates'][:, :, 0] + + return [xx, yy] + + # if we dont want a plane just return appropriate values + else: + xx = self.data['mesh']['x_coordinates'] + yy = self.data['mesh']['y_coordinates'] + zz = self.data['mesh']['z_coordinates'] + + return [xx, yy, zz] + + def get_2D_mesh(self): + """ + This function returns the x, y meshgrid as two numpy arrays. It is + identical to get_mesh with the option flat=True + + Returns + ------- + splitting : list length=2 + Contains arrays of voxel center coordinates in x and y dimensions + as meshgrid with shape [nx_voxel, ny_voxel] + """ + xx = self.data['mesh']['x_coordinates'][:, :, 0] + yy = self.data['mesh']['y_coordinates'][:, :, 0] + + return [xx, yy] + + def get_linear_voxels(self): + """ + Helper function to quickly grab voxel centers array stored linearly as + opposed to meshgrid-style. + """ + return self.data['mesh']['voxels']['centers'] + + def get_mesh_spacing(self): + """ + Returns the space in between voxel centers for the mesh in terms of the + mesh's spatial units. Assumes that voxel centers fall on integer values. + + Returns + ------- + dx : float + Distance between voxel centers in the same units as the other + spatial measurements + """ + centers = self.get_linear_voxels() + X = np.unique(centers[0, :]) + Y = np.unique(centers[1, :]) + Z = np.unique(centers[2, :]) + + dx = (X.max() - X.min()) / X.shape[0] + dy = (Y.max() - Y.min()) / Y.shape[0] + dz = (Z.max() - Z.min()) / Z.shape[0] + + if np.abs(dx - dy) > 1e-10 or np.abs(dy - dz) > 1e-10 \ + or np.abs(dx - dz) > 1e-10: + print('Warning: grid spacing may be axis dependent.') + + return round(dx) + + def get_containing_voxel_ijk(self, x, y, z): + """ + Internal function to get the meshgrid indices for the center of a voxel + that contains the given position. + + Note that pyMCDS stores meshgrids as 'cartesian' + (indexing='xy' in np.meshgrid) which means that we will have + to use these indices as [j, i, k] on the actual meshgrid objects + + Parameters + ---------- + x : float + x-coordinate for the position + y : float + y-coordinate for the position + z : float + z-coordinate for the position + + Returns + ------- + ijk : list length=3 + contains the i, j, and k indices for the containing voxel's center + """ + xx, yy, zz = self.get_mesh() + ds = self.get_mesh_spacing() + + if x > xx.max(): + warnings.warn('Position out of bounds: x out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting x = x_max!'.format(x, y, z)) + x = xx.max() + elif x < xx.min(): + warnings.warn('Position out of bounds: x out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting x = x_min!'.format(x, y, z)) + x = xx.min() + elif y > yy.max(): + warnings.warn('Position out of bounds: y out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting y = y_max!'.format(x, y, z)) + y = yy.max() + elif y < yy.min(): + warnings.warn('Position out of bounds: y out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting y = y_min!'.format(x, y, z)) + y = yy.min() + elif z > zz.max(): + warnings.warn('Position out of bounds: z out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting z = z_max!'.format(x, y, z)) + z = zz.max() + elif z < zz.min(): + warnings.warn('Position out of bounds: z out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting z = z_min!'.format(x, y, z)) + z = zz.min() + + i = np.round((x - xx.min()) / ds) + j = np.round((y - yy.min()) / ds) + k = np.round((z - zz.min()) / ds) + + ii, jj, kk = int(i), int(j), int(k) + + return [ii, jj, kk] + + ## MICROENVIRONMENT RELATED FUNCTIONS + + def get_substrate_names(self): + """ + Returns list of chemical species in microenvironment + + Returns + ------- + species_list : array (str), shape=[n_species,] + Contains names of chemical species in microenvironment + """ + species_list = [] + for name in self.data['continuum_variables']: + species_list.append(name) + + return species_list + + def get_concentrations(self, species_name, z_slice=None): + """ + Returns the concentration array for the specified chemical species + in the microenvironment. Can return either the whole 3D picture, or + a 2D plane of concentrations. + + Parameters + ---------- + species_name : str + Name of the chemical species for which to get concentrations + + z_slice : float + z-axis position to use as plane for 2D output. This value must match + a plane of voxel centers in the z-axis. + Returns + ------- + conc_arr : array (np.float) shape=[nx_voxels, ny_voxels, nz_voxels] + Contains the concentration of the specified chemical in each voxel. + The array spatially maps to a meshgrid of the voxel centers. + """ + if z_slice is not None: + # check to see that z_slice is a valid plane + zz = self.data['mesh']['z_coordinates'] + assert z_slice in zz, 'Specified z_slice {} not in z_coordinates'.format(z_slice) + + # do the processing if its ok + mask = zz == z_slice + full_conc = self.data['continuum_variables'][species_name]['data'] + conc_arr = full_conc[mask].reshape((zz.shape[0], zz.shape[1])) + else: + conc_arr = self.data['continuum_variables'][species_name]['data'] + + return conc_arr + + def get_concentrations_at(self, x, y, z): + """ + Return concentrations of each chemical species inside a particular voxel + that contains the point described in the arguments. + + Parameters + ---------- + x : float + x-position for the point of interest + y : float + y_position for the point of interest + z : float + z_position for the point of interest + + Returns + ------- + concs : array, shape=[n_substrates,] + array of concentrations in the order given by get_substrate_names() + """ + i, j, k = self.get_containing_voxel_ijk(x, y, z) + sub_name_list = self.get_substrate_names() + concs = np.zeros(len(sub_name_list)) + + for ix in range(len(sub_name_list)): + concs[ix] = self.get_concentrations(sub_name_list[ix])[j, i, k] + + return concs + + + ## CELL RELATED FUNCTIONS + + def get_cell_df(self): + """ + Builds DataFrame from data['discrete_cells'] + + Returns + ------- + cells_df : pd.Dataframe, shape=[n_cells, n_variables] + Dataframe containing the cell data for all cells at this time step + """ + cells_df = pd.DataFrame(self.data['discrete_cells']) + return cells_df + + def get_cell_variables(self): + """ + Returns the names of all of the cell variables tracked in ['discrete cells'] + dictionary + + Returns + ------- + var_list : list, shape=[n_variables] + Contains the names of the cell variables + """ + var_list = [] + for name in self.data['discrete_cells']: + var_list.append(name) + return var_list + + def get_cell_df_at(self, x, y, z): + """ + Returns a dataframe for cells in the same voxel as the position given by + x, y, and z. + + Parameters + ---------- + x : float + x-position for the point of interest + y : float + y_position for the point of interest + z : float + z_position for the point of interest + + Returns + ------- + vox_df : pd.DataFrame, shape=[n_cell_in_voxel, n_variables] + cell dataframe containing only cells in the same voxel as the point + specified by x, y, and z. + """ + ds = self.get_mesh_spacing() + xx, yy, zz = self.get_mesh() + i, j, k = self.get_containing_voxel_ijk(x, y, z) + x_vox = xx[j, i, k] + y_vox = yy[j, i, k] + z_vox = zz[j, i, k] + + cell_df = self.get_cell_df() + inside_voxel = ( (cell_df['position_x'] < x_vox + ds/2.) & + (cell_df['position_x'] > x_vox - ds/2.) & + (cell_df['position_y'] < y_vox + ds/2.) & + (cell_df['position_y'] > y_vox - ds/2.) & + (cell_df['position_z'] < z_vox + ds/2.) & + (cell_df['position_z'] > z_vox - ds/2.) ) + vox_df = cell_df[inside_voxel] + return vox_df + + def _read_xml(self, xml_file, output_path='.'): + """ + Does the actual work of initializing MultiCellDS by parsing the xml + """ + + output_path = Path(output_path) + xml_file = output_path / xml_file + tree = ET.parse(xml_file) + + print('Reading {}'.format(xml_file)) + + root = tree.getroot() + MCDS = {} + + # Get current simulated time + metadata_node = root.find('metadata') + time_node = metadata_node.find('current_time') + MCDS['metadata'] = {} + MCDS['metadata']['current_time'] = float(time_node.text) + MCDS['metadata']['time_units'] = time_node.get('units') + + # Get current runtime + time_node = metadata_node.find('current_runtime') + MCDS['metadata']['current_runtime'] = float(time_node.text) + MCDS['metadata']['runtime_units'] = time_node.get('units') + + # # find the microenvironment node + # me_node = root.find('microenvironment') + # me_node = me_node.find('domain') + + # # find the mesh node + # mesh_node = me_node.find('mesh') + # MCDS['metadata']['spatial_units'] = mesh_node.get('units') + # MCDS['mesh'] = {} + + # # while we're at it, find the mesh + # coord_str = mesh_node.find('x_coordinates').text + # delimiter = mesh_node.find('x_coordinates').get('delimiter') + # x_coords = np.array(coord_str.split(delimiter), dtype=np.float) + + # coord_str = mesh_node.find('y_coordinates').text + # delimiter = mesh_node.find('y_coordinates').get('delimiter') + # y_coords = np.array(coord_str.split(delimiter), dtype=np.float) + + # coord_str = mesh_node.find('z_coordinates').text + # delimiter = mesh_node.find('z_coordinates').get('delimiter') + # z_coords = np.array(coord_str.split(delimiter), dtype=np.float) + + # # reshape into a mesh grid + # xx, yy, zz = np.meshgrid(x_coords, y_coords, z_coords) + + # MCDS['mesh']['x_coordinates'] = xx + # MCDS['mesh']['y_coordinates'] = yy + # MCDS['mesh']['z_coordinates'] = zz + + # # Voxel data must be loaded from .mat file + # voxel_file = mesh_node.find('voxels').find('filename').text + # voxel_path = output_path / voxel_file + # try: + # initial_mesh = sio.loadmat(voxel_path)['mesh'] + # except: + # raise FileNotFoundError( + # "No such file or directory:\n'{}' referenced in '{}'".format(voxel_path, xml_file)) + # sys.exit(1) + + # print('Reading {}'.format(voxel_path)) + + # # center of voxel specified by first three rows [ x, y, z ] + # # volume specified by fourth row + # MCDS['mesh']['voxels'] = {} + # MCDS['mesh']['voxels']['centers'] = initial_mesh[:3, :] + # MCDS['mesh']['voxels']['volumes'] = initial_mesh[3, :] + + # # Continuum_variables, unlike in the matlab version the individual chemical + # # species will be primarily accessed through their names e.g. + # # MCDS['continuum_variables']['oxygen']['units'] + # # MCDS['continuum_variables']['glucose']['data'] + # MCDS['continuum_variables'] = {} + # variables_node = me_node.find('variables') + # file_node = me_node.find('data').find('filename') + + # # micro environment data is shape [4+n, len(voxels)] where n is the number + # # of species being tracked. the first 3 rows represent (x, y, z) of voxel + # # centers. The fourth row contains the voxel volume. The 5th row and up will + # # contain values for that species in that voxel. + # me_file = file_node.text + # me_path = output_path / me_file + # # Changes here + # try: + # me_data = sio.loadmat(me_path)['multiscale_microenvironment'] + # except: + # raise FileNotFoundError( + # "No such file or directory:\n'{}' referenced in '{}'".format(me_path, xml_file)) + # sys.exit(1) + + # print('Reading {}'.format(me_path)) + + # var_children = variables_node.findall('variable') + + # # we're going to need the linear x, y, and z coordinates later + # # but we dont need to get them in the loop + # X, Y, Z = np.unique(xx), np.unique(yy), np.unique(zz) + + # for si, species in enumerate(var_children): + # species_name = species.get('name') + # MCDS['continuum_variables'][species_name] = {} + # MCDS['continuum_variables'][species_name]['units'] = species.get( + # 'units') + + # print('Parsing {:s} data'.format(species_name)) + + # # initialize array for concentration data + # MCDS['continuum_variables'][species_name]['data'] = np.zeros(xx.shape) + + # # travel down one level on tree + # species = species.find('physical_parameter_set') + + # # diffusion data for each species + # MCDS['continuum_variables'][species_name]['diffusion_coefficient'] = {} + # MCDS['continuum_variables'][species_name]['diffusion_coefficient']['value'] \ + # = float(species.find('diffusion_coefficient').text) + # MCDS['continuum_variables'][species_name]['diffusion_coefficient']['units'] \ + # = species.find('diffusion_coefficient').get('units') + + # # decay data for each species + # MCDS['continuum_variables'][species_name]['decay_rate'] = {} + # MCDS['continuum_variables'][species_name]['decay_rate']['value'] \ + # = float(species.find('decay_rate').text) + # MCDS['continuum_variables'][species_name]['decay_rate']['units'] \ + # = species.find('decay_rate').get('units') + + # # store data from microenvironment file as numpy array + # # iterate over each voxel + # for vox_idx in range(MCDS['mesh']['voxels']['centers'].shape[1]): + # # find the center + # center = MCDS['mesh']['voxels']['centers'][:, vox_idx] + + # i = np.where(np.abs(center[0] - X) < 1e-10)[0][0] + # j = np.where(np.abs(center[1] - Y) < 1e-10)[0][0] + # k = np.where(np.abs(center[2] - Z) < 1e-10)[0][0] + + # MCDS['continuum_variables'][species_name]['data'][j, i, k] \ + # = me_data[4+si, vox_idx] + + # in order to get to the good stuff we have to pass through a few different + # hierarchal levels + cell_node = root.find('cellular_information') + cell_node = cell_node.find('cell_populations') + cell_node = cell_node.find('cell_population') + cell_node = cell_node.find('custom') + # we want the PhysiCell data, there is more of it + for child in cell_node.findall('simplified_data'): + if child.get('source') == 'PhysiCell': + cell_node = child + break + + print( 'working on discrete cell data...\n') + + MCDS['discrete_cells'] = {} + data_labels = [] + # iterate over 'label's which are children of 'labels' these will be used to + # label data arrays + n = 0; + for label in cell_node.find('labels').findall('label'): + # I don't like spaces in my dictionary keys + fixed_label = label.text.replace(' ', '_') + if int(label.get('size')) > 1: + # tags to differentiate repeated labels (usually space related) + if( n < 19 ): # rwh: why not this? if( nlabels == 3 ): + dir_label = ['_x', '_y', '_z'] + else: + dir_label = []; + for nn in range(100): + dir_label.append( '_%u' % nn ) + # print( dir_label ) + for i in range(int(label.get('size'))): + # print( fixed_label + dir_label[i] ) + data_labels.append(fixed_label + dir_label[i]) + else: + data_labels.append(fixed_label) + # print(fixed_label) + n += 1 + # load the file + cell_file = cell_node.find('filename').text + cell_path = output_path / cell_file + try: + cell_data = sio.loadmat(cell_path)['cells'] + except: + raise FileNotFoundError( + "No such file or directory:\n'{}' referenced in '{}'".format(cell_path, xml_file)) + sys.exit(1) + + print('Reading {}'.format(cell_path)) + + for col in range(len(data_labels)): + MCDS['discrete_cells'][data_labels[col]] = cell_data[col, :] + + return MCDS diff --git a/pyMCDS_new.py b/pyMCDS_new.py new file mode 100644 index 0000000..9d7ff03 --- /dev/null +++ b/pyMCDS_new.py @@ -0,0 +1,516 @@ +import xml.etree.ElementTree as ET +import numpy as np +import pandas as pd +import scipy.io as sio +import sys +import warnings +from pathlib import Path + +class pyMCDS: + """ + This class contains a dictionary of dictionaries that contains all of the + output from a single time step of a PhysiCell Model. This class assumes that + all output files are stored in the same directory. Data is loaded by reading + the .xml file for a particular timestep. + + Parameters + ---------- + xml_name: str + String containing the name of the xml file without the path + output_path: str, optional + String containing the path (relative or absolute) to the directory + where PhysiCell output files are stored (default= ".") + + Attributes + ---------- + data : dict + Hierarchical container for all of the data retrieved by parsing the xml + file and the files referenced therein. + """ + def __init__(self, xml_file, output_path='.'): + self.data = self._read_xml(xml_file, output_path) + + ## METADATA RELATED FUNCTIONS + + def get_time(self): + return self.data['metadata']['current_time'] + + ## MESH RELATED FUNCTIONS + + def get_mesh(self, flat=False): + """ + Return a meshgrid of the computational domain. Can return either full + 3D or a 2D plane for contour plots. + + Parameters + ---------- + flat : bool + If flat is set to true, we return only the x and y meshgrid. + Otherwise we return x, y, and z + + Returns + ------- + splitting : list length=2 if flat=True, else length=3 + Contains arrays of voxel center coordinates as meshgrid with shape + [nx_voxel, ny_voxel, nz_voxel] or [nx_voxel, ny_voxel] if flat=True. + """ + if flat == True: + xx = self.data['mesh']['x_coordinates'][:, :, 0] + yy = self.data['mesh']['y_coordinates'][:, :, 0] + + return [xx, yy] + + # if we dont want a plane just return appropriate values + else: + xx = self.data['mesh']['x_coordinates'] + yy = self.data['mesh']['y_coordinates'] + zz = self.data['mesh']['z_coordinates'] + + return [xx, yy, zz] + + def get_2D_mesh(self): + """ + This function returns the x, y meshgrid as two numpy arrays. It is + identical to get_mesh with the option flat=True + + Returns + ------- + splitting : list length=2 + Contains arrays of voxel center coordinates in x and y dimensions + as meshgrid with shape [nx_voxel, ny_voxel] + """ + xx = self.data['mesh']['x_coordinates'][:, :, 0] + yy = self.data['mesh']['y_coordinates'][:, :, 0] + + return [xx, yy] + + def get_linear_voxels(self): + """ + Helper function to quickly grab voxel centers array stored linearly as + opposed to meshgrid-style. + """ + return self.data['mesh']['voxels']['centers'] + + def get_mesh_spacing(self): + """ + Returns the space in between voxel centers for the mesh in terms of the + mesh's spatial units. Assumes that voxel centers fall on integer values. + + Returns + ------- + dx : float + Distance between voxel centers in the same units as the other + spatial measurements + """ + centers = self.get_linear_voxels() + X = np.unique(centers[0, :]) + Y = np.unique(centers[1, :]) + Z = np.unique(centers[2, :]) + + dx = (X.max() - X.min()) / X.shape[0] + dy = (Y.max() - Y.min()) / Y.shape[0] + dz = (Z.max() - Z.min()) / Z.shape[0] + + if np.abs(dx - dy) > 1e-10 or np.abs(dy - dz) > 1e-10 \ + or np.abs(dx - dz) > 1e-10: + print('Warning: grid spacing may be axis dependent.') + + return round(dx) + + def get_containing_voxel_ijk(self, x, y, z): + """ + Internal function to get the meshgrid indices for the center of a voxel + that contains the given position. + + Note that pyMCDS stores meshgrids as 'cartesian' + (indexing='xy' in np.meshgrid) which means that we will have + to use these indices as [j, i, k] on the actual meshgrid objects + + Parameters + ---------- + x : float + x-coordinate for the position + y : float + y-coordinate for the position + z : float + z-coordinate for the position + + Returns + ------- + ijk : list length=3 + contains the i, j, and k indices for the containing voxel's center + """ + xx, yy, zz = self.get_mesh() + ds = self.get_mesh_spacing() + + if x > xx.max(): + warnings.warn('Position out of bounds: x out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting x = x_max!'.format(x, y, z)) + x = xx.max() + elif x < xx.min(): + warnings.warn('Position out of bounds: x out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting x = x_min!'.format(x, y, z)) + x = xx.min() + elif y > yy.max(): + warnings.warn('Position out of bounds: y out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting y = y_max!'.format(x, y, z)) + y = yy.max() + elif y < yy.min(): + warnings.warn('Position out of bounds: y out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting y = y_min!'.format(x, y, z)) + y = yy.min() + elif z > zz.max(): + warnings.warn('Position out of bounds: z out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting z = z_max!'.format(x, y, z)) + z = zz.max() + elif z < zz.min(): + warnings.warn('Position out of bounds: z out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting z = z_min!'.format(x, y, z)) + z = zz.min() + + i = np.round((x - xx.min()) / ds) + j = np.round((y - yy.min()) / ds) + k = np.round((z - zz.min()) / ds) + + ii, jj, kk = int(i), int(j), int(k) + + return [ii, jj, kk] + + ## MICROENVIRONMENT RELATED FUNCTIONS + + def get_substrate_names(self): + """ + Returns list of chemical species in microenvironment + + Returns + ------- + species_list : array (str), shape=[n_species,] + Contains names of chemical species in microenvironment + """ + species_list = [] + for name in self.data['continuum_variables']: + species_list.append(name) + + return species_list + + def get_concentrations(self, species_name, z_slice=None): + """ + Returns the concentration array for the specified chemical species + in the microenvironment. Can return either the whole 3D picture, or + a 2D plane of concentrations. + + Parameters + ---------- + species_name : str + Name of the chemical species for which to get concentrations + + z_slice : float + z-axis position to use as plane for 2D output. This value must match + a plane of voxel centers in the z-axis. + Returns + ------- + conc_arr : array (np.float) shape=[nx_voxels, ny_voxels, nz_voxels] + Contains the concentration of the specified chemical in each voxel. + The array spatially maps to a meshgrid of the voxel centers. + """ + if z_slice is not None: + # check to see that z_slice is a valid plane + zz = self.data['mesh']['z_coordinates'] + assert z_slice in zz, 'Specified z_slice {} not in z_coordinates'.format(z_slice) + + # do the processing if its ok + mask = zz == z_slice + full_conc = self.data['continuum_variables'][species_name]['data'] + conc_arr = full_conc[mask].reshape((zz.shape[0], zz.shape[1])) + else: + conc_arr = self.data['continuum_variables'][species_name]['data'] + + return conc_arr + + def get_concentrations_at(self, x, y, z): + """ + Return concentrations of each chemical species inside a particular voxel + that contains the point described in the arguments. + + Parameters + ---------- + x : float + x-position for the point of interest + y : float + y_position for the point of interest + z : float + z_position for the point of interest + + Returns + ------- + concs : array, shape=[n_substrates,] + array of concentrations in the order given by get_substrate_names() + """ + i, j, k = self.get_containing_voxel_ijk(x, y, z) + sub_name_list = self.get_substrate_names() + concs = np.zeros(len(sub_name_list)) + + for ix in range(len(sub_name_list)): + concs[ix] = self.get_concentrations(sub_name_list[ix])[j, i, k] + + return concs + + + ## CELL RELATED FUNCTIONS + + def get_cell_df(self): + """ + Builds DataFrame from data['discrete_cells'] + + Returns + ------- + cells_df : pd.Dataframe, shape=[n_cells, n_variables] + Dataframe containing the cell data for all cells at this time step + """ + cells_df = pd.DataFrame(self.data['discrete_cells']) + return cells_df + + def get_cell_variables(self): + """ + Returns the names of all of the cell variables tracked in ['discrete cells'] + dictionary + + Returns + ------- + var_list : list, shape=[n_variables] + Contains the names of the cell variables + """ + var_list = [] + for name in self.data['discrete_cells']: + var_list.append(name) + return var_list + + def get_cell_df_at(self, x, y, z): + """ + Returns a dataframe for cells in the same voxel as the position given by + x, y, and z. + + Parameters + ---------- + x : float + x-position for the point of interest + y : float + y_position for the point of interest + z : float + z_position for the point of interest + + Returns + ------- + vox_df : pd.DataFrame, shape=[n_cell_in_voxel, n_variables] + cell dataframe containing only cells in the same voxel as the point + specified by x, y, and z. + """ + ds = self.get_mesh_spacing() + xx, yy, zz = self.get_mesh() + i, j, k = self.get_containing_voxel_ijk(x, y, z) + x_vox = xx[j, i, k] + y_vox = yy[j, i, k] + z_vox = zz[j, i, k] + + cell_df = self.get_cell_df() + inside_voxel = ( (cell_df['position_x'] < x_vox + ds/2.) & + (cell_df['position_x'] > x_vox - ds/2.) & + (cell_df['position_y'] < y_vox + ds/2.) & + (cell_df['position_y'] > y_vox - ds/2.) & + (cell_df['position_z'] < z_vox + ds/2.) & + (cell_df['position_z'] > z_vox - ds/2.) ) + vox_df = cell_df[inside_voxel] + return vox_df + + def _read_xml(self, xml_file, output_path='.'): + """ + Does the actual work of initializing MultiCellDS by parsing the xml + """ + + output_path = Path(output_path) + xml_file = output_path / xml_file + tree = ET.parse(xml_file) + + print('Reading {}'.format(xml_file)) + + root = tree.getroot() + MCDS = {} + + # Get current simulated time + metadata_node = root.find('metadata') + time_node = metadata_node.find('current_time') + MCDS['metadata'] = {} + MCDS['metadata']['current_time'] = float(time_node.text) + MCDS['metadata']['time_units'] = time_node.get('units') + + # Get current runtime + time_node = metadata_node.find('current_runtime') + MCDS['metadata']['current_runtime'] = float(time_node.text) + MCDS['metadata']['runtime_units'] = time_node.get('units') + + # find the microenvironment node + me_node = root.find('microenvironment') + me_node = me_node.find('domain') + + # find the mesh node + mesh_node = me_node.find('mesh') + MCDS['metadata']['spatial_units'] = mesh_node.get('units') + MCDS['mesh'] = {} + + # while we're at it, find the mesh + coord_str = mesh_node.find('x_coordinates').text + delimiter = mesh_node.find('x_coordinates').get('delimiter') + x_coords = np.array(coord_str.split(delimiter), dtype=np.float) + + coord_str = mesh_node.find('y_coordinates').text + delimiter = mesh_node.find('y_coordinates').get('delimiter') + y_coords = np.array(coord_str.split(delimiter), dtype=np.float) + + coord_str = mesh_node.find('z_coordinates').text + delimiter = mesh_node.find('z_coordinates').get('delimiter') + z_coords = np.array(coord_str.split(delimiter), dtype=np.float) + + # reshape into a mesh grid + xx, yy, zz = np.meshgrid(x_coords, y_coords, z_coords) + + MCDS['mesh']['x_coordinates'] = xx + MCDS['mesh']['y_coordinates'] = yy + MCDS['mesh']['z_coordinates'] = zz + + # Voxel data must be loaded from .mat file + voxel_file = mesh_node.find('voxels').find('filename').text + voxel_path = output_path / voxel_file + try: + initial_mesh = sio.loadmat(voxel_path)['mesh'] + except: + raise FileNotFoundError( + "No such file or directory:\n'{}' referenced in '{}'".format(voxel_path, xml_file)) + sys.exit(1) + + print('Reading {}'.format(voxel_path)) + + # center of voxel specified by first three rows [ x, y, z ] + # volume specified by fourth row + MCDS['mesh']['voxels'] = {} + MCDS['mesh']['voxels']['centers'] = initial_mesh[:3, :] + MCDS['mesh']['voxels']['volumes'] = initial_mesh[3, :] + + # Continuum_variables, unlike in the matlab version the individual chemical + # species will be primarily accessed through their names e.g. + # MCDS['continuum_variables']['oxygen']['units'] + # MCDS['continuum_variables']['glucose']['data'] + MCDS['continuum_variables'] = {} + variables_node = me_node.find('variables') + file_node = me_node.find('data').find('filename') + + # micro environment data is shape [4+n, len(voxels)] where n is the number + # of species being tracked. the first 3 rows represent (x, y, z) of voxel + # centers. The fourth row contains the voxel volume. The 5th row and up will + # contain values for that species in that voxel. + me_file = file_node.text + me_path = output_path / me_file + # Changes here + try: + me_data = sio.loadmat(me_path)['multiscale_microenvironment'] + except: + raise FileNotFoundError( + "No such file or directory:\n'{}' referenced in '{}'".format(me_path, xml_file)) + sys.exit(1) + + print('Reading {}'.format(me_path)) + + var_children = variables_node.findall('variable') + + # we're going to need the linear x, y, and z coordinates later + # but we dont need to get them in the loop + X, Y, Z = np.unique(xx), np.unique(yy), np.unique(zz) + + for si, species in enumerate(var_children): + species_name = species.get('name') + MCDS['continuum_variables'][species_name] = {} + MCDS['continuum_variables'][species_name]['units'] = species.get( + 'units') + + print('Parsing {:s} data'.format(species_name)) + + # initialize array for concentration data + MCDS['continuum_variables'][species_name]['data'] = np.zeros(xx.shape) + + # travel down one level on tree + species = species.find('physical_parameter_set') + + # diffusion data for each species + MCDS['continuum_variables'][species_name]['diffusion_coefficient'] = {} + MCDS['continuum_variables'][species_name]['diffusion_coefficient']['value'] \ + = float(species.find('diffusion_coefficient').text) + MCDS['continuum_variables'][species_name]['diffusion_coefficient']['units'] \ + = species.find('diffusion_coefficient').get('units') + + # decay data for each species + MCDS['continuum_variables'][species_name]['decay_rate'] = {} + MCDS['continuum_variables'][species_name]['decay_rate']['value'] \ + = float(species.find('decay_rate').text) + MCDS['continuum_variables'][species_name]['decay_rate']['units'] \ + = species.find('decay_rate').get('units') + + # store data from microenvironment file as numpy array + # iterate over each voxel + for vox_idx in range(MCDS['mesh']['voxels']['centers'].shape[1]): + # find the center + center = MCDS['mesh']['voxels']['centers'][:, vox_idx] + + i = np.where(np.abs(center[0] - X) < 1e-10)[0][0] + j = np.where(np.abs(center[1] - Y) < 1e-10)[0][0] + k = np.where(np.abs(center[2] - Z) < 1e-10)[0][0] + + MCDS['continuum_variables'][species_name]['data'][j, i, k] \ + = me_data[4+si, vox_idx] + + # in order to get to the good stuff we have to pass through a few different + # hierarchal levels + cell_node = root.find('cellular_information') + cell_node = cell_node.find('cell_populations') + cell_node = cell_node.find('cell_population') + cell_node = cell_node.find('custom') + # we want the PhysiCell data, there is more of it + for child in cell_node.findall('simplified_data'): + if child.get('source') == 'PhysiCell': + cell_node = child + break + + print( 'working on discrete cell data...\n') + + MCDS['discrete_cells'] = {} + data_labels = [] + # iterate over 'label's which are children of 'labels' these will be used to + # label data arrays + n = 0; + for label in cell_node.find('labels').findall('label'): + # I don't like spaces in my dictionary keys + fixed_label = label.text.replace(' ', '_') + if int(label.get('size')) > 1: + # tags to differentiate repeated labels (usually space related) + if( n < 19 ): # rwh: why not this? if( nlabels == 3 ): + dir_label = ['_x', '_y', '_z'] + else: + dir_label = []; + for nn in range(100): + dir_label.append( '_%u' % nn ) + # print( dir_label ) + for i in range(int(label.get('size'))): + # print( fixed_label + dir_label[i] ) + data_labels.append(fixed_label + dir_label[i]) + else: + data_labels.append(fixed_label) + # print(fixed_label) + n += 1 + # load the file + cell_file = cell_node.find('filename').text + cell_path = output_path / cell_file + try: + cell_data = sio.loadmat(cell_path)['cells'] + except: + raise FileNotFoundError( + "No such file or directory:\n'{}' referenced in '{}'".format(cell_path, xml_file)) + sys.exit(1) + + print('Reading {}'.format(cell_path)) + + for col in range(len(data_labels)): + MCDS['discrete_cells'][data_labels[col]] = cell_data[col, :] + + return MCDS diff --git a/pyMCDS_orig.py b/pyMCDS_orig.py new file mode 100644 index 0000000..d222736 --- /dev/null +++ b/pyMCDS_orig.py @@ -0,0 +1,505 @@ +import xml.etree.ElementTree as ET +import numpy as np +import pandas as pd +import scipy.io as sio +import sys +import warnings +from pathlib import Path + +class pyMCDS: + """ + This class contains a dictionary of dictionaries that contains all of the + output from a single time step of a PhysiCell Model. This class assumes that + all output files are stored in the same directory. Data is loaded by reading + the .xml file for a particular timestep. + + Parameters + ---------- + xml_name: str + String containing the name of the xml file without the path + output_path: str, optional + String containing the path (relative or absolute) to the directory + where PhysiCell output files are stored (default= ".") + + Attributes + ---------- + data : dict + Hierarchical container for all of the data retrieved by parsing the xml + file and the files referenced therein. + """ + def __init__(self, xml_file, output_path='.'): + self.data = self._read_xml(xml_file, output_path) + + ## METADATA RELATED FUNCTIONS + + def get_time(self): + return self.data['metadata']['current_time'] + + ## MESH RELATED FUNCTIONS + + def get_mesh(self, flat=False): + """ + Return a meshgrid of the computational domain. Can return either full + 3D or a 2D plane for contour plots. + + Parameters + ---------- + flat : bool + If flat is set to true, we return only the x and y meshgrid. + Otherwise we return x, y, and z + + Returns + ------- + splitting : list length=2 if flat=True, else length=3 + Contains arrays of voxel center coordinates as meshgrid with shape + [nx_voxel, ny_voxel, nz_voxel] or [nx_voxel, ny_voxel] if flat=True. + """ + if flat == True: + xx = self.data['mesh']['x_coordinates'][:, :, 0] + yy = self.data['mesh']['y_coordinates'][:, :, 0] + + return [xx, yy] + + # if we dont want a plane just return appropriate values + else: + xx = self.data['mesh']['x_coordinates'] + yy = self.data['mesh']['y_coordinates'] + zz = self.data['mesh']['z_coordinates'] + + return [xx, yy, zz] + + def get_2D_mesh(self): + """ + This function returns the x, y meshgrid as two numpy arrays. It is + identical to get_mesh with the option flat=True + + Returns + ------- + splitting : list length=2 + Contains arrays of voxel center coordinates in x and y dimensions + as meshgrid with shape [nx_voxel, ny_voxel] + """ + xx = self.data['mesh']['x_coordinates'][:, :, 0] + yy = self.data['mesh']['y_coordinates'][:, :, 0] + + return [xx, yy] + + def get_linear_voxels(self): + """ + Helper function to quickly grab voxel centers array stored linearly as + opposed to meshgrid-style. + """ + return self.data['mesh']['voxels']['centers'] + + def get_mesh_spacing(self): + """ + Returns the space in between voxel centers for the mesh in terms of the + mesh's spatial units. Assumes that voxel centers fall on integer values. + + Returns + ------- + dx : float + Distance between voxel centers in the same units as the other + spatial measurements + """ + centers = self.get_linear_voxels() + X = np.unique(centers[0, :]) + Y = np.unique(centers[1, :]) + Z = np.unique(centers[2, :]) + + dx = (X.max() - X.min()) / X.shape[0] + dy = (Y.max() - Y.min()) / Y.shape[0] + dz = (Z.max() - Z.min()) / Z.shape[0] + + if np.abs(dx - dy) > 1e-10 or np.abs(dy - dz) > 1e-10 \ + or np.abs(dx - dz) > 1e-10: + print('Warning: grid spacing may be axis dependent.') + + return round(dx) + + def get_containing_voxel_ijk(self, x, y, z): + """ + Internal function to get the meshgrid indices for the center of a voxel + that contains the given position. + + Note that pyMCDS stores meshgrids as 'cartesian' + (indexing='xy' in np.meshgrid) which means that we will have + to use these indices as [j, i, k] on the actual meshgrid objects + + Parameters + ---------- + x : float + x-coordinate for the position + y : float + y-coordinate for the position + z : float + z-coordinate for the position + + Returns + ------- + ijk : list length=3 + contains the i, j, and k indices for the containing voxel's center + """ + xx, yy, zz = self.get_mesh() + ds = self.get_mesh_spacing() + + if x > xx.max(): + warnings.warn('Position out of bounds: x out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting x = x_max!'.format(x, y, z)) + x = xx.max() + elif x < xx.min(): + warnings.warn('Position out of bounds: x out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting x = x_min!'.format(x, y, z)) + x = xx.min() + elif y > yy.max(): + warnings.warn('Position out of bounds: y out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting y = y_max!'.format(x, y, z)) + y = yy.max() + elif y < yy.min(): + warnings.warn('Position out of bounds: y out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting y = y_min!'.format(x, y, z)) + y = yy.min() + elif z > zz.max(): + warnings.warn('Position out of bounds: z out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting z = z_max!'.format(x, y, z)) + z = zz.max() + elif z < zz.min(): + warnings.warn('Position out of bounds: z out of bounds in pyMCDS._get_voxel_idx({0}, {1}, {2}). Setting z = z_min!'.format(x, y, z)) + z = zz.min() + + i = np.round((x - xx.min()) / ds) + j = np.round((y - yy.min()) / ds) + k = np.round((z - zz.min()) / ds) + + ii, jj, kk = int(i), int(j), int(k) + + return [ii, jj, kk] + + ## MICROENVIRONMENT RELATED FUNCTIONS + + def get_substrate_names(self): + """ + Returns list of chemical species in microenvironment + + Returns + ------- + species_list : array (str), shape=[n_species,] + Contains names of chemical species in microenvironment + """ + species_list = [] + for name in self.data['continuum_variables']: + species_list.append(name) + + return species_list + + def get_concentrations(self, species_name, z_slice=None): + """ + Returns the concentration array for the specified chemical species + in the microenvironment. Can return either the whole 3D picture, or + a 2D plane of concentrations. + + Parameters + ---------- + species_name : str + Name of the chemical species for which to get concentrations + + z_slice : float + z-axis position to use as plane for 2D output. This value must match + a plane of voxel centers in the z-axis. + Returns + ------- + conc_arr : array (np.float) shape=[nx_voxels, ny_voxels, nz_voxels] + Contains the concentration of the specified chemical in each voxel. + The array spatially maps to a meshgrid of the voxel centers. + """ + if z_slice is not None: + # check to see that z_slice is a valid plane + zz = self.data['mesh']['z_coordinates'] + assert z_slice in zz, 'Specified z_slice {} not in z_coordinates'.format(z_slice) + + # do the processing if its ok + mask = zz == z_slice + full_conc = self.data['continuum_variables'][species_name]['data'] + conc_arr = full_conc[mask].reshape((zz.shape[0], zz.shape[1])) + else: + conc_arr = self.data['continuum_variables'][species_name]['data'] + + return conc_arr + + def get_concentrations_at(self, x, y, z): + """ + Return concentrations of each chemical species inside a particular voxel + that contains the point described in the arguments. + + Parameters + ---------- + x : float + x-position for the point of interest + y : float + y_position for the point of interest + z : float + z_position for the point of interest + + Returns + ------- + concs : array, shape=[n_substrates,] + array of concentrations in the order given by get_substrate_names() + """ + i, j, k = self.get_containing_voxel_ijk(x, y, z) + sub_name_list = self.get_substrate_names() + concs = np.zeros(len(sub_name_list)) + + for ix in range(len(sub_name_list)): + concs[ix] = self.get_concentrations(sub_name_list[ix])[j, i, k] + + return concs + + + ## CELL RELATED FUNCTIONS + + def get_cell_df(self): + """ + Builds DataFrame from data['discrete_cells'] + + Returns + ------- + cells_df : pd.Dataframe, shape=[n_cells, n_variables] + Dataframe containing the cell data for all cells at this time step + """ + cells_df = pd.DataFrame(self.data['discrete_cells']) + return cells_df + + def get_cell_variables(self): + """ + Returns the names of all of the cell variables tracked in ['discrete cells'] + dictionary + + Returns + ------- + var_list : list, shape=[n_variables] + Contains the names of the cell variables + """ + var_list = [] + for name in self.data['discrete_cells']: + var_list.append(name) + return var_list + + def get_cell_df_at(self, x, y, z): + """ + Returns a dataframe for cells in the same voxel as the position given by + x, y, and z. + + Parameters + ---------- + x : float + x-position for the point of interest + y : float + y_position for the point of interest + z : float + z_position for the point of interest + + Returns + ------- + vox_df : pd.DataFrame, shape=[n_cell_in_voxel, n_variables] + cell dataframe containing only cells in the same voxel as the point + specified by x, y, and z. + """ + ds = self.get_mesh_spacing() + xx, yy, zz = self.get_mesh() + i, j, k = self.get_containing_voxel_ijk(x, y, z) + x_vox = xx[j, i, k] + y_vox = yy[j, i, k] + z_vox = zz[j, i, k] + + cell_df = self.get_cell_df() + inside_voxel = ( (cell_df['position_x'] < x_vox + ds/2.) & + (cell_df['position_x'] > x_vox - ds/2.) & + (cell_df['position_y'] < y_vox + ds/2.) & + (cell_df['position_y'] > y_vox - ds/2.) & + (cell_df['position_z'] < z_vox + ds/2.) & + (cell_df['position_z'] > z_vox - ds/2.) ) + vox_df = cell_df[inside_voxel] + return vox_df + + def _read_xml(self, xml_file, output_path='.'): + """ + Does the actual work of initializing MultiCellDS by parsing the xml + """ + + output_path = Path(output_path) + xml_file = output_path / xml_file + tree = ET.parse(xml_file) + + print('Reading {}'.format(xml_file)) + + root = tree.getroot() + MCDS = {} + + # Get current simulated time + metadata_node = root.find('metadata') + time_node = metadata_node.find('current_time') + MCDS['metadata'] = {} + MCDS['metadata']['current_time'] = float(time_node.text) + MCDS['metadata']['time_units'] = time_node.get('units') + + # Get current runtime + time_node = metadata_node.find('current_runtime') + MCDS['metadata']['current_runtime'] = float(time_node.text) + MCDS['metadata']['runtime_units'] = time_node.get('units') + + # find the microenvironment node + me_node = root.find('microenvironment') + me_node = me_node.find('domain') + + # find the mesh node + mesh_node = me_node.find('mesh') + MCDS['metadata']['spatial_units'] = mesh_node.get('units') + MCDS['mesh'] = {} + + # while we're at it, find the mesh + coord_str = mesh_node.find('x_coordinates').text + delimiter = mesh_node.find('x_coordinates').get('delimiter') + x_coords = np.array(coord_str.split(delimiter), dtype=np.float) + + coord_str = mesh_node.find('y_coordinates').text + delimiter = mesh_node.find('y_coordinates').get('delimiter') + y_coords = np.array(coord_str.split(delimiter), dtype=np.float) + + coord_str = mesh_node.find('z_coordinates').text + delimiter = mesh_node.find('z_coordinates').get('delimiter') + z_coords = np.array(coord_str.split(delimiter), dtype=np.float) + + # reshape into a mesh grid + xx, yy, zz = np.meshgrid(x_coords, y_coords, z_coords) + + MCDS['mesh']['x_coordinates'] = xx + MCDS['mesh']['y_coordinates'] = yy + MCDS['mesh']['z_coordinates'] = zz + + # Voxel data must be loaded from .mat file + voxel_file = mesh_node.find('voxels').find('filename').text + voxel_path = output_path / voxel_file + try: + initial_mesh = sio.loadmat(voxel_path)['mesh'] + except: + raise FileNotFoundError( + "No such file or directory:\n'{}' referenced in '{}'".format(voxel_path, xml_file)) + sys.exit(1) + + print('Reading {}'.format(voxel_path)) + + # center of voxel specified by first three rows [ x, y, z ] + # volume specified by fourth row + MCDS['mesh']['voxels'] = {} + MCDS['mesh']['voxels']['centers'] = initial_mesh[:3, :] + MCDS['mesh']['voxels']['volumes'] = initial_mesh[3, :] + + # Continuum_variables, unlike in the matlab version the individual chemical + # species will be primarily accessed through their names e.g. + # MCDS['continuum_variables']['oxygen']['units'] + # MCDS['continuum_variables']['glucose']['data'] + MCDS['continuum_variables'] = {} + variables_node = me_node.find('variables') + file_node = me_node.find('data').find('filename') + + # micro environment data is shape [4+n, len(voxels)] where n is the number + # of species being tracked. the first 3 rows represent (x, y, z) of voxel + # centers. The fourth row contains the voxel volume. The 5th row and up will + # contain values for that species in that voxel. + me_file = file_node.text + me_path = output_path / me_file + # Changes here + try: + me_data = sio.loadmat(me_path)['multiscale_microenvironment'] + except: + raise FileNotFoundError( + "No such file or directory:\n'{}' referenced in '{}'".format(me_path, xml_file)) + sys.exit(1) + + print('Reading {}'.format(me_path)) + + var_children = variables_node.findall('variable') + + # we're going to need the linear x, y, and z coordinates later + # but we dont need to get them in the loop + X, Y, Z = np.unique(xx), np.unique(yy), np.unique(zz) + + for si, species in enumerate(var_children): + species_name = species.get('name') + MCDS['continuum_variables'][species_name] = {} + MCDS['continuum_variables'][species_name]['units'] = species.get( + 'units') + + print('Parsing {:s} data'.format(species_name)) + + # initialize array for concentration data + MCDS['continuum_variables'][species_name]['data'] = np.zeros(xx.shape) + + # travel down one level on tree + species = species.find('physical_parameter_set') + + # diffusion data for each species + MCDS['continuum_variables'][species_name]['diffusion_coefficient'] = {} + MCDS['continuum_variables'][species_name]['diffusion_coefficient']['value'] \ + = float(species.find('diffusion_coefficient').text) + MCDS['continuum_variables'][species_name]['diffusion_coefficient']['units'] \ + = species.find('diffusion_coefficient').get('units') + + # decay data for each species + MCDS['continuum_variables'][species_name]['decay_rate'] = {} + MCDS['continuum_variables'][species_name]['decay_rate']['value'] \ + = float(species.find('decay_rate').text) + MCDS['continuum_variables'][species_name]['decay_rate']['units'] \ + = species.find('decay_rate').get('units') + + # store data from microenvironment file as numpy array + # iterate over each voxel + for vox_idx in range(MCDS['mesh']['voxels']['centers'].shape[1]): + # find the center + center = MCDS['mesh']['voxels']['centers'][:, vox_idx] + + i = np.where(np.abs(center[0] - X) < 1e-10)[0][0] + j = np.where(np.abs(center[1] - Y) < 1e-10)[0][0] + k = np.where(np.abs(center[2] - Z) < 1e-10)[0][0] + + MCDS['continuum_variables'][species_name]['data'][j, i, k] \ + = me_data[4+si, vox_idx] + + # in order to get to the good stuff we have to pass through a few different + # hierarchal levels + cell_node = root.find('cellular_information') + cell_node = cell_node.find('cell_populations') + cell_node = cell_node.find('cell_population') + cell_node = cell_node.find('custom') + # we want the PhysiCell data, there is more of it + for child in cell_node.findall('simplified_data'): + if child.get('source') == 'PhysiCell': + cell_node = child + break + + MCDS['discrete_cells'] = {} + data_labels = [] + # iterate over 'label's which are children of 'labels' these will be used to + # label data arrays + for label in cell_node.find('labels').findall('label'): + # I don't like spaces in my dictionary keys + fixed_label = label.text.replace(' ', '_') + if int(label.get('size')) > 1: + # tags to differentiate repeated labels (usually space related) + dir_label = ['_x', '_y', '_z'] + for i in range(int(label.get('size'))): + data_labels.append(fixed_label + dir_label[i]) + else: + data_labels.append(fixed_label) + + # load the file + cell_file = cell_node.find('filename').text + cell_path = output_path / cell_file + try: + cell_data = sio.loadmat(cell_path)['cells'] + except: + raise FileNotFoundError( + "No such file or directory:\n'{}' referenced in '{}'".format(cell_path, xml_file)) + sys.exit(1) + + print('Reading {}'.format(cell_path)) + + for col in range(len(data_labels)): + MCDS['discrete_cells'][data_labels[col]] = cell_data[col, :] + + return MCDS From 508b5a0b6065788782f290faf13d8ff09228b51f Mon Sep 17 00:00:00 2001 From: Randy Heiland Date: Thu, 29 Sep 2022 05:42:18 -0400 Subject: [PATCH 15/15] init --- analysis/plot_data_ellip.py | 129 ++++ analysis/plot_ellip.py | 128 ++++ analysis/vis_tab_cells_ellipse.py | 842 ++++++++++++++++++++++++ analysis/vis_tab_ellipse.py | 1019 +++++++++++++++++++++++++++++ 4 files changed, 2118 insertions(+) create mode 100644 analysis/plot_data_ellip.py create mode 100644 analysis/plot_ellip.py create mode 100644 analysis/vis_tab_cells_ellipse.py create mode 100644 analysis/vis_tab_ellipse.py diff --git a/analysis/plot_data_ellip.py b/analysis/plot_data_ellip.py new file mode 100644 index 0000000..8158437 --- /dev/null +++ b/analysis/plot_data_ellip.py @@ -0,0 +1,129 @@ +""" +Provide simple plotting functionality for PhysiCell output results. + +Authors: +Randy Heiland (heiland@iu.edu) +Dr. Paul Macklin (macklinp@iu.edu) + +--- Versions --- +0.1 - initial version +""" +# https://doc.qt.io/qtforpython/gettingstarted.html + +import os +import sys +import getopt +import shutil +from pathlib import Path +import xml.etree.ElementTree as ET # https://docs.python.org/2/library/xml.etree.elementtree.html +from xml.dom import minidom + +from PyQt5 import QtCore, QtGui +from PyQt5.QtWidgets import * + +from vis_tab_ellipse import Vis + +def SingleBrowse(self): + # if len(self.csv) < 2: + filePath = QFileDialog.getOpenFileName(self,'',".",'*.xml') + + # if filePath != "" and not filePath in self.csv: + # self.csv.append(filePath) + # print(self.csv) + +class PhysiCellXMLCreator(QWidget): + # def __init__(self, parent = None): + def __init__(self, parent = None): + super(PhysiCellXMLCreator, self).__init__(parent) + + self.title_prefix = "PhysiCell Visualization" + self.setWindowTitle(self.title_prefix) + + # Menus + vlayout = QVBoxLayout(self) + # vlayout.setContentsMargins(5, 35, 5, 5) + # menuWidget = QWidget(self.menu()) + # vlayout.addWidget(menuWidget) + + # self.setWindowIcon(self.style().standardIcon(getattr(QStyle, 'SP_DialogNoButton'))) + # self.setWindowIcon(QtGui.QIcon('physicell_logo_25pct.png')) + # self.grid = QGridLayout() + # lay.addLayout(self.grid) + self.setLayout(vlayout) + # self.setMinimumSize(400, 790) # width, height (height >= Cell Types|Death params) + # self.setMinimumSize(400, 500) # width, height (height >= Cell Types|Death params) + # self.setMinimumSize(800, 620) # width, height (height >= Cell Types|Death params) + # self.setMinimumSize(800, 660) # width, height (height >= Cell Types|Death params) + # self.setMinimumSize(800, 800) # width, height (height >= Cell Types|Death params) + # self.setMinimumSize(700, 770) # width, height (height >= Cell Types|Death params) + self.setMinimumSize(800, 770) # width, height (height >= Cell Types|Death params) + # self.setMinimumSize(600, 600) # width, height (height >= Cell Types|Death params) + # self.resize(400, 790) # width, height (height >= Cell Types|Death params) + + # self.menubar = QtWidgets.QMenuBar(self) + # self.file_menu = QtWidgets.QMenu('File') + # self.file_menu.insertAction("Open") + # self.menubar.addMenu(self.file_menu) + + # GUI tabs + + # By default, let's startup the app with a default of template2D (a copy) + # self.new_model_cb() # default on startup + # read_file = "../data/subcellular_flat.xml" + # read_file = "../data/cancer_biorobots_flat.xml" + # read_file = "../data/pred_prey_flat.xml" + + model_name = "pred_prey_flat" + model_name = "biorobots_flat" + model_name = "cancer_biorobots_flat" + model_name = "test1" + model_name = "test-gui" + model_name = "covid19_v5_flat" + model_name = "template" + # model_name = "randy_test" #rwh + # read_file = "data/" + model_name + ".xml" + + # then what?? + # binDirectory = os.path.realpath(os.path.abspath(__file__)) + binDirectory = os.path.dirname(os.path.abspath(__file__)) + dataDirectory = os.path.join(binDirectory,'..','data') + + # self.tree = ET.parse(self.config_file) + # self.xml_root = self.tree.getroot() + + + #------------------ + tabWidget = QTabWidget() + self.vis_tab = Vis() + # self.vis_tab.xml_root = self.xml_root + tabWidget.addTab(self.vis_tab,"") + + vlayout.addWidget(tabWidget) + # self.addTab(self.sbml_tab,"SBML") + + tabWidget.setCurrentIndex(0) # Config (default) + + + def menu(self): + menubar = QMenuBar(self) + menubar.setNativeMenuBar(False) + + #-------------- + file_menu = menubar.addMenu('&File') + + # # file_menu.addAction("Open", self.open_as_cb, QtGui.QKeySequence('Ctrl+o')) + # file_menu.addAction("Save", self.save_cb, QtGui.QKeySequence('Ctrl+s')) + + menubar.adjustSize() # Argh. Otherwise, only 1st menu appears, with ">>" to others! + +def main(): + inputfile = '' + + app = QApplication(sys.argv) + ex = PhysiCellXMLCreator() + # ex.setGeometry(100,100, 800,600) + ex.show() + sys.exit(app.exec_()) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/analysis/plot_ellip.py b/analysis/plot_ellip.py new file mode 100644 index 0000000..5d65c00 --- /dev/null +++ b/analysis/plot_ellip.py @@ -0,0 +1,128 @@ +""" +Provide simple plotting functionality for PhysiCell output results. + +Authors: +Randy Heiland (heiland@iu.edu) +Dr. Paul Macklin (macklinp@iu.edu) + +--- Versions --- +0.1 - initial version +""" +# https://doc.qt.io/qtforpython/gettingstarted.html + +import os +import sys +import getopt +import shutil +from pathlib import Path +import xml.etree.ElementTree as ET # https://docs.python.org/2/library/xml.etree.elementtree.html +from xml.dom import minidom + +from PyQt5 import QtCore, QtGui +from PyQt5.QtWidgets import * + +from vis_tab_cells_ellipse import Vis + +def SingleBrowse(self): + # if len(self.csv) < 2: + filePath = QFileDialog.getOpenFileName(self,'',".",'*.xml') + + # if filePath != "" and not filePath in self.csv: + # self.csv.append(filePath) + # print(self.csv) + +class PhysiCellXMLCreator(QWidget): + # def __init__(self, parent = None): + def __init__(self, parent = None): + super(PhysiCellXMLCreator, self).__init__(parent) + + self.title_prefix = "PhysiCell Visualization" + self.setWindowTitle(self.title_prefix) + + # Menus + vlayout = QVBoxLayout(self) + # vlayout.setContentsMargins(5, 35, 5, 5) + # menuWidget = QWidget(self.menu()) + # vlayout.addWidget(menuWidget) + + # self.setWindowIcon(self.style().standardIcon(getattr(QStyle, 'SP_DialogNoButton'))) + # self.setWindowIcon(QtGui.QIcon('physicell_logo_25pct.png')) + # self.grid = QGridLayout() + # lay.addLayout(self.grid) + self.setLayout(vlayout) + # self.setMinimumSize(400, 790) # width, height (height >= Cell Types|Death params) + # self.setMinimumSize(400, 500) # width, height (height >= Cell Types|Death params) + # self.setMinimumSize(800, 620) # width, height (height >= Cell Types|Death params) + # self.setMinimumSize(800, 660) # width, height (height >= Cell Types|Death params) + # self.setMinimumSize(800, 800) # width, height (height >= Cell Types|Death params) + self.setMinimumSize(700, 770) # width, height (height >= Cell Types|Death params) + # self.setMinimumSize(600, 600) # width, height (height >= Cell Types|Death params) + # self.resize(400, 790) # width, height (height >= Cell Types|Death params) + + # self.menubar = QtWidgets.QMenuBar(self) + # self.file_menu = QtWidgets.QMenu('File') + # self.file_menu.insertAction("Open") + # self.menubar.addMenu(self.file_menu) + + # GUI tabs + + # By default, let's startup the app with a default of template2D (a copy) + # self.new_model_cb() # default on startup + # read_file = "../data/subcellular_flat.xml" + # read_file = "../data/cancer_biorobots_flat.xml" + # read_file = "../data/pred_prey_flat.xml" + + model_name = "pred_prey_flat" + model_name = "biorobots_flat" + model_name = "cancer_biorobots_flat" + model_name = "test1" + model_name = "test-gui" + model_name = "covid19_v5_flat" + model_name = "template" + # model_name = "randy_test" #rwh + # read_file = "data/" + model_name + ".xml" + + # then what?? + # binDirectory = os.path.realpath(os.path.abspath(__file__)) + binDirectory = os.path.dirname(os.path.abspath(__file__)) + dataDirectory = os.path.join(binDirectory,'..','data') + + # self.tree = ET.parse(self.config_file) + # self.xml_root = self.tree.getroot() + + + #------------------ + tabWidget = QTabWidget() + self.vis_tab = Vis() + # self.vis_tab.xml_root = self.xml_root + tabWidget.addTab(self.vis_tab,"Plot") + + vlayout.addWidget(tabWidget) + # self.addTab(self.sbml_tab,"SBML") + + tabWidget.setCurrentIndex(0) # Config (default) + + + def menu(self): + menubar = QMenuBar(self) + menubar.setNativeMenuBar(False) + + #-------------- + file_menu = menubar.addMenu('&File') + + # # file_menu.addAction("Open", self.open_as_cb, QtGui.QKeySequence('Ctrl+o')) + # file_menu.addAction("Save", self.save_cb, QtGui.QKeySequence('Ctrl+s')) + + menubar.adjustSize() # Argh. Otherwise, only 1st menu appears, with ">>" to others! + +def main(): + inputfile = '' + + app = QApplication(sys.argv) + ex = PhysiCellXMLCreator() + # ex.setGeometry(100,100, 800,600) + ex.show() + sys.exit(app.exec_()) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/analysis/vis_tab_cells_ellipse.py b/analysis/vis_tab_cells_ellipse.py new file mode 100644 index 0000000..72dd552 --- /dev/null +++ b/analysis/vis_tab_cells_ellipse.py @@ -0,0 +1,842 @@ +import sys +import os +import time +import xml.etree.ElementTree as ET # https://docs.python.org/2/library/xml.etree.elementtree.html +from pathlib import Path +# from ipywidgets import Layout, Label, Text, Checkbox, Button, BoundedIntText, HBox, VBox, Box, \ + # FloatText, Dropdown, SelectMultiple, RadioButtons, interactive +# import matplotlib.pyplot as plt +from matplotlib.colors import BoundaryNorm +from matplotlib.ticker import MaxNLocator +from matplotlib.collections import LineCollection +from matplotlib.patches import Circle, Ellipse, Rectangle +from matplotlib.collections import PatchCollection +import matplotlib.colors as mplc +from matplotlib import gridspec +from collections import deque + +from PyQt5 import QtCore, QtGui +from PyQt5.QtWidgets import QFrame,QApplication,QWidget,QTabWidget,QFormLayout,QLineEdit, QHBoxLayout,QVBoxLayout, \ + QRadioButton,QLabel,QCheckBox,QComboBox,QScrollArea, QMainWindow,QGridLayout, QPushButton, QFileDialog, QMessageBox + +import math +import numpy as np +import scipy.io # .io.loadmat(full_fname, info_dict) +import matplotlib +matplotlib.use('Qt5Agg') +import matplotlib.pyplot as plt +from mpl_toolkits.axes_grid1 import make_axes_locatable + +# from PyQt5 import QtCore, QtWidgets + +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg +# from matplotlib.figure import Figure + +class Vis(QWidget): + def __init__(self): + super().__init__() + # global self.config_params + + self.xml_root = None + self.current_svg_frame = 0 + self.timer = QtCore.QTimer() + # self.t.timeout.connect(self.task) + self.timer.timeout.connect(self.play_plot_cb) + + # self.tab = QWidget() + # self.tabs.resize(200,5) + + self.use_defaults = True + self.title_str = "" + self.xmin = -1000 + self.xmax = 1000 + self.x_range = self.xmax - self.xmin + + self.ymin = -1000 + self.ymax = 1000 + self.y_range = self.ymax - self.ymin + self.show_nucleus = False + self.show_edge = False + self.alpha = 0.7 + # self.cells_toggle = None + # self.substrates_toggle = None + + basic_length = 12.0 + self.figsize_width_substrate = 15.0 # allow extra for colormap + self.figsize_height_substrate = basic_length + + self.figsize_width_2Dplot = basic_length + self.figsize_height_2Dplot = basic_length + + # rwh: TODO these params + self.modulo = 1 + self.field_index = 4 + # define dummy size of mesh (set in the tool's primary module) + self.numx = 0 + self.numy = 0 + self.colormap_min = 0.0 + self.colormap_max = 10.0 + self.colormap_fixed_toggle = False + self.fontsize = 10 + + + # self.width_substrate = basic_length # allow extra for colormap + # self.height_substrate = basic_length + + self.figsize_width_svg = basic_length + self.figsize_height_svg = basic_length + + # self.output_dir = "/Users/heiland/dev/PhysiCell_V.1.8.0_release/output" + self.output_dir = "./output" + + self.customized_output_freq = False + + #------------------------------------------- + label_width = 110 + domain_value_width = 100 + value_width = 60 + label_height = 20 + units_width = 70 + + # self.create_figure() + + self.scroll = QScrollArea() # might contain centralWidget + + self.config_params = QWidget() + + self.main_layout = QVBoxLayout() + + self.vbox = QVBoxLayout() + self.vbox.addStretch(0) + + # self.substrates_toggle = None + + #------------------ + controls_vbox = QVBoxLayout() + + controls_hbox = QHBoxLayout() + w = QPushButton("Directory") + w.clicked.connect(self.open_directory_cb) + controls_hbox.addWidget(w) + + # self.output_dir = "/Users/heiland/dev/PhysiCell_V.1.8.0_release/output" + self.output_dir_w = QLineEdit() + self.output_dir_w.setText("./output") + # w.setText("/Users/heiland/dev/PhysiCell_V.1.8.0_release/output") + # w.setText(self.output_dir) + # w.textChanged[str].connect(self.output_dir_changed) + # w.textChanged.connect(self.output_dir_changed) + controls_hbox.addWidget(self.output_dir_w) + + self.back_button = QPushButton("<") + self.back_button.clicked.connect(self.back_plot_cb) + controls_hbox.addWidget(self.back_button) + + self.forward_button = QPushButton(">") + self.forward_button.clicked.connect(self.forward_plot_cb) + controls_hbox.addWidget(self.forward_button) + + self.play_button = QPushButton("Play") + # self.play_button.clicked.connect(self.play_plot_cb) + self.play_button.clicked.connect(self.animate) + controls_hbox.addWidget(self.play_button) + + self.reset_button = QPushButton("Reset") + # self.play_button.clicked.connect(self.play_plot_cb) + self.reset_button.clicked.connect(self.reset_plot_cb) + controls_hbox.addWidget(self.reset_button) + controls_vbox.addLayout(controls_hbox) + + #------------- + hbox = QHBoxLayout() + + self.cells_toggle = QCheckBox("cells") + self.cells_toggle.setChecked(True) + # self.substrates_toggle.stateChanged.connect(self.substrates_toggle_cb) + hbox.addWidget(self.cells_toggle) + + self.substrates_toggle = QCheckBox("substrates") + self.substrates_toggle.setChecked(True) + hbox.addWidget(self.substrates_toggle) + # controls_vbox.addLayout(hbox) + + # self.prepare_button = QPushButton("Prepare") + # self.prepare_button.clicked.connect(self.prepare_plot_cb) + # controls_hbox.addWidget(self.prepare_button) + + #================================================================== + self.config_params.setLayout(self.vbox) + + self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + self.scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + self.scroll.setWidgetResizable(True) + + self.create_figure() + + # self.scroll.setWidget(self.config_params) # self.config_params = QWidget() + self.scroll.setWidget(self.canvas) # self.config_params = QWidget() + self.layout = QVBoxLayout(self) + # self.layout.addLayout(controls_hbox) + self.layout.addLayout(controls_vbox) + + self.layout.addWidget(self.scroll) + + + self.reset_plot_cb("") + + + def open_directory_cb(self): + dialog = QFileDialog() + dir_path = dialog.getExistingDirectory(self, 'Select an output directory') + print("open_directory_cb: output_dir=",dir_path) + # if self.output_dir is "": + if dir_path == "": + return + + self.output_dir = dir_path + + self.output_dir_w.setText(self.output_dir) + # Verify initial.xml and at least one .svg file exist. Obtain bounds from initial.xml + # tree = ET.parse(self.output_dir + "/" + "initial.xml") + xml_file = Path(self.output_dir, "initial.xml") + if not os.path.isfile(xml_file): + print("Expecting initial.xml, but does not exist.") + msgBox = QMessageBox() + msgBox.setIcon(QMessageBox.Information) + msgBox.setText("Did not find 'initial.xml' in this directory.") + msgBox.setStandardButtons(QMessageBox.Ok) + msgBox.exec() + return + + tree = ET.parse(Path(self.output_dir, "initial.xml")) + xml_root = tree.getroot() + + bds_str = xml_root.find(".//microenvironment//domain//mesh//bounding_box").text + bds = bds_str.split() + print('bds=',bds) + self.xmin = float(bds[0]) + self.xmax = float(bds[3]) + self.x_range = self.xmax - self.xmin + + self.ymin = float(bds[1]) + self.ymax = float(bds[4]) + self.y_range = self.ymax - self.ymin + + # and plot 1st frame (.svg) + self.current_svg_frame = 0 + self.forward_plot_cb("") + + + # def output_dir_changed(self, text): + # self.output_dir = text + # print(self.output_dir) + + def back_plot_cb(self, text): + self.current_svg_frame -= 1 + if self.current_svg_frame < 0: + self.current_svg_frame = 0 + print('svg # ',self.current_svg_frame) + # self.plot_svg(self.current_svg_frame) + # self.plot_substrate(self.current_svg_frame) + self.plot_svg(self.current_svg_frame) + self.canvas.update() + self.canvas.draw() + + def forward_plot_cb(self, text): + self.current_svg_frame += 1 + print('svg # ',self.current_svg_frame) + # self.plot_substrate(self.current_svg_frame) + self.plot_svg(self.current_svg_frame) + self.canvas.update() + self.canvas.draw() + + def reset_plot_cb(self, text): + print("-------------- reset_plot_cb() ----------------") + xml_file = Path(self.output_dir, "initial.xml") + if not os.path.isfile(xml_file): + print("Expecting initial.xml, but does not exist.") + msgBox = QMessageBox() + msgBox.setIcon(QMessageBox.Information) + msgBox.setText("Did not find 'initial.xml' in this directory.") + msgBox.setStandardButtons(QMessageBox.Ok) + msgBox.exec() + return + + # if self.first_time: + # self.first_time = False + # full_xml_filename = Path(os.path.join(self.output_dir, 'config.xml')) + # if full_xml_filename.is_file(): + # tree = ET.parse(full_xml_filename) # this file cannot be overwritten; part of tool distro + # xml_root = tree.getroot() + # self.svg_delta_t = int(xml_root.find(".//SVG//interval").text) + # self.substrate_delta_t = int(xml_root.find(".//full_data//interval").text) + # # print("---- svg,substrate delta_t values=",self.svg_delta_t,self.substrate_delta_t) + # self.modulo = int(self.substrate_delta_t / self.svg_delta_t) + # # print("---- modulo=",self.modulo) + + # all_files = sorted(glob.glob(os.path.join(self.output_dir, 'snap*.svg'))) # if .svg + # if len(all_files) > 0: + # last_file = all_files[-1] + # # print("substrates.py/update(): len(snap*.svg) = ",len(all_files)," , last_file=",last_file) + # self.max_frames.value = int(last_file[-12:-4]) # assumes naming scheme: "snapshot%08d.svg" + # else: + # substrate_files = sorted(glob.glob(os.path.join(self.output_dir, 'output*.xml'))) + # if len(substrate_files) > 0: + # last_file = substrate_files[-1] + # self.max_frames.value = int(last_file[-12:-4]) + + + tree = ET.parse(Path(self.output_dir, "initial.xml")) + xml_root = tree.getroot() + + bds_str = xml_root.find(".//microenvironment//domain//mesh//bounding_box").text + bds = bds_str.split() + print('bds=',bds) + self.xmin = float(bds[0]) + self.xmax = float(bds[3]) + self.x_range = self.xmax - self.xmin + + self.ymin = float(bds[1]) + self.ymax = float(bds[4]) + self.y_range = self.ymax - self.ymin + + # self.numx = math.ceil( (self.xmax - self.xmin) / config_tab.xdelta.value) + # self.numy = math.ceil( (self.ymax - self.ymin) / config_tab.ydelta.value) + self.numx = math.ceil( (self.xmax - self.xmin) / 20.) + self.numy = math.ceil( (self.ymax - self.ymin) / 20.) + print(" calc: numx,numy = ",self.numx, self.numy) + + self.current_svg_frame = 0 + print('svg # ',self.current_svg_frame) + self.plot_svg(self.current_svg_frame) + # self.plot_substrate(self.current_svg_frame) + self.canvas.update() + self.canvas.draw() + + # def task(self): + # self.dc.update_figure() + def play_plot_cb(self): + for idx in range(1): + self.current_svg_frame += 1 + print('svg # ',self.current_svg_frame) + + fname = "snapshot%08d.svg" % self.current_svg_frame + full_fname = os.path.join(self.output_dir, fname) + # print("full_fname = ",full_fname) + # with debug_view: + # print("plot_svg:", full_fname) + # print("-- plot_svg:", full_fname) + if not os.path.isfile(full_fname): + # print("Once output files are generated, click the slider.") + print("ERROR: filename not found.") + self.timer.stop() + return + + # self.plot_substrate(self.current_svg_frame) + self.plot_svg(self.current_svg_frame) + self.canvas.update() + self.canvas.draw() + + def animate(self, text): + self.current_svg_frame = 0 + # self.timer = QtCore.QTimer() + # self.timer.timeout.connect(self.play_plot_cb) + # self.timer.start(2000) # every 2 sec + self.timer.start(100) + + # def play_plot_cb0(self, text): + # for idx in range(10): + # self.current_svg_frame += 1 + # print('svg # ',self.current_svg_frame) + # self.plot_svg(self.current_svg_frame) + # self.canvas.update() + # self.canvas.draw() + # # time.sleep(1) + # # self.ax0.clear() + # # self.canvas.pause(0.05) + + def prepare_plot_cb(self, text): + self.current_svg_frame += 1 + print('svg # ',self.current_svg_frame) + # self.plot_substrate(self.current_svg_frame) + self.plot_svg(self.current_svg_frame) + self.canvas.update() + self.canvas.draw() + + def create_figure(self): + # self.figure = plt.figure() + self.figure = plt.figure(figsize=(self.figsize_width_substrate, self.figsize_height_substrate)) + self.canvas = FigureCanvasQTAgg(self.figure) + self.canvas.setStyleSheet("background-color:transparent;") + + # Adding one subplot for image + self.ax0 = self.figure.add_subplot(111) + + # self.fig = plt.figure(figsize=(self.figsize_width_substrate, self.figsize_height_substrate)) + # self.ax0 = plt.figure(figsize=(self.figsize_width_substrate, self.figsize_height_substrate)) + + # self.ax0.get_xaxis().set_visible(False) + # self.ax0.get_yaxis().set_visible(False) + # plt.tight_layout() + + # np.random.seed(19680801) # for reproducibility + # N = 50 + # x = np.random.rand(N) * 2000 + # y = np.random.rand(N) * 2000 + # colors = np.random.rand(N) + # area = (30 * np.random.rand(N))**2 # 0 to 15 point radii + # self.ax0.scatter(x, y, s=area, c=colors, alpha=0.5) + + # self.plot_substrate(self.current_svg_frame) + self.plot_svg(self.current_svg_frame) + + # self.imageInit = [[255] * 320 for i in range(240)] + # self.imageInit[0][0] = 0 + + # Init image and add colorbar + # self.image = self.ax0.imshow(self.imageInit, interpolation='none') + # divider = make_axes_locatable(self.ax0) + # cax = divider.new_vertical(size="5%", pad=0.05, pack_start=True) + # self.colorbar = self.figure.add_axes(cax) + # self.figure.colorbar(self.image, cax=cax, orientation='horizontal') + + # plt.subplots_adjust(left=0, bottom=0.05, right=1, top=1, wspace=0, hspace=0) + + self.canvas.draw() + + #--------------------------------------------------------------------------- + def ellipses(self, x, y, w,h, c='b', vmin=None, vmax=None, **kwargs): + + + if np.isscalar(c): + kwargs.setdefault('color', c) + c = None + + if 'fc' in kwargs: + kwargs.setdefault('facecolor', kwargs.pop('fc')) + if 'ec' in kwargs: + kwargs.setdefault('edgecolor', kwargs.pop('ec')) + if 'ls' in kwargs: + kwargs.setdefault('linestyle', kwargs.pop('ls')) + if 'lw' in kwargs: + kwargs.setdefault('linewidth', kwargs.pop('lw')) + # You can set `facecolor` with an array for each patch, + # while you can only set `facecolors` with a value for all. + +# ww = X / 10.0 +# hh = Y / 15.0 +# aa = X * 9 +# fig, ax = plt.subplots() +# ec = EllipseCollection(ww, hh, aa, units='x', offsets=XY, offset_transform=ax.transData) + + zipped = np.broadcast(x, y, w,h) + patches = [Ellipse((x_, y_), w_, h_) + for x_, y_, w_, h_ in zipped] + collection = PatchCollection(patches, **kwargs) + if c is not None: + c = np.broadcast_to(c, zipped.shape).ravel() + collection.set_array(c) + collection.set_clim(vmin, vmax) + + # ax = plt.gca() + # ax.add_collection(collection) + # ax.autoscale_view() + self.ax0.add_collection(collection) + self.ax0.autoscale_view() + # plt.draw_if_interactive() + if c is not None: + # plt.sci(collection) + self.ax0.sci(collection) + # return collection + + #--------------------------------------------------------------------------- + def circles(self, x, y, s, c='b', vmin=None, vmax=None, **kwargs): + """ + See https://gist.github.com/syrte/592a062c562cd2a98a83 + + Make a scatter plot of circles. + Similar to plt.scatter, but the size of circles are in data scale. + Parameters + ---------- + x, y : scalar or array_like, shape (n, ) + Input data + s : scalar or array_like, shape (n, ) + Radius of circles. + c : color or sequence of color, optional, default : 'b' + `c` can be a single color format string, or a sequence of color + specifications of length `N`, or a sequence of `N` numbers to be + mapped to colors using the `cmap` and `norm` specified via kwargs. + Note that `c` should not be a single numeric RGB or RGBA sequence + because that is indistinguishable from an array of values + to be colormapped. (If you insist, use `color` instead.) + `c` can be a 2-D array in which the rows are RGB or RGBA, however. + vmin, vmax : scalar, optional, default: None + `vmin` and `vmax` are used in conjunction with `norm` to normalize + luminance data. If either are `None`, the min and max of the + color array is used. + kwargs : `~matplotlib.collections.Collection` properties + Eg. alpha, edgecolor(ec), facecolor(fc), linewidth(lw), linestyle(ls), + norm, cmap, transform, etc. + Returns + ------- + paths : `~matplotlib.collections.PathCollection` + Examples + -------- + a = np.arange(11) + circles(a, a, s=a*0.2, c=a, alpha=0.5, ec='none') + plt.colorbar() + License + -------- + This code is under [The BSD 3-Clause License] + (http://opensource.org/licenses/BSD-3-Clause) + """ + + if np.isscalar(c): + kwargs.setdefault('color', c) + c = None + + if 'fc' in kwargs: + kwargs.setdefault('facecolor', kwargs.pop('fc')) + if 'ec' in kwargs: + kwargs.setdefault('edgecolor', kwargs.pop('ec')) + if 'ls' in kwargs: + kwargs.setdefault('linestyle', kwargs.pop('ls')) + if 'lw' in kwargs: + kwargs.setdefault('linewidth', kwargs.pop('lw')) + # You can set `facecolor` with an array for each patch, + # while you can only set `facecolors` with a value for all. + + zipped = np.broadcast(x, y, s) + patches = [Circle((x_, y_), s_) + for x_, y_, s_ in zipped] + collection = PatchCollection(patches, **kwargs) + if c is not None: + c = np.broadcast_to(c, zipped.shape).ravel() + collection.set_array(c) + collection.set_clim(vmin, vmax) + + # ax = plt.gca() + # ax.add_collection(collection) + # ax.autoscale_view() + self.ax0.add_collection(collection) + self.ax0.autoscale_view() + # plt.draw_if_interactive() + if c is not None: + # plt.sci(collection) + self.ax0.sci(collection) + # return collection + + #------------------------------------------------------------ + # def plot_svg(self, frame, rdel=''): + def plot_svg(self, frame): + # global current_idx, axes_max + global current_frame + current_frame = frame + fname = "snapshot%08d.svg" % frame + full_fname = os.path.join(self.output_dir, fname) + print("full_fname = ",full_fname) + # with debug_view: + # print("plot_svg:", full_fname) + print("-- plot_svg:", full_fname) + if not os.path.isfile(full_fname): + # print("Once output files are generated, click the slider.") + print("ERROR: filename not found.") + return + + self.ax0.cla() + self.title_str = "" + + xlist = deque() + ylist = deque() + rxlist = deque() + rylist = deque() + rgb_list = deque() + + # print('\n---- ' + fname + ':') +# tree = ET.parse(fname) + tree = ET.parse(full_fname) + root = tree.getroot() + # print('--- root.tag ---') + # print(root.tag) + # print('--- root.attrib ---') + # print(root.attrib) + # print('--- child.tag, child.attrib ---') + numChildren = 0 + for child in root: + # print(child.tag, child.attrib) + # print("keys=",child.attrib.keys()) + if self.use_defaults and ('width' in child.attrib.keys()): + self.axes_max = float(child.attrib['width']) + # print("debug> found width --> axes_max =", axes_max) + if child.text and "Current time" in child.text: + svals = child.text.split() + # remove the ".00" on minutes + self.title_str += " cells: " + svals[2] + "d, " + svals[4] + "h, " + svals[7][:-3] + "m" + + # self.cell_time_mins = int(svals[2])*1440 + int(svals[4])*60 + int(svals[7][:-3]) + # self.title_str += " cells: " + str(self.cell_time_mins) + "m" # rwh + + # print("width ",child.attrib['width']) + # print('attrib=',child.attrib) + # if (child.attrib['id'] == 'tissue'): + if ('id' in child.attrib.keys()): + # print('-------- found tissue!!') + tissue_parent = child + break + + # print('------ search tissue') + cells_parent = None + + for child in tissue_parent: + # print('attrib=',child.attrib) + if (child.attrib['id'] == 'cells'): + # print('-------- found cells, setting cells_parent') + cells_parent = child + break + numChildren += 1 + + num_cells = 0 + # print('------ search cells') + for child in cells_parent: + # print(child.tag, child.attrib) + # print('attrib=',child.attrib) + for circle in child: # two circles in each child: outer + nucleus + # circle.attrib={'cx': '1085.59','cy': '1225.24','fill': 'rgb(159,159,96)','r': '6.67717','stroke': 'rgb(159,159,96)','stroke-width': '0.5'} + # print(' --- cx,cy=',circle.attrib['cx'],circle.attrib['cy']) + xval = float(circle.attrib['cx']) + + # map SVG coords into comp domain + # xval = (xval-self.svg_xmin)/self.svg_xrange * self.x_range + self.xmin + xval = xval/self.x_range * self.x_range + self.xmin + + s = circle.attrib['fill'] + # print("s=",s) + # print("type(s)=",type(s)) + if (s[0:3] == "rgb"): # if an rgb string, e.g. "rgb(175,175,80)" + rgb = list(map(int, s[4:-1].split(","))) + rgb[:] = [x / 255. for x in rgb] + else: # otherwise, must be a color name + rgb_tuple = mplc.to_rgb(mplc.cnames[s]) # a tuple + rgb = [x for x in rgb_tuple] + + # test for bogus x,y locations (rwh TODO: use max of domain?) + too_large_val = 10000. + if (np.fabs(xval) > too_large_val): + print("bogus xval=", xval) + break + yval = float(circle.attrib['cy']) + # yval = (yval - self.svg_xmin)/self.svg_xrange * self.y_range + self.ymin + yval = yval/self.y_range * self.y_range + self.ymin + if (np.fabs(yval) > too_large_val): + print("bogus xval=", xval) + break +# +# now: +# + # rval = float(circle.attrib['r']) + rxval = float(circle.attrib['rx']) + ryval = float(circle.attrib['ry']) + # if (rgb[0] > rgb[1]): + # print(num_cells,rgb, rval) + xlist.append(xval) + ylist.append(yval) + rxlist.append(rxval) + rylist.append(ryval) + rgb_list.append(rgb) + + # For .svg files with cells that *have* a nucleus, there will be a 2nd + if (not self.show_nucleus): + #if (not self.show_nucleus): + break + + num_cells += 1 + + # if num_cells > 3: # for debugging + # print(fname,': num_cells= ',num_cells," --- debug exit.") + # sys.exit(1) + # break + + # print(fname,': num_cells= ',num_cells) + + xvals = np.array(xlist) + yvals = np.array(ylist) + rxvals = np.array(rxlist) + ryvals = np.array(rylist) + rgbs = np.array(rgb_list) + # print("xvals[0:5]=",xvals[0:5]) + # print("rvals[0:5]=",rvals[0:5]) + # print("rvals.min, max=",rvals.min(),rvals.max()) + + # rwh - is this where I change size of render window?? (YES - yipeee!) + # plt.figure(figsize=(6, 6)) + # plt.cla() + # if (self.substrates_toggle.value): + self.title_str += " (" + str(num_cells) + " agents)" + # title_str = " (" + str(num_cells) + " agents)" + # else: + # mins= round(int(float(root.find(".//current_time").text))) # TODO: check units = mins + # hrs = int(mins/60) + # days = int(hrs/24) + # title_str = '%dd, %dh, %dm' % (int(days),(hrs%24), mins - (hrs*60)) + # plt.title(self.title_str) + self.ax0.set_title(self.title_str, fontsize=5) + # self.ax0.set_title(self.title_str, prop={'size':'small'}) + + # plt.xlim(self.xmin, self.xmax) + # plt.ylim(self.ymin, self.ymax) + self.ax0.set_xlim(self.xmin, self.xmax) + self.ax0.set_ylim(self.ymin, self.ymax) + self.ax0.tick_params(labelsize=4) + + # self.ax0.colorbar(collection) + + # plt.xlim(axes_min,axes_max) + # plt.ylim(axes_min,axes_max) + # plt.scatter(xvals,yvals, s=rvals*scale_radius, c=rgbs) + + # TODO: make figsize a function of plot_size? What about non-square plots? + # self.fig = plt.figure(figsize=(9, 9)) + +# axx = plt.axes([0, 0.05, 0.9, 0.9]) # left, bottom, width, height +# axx = fig.gca() +# print('fig.dpi=',fig.dpi) # = 72 + + # im = ax.imshow(f.reshape(100,100), interpolation='nearest', cmap=cmap, extent=[0,20, 0,20]) + # ax.xlim(axes_min,axes_max) + # ax.ylim(axes_min,axes_max) + + # convert radii to radii in pixels + # ax1 = self.fig.gca() + # N = len(xvals) + # rr_pix = (ax1.transData.transform(np.vstack([rvals, rvals]).T) - + # ax1.transData.transform(np.vstack([np.zeros(N), np.zeros(N)]).T)) + # rpix, _ = rr_pix.T + + # markers_size = (144. * rpix / self.fig.dpi)**2 # = (2*rpix / fig.dpi * 72)**2 + # markers_size = markers_size/4000000. + # print('max=',markers_size.max()) + + #rwh - temp fix - Ah, error only occurs when "edges" is toggled on + if (self.show_edge): + try: + # plt.scatter(xvals,yvals, s=markers_size, c=rgbs, edgecolor='black', linewidth=0.5) + # self.circles(xvals,yvals, s=rvals, color=rgbs, alpha=self.alpha, edgecolor='black', linewidth=0.5) + self.circles(xvals,yvals, s=rxvals, color=rgbs, alpha=self.alpha, edgecolor='black', linewidth=0.5) + # cell_circles = self.circles(xvals,yvals, s=rvals, color=rgbs, edgecolor='black', linewidth=0.5) + # plt.sci(cell_circles) + except (ValueError): + pass + else: + # plt.scatter(xvals,yvals, s=markers_size, c=rgbs) + # self.circles(xvals,yvals, s=rvals, color=rgbs, alpha=self.alpha) + # self.circles(xvals,yvals, s=rxvals, color=rgbs, alpha=self.alpha) + self.ellipses(xvals,yvals, w=rxvals, h=ryvals, color=rgbs, alpha=self.alpha) + + #--------------------------------------------------------------------------- + # assume "frame" is cell frame #, unless Cells is togggled off, then it's the substrate frame # + # def plot_substrate(self, frame, grid): + def plot_substrate(self, frame): + + # print("plot_substrate(): frame*self.substrate_delta_t = ",frame*self.substrate_delta_t) + # print("plot_substrate(): frame*self.svg_delta_t = ",frame*self.svg_delta_t) + # print("plot_substrate(): fig width: SVG+2D = ",self.figsize_width_svg + self.figsize_width_2Dplot) # 24 + # print("plot_substrate(): fig width: substrate+2D = ",self.figsize_width_substrate + self.figsize_width_2Dplot) # 27 + + self.title_str = '' + + # Recall: + # self.svg_delta_t = config_tab.svg_interval.value + # self.substrate_delta_t = config_tab.mcds_interval.value + # self.modulo = int(self.substrate_delta_t / self.svg_delta_t) + # self.therapy_activation_time = user_params_tab.therapy_activation_time.value + + # print("plot_substrate(): pre_therapy: max svg, substrate frames = ",max_svg_frame_pre_therapy, max_substrate_frame_pre_therapy) + + # Assume: # .svg files >= # substrate files +# if (self.cells_toggle.value): + + if self.substrates_toggle.isChecked(): + self.fig, (self.ax0) = plt.subplots(1, 1, figsize=(self.figsize_width_substrate, self.figsize_height_substrate)) + + self.substrate_frame = int(frame / self.modulo) + + fname = "output%08d_microenvironment0.mat" % self.substrate_frame + xml_fname = "output%08d.xml" % self.substrate_frame + # fullname = output_dir_str + fname + + # fullname = fname + full_fname = os.path.join(self.output_dir, fname) + print("--- plot_substrate(): full_fname=",full_fname) + full_xml_fname = os.path.join(self.output_dir, xml_fname) + # self.output_dir = '.' + + # if not os.path.isfile(fullname): + if not os.path.isfile(full_fname): + print("Once output files are generated, click the slider.") # No: output00000000_microenvironment0.mat + return + + # tree = ET.parse(xml_fname) + tree = ET.parse(full_xml_fname) + xml_root = tree.getroot() + mins = round(int(float(xml_root.find(".//current_time").text))) # TODO: check units = mins + self.substrate_mins= round(int(float(xml_root.find(".//current_time").text))) # TODO: check units = mins + + hrs = int(mins/60) + days = int(hrs/24) + self.title_str = 'substrate: %dd, %dh, %dm' % (int(days),(hrs%24), mins - (hrs*60)) + # self.title_str = 'substrate: %dm' % (mins ) # rwh + + info_dict = {} + scipy.io.loadmat(full_fname, info_dict) + M = info_dict['multiscale_microenvironment'] + f = M[self.field_index, :] # 4=tumor cells field, 5=blood vessel density, 6=growth substrate + + try: + print("numx, numy = ",self.numx, self.numy) + xgrid = M[0, :].reshape(self.numy, self.numx) + ygrid = M[1, :].reshape(self.numy, self.numx) + except: + print("substrates.py: mismatched mesh size for reshape: numx,numy=",self.numx, self.numy) + pass +# xgrid = M[0, :].reshape(self.numy, self.numx) +# ygrid = M[1, :].reshape(self.numy, self.numx) + + num_contours = 15 + # levels = MaxNLocator(nbins=num_contours).tick_values(self.colormap_min.value, self.colormap_max.value) + levels = MaxNLocator(nbins=num_contours).tick_values(self.colormap_min, self.colormap_max) + contour_ok = True + # if (self.colormap_fixed_toggle.isChecked()): + if (self.colormap_fixed_toggle): + try: + substrate_plot = self.ax0.contourf(xgrid, ygrid, M[self.field_index, :].reshape(self.numy, self.numx), + levels=levels, extend='both', cmap="viridis", fontsize=self.fontsize) + except: + contour_ok = False + print('got error on contourf 1.') + else: + try: + print("field min,max= ", M[self.field_index, :].min(), M[self.field_index, :].max()) + substrate_plot = self.ax0.contourf(xgrid, ygrid, M[self.field_index, :].reshape(self.numy,self.numx), + num_contours, cmap = "viridis" ) # cmap=self.colormap_dd.value) + except: + contour_ok = False + print('\n -->> got error on contourf 2 \n') # rwh: argh, getting here + + if (contour_ok): + self.ax0.set_title(self.title_str, fontsize=self.fontsize) + # cbar = self.figure.colorbar(substrate_plot, ax=self.ax0) + cbar = self.figure.colorbar(substrate_plot, cax=self.ax0) + + cbar.ax.tick_params(labelsize=self.fontsize) + + self.ax0.set_xlim(self.xmin, self.xmax) + self.ax0.set_ylim(self.ymin, self.ymax) + + # Now plot the cells (possibly on top of the substrate) + if self.cells_toggle.isChecked(): + if not self.substrates_toggle.isChecked(): + self.fig, (self.ax0) = plt.subplots(1, 1, figsize=(self.figsize_width_svg, self.figsize_height_svg)) + + self.svg_frame = frame + # print('plot_svg with frame=',self.svg_frame) + self.plot_svg(self.svg_frame) \ No newline at end of file diff --git a/analysis/vis_tab_ellipse.py b/analysis/vis_tab_ellipse.py new file mode 100644 index 0000000..9bdb341 --- /dev/null +++ b/analysis/vis_tab_ellipse.py @@ -0,0 +1,1019 @@ +import sys +import os +import time +import xml.etree.ElementTree as ET # https://docs.python.org/2/library/xml.etree.elementtree.html +from pathlib import Path +# from ipywidgets import Layout, Label, Text, Checkbox, Button, BoundedIntText, HBox, VBox, Box, \ + # FloatText, Dropdown, SelectMultiple, RadioButtons, interactive +# import matplotlib.pyplot as plt +import matplotlib as mpl +from matplotlib.colors import BoundaryNorm +from matplotlib.ticker import MaxNLocator +from matplotlib.collections import LineCollection +from matplotlib.patches import Circle, Ellipse, Rectangle +from matplotlib.collections import PatchCollection +import matplotlib.colors as mplc +from matplotlib import gridspec +from collections import deque + +from PyQt5 import QtCore, QtGui +from PyQt5.QtWidgets import QFrame,QApplication,QWidget,QTabWidget,QFormLayout,QLineEdit, QHBoxLayout,QVBoxLayout, \ + QRadioButton,QLabel,QCheckBox,QComboBox,QScrollArea, QMainWindow,QGridLayout, QPushButton, QFileDialog, QMessageBox + +import math +import numpy as np +import scipy.io # .io.loadmat(full_fname, info_dict) +import matplotlib +matplotlib.use('Qt5Agg') +import matplotlib.pyplot as plt +from mpl_toolkits.axes_grid1 import make_axes_locatable + +# from PyQt5 import QtCore, QtWidgets + +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg +# from matplotlib.figure import Figure + +class Vis(QWidget): + def __init__(self): + super().__init__() + # global self.config_params + + self.xml_root = None + self.frame_count = 0 + # self.current_svg_frame = 0 + self.timer = QtCore.QTimer() + # self.t.timeout.connect(self.task) + self.timer.timeout.connect(self.play_plot_cb) + + # self.tab = QWidget() + # self.tabs.resize(200,5) + + self.use_defaults = True + self.title_str = "" + self.xmin = -1000 + self.xmax = 1000 + self.x_range = self.xmax - self.xmin + + self.ymin = -1000 + self.ymax = 1000 + self.y_range = self.ymax - self.ymin + self.show_nucleus = False + self.show_edge = False + self.alpha = 0.7 + + self.cell_mod = 1 + self.substrate_mod = 1 + + # self.cells_toggle = None + # self.substrates_toggle = None + + basic_length = 12.0 + basic_length = 10.0 + self.figsize_width_substrate = 18.0 # allow extra for colormap + self.figsize_width_substrate = 12.0 # allow extra for colormap + self.figsize_height_substrate = basic_length + + self.figsize_width_2Dplot = basic_length + self.figsize_height_2Dplot = basic_length + + # rwh: TODO these params + self.modulo = 1 + self.field_index = 4 # "4" is the 0th substrate + # define dummy size of mesh (set in the tool's primary module) + self.numx = 0 + self.numy = 0 + self.colormap_min = 0.5 + self.colormap_max = 1.0 + self.colormap_fixed_toggle = False + # self.fontsize = 10 + self.fontsize = 5 + + # self.canvas = None + self.first_time = True + + + # self.width_substrate = basic_length # allow extra for colormap + # self.height_substrate = basic_length + + self.figsize_width_svg = basic_length + self.figsize_height_svg = basic_length + + # self.output_dir = "/Users/heiland/dev/PhysiCell_V.1.8.0_release/output" + self.output_dir = "./output" + + self.customized_output_freq = False + + #------------------------------------------- + label_width = 110 + domain_value_width = 100 + value_width = 60 + label_height = 20 + units_width = 70 + + # self.create_figure() + + self.scroll = QScrollArea() # might contain centralWidget + + self.config_params = QWidget() + + self.main_layout = QVBoxLayout() + + self.vbox = QVBoxLayout() + self.vbox.addStretch(0) + + # self.substrates_toggle = None + + #------------------ + controls_vbox = QVBoxLayout() + + controls_hbox = QHBoxLayout() + w = QPushButton("Directory") + w.clicked.connect(self.open_directory_cb) + controls_hbox.addWidget(w) + + # self.output_dir = "/Users/heiland/dev/PhysiCell_V.1.8.0_release/output" + self.output_dir_w = QLineEdit() + self.output_dir_w.setText("./output") + # w.setText("/Users/heiland/dev/PhysiCell_V.1.8.0_release/output") + # w.setText(self.output_dir) + # w.textChanged[str].connect(self.output_dir_changed) + # w.textChanged.connect(self.output_dir_changed) + controls_hbox.addWidget(self.output_dir_w) + + self.back0_button = QPushButton("<<") + self.back0_button.clicked.connect(self.back0_plot_cb) + controls_hbox.addWidget(self.back0_button) + + self.back_button = QPushButton("<") + self.back_button.clicked.connect(self.back_plot_cb) + controls_hbox.addWidget(self.back_button) + + self.forward_button = QPushButton(">") + self.forward_button.clicked.connect(self.forward_plot_cb) + controls_hbox.addWidget(self.forward_button) + + self.play_button = QPushButton("Play") + # self.play_button.clicked.connect(self.play_plot_cb) + self.play_button.clicked.connect(self.animate) + controls_hbox.addWidget(self.play_button) + + self.reset_button = QPushButton("Reset") + self.reset_button.setEnabled(False) + # self.play_button.clicked.connect(self.play_plot_cb) + self.reset_button.clicked.connect(self.reset_plot_cb) + controls_hbox.addWidget(self.reset_button) + controls_vbox.addLayout(controls_hbox) + + #------------- + hbox = QHBoxLayout() + + widget_width = 60 + self.cells_toggle = QCheckBox("cells") + self.cells_toggle.setFixedWidth(widget_width) + self.cells_toggle.setChecked(True) + self.cells_toggle.stateChanged.connect(self.cells_toggle_cb) + hbox.addWidget(self.cells_toggle) + + label = QLabel("mod") + label.setFixedWidth(30) + label.setAlignment(QtCore.Qt.AlignRight) + hbox.addWidget(label) + + self.cell_mod_val = QLineEdit() + self.cell_mod_val.setFixedWidth(50) + self.cell_mod_val.setText(str(self.cell_mod)) + self.cell_mod_val.textChanged.connect(self.cell_modulo_cb) + self.cell_mod_val.setValidator(QtGui.QIntValidator()) + hbox.addWidget(self.cell_mod_val) + + self.cells_edges_toggle = QCheckBox("edges") + self.cells_edges_toggle.setFixedWidth(widget_width) + self.cells_edges_toggle.setChecked(False) + self.cells_edges_toggle.stateChanged.connect(self.cells_edges_toggle_cb) + hbox.addWidget(self.cells_edges_toggle) + + self.substrates_toggle = QCheckBox("substrates") + self.substrates_toggle.setFixedWidth(100) + self.substrates_toggle.setEnabled(False) + self.substrates_toggle.setChecked(True) + self.substrates_toggle.stateChanged.connect(self.substrates_toggle_cb) + hbox.addWidget(self.substrates_toggle) + + label = QLabel("mod") + label.setFixedWidth(30) + label.setAlignment(QtCore.Qt.AlignRight) + hbox.addWidget(label) + + self.substrate_mod_val = QLineEdit() + self.substrate_mod_val.setFixedWidth(50) + self.substrate_mod_val.setText(str(self.substrate_mod)) + self.substrate_mod_val.textChanged.connect(self.substrate_modulo_cb) + self.substrate_mod_val.setValidator(QtGui.QIntValidator()) + hbox.addWidget(self.substrate_mod_val) + + self.substrate_dropdown = QComboBox() + self.substrate_dropdown.setFixedWidth(250) + # self.cycle_dropdown.currentIndex.connect(self.cycle_changed_cb) + self.substrate_dropdown.currentIndexChanged.connect(self.substrate_changed_cb) + # self.substrate_dropdown.addItem("foo") + hbox.addWidget(self.substrate_dropdown) + + controls_vbox.addLayout(hbox) + + # self.prepare_button = QPushButton("Prepare") + # self.prepare_button.clicked.connect(self.prepare_plot_cb) + # controls_hbox.addWidget(self.prepare_button) + + #================================================================== + self.config_params.setLayout(self.vbox) + + self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + self.scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + self.scroll.setWidgetResizable(True) + + self.create_figure() + + # self.scroll.setWidget(self.config_params) # self.config_params = QWidget() + self.scroll.setWidget(self.canvas) # self.config_params = QWidget() + self.layout = QVBoxLayout(self) + # self.layout.addLayout(controls_hbox) + self.layout.addLayout(controls_vbox) + + self.layout.addWidget(self.scroll) + + + self.reset_plot_cb("") + + + def open_directory_cb(self): + dialog = QFileDialog() + dir_path = dialog.getExistingDirectory(self, 'Select an output directory') + print("open_directory_cb: output_dir=",dir_path) + # if self.output_dir is "": + if dir_path == "": + return + + self.output_dir = dir_path + + self.output_dir_w.setText(self.output_dir) + # Verify initial.xml and at least one .svg file exist. Obtain bounds from initial.xml + # tree = ET.parse(self.output_dir + "/" + "initial.xml") + xml_file = Path(self.output_dir, "initial.xml") + if not os.path.isfile(xml_file): + print("Expecting initial.xml, but does not exist.") + msgBox = QMessageBox() + msgBox.setIcon(QMessageBox.Information) + msgBox.setText("Did not find 'initial.xml' in this directory.") + msgBox.setStandardButtons(QMessageBox.Ok) + msgBox.exec() + return + + tree = ET.parse(Path(self.output_dir, "initial.xml")) + xml_root = tree.getroot() + + bds_str = xml_root.find(".//microenvironment//domain//mesh//bounding_box").text + bds = bds_str.split() + print('bds=',bds) + self.xmin = float(bds[0]) + self.xmax = float(bds[3]) + self.x_range = self.xmax - self.xmin + + self.ymin = float(bds[1]) + self.ymax = float(bds[4]) + self.y_range = self.ymax - self.ymin + + # and plot 1st frame (.svg) + self.current_svg_frame = 0 + self.forward_plot_cb("") + + + def cells_toggle_cb(self): + self.plot_substrate() + self.canvas.update() + self.canvas.draw() + + def cell_modulo_cb(self, text): + print("cell_modulo_cb(): text = ",text) + if len(text) > 0: + self.cell_mod = int(text) + + def cells_edges_toggle_cb(self,bval): + self.show_edge = bval + self.plot_substrate() + self.canvas.update() + self.canvas.draw() + + def substrates_toggle_cb(self): + self.plot_substrate() + self.canvas.update() + self.canvas.draw() + + def substrate_modulo_cb(self, text): + print("substrate_modulo_cb(): text = ",text) + if len(text) > 0: + self.substrate_mod = int(text) + + def substrate_changed_cb(self): + print("\n== substrate_changed_cb(): ", self.substrate_dropdown.currentText(),self.substrate_dropdown.currentIndex() ) + if not self.first_time: + self.field_index = int(self.substrate_dropdown.currentIndex()) + 4 + self.plot_substrate() + print("== substrate_changed_cb(): self.field_index = ",self.field_index ) + self.canvas.update() + self.canvas.draw() + else: + self.first_time = False + + + # def output_dir_changed(self, text): + # self.output_dir = text + # print(self.output_dir) + + def back0_plot_cb(self, text): + self.frame_count = 0 + print('frame # ',self.frame_count) + self.plot_substrate() + # self.canvas.update() + self.canvas.draw() + self.timer.stop() + + def back_plot_cb(self, text): + self.frame_count -= 1 + if self.frame_count < 0: + self.frame_count = 0 + print('frame # ',self.frame_count) + self.plot_substrate() + # self.plot_svg(self.current_svg_frame) + + # self.canvas.update() + self.canvas.draw() + + def forward_plot_cb(self, text): + self.frame_count += 1 + print('frame # ',self.frame_count) + self.plot_substrate() + # self.plot_svg(self.current_svg_frame) + # self.canvas.update() + self.canvas.draw() + + def reset_plot_cb(self, text): + print("-------------- reset_plot_cb() ----------------") + # self.create_figure() + + xml_file = Path(self.output_dir, "initial.xml") + if not os.path.isfile(xml_file): + print("Expecting initial.xml, but does not exist.") + msgBox = QMessageBox() + msgBox.setIcon(QMessageBox.Information) + msgBox.setText("Did not find 'initial.xml' in this directory.") + msgBox.setStandardButtons(QMessageBox.Ok) + msgBox.exec() + return + + # if self.first_time: + # self.first_time = False + # full_xml_filename = Path(os.path.join(self.output_dir, 'config.xml')) + # if full_xml_filename.is_file(): + # tree = ET.parse(full_xml_filename) # this file cannot be overwritten; part of tool distro + # xml_root = tree.getroot() + # self.svg_delta_t = int(xml_root.find(".//SVG//interval").text) + # self.substrate_delta_t = int(xml_root.find(".//full_data//interval").text) + # # print("---- svg,substrate delta_t values=",self.svg_delta_t,self.substrate_delta_t) + # self.modulo = int(self.substrate_delta_t / self.svg_delta_t) + # # print("---- modulo=",self.modulo) + + # all_files = sorted(glob.glob(os.path.join(self.output_dir, 'snap*.svg'))) # if .svg + # if len(all_files) > 0: + # last_file = all_files[-1] + # # print("substrates.py/update(): len(snap*.svg) = ",len(all_files)," , last_file=",last_file) + # self.max_frames.value = int(last_file[-12:-4]) # assumes naming scheme: "snapshot%08d.svg" + # else: + # substrate_files = sorted(glob.glob(os.path.join(self.output_dir, 'output*.xml'))) + # if len(substrate_files) > 0: + # last_file = substrate_files[-1] + # self.max_frames.value = int(last_file[-12:-4]) + + + tree = ET.parse(Path(self.output_dir, "initial.xml")) + xml_root = tree.getroot() + + bds_str = xml_root.find(".//microenvironment//domain//mesh//bounding_box").text + bds = bds_str.split() + print('bds=',bds) + self.xmin = float(bds[0]) + self.xmax = float(bds[3]) + self.x_range = self.xmax - self.xmin + + self.ymin = float(bds[1]) + self.ymax = float(bds[4]) + self.y_range = self.ymax - self.ymin + + # self.numx = math.ceil( (self.xmax - self.xmin) / config_tab.xdelta.value) + # self.numy = math.ceil( (self.ymax - self.ymin) / config_tab.ydelta.value) + self.numx = math.ceil( (self.xmax - self.xmin) / 20.) + self.numy = math.ceil( (self.ymax - self.ymin) / 20.) + print(" calc: numx,numy = ",self.numx, self.numy) + + vars_uep = xml_root.find(".//microenvironment//domain//variables") + self.substrates = [] + self.substrate_dropdown.clear() + for var in vars_uep.findall("variable"): + print(" substrate name = ",var.attrib["name"]) + self.substrates.append(var.attrib["name"]) + self.substrate_dropdown.addItem(var.attrib["name"]) + + + self.cbar = None + + self.frame_count = 0 + self.plot_substrate() + # self.plot_svg(self.current_svg_frame) + # self.canvas.clear() + self.canvas.update() + self.canvas.draw() + + + # def task(self): + # self.dc.update_figure() + def play_plot_cb(self): + for idx in range(1): + self.frame_count += 1 + print('frame # ',self.frame_count) + + self.current_svg_frame = int(self.frame_count / self.cell_mod) + + fname = "snapshot%08d.svg" % self.current_svg_frame + full_fname = os.path.join(self.output_dir, fname) + # print("full_fname = ",full_fname) + # with debug_view: + # print("plot_svg:", full_fname) + # print("-- plot_svg:", full_fname) + if not os.path.isfile(full_fname): + # print("Once output files are generated, click the slider.") + print("ERROR: filename not found.") + self.frame_count -= 1 + self.timer.stop() + return + + self.plot_substrate() + # self.plot_svg(self.current_svg_frame) + self.canvas.update() + self.canvas.draw() + + def animate(self, text): + self.frame_count = 0 + # self.timer = QtCore.QTimer() + # self.timer.timeout.connect(self.play_plot_cb) + # self.timer.start(2000) # every 2 sec + # self.timer.start(100) + self.timer.start(50) + + # def play_plot_cb0(self, text): + # for idx in range(10): + # self.current_svg_frame += 1 + # print('svg # ',self.current_svg_frame) + # self.plot_svg(self.current_svg_frame) + # self.canvas.update() + # self.canvas.draw() + # # time.sleep(1) + # # self.ax0.clear() + # # self.canvas.pause(0.05) + + # def prepare_plot_cb(self, text): + # self.current_svg_frame += 1 + # print('svg # ',self.current_svg_frame) + # # self.plot_substrate(self.current_svg_frame) + # self.plot_svg(self.current_svg_frame) + # self.canvas.update() + # self.canvas.draw() + + def create_figure(self): + # self.figure = plt.figure() + self.figure = plt.figure(figsize=(self.figsize_width_substrate, self.figsize_height_substrate)) + self.canvas = FigureCanvasQTAgg(self.figure) + # self.canvas.setStyleSheet("background-color:transparent;") + + # Adding one subplot for image + # self.ax0 = self.figure.add_subplot(111, frameon=True) + # self.ax0 = self.figure.add_axes([0.1,0.1,0.8,0.8]) + + # self.fig = plt.figure(figsize=(self.figsize_width_substrate, self.figsize_height_substrate)) + # self.ax0 = plt.figure(figsize=(self.figsize_width_substrate, self.figsize_height_substrate)) + + # self.ax0.get_xaxis().set_visible(True) + # self.ax0.get_yaxis().set_visible(True) + + # self.ax0.set_frame_on(True) + + print(mpl.rcParams['axes.edgecolor']) + # self.ax0.grid(True, linestyle='-.') + # plt.tight_layout() + # self.ax0.tight_layout() + + # np.random.seed(19680801) # for reproducibility + # N = 50 + # x = np.random.rand(N) * 2000 + # y = np.random.rand(N) * 2000 + # colors = np.random.rand(N) + # area = (30 * np.random.rand(N))**2 # 0 to 15 point radii + # self.ax0.scatter(x, y, s=area, c=colors, alpha=0.5) + + self.plot_substrate() + + # self.ax0.get_xaxis().set_visible(True) + # self.ax0.get_yaxis().set_visible(True) + + # self.plot_svg(self.current_svg_frame) + + # self.imageInit = [[255] * 320 for i in range(240)] + # self.imageInit[0][0] = 0 + + # Init image and add colorbar + # self.image = self.ax0.imshow(self.imageInit, interpolation='none') + # divider = make_axes_locatable(self.ax0) + # cax = divider.new_vertical(size="5%", pad=0.05, pack_start=True) + # self.colorbar = self.figure.add_axes(cax) + # self.figure.colorbar(self.image, cax=cax, orientation='horizontal') + + # plt.subplots_adjust(left=0, bottom=0.05, right=1, top=1, wspace=0, hspace=0) + + self.canvas.draw() + + #--------------------------------------------------------------------------- + def ellipses(self, x, y, w,h, c='b', vmin=None, vmax=None, **kwargs): + + if np.isscalar(c): + kwargs.setdefault('color', c) + c = None + + if 'fc' in kwargs: + kwargs.setdefault('facecolor', kwargs.pop('fc')) + if 'ec' in kwargs: + kwargs.setdefault('edgecolor', kwargs.pop('ec')) + if 'ls' in kwargs: + kwargs.setdefault('linestyle', kwargs.pop('ls')) + if 'lw' in kwargs: + kwargs.setdefault('linewidth', kwargs.pop('lw')) + # You can set `facecolor` with an array for each patch, + # while you can only set `facecolors` with a value for all. + +# ww = X / 10.0 +# hh = Y / 15.0 +# aa = X * 9 +# fig, ax = plt.subplots() +# ec = EllipseCollection(ww, hh, aa, units='x', offsets=XY, offset_transform=ax.transData) + + zipped = np.broadcast(x, y, w,h) + patches = [Ellipse((x_, y_), w_, h_) + for x_, y_, w_, h_ in zipped] + collection = PatchCollection(patches, **kwargs) + if c is not None: + c = np.broadcast_to(c, zipped.shape).ravel() + collection.set_array(c) + collection.set_clim(vmin, vmax) + + ax = plt.gca() + ax.add_collection(collection) + ax.autoscale_view() + # self.ax0.add_collection(collection) + # self.ax0.autoscale_view() + # plt.draw_if_interactive() + if c is not None: + # plt.sci(collection) + self.ax0.sci(collection) + # return collection + + #--------------------------------------------------------------------------- + def circles(self, x, y, s, c='b', vmin=None, vmax=None, **kwargs): + """ + See https://gist.github.com/syrte/592a062c562cd2a98a83 + + Make a scatter plot of circles. + Similar to plt.scatter, but the size of circles are in data scale. + Parameters + ---------- + x, y : scalar or array_like, shape (n, ) + Input data + s : scalar or array_like, shape (n, ) + Radius of circles. + c : color or sequence of color, optional, default : 'b' + `c` can be a single color format string, or a sequence of color + specifications of length `N`, or a sequence of `N` numbers to be + mapped to colors using the `cmap` and `norm` specified via kwargs. + Note that `c` should not be a single numeric RGB or RGBA sequence + because that is indistinguishable from an array of values + to be colormapped. (If you insist, use `color` instead.) + `c` can be a 2-D array in which the rows are RGB or RGBA, however. + vmin, vmax : scalar, optional, default: None + `vmin` and `vmax` are used in conjunction with `norm` to normalize + luminance data. If either are `None`, the min and max of the + color array is used. + kwargs : `~matplotlib.collections.Collection` properties + Eg. alpha, edgecolor(ec), facecolor(fc), linewidth(lw), linestyle(ls), + norm, cmap, transform, etc. + Returns + ------- + paths : `~matplotlib.collections.PathCollection` + Examples + -------- + a = np.arange(11) + circles(a, a, s=a*0.2, c=a, alpha=0.5, ec='none') + plt.colorbar() + License + -------- + This code is under [The BSD 3-Clause License] + (http://opensource.org/licenses/BSD-3-Clause) + """ + + if np.isscalar(c): + kwargs.setdefault('color', c) + c = None + + if 'fc' in kwargs: + kwargs.setdefault('facecolor', kwargs.pop('fc')) + if 'ec' in kwargs: + kwargs.setdefault('edgecolor', kwargs.pop('ec')) + if 'ls' in kwargs: + kwargs.setdefault('linestyle', kwargs.pop('ls')) + if 'lw' in kwargs: + kwargs.setdefault('linewidth', kwargs.pop('lw')) + # You can set `facecolor` with an array for each patch, + # while you can only set `facecolors` with a value for all. + + zipped = np.broadcast(x, y, s) + patches = [Circle((x_, y_), s_) + for x_, y_, s_ in zipped] + collection = PatchCollection(patches, **kwargs) + if c is not None: + c = np.broadcast_to(c, zipped.shape).ravel() + collection.set_array(c) + collection.set_clim(vmin, vmax) + + ax = plt.gca() + ax.add_collection(collection) + ax.autoscale_view() + # self.ax0.add_collection(collection) + # self.ax0.autoscale_view() + # plt.draw_if_interactive() + if c is not None: + plt.sci(collection) + # self.ax0.sci(collection) + # return collection + + #------------------------------------------------------------ + # def plot_svg(self, frame, rdel=''): + def plot_svg(self): + # global current_idx, axes_max + global current_frame + + # fname = "snapshot%08d.svg" % frame + fname = "snapshot%08d.svg" % self.frame_count + full_fname = os.path.join(self.output_dir, fname) + print("full_fname = ",full_fname) + # with debug_view: + # print("plot_svg:", full_fname) + print("-- plot_svg:", full_fname) + if not os.path.isfile(full_fname): + # print("Once output files are generated, click the slider.") + print("ERROR: filename not found.") + return + + # self.ax0.cla() + self.title_str = "" + + xlist = deque() + ylist = deque() + rxlist = deque() + rylist = deque() + rgb_list = deque() + + # print('\n---- ' + fname + ':') +# tree = ET.parse(fname) + tree = ET.parse(full_fname) + root = tree.getroot() + # print('--- root.tag ---') + # print(root.tag) + # print('--- root.attrib ---') + # print(root.attrib) + # print('--- child.tag, child.attrib ---') + numChildren = 0 + for child in root: + # print(child.tag, child.attrib) + # print("keys=",child.attrib.keys()) + if self.use_defaults and ('width' in child.attrib.keys()): + self.axes_max = float(child.attrib['width']) + # print("debug> found width --> axes_max =", axes_max) + if child.text and "Current time" in child.text: + svals = child.text.split() + # remove the ".00" on minutes + self.title_str += " cells: " + svals[2] + "d, " + svals[4] + "h, " + svals[7][:-3] + "m" + + # self.cell_time_mins = int(svals[2])*1440 + int(svals[4])*60 + int(svals[7][:-3]) + # self.title_str += " cells: " + str(self.cell_time_mins) + "m" # rwh + + # print("width ",child.attrib['width']) + # print('attrib=',child.attrib) + # if (child.attrib['id'] == 'tissue'): + if ('id' in child.attrib.keys()): + # print('-------- found tissue!!') + tissue_parent = child + break + + # print('------ search tissue') + cells_parent = None + + for child in tissue_parent: + # print('attrib=',child.attrib) + if (child.attrib['id'] == 'cells'): + # print('-------- found cells, setting cells_parent') + cells_parent = child + break + numChildren += 1 + + num_cells = 0 + # print('------ search cells') + for child in cells_parent: + # print(child.tag, child.attrib) + # print('attrib=',child.attrib) + for circle in child: # two circles in each child: outer + nucleus + # circle.attrib={'cx': '1085.59','cy': '1225.24','fill': 'rgb(159,159,96)','r': '6.67717','stroke': 'rgb(159,159,96)','stroke-width': '0.5'} + # print(' --- cx,cy=',circle.attrib['cx'],circle.attrib['cy']) + xval = float(circle.attrib['cx']) + + # map SVG coords into comp domain + # xval = (xval-self.svg_xmin)/self.svg_xrange * self.x_range + self.xmin + xval = xval/self.x_range * self.x_range + self.xmin + print("xrange, xmin= ",self.x_range,self.xmin) + + s = circle.attrib['fill'] + # print("s=",s) + # print("type(s)=",type(s)) + if (s[0:3] == "rgb"): # if an rgb string, e.g. "rgb(175,175,80)" + rgb = list(map(int, s[4:-1].split(","))) + rgb[:] = [x / 255. for x in rgb] + else: # otherwise, must be a color name + rgb_tuple = mplc.to_rgb(mplc.cnames[s]) # a tuple + rgb = [x for x in rgb_tuple] + + # test for bogus x,y locations (rwh TODO: use max of domain?) + too_large_val = 10000. + if (np.fabs(xval) > too_large_val): + print("bogus xval=", xval) + break + yval = float(circle.attrib['cy']) + # yval = (yval - self.svg_xmin)/self.svg_xrange * self.y_range + self.ymin + yval = yval/self.y_range * self.y_range + self.ymin + if (np.fabs(yval) > too_large_val): + print("bogus xval=", xval) + break +# +# now: +# + # rval = float(circle.attrib['r']) + rxval = float(circle.attrib['rx']) + ryval = float(circle.attrib['ry']) + # if (rgb[0] > rgb[1]): + # print(num_cells,rgb, rval) + xlist.append(xval) + ylist.append(yval) + rxlist.append(rxval) + rylist.append(ryval) + rgb_list.append(rgb) + + # For .svg files with cells that *have* a nucleus, there will be a 2nd + if (not self.show_nucleus): + #if (not self.show_nucleus): + break + + num_cells += 1 + + # if num_cells > 3: # for debugging + # print(fname,': num_cells= ',num_cells," --- debug exit.") + # sys.exit(1) + # break + + # print(fname,': num_cells= ',num_cells) + + xvals = np.array(xlist) + yvals = np.array(ylist) + rxvals = np.array(rxlist) + ryvals = np.array(rylist) + rgbs = np.array(rgb_list) + # print("xvals[0:5]=",xvals[0:5]) + # print("rvals[0:5]=",rvals[0:5]) + # print("rvals.min, max=",rvals.min(),rvals.max()) + + # rwh - is this where I change size of render window?? (YES - yipeee!) + # plt.figure(figsize=(6, 6)) + # plt.cla() + # if (self.substrates_toggle.value): + self.title_str += " (" + str(num_cells) + " agents)" + # title_str = " (" + str(num_cells) + " agents)" + # else: + # mins= round(int(float(root.find(".//current_time").text))) # TODO: check units = mins + # hrs = int(mins/60) + # days = int(hrs/24) + # title_str = '%dd, %dh, %dm' % (int(days),(hrs%24), mins - (hrs*60)) + # plt.title(self.title_str) + plt.title(self.title_str, fontsize=self.fontsize) + # self.ax0.set_title(self.title_str, fontsize=5) + # self.ax0.set_title(self.title_str, prop={'size':'small'}) + + plt.xlim(self.xmin, self.xmax) + plt.ylim(self.ymin, self.ymax) + + # self.ax0.set_xlim(self.xmin, self.xmax) + # self.ax0.set_ylim(self.ymin, self.ymax) + # self.ax0.tick_params(labelsize=4) + + # self.ax0.colorbar(collection) + + # plt.xlim(axes_min,axes_max) + # plt.ylim(axes_min,axes_max) + # plt.scatter(xvals,yvals, s=rvals*scale_radius, c=rgbs) + + # TODO: make figsize a function of plot_size? What about non-square plots? + # self.fig = plt.figure(figsize=(9, 9)) + +# axx = plt.axes([0, 0.05, 0.9, 0.9]) # left, bottom, width, height +# axx = fig.gca() +# print('fig.dpi=',fig.dpi) # = 72 + + # im = ax.imshow(f.reshape(100,100), interpolation='nearest', cmap=cmap, extent=[0,20, 0,20]) + # ax.xlim(axes_min,axes_max) + # ax.ylim(axes_min,axes_max) + + # convert radii to radii in pixels + # ax1 = self.fig.gca() + # N = len(xvals) + # rr_pix = (ax1.transData.transform(np.vstack([rvals, rvals]).T) - + # ax1.transData.transform(np.vstack([np.zeros(N), np.zeros(N)]).T)) + # rpix, _ = rr_pix.T + + # markers_size = (144. * rpix / self.fig.dpi)**2 # = (2*rpix / fig.dpi * 72)**2 + # markers_size = markers_size/4000000. + # print('max=',markers_size.max()) + + #rwh - temp fix - Ah, error only occurs when "edges" is toggled on + if (self.show_edge): + try: + # plt.scatter(xvals,yvals, s=markers_size, c=rgbs, edgecolor='black', linewidth=0.5) + # self.circles(xvals,yvals, s=rvals, color=rgbs, alpha=self.alpha, edgecolor='black', linewidth=0.5) + self.circles(xvals,yvals, s=rxvals, color=rgbs, alpha=self.alpha, edgecolor='black', linewidth=0.5) + # cell_circles = self.circles(xvals,yvals, s=rvals, color=rgbs, edgecolor='black', linewidth=0.5) + # plt.sci(cell_circles) + except (ValueError): + pass + else: + # plt.scatter(xvals,yvals, s=markers_size, c=rgbs) + # self.circles(xvals,yvals, s=rvals, color=rgbs, alpha=self.alpha) + # self.circles(xvals,yvals, s=rxvals, color=rgbs, alpha=self.alpha) + self.ellipses(xvals,yvals, w=rxvals, h=ryvals, color=rgbs, alpha=self.alpha) + + #--------------------------------------------------------------------------- + # assume "frame" is cell frame #, unless Cells is togggled off, then it's the substrate frame # + # def plot_substrate(self, frame, grid): + def plot_substrate(self): + # global cbar + + # print("plot_substrate(): frame*self.substrate_delta_t = ",frame*self.substrate_delta_t) + # print("plot_substrate(): frame*self.svg_delta_t = ",frame*self.svg_delta_t) + # print("plot_substrate(): fig width: SVG+2D = ",self.figsize_width_svg + self.figsize_width_2Dplot) # 24 + # print("plot_substrate(): fig width: substrate+2D = ",self.figsize_width_substrate + self.figsize_width_2Dplot) # 27 + + # self.ax0.cla() + # self.ax0.axis('on') + + self.title_str = '' + + # Recall: + # self.svg_delta_t = config_tab.svg_interval.value + # self.substrate_delta_t = config_tab.mcds_interval.value + # self.modulo = int(self.substrate_delta_t / self.svg_delta_t) + # self.therapy_activation_time = user_params_tab.therapy_activation_time.value + + # print("plot_substrate(): pre_therapy: max svg, substrate frames = ",max_svg_frame_pre_therapy, max_substrate_frame_pre_therapy) + + # Assume: # .svg files >= # substrate files +# if (self.cells_toggle.value): + + if self.substrates_toggle.isChecked(): + # self.fig, (self.ax0) = plt.subplots(1, 1, figsize=(self.figsize_width_substrate, self.figsize_height_substrate)) + + self.substrate_frame = int(self.frame_count / self.substrate_mod) + + fname = "output%08d_microenvironment0.mat" % self.substrate_frame + xml_fname = "output%08d.xml" % self.substrate_frame + # fullname = output_dir_str + fname + + # fullname = fname + full_fname = os.path.join(self.output_dir, fname) + print("--- plot_substrate(): full_fname=",full_fname) + full_xml_fname = os.path.join(self.output_dir, xml_fname) + # self.output_dir = '.' + + # if not os.path.isfile(fullname): + if not os.path.isfile(full_fname): + # print("Once output files are generated, click the slider.") # No: output00000000_microenvironment0.mat + print("-- Error: no file ",full_fname) # No: output00000000_microenvironment0.mat + + # if self.cells_toggle.isChecked(): + # self.svg_frame = frame + # # print('plot_svg with frame=',self.svg_frame) + # self.plot_svg(self.svg_frame) + + # return + + else: + + # tree = ET.parse(xml_fname) + tree = ET.parse(full_xml_fname) + xml_root = tree.getroot() + mins = round(int(float(xml_root.find(".//current_time").text))) # TODO: check units = mins + self.substrate_mins= round(int(float(xml_root.find(".//current_time").text))) # TODO: check units = mins + + hrs = int(mins/60) + days = int(hrs/24) + self.title_str = 'substrate: %dd, %dh, %dm' % (int(days),(hrs%24), mins - (hrs*60)) + # self.title_str = 'substrate: %dm' % (mins ) # rwh + + info_dict = {} + scipy.io.loadmat(full_fname, info_dict) + M = info_dict['multiscale_microenvironment'] + f = M[self.field_index, :] # 4=tumor cells field, 5=blood vessel density, 6=growth substrate + + try: + print("numx, numy = ",self.numx, self.numy) + xgrid = M[0, :].reshape(self.numy, self.numx) + ygrid = M[1, :].reshape(self.numy, self.numx) + except: + print("substrates.py: mismatched mesh size for reshape: numx,numy=",self.numx, self.numy) + pass + # xgrid = M[0, :].reshape(self.numy, self.numx) + # ygrid = M[1, :].reshape(self.numy, self.numx) + + num_contours = 15 + # levels = MaxNLocator(nbins=num_contours).tick_values(self.colormap_min.value, self.colormap_max.value) + levels = MaxNLocator(nbins=num_contours).tick_values(self.colormap_min, self.colormap_max) + contour_ok = True + # if (self.colormap_fixed_toggle.isChecked()): + if (self.colormap_fixed_toggle): + try: + substrate_plot = plt.contourf(xgrid, ygrid, M[self.field_index, :].reshape(self.numy, self.numx), + levels=levels, extend='both', cmap="viridis", fontsize=self.fontsize) + except: + contour_ok = False + print('got error on contourf 1.') + else: + try: + print("field min,max= ", M[self.field_index, :].min(), M[self.field_index, :].max()) + print("self.field_index = ", self.field_index) + substrate_plot = plt.contourf(xgrid, ygrid, M[self.field_index, :].reshape(self.numy,self.numx), + num_contours, cmap = "viridis" ) # cmap=self.colormap_dd.value) + except: + contour_ok = False + print('\n -->> got error on contourf 2 \n') # rwh: argh, getting here + + if (contour_ok): + # self.ax0.set_title(self.title_str, fontsize=self.fontsize) + plt.title(self.title_str, fontsize=5) + + # self.image = self.ax0.imshow(self.imageInit, interpolation='none') + # divider = make_axes_locatable(self.ax0) + # cax = divider.new_vertical(size="5%", pad=0.05, pack_start=True) + # self.colorbar = self.figure.add_axes(cax) + # self.figure.colorbar(self.image, cax=cax, orientation='horizontal') + # plt.subplots_adjust(left=0, bottom=0.05, right=1, top=1, wspace=0, hspace=0) + + # cbar = self.figure.colorbar(substrate_plot, ax=self.ax0) + # cbar = self.figure.colorbar(substrate_plot, cax=self.ax0) + # cbar = self.figure.colorbar(substrate_plot, cax=self.ax0, orientation='horizontal') + if self.cbar == None: # if we always do this, it creates an additional colorbar! + # cbar = plt.colorbar(my_plot) + # self.cbar = self.ax0.colorbar(substrate_plot) + self.cbar = self.figure.colorbar(substrate_plot) + self.cbar.ax.tick_params(labelsize=self.fontsize) + else: + # self.cbar.ax0.clear() + self.cbar.ax.clear() + # self.figure.cbar = self.figure.colorbar(substrate_plot, cax=self.ax0) + self.cbar = self.figure.colorbar(substrate_plot , cax=self.cbar.ax) + + # cbar.ax.tick_params(labelsize=self.fontsize) + + # print("l. 805: xmin,xmax = ",self.xmin, self.xmax) + # print("l. 805: ymin,ymax = ",self.ymin, self.ymax) + # self.ax0.set_xlim(self.xmin, self.xmax) + # self.ax0.set_ylim(self.ymin, self.ymax) + # self.ax0.tick_params(labelsize=4) + plt.xticks(fontsize= self.fontsize) + plt.yticks(fontsize= self.fontsize) + + # Now plot the cells (possibly on top of the substrate) + if self.cells_toggle.isChecked(): + # if not self.substrates_toggle.isChecked(): + # self.fig, (self.ax0) = plt.subplots(1, 1, figsize=(self.figsize_width_svg, self.figsize_height_svg)) + + # self.svg_frame = frame + # print('plot_svg with frame=',self.svg_frame) + self.plot_svg() \ No newline at end of file