diff --git a/analysis/cell_track_plotter.py b/analysis/cell_track_plotter.py new file mode 100644 index 0000000..66f7818 --- /dev/null +++ b/analysis/cell_track_plotter.py @@ -0,0 +1,349 @@ +# +# cell_tracker.py - plot 2-D cell tracks associated with PhysiCell .svg files +# +# Usage: +# 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): +# python cell_tracks.py 0 1 100 +# +# 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, 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 + 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. + + """ + + 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 ... ) \ + # 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) + ax.set_xlabel('microns') + ax.set_ylabel('microns') + 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 = '' + snapshot = 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 + snapshot + '.png', dpi=256) + if show_plot is True: + plt.show() + # plt.close() + +if __name__ == '__main__': + + #################################################################################################################### + #################################### Usage example and input loading ######################## + #################################################################################################################### + + 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(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, produce_for_panel) + + + 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, 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 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 new file mode 100644 index 0000000..036a90e --- /dev/null +++ b/analysis/cell_tracker_movie.py @@ -0,0 +1,420 @@ +# +# 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 +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, 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 + 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. + 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 --- + 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 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 + + 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']) - 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 + + # 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. 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] + #### 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(-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}' + + # 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) + + if output_plot is True: + 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`). + + +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. + """ + + #### 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 + + +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 output 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 7 arguments to gain the most control') + usage_str = "Usage: %s " \ + " \n" % (sys.argv[0]) + print(usage_str) + + exit(1) 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 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() 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() 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/extracellular_matrx_plotting/cell_plus_environment_plotter.py b/analysis/extracellular_matrx_plotting/cell_plus_environment_plotter.py new file mode 100644 index 0000000..2322dcd --- /dev/null +++ b/analysis/extracellular_matrx_plotting/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 = True + + #################################################################################################################### + #################################### 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('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. + 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 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'] 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 diff --git a/analysis/params_biorobots.txt b/analysis/params_biorobots.txt new file mode 100644 index 0000000..c5908bd --- /dev/null +++ b/analysis/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/analysis/params_run.py b/analysis/params_run.py index 5acccae..4c61d2d 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,89 @@ 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 == '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 + 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 + # print("---- full_path: ",full_path) - xml_root.find('.//' + key).text = val - -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/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 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/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 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