From f70fafbe76a0349082f195dddfc303a28ebb3a18 Mon Sep 17 00:00:00 2001 From: bue Date: Wed, 26 Aug 2026 22:53:55 -0400 Subject: [PATCH 01/15] @ pcdl : in the middle of mcdsts.make_simularium and pcdl_make_simularium implementation, unit tests are still missing. --- jakku/biocondarecipes_recipes/pcdl/meta.yaml | 2 +- man/REFERENCE.md | 5 +- man/docstring/mcdsts.make_simularium.md | 43 +++++++ man/docstring/pcdl_get_muspan.md | 64 ++++++++++ man/docstring/pcdl_make_simularium.md | 50 ++++++++ man/scarab.py | 6 + pcdl/commandline.py | 117 +++++++++++++++++++ pcdl/timeseries.py | 109 +++++++++++++++++ pyproject.toml | 1 + 9 files changed, 395 insertions(+), 2 deletions(-) create mode 100644 man/docstring/mcdsts.make_simularium.md create mode 100644 man/docstring/pcdl_get_muspan.md create mode 100644 man/docstring/pcdl_make_simularium.md diff --git a/jakku/biocondarecipes_recipes/pcdl/meta.yaml b/jakku/biocondarecipes_recipes/pcdl/meta.yaml index 29e4887..b199787 100644 --- a/jakku/biocondarecipes_recipes/pcdl/meta.yaml +++ b/jakku/biocondarecipes_recipes/pcdl/meta.yaml @@ -88,4 +88,4 @@ extra: recipe-maintainers: - elmbeech autobump: - enable: False # see https://github.com/bioconda/bioconda-recipes/pull/57971 + enable: True # see https://github.com/bioconda/bioconda-recipes/pull/57971 diff --git a/man/REFERENCE.md b/man/REFERENCE.md index 06d04c3..ae2d265 100644 --- a/man/REFERENCE.md +++ b/man/REFERENCE.md @@ -119,7 +119,7 @@ help(pcdl.TimeStep._anndextract) Basically, there are four types of functions: + set_ : set a python3 variable. + get_ : recall a python3 variable. -+ make_ : make functions generate file output (gif, gml, mp4, ome.tiff, vtk). ++ make_ : make functions generate file output (gif, gml, mp4, ome.tiff, simularium, vtk). + plot_ : plot functions generate a matplotlib figure, or axis object, or file output (jpeg, png, tiff), depending on your parameter settings. ### TimeSeries initialization @@ -143,6 +143,7 @@ Basically, there are four types of functions: + [help(mcdsts.get_cell_attribute)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_cell_attribute.md) #! workhorse function + [help(mcdsts.plot_scatter)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.plot_scatter.md) # !workhorse function + [help(mcdsts.make_cell_vtk)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.make_cell_vtk.md) #! workhorse function ++ [help(mcdsts.make_simularium)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.make_simularium.md) #! workhorse function + [help(mcdsts.get_anndata)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_anndata.md) #! workhorse function ### TimeSeries cell graph @@ -191,11 +192,13 @@ The command line interface functions mimic the name and parameter arguments as c ### Command line cells + [pcdl_get_celltype_list --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_celltype_list.md) #! workhorse function ++ [pcdl_get_cell_attribute_list --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_cell_attribute_list.md) #! workhorse function + [pcdl_get_cell_attribute --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_cell_attribute.md) #! workhorse function + [pcdl_get_cell_df --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_cell_df.md) #! workhorse function + [pcdl_get_anndata --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_anndata.md) #! workhorse function + [pcdl_plot_scatter --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_plot_scatter.md) #! workhorse function + [pcdl_make_cell_vtk --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_cell_vtk.md) #! workhorse function ++ [pcdl_make_simularium --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_simularium.md) #! workhorse function ### Command line cell graph + [pcdl_make_graph_gml --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_graph_gml.md) #! workhorse function diff --git a/man/docstring/mcdsts.make_simularium.md b/man/docstring/mcdsts.make_simularium.md new file mode 100644 index 0000000..7e1ca7c --- /dev/null +++ b/man/docstring/mcdsts.make_simularium.md @@ -0,0 +1,43 @@ +# mcdsts.make_simularium() + + +## input: +``` + focus_cat: list of 1 or 2 string; default is ['cell_type','current_phase'] + specify 1 or 2 categorical column labels, to be found + in mcdsts.get_cell_df(). + + trajectory_title: string; default 'timeseries' + the trajectory_title will be used as + .simularium file name and displayed + in the simulation. + + scale_factor: float number; default is None + A multiplier for the scene, use if visualization is + too large or small. If None is provided, one will + be calculated based on the position data. + bue 20260822: does not seem to work in current simularium 1.13.0. + + camera_defaults: simulariumio.CameraData object; default is None + camera's initial settings which it also returns to + when reset. + + model_meta_data: simulariumio.ModelMetaData; default is None + Metadata for the model that produced this + trajectory. + +``` + +## output: +``` + .simularium file + +``` + +## description: +``` + function returns a simularium trajectory file that can be run with the + online Simularium Viewer. + + https://simularium.allencell.org/ + +``` \ No newline at end of file diff --git a/man/docstring/pcdl_get_muspan.md b/man/docstring/pcdl_get_muspan.md new file mode 100644 index 0000000..215d4fe --- /dev/null +++ b/man/docstring/pcdl_get_muspan.md @@ -0,0 +1,64 @@ +``` +usage: pcdl_get_muspan [-h] [--custom_data_type [CUSTOM_DATA_TYPE ...]] + [--microenv MICROENV] [--graph GRAPH] + [--physiboss PHYSIBOSS] [--settingxml SETTINGXML] + [-v VERBOSE] [--values VALUES] [--drop [DROP ...]] + [--keep [KEEP ...]] + [path] [z_slice] + +function to transform mcds time steps into muspan domain objects for +downstream analysis. + +positional arguments: + path path to the PhysiCell output directory or a + outputnnnnnnnn.xml file. default is . . + z_slice z-axis position to slice a 2D xy-plain out of the 3D + mesh. if z_slice position numeric but not an exact + mesh center coordinate, then z_slice will be adjusted + to the nearest mesh center value, the smaller one, if + the coordinate lies on a saddle point. default is 0.0. + +options: + -h, --help show this help message and exit + --custom_data_type [CUSTOM_DATA_TYPE ...] + parameter to specify custom_data variable types other + than float (namely: int, bool, str) like this + var:dtype myint:int mybool:bool mystr:str . downstream + float and int will be handled as numeric, bool as + Boolean, and str as categorical data. default is an + empty string. + --microenv MICROENV should the microenvironment be extracted and loaded + into the muspan domain object? setting microenv to + False will use less memory and speed up processing. + default is True. + --graph GRAPH should neighbor graph, attach graph, and attached + spring graph be extracted and loaded into the muspan + domain object? default is True. + --physiboss PHYSIBOSS + if found, should physiboss state data be extracted and + loaded into the muspan domain object? default is True. + --settingxml SETTINGXML + the settings.xml that is loaded, from which the cell + type ID label mapping, is extracted, if this + information is not found in the output xml file. set + to None or False if the xml file is missing! default + is False. + -v, --verbose VERBOSE + setting verbose to False for less text output, while + processing. default is True. + --values VALUES minimal number of values a variable has to have in any + of the mcds time steps to be outputted. variables that + have only 1 state carry no information. None is a + state too. default is 1. + --drop [DROP ...] set of column labels to be dropped for the dataframe. + don't worry: essential columns like ID, coordinates + and time will never be dropped. Attention: when the + keep parameter is given, then the drop parameter has + to be an empty string! default is an empty string. + --keep [KEEP ...] set of column labels to be kept in the dataframe. set + values=1 to be sure that all variables are kept. don't + worry: essential columns like ID, coordinates and time + will always be kept. default is an empty string. + +homepage: https://github.com/elmbeech/physicelldataloader +``` diff --git a/man/docstring/pcdl_make_simularium.md b/man/docstring/pcdl_make_simularium.md new file mode 100644 index 0000000..dccd2c1 --- /dev/null +++ b/man/docstring/pcdl_make_simularium.md @@ -0,0 +1,50 @@ +``` +usage: pcdl_make_simularium [-h] [--custom_data_type [CUSTOM_DATA_TYPE ...]] + [--physiboss PHYSIBOSS] [--settingxml SETTINGXML] + [-v VERBOSE] [--trajectory_title TRAJECTORY_TITLE] + [--scale_factor [SCALE_FACTOR]] + [path] [focus_cat ...] + +function returns a simularium trajectory file which can be viewed with the +simularium viewer (https://simularium.allencell.org/). + +positional arguments: + path path to the PhysiCell output directory or a + outputnnnnnnnn.xml file. default is . . + focus_cat one or two categorical mcds.get_cell_df dataframe + column names, used for cell attributes. default is + cell_type current_phase. + +options: + -h, --help show this help message and exit + --custom_data_type [CUSTOM_DATA_TYPE ...] + parameter to specify custom_data variable types other + than float (namely: int, bool, str) like this + var:dtype myint:int mybool:bool mystr:str . downstream + float and int will be handled as numeric, bool as + Boolean, and str as categorical data. default is an + empty string. + --physiboss PHYSIBOSS + if found, should physiboss state data be extracted and + loaded into the df_cell dataframe? default is True. + --settingxml SETTINGXML + the settings.xml that is loaded, from which the cell + type ID label mapping, is extracted, if this + information is not found in the output xml file. set + to None or False if the xml file is missing! default + is False. + -v, --verbose VERBOSE + setting verbose to False for less text output, while + processing. default is True. + --trajectory_title, --tt TRAJECTORY_TITLE + the trajectory_title will be used for + .simularium file name and displayed + in the simulation. + --scale_factor, --sf [SCALE_FACTOR] + a multiplier for the scene, use if visualization is + too large or small. if none is provided, one will be + calculated based on the position data. bue 20260822: + does not seem to work in current simularium 1.13.0. + +homepage: https://github.com/elmbeech/physicelldataloader +``` diff --git a/man/scarab.py b/man/scarab.py index 8252c60..00148d1 100644 --- a/man/scarab.py +++ b/man/scarab.py @@ -409,6 +409,10 @@ def docstring_md(s_function, ls_doc, s_header=None, s_opath='man/docstring/'): s_function = 'mcdsts.make_cell_vtk', ls_doc = pcdl.TimeSeries.make_cell_vtk.__doc__.split('\n'), ) +docstring_md( + s_function = 'mcdsts.make_simularium', + ls_doc = pcdl.TimeSeries.make_simularium.__doc__.split('\n'), +) docstring_md( s_function = 'mcdsts.get_anndata', ls_doc = pcdl.TimeSeries.get_anndata.__doc__.split('\n'), @@ -475,8 +479,10 @@ def docstring_md(s_function, ls_doc, s_header=None, s_opath='man/docstring/'): help_md(s_command='pcdl_make_graph_gml') help_md(s_command='pcdl_plot_scatter') help_md(s_command='pcdl_make_cell_vtk') +help_md(s_command='pcdl_make_simularium') # substrate and cell agent help_md(s_command='pcdl_get_spatialdata') +help_md(s_command='pcdl_get_muspan') help_md(s_command='pcdl_plot_timeseries') help_md(s_command='pcdl_make_ome_tiff') help_md(s_command='pcdl_render_neuroglancer') diff --git a/pcdl/commandline.py b/pcdl/commandline.py index 7123a46..0d6a058 100644 --- a/pcdl/commandline.py +++ b/pcdl/commandline.py @@ -2007,6 +2007,123 @@ def make_cell_vtk(): return 0 +def make_simularium(): + # argv + parser = argparse.ArgumentParser( + prog = 'pcdl_make_simularium', + description = 'function returns a simularium trajectory file which can be viewed with the simularium viewer (https://simularium.allencell.org/).', + epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', + ) + + # TimeSeries path + parser.add_argument( + 'path', + nargs = '?', + default = '.', + help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .', + ) + # TimeSeries output_path '.' + # TimeSeries custom_data_type + parser.add_argument( + '--custom_data_type', + nargs = '*', + default = [], + help = 'parameter to specify custom_data variable types other than float (namely: int, bool, str) like this var:dtype myint:int mybool:bool mystr:str . downstream float and int will be handled as numeric, bool as Boolean, and str as categorical data. default is an empty string.', + ) + # TimeSeries microenv + # TimeSeries graph False + # TimeSeries physiboss + parser.add_argument( + '--physiboss', + default = 'true', + help = 'if found, should physiboss state data be extracted and loaded into the df_cell dataframe? default is True.', + ) + # TimeSeries settingxml + parser.add_argument( + '--settingxml', + default = 'false', + help = 'the settings.xml that is loaded, from which the cell type ID label mapping, is extracted, if this information is not found in the output xml file. set to None or False if the xml file is missing! default is False.', + ) + # TimeSeries verbose + parser.add_argument( + '-v', '--verbose', + default = 'true', + help = 'setting verbose to False for less text output, while processing. default is True.', + ) + # make_simularium focus_cat + parser.add_argument( + 'focus_cat', + nargs = '*', + default = ['cell_type','current_phase'], + help = 'one or two categorical mcds.get_cell_df dataframe column names, used for cell attributes. default is cell_type current_phase.', + ) + # make_simularium trajectory_title + parser.add_argument( + '--trajectory_title', '--tt', + default = 'timeseries', + help = 'the trajectory_title will be used for .simularium file name and displayed in the simulation.', + ) + # make_simularium scale_factor + parser.add_argument( + '--scale_factor', '--sf', + nargs = '?', + default = 'none', + help = 'a multiplier for the scene, use if visualization is too large or small. if none is provided, one will be calculated based on the position data. bue 20260822: does not seem to work in current simularium 1.13.0.', + ) + + # parse arguments + args = parser.parse_args() + print(args) + + # process arguments + s_path = args.path.replace('\\','/') + while (s_path.find('//') > -1): + s_path = s_path.replace('//','/') + if (s_path.endswith('/')) and (len(s_path) > 1): + s_path = s_path[:-1] + s_pathfile = s_path + if not s_pathfile.endswith('.xml'): + s_pathfile = s_pathfile + '/initial.xml' + else: + s_path = '/'.join(s_pathfile.split('/')[:-1]) + if not os.path.exists(s_pathfile): + sys.exit(f'Error @ pcdl_make_cell_vtk : {s_pathfile} path does not look like a physicell output directory ({s_path}/initial.xml is missing).') + + # custom_data_type + d_vartype = {} + for vartype in args.custom_data_type: + s_var, s_type = vartype.split(':') + if s_type in {'bool'}: o_type = bool + elif s_type in {'int'}: o_type = int + elif s_type in {'float'}: o_type = float + elif s_type in {'str'}: o_type = str + else: + sys.exit(f'Error @ pcdl_make_cell_vtk : {s_var} {s_type} has an unknowen data type. knowen are bool, int, float, str.') + d_vartype.update({s_var : o_type}) + + # run + mcdsts = pcdl.TimeSeries( + output_path = s_path, + custom_data_type = d_vartype, + load = True, + microenv = False, + graph = False, + physiboss = False if args.physiboss.lower().startswith('f') else True, + settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, + verbose = False if args.verbose.lower().startswith('f') else True, + ) + mcdsts.make_simularium( + focus_cat = args.focus_cat, + trajectory_title = args.trajectory_title, + scale_factor = None if (args.scale_factor.lower() == 'none') else float(args.scale_factor), + camera_defaults=None, + model_meta_data=None, + ) + + # going home + return 0 + + ################################################### # substrate and cell agent command line function # ################################################### diff --git a/pcdl/timeseries.py b/pcdl/timeseries.py index a6a3f01..78404b2 100644 --- a/pcdl/timeseries.py +++ b/pcdl/timeseries.py @@ -1947,3 +1947,112 @@ def get_muspan(self, z_slice=None, values=1, drop=set(), keep=set()): # output return do_domain + + + ## SIMULARIUM RELATED FUNCTIONS ## + + def make_simularium(self, focus_cat=['cell_type','current_phase'], trajectory_title='timeseries', scale_factor=None, camera_defaults=None, model_meta_data=None): + """ + input: + focus_cat: list of 1 or 2 string; default is ['cell_type','current_phase'] + specify 1 or 2 categorical column labels, to be found + in mcdsts.get_cell_df(). + + trajectory_title: string; default 'timeseries' + the trajectory_title will be used as + .simularium file name and displayed + in the simulation. + + scale_factor: float number; default is None + A multiplier for the scene, use if visualization is + too large or small. If None is provided, one will + be calculated based on the position data. + bue 20260822: does not seem to work in current simularium 1.13.0. + + camera_defaults: simulariumio.CameraData object; default is None + camera's initial settings which it also returns to + when reset. + + model_meta_data: simulariumio.ModelMetaData; default is None + Metadata for the model that produced this + trajectory. + + output: + .simularium file + + description: + function returns a simularium trajectory file that can be run with the + online Simularium Viewer. + + https://simularium.allencell.org/ + """ + # library + sim = optional_import(s_module='simulariumio', s_caller='TimeSeries.make_simularium') + + # handle input + model_meta_data=sim.ModelMetaData() if model_meta_data is None else model_meta_data + camera_defaults=sim.CameraData() if camera_defaults is None else camera_defaults + + # here we go + if self.verbose: + print(f'generating {trajectory_title}.simularium file ...') + + # extract box from mcdsts + ltr_domain = self.get_mcds_list()[0].get_mesh_mnp_range() + lr_space = self.get_mcds_list()[0].get_mesh_spacing() + lr_box = np.array([ + ltr_domain[0][1] - ltr_domain[0][0], + ltr_domain[1][1] - ltr_domain[1][0], + ltr_domain[2][1] - ltr_domain[2][0], + ]) + lr_box[0] = lr_box[0] if lr_box[0] != 0.0 else lr_space[0] + lr_box[1] = lr_box[1] if lr_box[1] != 0.0 else lr_space[1] + lr_box[2] = lr_box[2] if lr_box[2] != 0.0 else lr_space[2] + ar_box = np.array(lr_box) + + # extract cell dataframe from mcdsts + df_cell = self.get_cell_df() + + # handle agent annotation + se_type = df_cell.loc[:, focus_cat].astype(str).agg('#'.join, axis=1) + + # extract units from mcdsts + ds_unit = self.get_mcds_list()[0].get_unit_dict() + + # generate simularium dataframe + with pd.option_context('future.infer_string', False): + df_sim = pd.DataFrame({ + 'time': df_cell.loc[:, 'time'].to_numpy(dtype=float), + 'unique_id': df_cell.loc[:, 'ID'].to_numpy(dtype=int), + 'type': se_type.to_numpy(dtype=object), + 'positionX': df_cell.loc[:, 'position_x'].to_numpy(dtype=float), + 'positionY': df_cell.loc[:, 'position_y'].to_numpy(dtype=float), + 'positionZ': df_cell.loc[:, 'position_z'].to_numpy(dtype=float), + 'radius': df_cell.loc[:, 'radius'].to_numpy(dtype=float), + 'rotationX': np.zeros(df_cell.shape[0], dtype=float), + 'rotationY': np.zeros(df_cell.shape[0], dtype=float), + 'rotationZ': np.zeros(df_cell.shape[0], dtype=float), + }) + df_sim.sort_values(['time', 'unique_id'], inplace=True) + # SimulariumIO 1.13.0 AgentData.from_dataframe expects traj.loc[0, ...] + # to select the whole trajectory, so all rows need the same index label. + df_sim.index = np.zeros(df_sim.shape[0], dtype=int) + + # generate simmularium trajectorydata object + o_sim = sim.TrajectoryData( + meta_data = sim.MetaData( + box_size=ar_box, + camera_defaults=camera_defaults, + scale_factor=scale_factor, + trajectory_title=trajectory_title, + model_meta_data=model_meta_data, + ), + agent_data = sim.AgentData.from_dataframe(df_sim), + time_units = sim.UnitData(ds_unit['time']), + spatial_units = sim.UnitData(ds_unit['spatial_unit']), + #plots=, + ) + + # transform data and save trajectorydata object to simularium file + sim.TrajectoryConverter(o_sim).save(trajectory_title) + if self.verbose: + print(f'simularium viewer at: https://simularium.allencell.org/') diff --git a/pyproject.toml b/pyproject.toml index dd74a72..780afa8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,6 +141,7 @@ pcdl_get_anndata = "pcdl.commandline:get_anndata" pcdl_make_graph_gml = "pcdl.commandline:make_graph_gml" pcdl_plot_scatter = "pcdl.commandline:plot_scatter" pcdl_make_cell_vtk = "pcdl.commandline:make_cell_vtk" +pcdl_make_simularium = "pcdl.commandline:make_simularium" # substrate and cell agent pcdl_get_muspan = "pcdl.commandline:get_muspan" pcdl_get_spatialdata = "pcdl.commandline:get_spatialdata" From 0e6fec0dd8cb7ad2f238cd36210af8353f94b4ec Mon Sep 17 00:00:00 2001 From: bue Date: Fri, 28 Aug 2026 13:04:08 -0400 Subject: [PATCH 02/15] @ pcdl : mcdsts.make_simularium and pcdl_make_simularium implementation works. --- README.md | 29 +++++++--- jakku/toolsiuc_tools/pcdl/pcdl_macros.xml | 11 ++-- man/TUTORIAL_introduction.md | 14 ++--- man/TUTORIAL_simularium.md | 0 pcdl/commandline.py | 4 +- pcdl/timeseries.py | 7 ++- pyproject.toml | 1 + test/test_commandline_2d.py | 69 +++++++++++++++++++++++ test/test_timeseries_2d.py | 23 ++++++++ test/test_timeseries_3d.py | 23 ++++++++ 10 files changed, 159 insertions(+), 22 deletions(-) create mode 100644 man/TUTORIAL_simularium.md diff --git a/README.md b/README.md index 117e170..7b9b075 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,15 @@ The pcdl python3 library maintains four branches: + **Branch version 1** is the original PhysiCell-Tools/python-loader code. + **Branch version 2** will be strictly compatible with the original PhysiCell-Tools/python-loader code, although pip installable. + **Branch version 3** might break with old habits, although tries to be as much downward compatible as possible. - The aim of the v3 branch is to get a very lean and agile python3 physicell output interface for the ones coming from the python3 world. + The aim of the v3 branch was to get a very lean and agile python3 physicell output interface for the ones coming from the python3 world. + Finally, **Branch version 4** reimplemented the backend in a more python3, less C++ like manner. ## Header: + Language: python [>= 3.11](https://devguide.python.org/versions/) -+ Library dependencies: anndata, bioio, geopandas, matplotlib, networkx, neuroglancer, numpy, pandas, (requests), scikit-image, scipy, shapely, spatialdata, vtk ++ Library core dependencies: matplotlib, numpy, pandas, scipy ++ Library optional dependencies: anndata, bioio, geopandas, networkx, neuroglancer, requests, scikit-image, shapely, simulariumio, spatialdata, vtk + Date of origin original PhysiCell-Tools python-loader: 2019-09-02 + Date of origin pcdl fork: 2022-08-30 + Doi: https://doi.org/10.5281/ZENODO.8176399 @@ -131,8 +132,13 @@ Developers, please make pull requests to the https://github.com/elmbeech/physice ## Release Notes: ++ branch v4: active feature development, bug fixes, and security maintenance. + ++ version 4.1.9 (2026-08-28): elmbeech/physicelldataloader + + new TimeSeries **make_simularium** and **pcdl_make_simularium** function. special thanks to Randy Heiland! + + version 4.1.8 (2026-08-26): elmbeech/physicelldataloader - + branch v4 compatible with bioconda and galaxy. + + branch v4 compatible with bioconda and galaxy. special thanks to Björn Grüning! + version 4.1.7 (2026-08-21): elmbeech/physicelldataloader + compatible with pandas v3.0.0. @@ -144,9 +150,9 @@ Developers, please make pull requests to the https://github.com/elmbeech/physice + bugfix library dependencies and library versions. + version 4.1.4 (2026-04-04): elmbeech/physicelldataloader - + pyMCDSts **mcdsts.make_contour** extrema parameter is replaced by vmin and vmax to be compatible with mcds.make\_contour and plt.contour. + + TimeSeries **mcdsts.make_contour** extrema parameter is replaced by vmin and vmax to be compatible with mcds.make\_contour and plt.contour. + **make_cell_vtk** and **make_conc_vtk** now offer an ext parameter, allowing to manually specify the exact file extension. special thanks to Danyon Gedris! - + new **make_muspan** TimeStep class and TimeSeris class function and **pcdl_get_muspan** command line command. special thanks to Joshua Moore and Joshua Bull! + + new **make_muspan** TimeStep class and TimeSeries class function and **pcdl_get_muspan** command line command. special thanks to Joshua Moore and Joshua Bull! + version 4.1.3 (2026-03-21): elmbeech/physicelldataloader + new **pcdl.pccmap** color map. this is an adaptation of the physicell pathology paint by number color map. @@ -160,7 +166,7 @@ Developers, please make pull requests to the https://github.com/elmbeech/physice + reduced memory footprint. + version 4.1.0 (2025-12-31): elmbeech/physicelldataloader - + new **get_spatialdata** TimeStep class and TimeSeris class function and **pcdl_get_spatialdata** command line command. special thanks to Luca Marconato! + + new **get_spatialdata** TimeStep class and TimeSeries class function and **pcdl_get_spatialdata** command line command. special thanks to Luca Marconato! + with this release, pcdl officially became an [scverse ecosystem](https://scverse.org/packages/#ecosystem) package. + version 4.0.5 (2025-10-22): elmbeech/physicelldataloader @@ -173,7 +179,7 @@ Developers, please make pull requests to the https://github.com/elmbeech/physice + command line commands now return **error code 0** if the command runs successfully. + version 4.0.3 (2025-07-20): elmbeech/physicelldataloader - + TimeStep and TimeSeris **plot_contour**, **plot_scatter**, and **plot_timeseries** handle now **kwargs** arguments. + + TimeStep and TimeSeries **plot_contour**, **plot_scatter**, and **plot_timeseries** handle now **kwargs** arguments. + minor bugfixes. + version 4.0.2 (2025-06-29): elmbeech/physicelldataloader @@ -199,6 +205,9 @@ Developers, please make pull requests to the https://github.com/elmbeech/physice + new **render_neuroglancer** function, to render ome tiff image into neuroglancer. + new **pcdl_render_neuroglancer** function, to render ome tiff images into neuroglancer. + ++ branch v3: end-of-life reached. + + version 3.3.8 (2025-07-23): elmbeech/physicelldataloader + command line commands now return **error code 0** if the command runs successfully. @@ -405,11 +414,17 @@ Developers, please make pull requests to the https://github.com/elmbeech/physice + the underling [mcds object data dictionary structure](https://github.com/elmbeech/physicelldataloader/tree/master/man/img/physicelldataloader_data_dictionary_v3.0.0.png) has changed. + [pytest](https://en.wikipedia.org/wiki/Pytest) unit tests exist now for all pyMCDS and pyMCDSts functions. + ++ branch v2: end-of-life reached. + + version 2.0.3 (2023-06-16): elmbeech/physicelldataloader pypa odyssey is coming to an end. + version 2.0.2 (2023-01-06): elmbeech/physicelldataloader reset patch voxel spacing bugfix, so that branch2 is full compatible with branch1 again. use branch3 for a bugfixed version! + version 2.0.1 (2022-11-08): elmbeech/physicelldataloader beta release patch voxel spacing bugfix. + version 2.0.0 (2022-08-30): elmbeech/physicelldataloader pip installable release, derived from and compatible with PhysiCell-Tools/python-loader release 1.1.0 (2022-07-20). + ++ branch v1: end-of-life reached. + + version 1.1.1 (2022-07-01): elmbeech/physicelldataloader deprecated np.float replaced with np.float64. + version 1.1.0 (2022-05-09): Physicell-Tools/python-loader release compatible with pre-v1.10.x of PhysiCell. + version 1.0.1 (2020-01-25): Physicell-Tools/python-loader time-series related bug fix. diff --git a/jakku/toolsiuc_tools/pcdl/pcdl_macros.xml b/jakku/toolsiuc_tools/pcdl/pcdl_macros.xml index ac6b555..dd88505 100644 --- a/jakku/toolsiuc_tools/pcdl/pcdl_macros.xml +++ b/jakku/toolsiuc_tools/pcdl/pcdl_macros.xml @@ -229,18 +229,19 @@ + + + + --> - + ^[a-zA-Z0-9_\-.]*\.vtp$ - + ^[a-zA-Z0-9_\-.]*\.vtr$ diff --git a/man/TUTORIAL_introduction.md b/man/TUTORIAL_introduction.md index 8aea2fb..64c58ee 100644 --- a/man/TUTORIAL_introduction.md +++ b/man/TUTORIAL_introduction.md @@ -3,9 +3,8 @@ If you have not already done so, please install the latest version of physicell data loader (pcdl), as described in the [HowTo](https://github.com/elmbeech/physicelldataloader/blob/master/man/HOWTO.md) section.\ The current development happens in branch v4. -Branch v3 and v4 are maintained and [pip](https://pypi.org/project/pcdl/) installable. -Branch v3 is also installable through [bioconada](https://bioconda.github.io/). -Branch v1, v2 exists, if ever needed, for reproducibility of old results. +Branch v4 is maintained and [pip](https://pypi.org/project/pcdl/) and [bioconada](https://bioconda.github.io/) installable. +Branch v1, v2, v3 exists, if ever needed, for reproducibility of old results. ## Tutorial - branch v1 and v2 @@ -13,26 +12,27 @@ The original python-loader tutorial can be found here. + http://www.mathcancer.org/blog/python-loader/ -## Tutorial - branch v3 and v4 +## Tutorial - branch v4 ### History In the very early days, [PhysiCell](https://github.com/MathCancer/PhysiCell) output was with the help of a MATLAB script loaded into MATLAB for analysis.\ In 2019, a similar loader script was written for python3. -The name of this script filed was pyMCDS.py and basically defined one class named pyMCDS. +The name of this script file was pyMCDS.py and basically defined one class named pyMCDS. In autumn 2022, an endeavor was undertaken to pack the original pyMCDS.py script into a pip installable python3 library and develop it further, but always in such a way that, if necessary, the code could still be run like in the early days.\ The result is the pcdl physicell data loader library branch v2, v3. In spring 2025, the code was stripped of some relics (mainly in the back end) from the early days to make the code more python3 than C++ like, which resulted in branch v4. +In summer 2026, branch v4 became throug a core and optional dependency installation feature bioconda compatible and, as such, made branch v3 superfluous. -The result from all of this is the pcdl physicell data loader library here.\ +The result from all of this development is the pcdl physicell data loader library here.\ In the big picture, the pyMCDS class evolved into the TimeStep class, which is slightly heavier but much more powerful for downstream data analysis than the original pyMCDS class. Additionally, a TimeSeries class was added. If you inspect branch v3 pcdl source code, you will see that the [pyMCDS.py](https://github.com/elmbeech/physicelldataloader/blob/v3/pcdl/pyMCDS.py) file still exists. And if you feel so, it is still possible to [load and process PhysiCell output the ancient way](https://github.com/elmbeech/physicelldataloader/blob/master/man/HOWTO.md#how-to-run-physicelldataloader-like-in-the-early-days-before-autumn-2022)!\ -Naturally, the full-fledged pcdl library with the TimeSteps and TimeSeries class is much more powerful than pyMCDS.py only. +Naturally, the full-fledged branch v4 pcdl library with the TimeSteps and TimeSeries class is much more powerful than pyMCDS.py only. ### Concept diff --git a/man/TUTORIAL_simularium.md b/man/TUTORIAL_simularium.md new file mode 100644 index 0000000..e69de29 diff --git a/pcdl/commandline.py b/pcdl/commandline.py index 0d6a058..195e09a 100644 --- a/pcdl/commandline.py +++ b/pcdl/commandline.py @@ -2087,7 +2087,7 @@ def make_simularium(): else: s_path = '/'.join(s_pathfile.split('/')[:-1]) if not os.path.exists(s_pathfile): - sys.exit(f'Error @ pcdl_make_cell_vtk : {s_pathfile} path does not look like a physicell output directory ({s_path}/initial.xml is missing).') + sys.exit(f'Error @ pcdl_simularium : {s_pathfile} path does not look like a physicell output directory ({s_path}/initial.xml is missing).') # custom_data_type d_vartype = {} @@ -2098,7 +2098,7 @@ def make_simularium(): elif s_type in {'float'}: o_type = float elif s_type in {'str'}: o_type = str else: - sys.exit(f'Error @ pcdl_make_cell_vtk : {s_var} {s_type} has an unknowen data type. knowen are bool, int, float, str.') + sys.exit(f'Error @ pcdl_simularium : {s_var} {s_type} has an unknowen data type. knowen are bool, int, float, str.') d_vartype.update({s_var : o_type}) # run diff --git a/pcdl/timeseries.py b/pcdl/timeseries.py index 78404b2..3853018 100644 --- a/pcdl/timeseries.py +++ b/pcdl/timeseries.py @@ -2053,6 +2053,11 @@ def make_simularium(self, focus_cat=['cell_type','current_phase'], trajectory_t ) # transform data and save trajectorydata object to simularium file - sim.TrajectoryConverter(o_sim).save(trajectory_title) + s_save = self.path + '/' + trajectory_title + s_pathfile = s_save + '.simularium' + sim.TrajectoryConverter(o_sim).save(s_save) if self.verbose: print(f'simularium viewer at: https://simularium.allencell.org/') + + # return error code + return s_pathfile diff --git a/pyproject.toml b/pyproject.toml index 780afa8..7384b24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,7 @@ full = [ "neuroglancer", "scikit-image>=0.24.0", "shapely>=2.0.1", # spatialdata 0.7.3 compatible + "simulariumio", "spatialdata>=0.7.3", ] diff --git a/test/test_commandline_2d.py b/test/test_commandline_2d.py index 784d69e..a6333df 100644 --- a/test/test_commandline_2d.py +++ b/test/test_commandline_2d.py @@ -1474,6 +1474,75 @@ def test_pcdl_make_cell_vtk_timestep_ext(self): assert o_result.returncode == 0 +class TestCommandLineInterfaceSimularium(object): + ''' tests for one pcdl command line interface function. ''' + + # timeseries (there is no timestep function): + # + path nop + # + customtype ([], _sample:bool_) ok + # + microenv nop + # + physiboss (true, _false_) ok + # + settingxml (string, _none_, _false_) ok + # + verbose (true, _false_) nop + # + focus_cat (['cell_type','current_phase'], _['current_phase','cell_type']_) ok + # + trajectory_title (timeseries, _zeitreihe_) ok + # + scale_factor (None, 1.0) ok + + def test_pcdl_make_simularium_timeseries_default(self): + o_result = subprocess.run(['pcdl_make_simularium', s_path_2d], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries.simularium') + assert o_result.returncode == 0 + + def test_pcdl_make_simularium_timeseries_customtype_attribute_one(self): + o_result = subprocess.run(['pcdl_make_simularium', s_path_2d, 'sample', '--custom_data_type', 'sample:bool'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries.simularium') + assert o_result.returncode == 0 + + def test_pcdl_make_simularium_timeseries_physiboss(self): + o_result = subprocess.run(['pcdl_make_simularium', s_path_2d, '--physiboss', 'false'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries.simularium') + assert o_result.returncode == 0 + + def test_pcdl_make_simularium_timeseries_settingxmlfalse_attribute_one(self): + o_result = subprocess.run(['pcdl_make_simularium', s_path_2d, 'default_fusion_rates', '--settingxml', 'false'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries.simularium') + assert o_result.returncode == 0 + + def test_pcdl_make_simularium_timeseries_settingxmlnone_attribute_one(self): + o_result = subprocess.run(['pcdl_make_simularium', s_path_2d, 'default_fusion_rates', '--settingxml', 'none'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries.simularium') + assert o_result.returncode == 0 + + def test_pcdl_make_simularium_timeseries_attribute_many(self): + o_result = subprocess.run(['pcdl_make_simularium', s_path_2d, 'current_phase', 'cell_type', '--trajectory_title', 'zeitreihe', '--scale_factor', '1.0'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/zeitreihe.simularium') + assert o_result.returncode == 0 + + ####################################### # substrate and cell agenat test code # ####################################### diff --git a/test/test_timeseries_2d.py b/test/test_timeseries_2d.py index 9340a28..992583b 100644 --- a/test/test_timeseries_2d.py +++ b/test/test_timeseries_2d.py @@ -1084,3 +1084,26 @@ def test_mcdsts_get_spatialdata_keepmcdsfalse(self): (lo_sdmcds_output == lo_sdmcds_memory) and \ (str(type(lo_sdmcds_output[8])) == "") + +## simularium time series related functions ## +class TestTimeSeriesSimularium(object): + ''' test for pcdl.TimeSeries class. ''' + + ## get_simularium command ## + def test_mcdsts_get_simularium_default(self): + import simulariumio as sim + mcdsts = pcdl.TimeSeries(s_path_2d, verbose=False) + s_pathfile = mcdsts.make_simularium(focus_cat=['cell_type','current_phase'], trajectory_title='timeseries', scale_factor=None, camera_defaults=None, model_meta_data=None) + assert(str(type(mcdsts)) == "") and \ + (s_pathfile.endswith('/pcdl/output_2d/timeseries.simularium')) and \ + (os.path.exists(s_pathfile)) + os.remove(s_pathfile) + + def test_mcdsts_get_simularium_nondefault(self): + import simulariumio as sim + mcdsts = pcdl.TimeSeries(s_path_2d, verbose=False) + s_pathfile = mcdsts.make_simularium(focus_cat=['current_phase', 'dead'], trajectory_title='zeitreihe', scale_factor=0.1, camera_defaults=sim.CameraData(), model_meta_data=sim.ModelMetaData()) + assert(str(type(mcdsts)) == "") and \ + (s_pathfile.endswith('/pcdl/output_2d/zeitreihe.simularium')) and \ + (os.path.exists(s_pathfile)) + os.remove(s_pathfile) diff --git a/test/test_timeseries_3d.py b/test/test_timeseries_3d.py index 7e0fe0c..94801e4 100644 --- a/test/test_timeseries_3d.py +++ b/test/test_timeseries_3d.py @@ -946,3 +946,26 @@ def test_mcdsts_get_spatialdata_keepmcdsfalse(self): (lo_sdmcds_output == lo_sdmcds_memory) and \ (str(type(lo_sdmcds_output[8])) == "") + +## simularium time series related functions ## +class TestTimeSeriesSimularium(object): + ''' test for pcdl.TimeSeries class. ''' + + ## get_simularium command ## + def test_mcdsts_get_simularium_default(self): + import simulariumio as sim + mcdsts = pcdl.TimeSeries(s_path_3d, verbose=False) + s_pathfile = mcdsts.make_simularium(focus_cat=['cell_type','current_phase'], trajectory_title='timeseries', scale_factor=None, camera_defaults=None, model_meta_data=None) + assert(str(type(mcdsts)) == "") and \ + (s_pathfile.endswith('/pcdl/output_3d/timeseries.simularium')) and \ + (os.path.exists(s_pathfile)) + os.remove(s_pathfile) + + def test_mcdsts_get_simularium_nondefault(self): + import simulariumio as sim + mcdsts = pcdl.TimeSeries(s_path_3d, verbose=False) + s_pathfile = mcdsts.make_simularium(focus_cat=['current_phase', 'dead'], trajectory_title='zeitreihe', scale_factor=0.1, camera_defaults=sim.CameraData(), model_meta_data=sim.ModelMetaData()) + assert(str(type(mcdsts)) == "") and \ + (s_pathfile.endswith('/pcdl/output_3d/zeitreihe.simularium')) and \ + (os.path.exists(s_pathfile)) + os.remove(s_pathfile) From 8a343f768e0a6e73b878f4f8bd9c3a4daed2f9f8 Mon Sep 17 00:00:00 2001 From: bue Date: Fri, 28 Aug 2026 13:30:07 -0400 Subject: [PATCH 03/15] @ physicell data loader : next release v4.1.9. --- .github/workflows/apple.yml | 4 ++-- .github/workflows/linux.yml | 4 ++-- .github/workflows/windows.yml | 4 ++-- jakku/toolsiuc_tools/pcdl/pcdl_get_anndata.xml | 4 ++-- pcdl/VERSION.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/apple.yml b/.github/workflows/apple.yml index 125aa37..6e459df 100644 --- a/.github/workflows/apple.yml +++ b/.github/workflows/apple.yml @@ -7,7 +7,7 @@ run-name: ${{ github.actor }}::pytest pcdl library on mac os x; the latest pytho on: push: - branches: ["utest", "master"] # ["v3", "v4"] + branches: ["master"] pull_request: branches: ["development", "master"] @@ -34,7 +34,7 @@ jobs: run: | brew install ffmpeg imagemagick python -m pip install --upgrade pip - python -m pip install flake8 pytest anndata bioio bioio-ome-tiff geopandas matplotlib networkx neuroglancer numpy pandas requests scikit-image scipy shapely spatialdata vtk + python -m pip install flake8 pytest anndata bioio bioio-ome-tiff geopandas matplotlib networkx neuroglancer numpy pandas requests scikit-image scipy shapely simulariumio spatialdata vtk python -m pip install /Users/runner/work/physicelldataloader/physicelldataloader -v #if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: lint with flake8 diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 5bc0a9c..1b410b7 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -7,7 +7,7 @@ run-name: ${{ github.actor }}::pytest pcdl library on linux os; all python3 vers on: push: - branches: ["utest", "master"] # ["v3","v4"] + branches: ["master"] pull_request: branches: ["development", "master"] @@ -35,7 +35,7 @@ jobs: sudo apt update sudo apt install ffmpeg imagemagick python -m pip install --upgrade pip - python -m pip install flake8 pytest anndata bioio bioio-ome-tiff geopandas matplotlib networkx neuroglancer numpy pandas requests scikit-image scipy shapely spatialdata vtk + python -m pip install flake8 pytest anndata bioio bioio-ome-tiff geopandas matplotlib networkx neuroglancer numpy pandas requests scikit-image scipy shapely simulariumio spatialdata vtk python -m pip install /home/runner/work/physicelldataloader/physicelldataloader -v #if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: lint with flake8 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index ecb7338..54b70e9 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -7,7 +7,7 @@ run-name: ${{ github.actor }}::pytest pcdl library on windows os; the latest pyt on: push: - branches: ["utest", "master"] # ["v3", "v4"] + branches: ["master"] pull_request: branches: ["development", "master"] @@ -34,7 +34,7 @@ jobs: run: | choco install ffmpeg imagemagick python -m pip install --upgrade pip - python -m pip install flake8 pytest anndata bioio bioio-ome-tiff geopandas matplotlib networkx neuroglancer numpy pandas requests scikit-image scipy shapely spatialdata vtk + python -m pip install flake8 pytest anndata bioio bioio-ome-tiff geopandas matplotlib networkx neuroglancer numpy pandas requests scikit-image scipy shapely simulariumio spatialdata vtk python -m pip install D:\a\physicelldataloader\physicelldataloader -v #echo 'set PYTHONPATH=D:\a\physicelldataloader\physicelldataloader' >> $GITHUB_ENV #if [ -f requirements.txt ]; then pip install -r requirements.txt; fi diff --git a/jakku/toolsiuc_tools/pcdl/pcdl_get_anndata.xml b/jakku/toolsiuc_tools/pcdl/pcdl_get_anndata.xml index 57da49d..5b80cea 100644 --- a/jakku/toolsiuc_tools/pcdl/pcdl_get_anndata.xml +++ b/jakku/toolsiuc_tools/pcdl/pcdl_get_anndata.xml @@ -115,8 +115,8 @@ Function to transform mcds time steps into one or many anndata objects for downstream analysis. Scale parameter details: Possible values are None, maxabs, minmax, std. -None: no scaling. Set scale to None if you would like to have raw data or entirely scale, transform, and normalize the data later. -Maxabs: maximum absolute value distance scaler will linearly map all values into a [-1, 1] interval. If the original data has no negative values, the result will be the same as with the minmax scaler (except with attributes with only one value). If the attribute has only zeros, the value will be set to 0. +None: no scaling. Set scale to None if you would like to have raw data or entirely scale, transform, and normalize the data later. +Maxabs: maximum absolute value distance scaler will linearly map all values into a [-1, 1] interval. If the original data has no negative values, the result will be the same as with the minmax scaler (except with attributes with only one value). If the attribute has only zeros, the value will be set to 0. Minmax: minimum maximum distance scaler will map all values linearly into a [0, 1] interval. If the attribute has only one value, the value will be set to 0. Std: standard deviation scaler will result in sigmas. Each attribute will be mean centered around 0. Ddof delta degree of freedom is set to 1 because it is assumed that the values are samples out of the population and not the entire population. It is incomprehensible to me that the equivalent sklearn method has ddof set to 0. If the attribute has only one value, the value will be set to 0. diff --git a/pcdl/VERSION.py b/pcdl/VERSION.py index f589b73..a9e714a 100644 --- a/pcdl/VERSION.py +++ b/pcdl/VERSION.py @@ -1 +1 @@ -__version__ = '4.1.8' +__version__ = '4.1.9' From f594542b730bb7906e0fd598d48f52968b9af969 Mon Sep 17 00:00:00 2001 From: bue Date: Sun, 30 Aug 2026 16:40:29 -0400 Subject: [PATCH 04/15] @ man : add simularium and galaxy man pages. --- man/TUTORIAL_galaxy.md | 60 ++++++++++++++++++++++++++++++++++ man/TUTORIAL_simularium.md | 66 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/man/TUTORIAL_galaxy.md b/man/TUTORIAL_galaxy.md index e69de29..5034237 100644 --- a/man/TUTORIAL_galaxy.md +++ b/man/TUTORIAL_galaxy.md @@ -0,0 +1,60 @@ +# PhysiCell Data Loader Tutorial: pcdl and Galaxy + +[Galaxy](https://en.wikipedia.org/wiki/Galaxy_(computational_biology)) is an open source, web-based platform for bioinformatics anlysis. + +There a hand full of galaxy server instances around the world (USA, Europe, France, Australia, possibly others). +We here focus on [the European instance of Galaxy](https://usegalaxy.eu/) since it has pcdl tools installed. + +Special thanks to Björn Grüning, who helped me to port pcdl into Galaxy! + + +## Run PhysiCell Studio on Galaxy + +1. In your web browser, open the European instance of Galaxy: https://usegalaxy.eu/ +2. Optionally, log into your user account. Create one if you do not already have one. +3. Near the top left corner, click "Tools". +4. Near the left-side top corner, in the "Tools" search bar, type "PhysiCell Studio". +5. Choose "PhysiCell Studio". +6. Choose "Run Tool" (center panel or top right). +7. How to load, develop, or run a PhysiCell model in the studio can be learned here: + + https://physicell-studio.readthedocs.io/en/latest/index.html + + https://github.com/PhysiCell-Tools/PhysiCell-Studio +8. [TKBue] + + +## Upload local PhysiCell output to Galaxy + +1. In your web browser, open the European instance of Galaxy: https://usegalaxy.eu/ +2. Optionally, log into your user account. Create one if you do not already have one. +3. Near the top left corner, click "Upload" (New upload Beta). +4. Near the top left corner, choose "Upload from Computer". +5. In the center panel click "Brows Files". +6. Click your self through to the PhysiCell output folder. +7. Click on one file in the output folder, then on your keyboard press Ctrl + A to choose all files. +8. Click "Open". After a while the filenames will appear in the central panel. +9. On the center panel bottom, flip the switch "Create a collection from these files" on. +10. "Enter a collection name" (e.g. output) and choose "Collection Type" "List". +11. Click "Start". +12. After a while, the uploaded data collection should appear in the history in the right-side panel. + + +## The pcdl tools on Galaxy + +1. In your web browser, open the European instance of Galaxy: https://usegalaxy.eu/ +2. Optionally, log into your user account. Create one if you do not already have one. +3. Upload or generate PhysiCell output as described above. +4. Near the top left corner, click "Tools". +5. Near the left-side top corner, in the "Tools" serach bar type "pcdl". +6. Chosse one of the pcdl_ tools (e.g. pcdl\_get\_version). +7. Mouse drag and drop the uploaded or generated PhysiCell output data collecton in the left-side history panel into the "essential:" "data collection" field in the middle panel. +8. Optionally, tweak the "essential" and "advanced:" parameters. +9. Click "Run Tool". +10. After a while, the result file(s) should appear in the left-side history panel. + + +### ✨ Further readings + +You can learn more about Galaxy here: ++ https://galaxyproject.org/tutorials/g101/ + +That's it. The rest is analysis within Galaxy! diff --git a/man/TUTORIAL_simularium.md b/man/TUTORIAL_simularium.md index e69de29..fdf546e 100644 --- a/man/TUTORIAL_simularium.md +++ b/man/TUTORIAL_simularium.md @@ -0,0 +1,66 @@ +# PhysiCell Data Loader Tutorial: pcdl and Simularium + +The [simularium](https://simularium.allencell.org/) viewer was developed to share and interrogate interactive 3D visualizations of biological simulation trajectories and related plots directly in a web browser. + +Simularium files can easily be generated from mcds time series. + + +### ✨ command line + +#### command line time series + +Generate a simularium trajectory file. + +```bash +pcdl_make_simularium output_2d/ +``` + +#### command line man page + +```bash +pcdl_make_simularium -h +``` + + +### ✨ python + +#### python time series + +Load a time series and generate a simularium trajectory file. + +```python +import pcdl + +mcdsts = pcdl.TimeSeries('output_2d/') +mcdsts.make_simularium() +``` + +#### python docstrings + +```python +import pcdl + +mcdsts = pcdl.TimeSeries('output_2d/') +help(mcdsts.make_simularium()) +``` + +### ✨ simularium viewer + +1. Open the simularium viewer web page: https://simularium.allencell.org/ +2. On the top right corner, click the "Load models" dropdown menu and choose "Simularium file". +3. In the popup window choose "From your device" "Select File". +4. Select the simularium file and click "Open". +5. Click "Load". +6. Explore the loaded file. + + +### ✨ Further readings' + +Please work through the official documentation to learn more about how simularium software. + ++ https://simularium.allencell.org/ ++ https://github.com/simularium ++ https://simularium.github.io/simulariumio/# ++ https://doi.org/10.1038/s41592-022-01442-1 + +That's it. The rest is analysis within simularium! From 3858a86cc92e1695cf2671bcb2763237aab2942f Mon Sep 17 00:00:00 2001 From: bue Date: Sun, 30 Aug 2026 16:47:14 -0400 Subject: [PATCH 05/15] @ man : add simularium and galaxy man pages. --- man/TUTORIAL_galaxy.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/man/TUTORIAL_galaxy.md b/man/TUTORIAL_galaxy.md index 5034237..a3033d6 100644 --- a/man/TUTORIAL_galaxy.md +++ b/man/TUTORIAL_galaxy.md @@ -44,9 +44,9 @@ Special thanks to Björn Grüning, who helped me to port pcdl into Galaxy! 2. Optionally, log into your user account. Create one if you do not already have one. 3. Upload or generate PhysiCell output as described above. 4. Near the top left corner, click "Tools". -5. Near the left-side top corner, in the "Tools" serach bar type "pcdl". -6. Chosse one of the pcdl_ tools (e.g. pcdl\_get\_version). -7. Mouse drag and drop the uploaded or generated PhysiCell output data collecton in the left-side history panel into the "essential:" "data collection" field in the middle panel. +5. Near the left-side top corner, in the "Tools" search bar, type "pcdl". +6. Choose one of the pcdl_ tools (e.g. pcdl\_get\_version). +7. Mouse drag and drop the uploaded or generated PhysiCell output data collection in the left-side history panel into the "essential:" "data collection" field in the middle panel. 8. Optionally, tweak the "essential" and "advanced:" parameters. 9. Click "Run Tool". 10. After a while, the result file(s) should appear in the left-side history panel. From 1eda93dc84a59be5ac04d95eba554576e43b819e Mon Sep 17 00:00:00 2001 From: bue Date: Sun, 30 Aug 2026 16:53:21 -0400 Subject: [PATCH 06/15] @ man : add simularium and galaxy man pages. --- man/TUTORIAL_simularium.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/TUTORIAL_simularium.md b/man/TUTORIAL_simularium.md index fdf546e..82968e8 100644 --- a/man/TUTORIAL_simularium.md +++ b/man/TUTORIAL_simularium.md @@ -47,7 +47,7 @@ help(mcdsts.make_simularium()) ### ✨ simularium viewer 1. Open the simularium viewer web page: https://simularium.allencell.org/ -2. On the top right corner, click the "Load models" dropdown menu and choose "Simularium file". +2. Near the top right corner, click the "Load models" dropdown menu and choose "Simularium file". 3. In the popup window choose "From your device" "Select File". 4. Select the simularium file and click "Open". 5. Click "Load". From 8017f43a7b6246f41c9bf8c536341da7c7b921a8 Mon Sep 17 00:00:00 2001 From: bue Date: Mon, 31 Aug 2026 00:33:46 -0400 Subject: [PATCH 07/15] @ pcdl : in the middle of getting things in order. --- README.md | 1 + pcdl/commandline.py | 843 ++++++++++++++-------------- pcdl/timeseries.py | 1280 +++++++++++++++++++++---------------------- pcdl/timestep.py | 1136 +++++++++++++++++++------------------- pyproject.toml | 9 +- 5 files changed, 1626 insertions(+), 1643 deletions(-) diff --git a/README.md b/README.md index 7b9b075..c8a51d5 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ Developers, please make pull requests to the https://github.com/elmbeech/physice ## Road Map: ++ downcast, to\file, and read function. + evt generate lineage tree graph output files. diff --git a/pcdl/commandline.py b/pcdl/commandline.py index 195e09a..18cca4f 100644 --- a/pcdl/commandline.py +++ b/pcdl/commandline.py @@ -250,11 +250,11 @@ def get_substrate_list(): return 0 -def get_conc_attribute(): +def get_conc_df(): # argv parser = argparse.ArgumentParser( - prog = 'pcdl_get_conc_attribute', - description = 'function to detect informative substrate concentration variables in a time series. this function detects even variables which have less than the minimal state count in each time step, but different values from time step to time step. the output is a json file with an entry of all non-coordinate column names that, at least in one of the time steps or in between time steps, reach the given minimal value count. key is the column name, mapped is a list of all values (bool, str, and, if allvalues is True, int and float) or a list with minimum and maximum values (int, float).', + prog = 'pcdl_get_conc_df', + description = 'this function extracts dataframes with concentration values for all chemical species in all voxels and saves them as csv files. additionally, this dataframe lists voxel and mesh center coordinates.', epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', ) @@ -263,7 +263,7 @@ def get_conc_attribute(): 'path', nargs = '?', default = '.', - help = 'path to the PhysiCell output directory. default is . .', + help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .', ) # TimeSeries output_path '.' # TimeSeries custom_data_type nop @@ -277,7 +277,7 @@ def get_conc_attribute(): default = 'true', help = 'setting verbose to False for less text output, while processing. default is True.', ) - # get_conc_attribute values + # get_conc_df values parser.add_argument( 'values', nargs = '?', @@ -285,25 +285,25 @@ def get_conc_attribute(): type = int, help = 'minimal number of values a variable has to have in any of the mcds time steps to be outputted. variables that have only 1 state carry no information. None is a state too. default is 1.', ) - # get_conc_attribute drop + # get_conc_df drop parser.add_argument( '--drop', nargs = '*', default = [], help = "set of column labels to be dropped for the dataframe. don't worry: essential columns like ID, coordinates and time will never be dropped. Attention: when the keep parameter is given, then the drop parameter has to be an empty string! default is an empty string.", ) - # get_conc_attribute keep + # get_conc_df keep parser.add_argument( '--keep', nargs = '*', default = [], help = "set of column labels to be kept in the dataframe. set values=1 to be sure that all variables are kept. don't worry: essential columns like ID, coordinates and time will always be kept. default is an empty string.", ) - # get_conc_attribute allvalues + # get_conc_df collapse parser.add_argument( - '--allvalues', - default = 'false', - help = 'for numeric data, should only the min and max values or all values be returned? default is false.', + '--collapse', + default = 'true', + help = 'should all mcds time steps from the time series be collapsed into one big csv, or a many csv, one for each time step? default is True.' ) # parse arguments @@ -322,42 +322,69 @@ def get_conc_attribute(): else: s_path = '/'.join(s_path.split('/')[:-1]) if not os.path.exists(s_pathfile): - sys.exit(f'Error @ pcdl_get_conc_attribute : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') + sys.exit(f'Error @ pcdl_get_conc_df : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') # run - mcdsts = pcdl.TimeSeries( - output_path = s_path, - #custom_data_type, - load = True, - microenv = True, - graph = False, - physiboss = False, - settingxml = None, - verbose = False if args.verbose.lower().startswith('f') else True, - ) - s_values = 'minmax' - b_allvalues = True if args.allvalues.lower().startswith('t') else False - if b_allvalues: - s_values = 'all' - dl_variable = mcdsts.get_conc_attribute( - values = args.values, - drop = set(args.drop), - keep = set(args.keep), - allvalues = b_allvalues, - ) - s_ofile = f"timeseries_conc_attribute_{s_values.replace(' ','_')}.json" - s_opathfile = s_path + '/' + s_ofile - json.dump(dl_variable, open(s_opathfile, 'w'), sort_keys=True) + if os.path.isfile(args.path): + mcds = pcdl.TimeStep( + xmlfile = s_pathfile, + output_path = '.', + #custom_data_type, + microenv = True, + graph = False, + physiboss = False, + settingxml = None, + verbose = False if args.verbose.lower().startswith('f') else True + ) + df_conc = mcds.get_conc_df( + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + ) + # going home + s_opathfile = s_pathfile.replace('.xml','_conc.csv') + df_conc.to_csv(s_opathfile) + print(s_opathfile) + + else: + mcdsts = pcdl.TimeSeries( + output_path = s_path, + #custom_data_type, + load = True, + microenv = True, + graph = False, + physiboss = False, + settingxml = None, + verbose = False if args.verbose.lower().startswith('f') else True, + ) + # handle collaps + b_collapse = False if args.collapse.lower().startswith('f') else True + ldf_conc = mcdsts.get_conc_df( + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + collapse = b_collapse, + ) + # going home + if b_collapse: + s_opathfile = f'{s_path}/timeseries_conc.csv' + ldf_conc.to_csv(s_opathfile) + print(s_opathfile) + else: + ls_opathfile = [f"{s_path}/{s_xmlfile.replace('.xml','_conc.csv')}" for s_xmlfile in mcdsts.get_xmlfile_list()] + for i, df_conc in enumerate(ldf_conc): + df_conc.to_csv(ls_opathfile[i]) + print(ls_opathfile) + # going home - print(s_opathfile) return 0 -def get_conc_df(): +def get_conc_attribute(): # argv parser = argparse.ArgumentParser( - prog = 'pcdl_get_conc_df', - description = 'this function extracts dataframes with concentration values for all chemical species in all voxels and saves them as csv files. additionally, this dataframe lists voxel and mesh center coordinates.', + prog = 'pcdl_get_conc_attribute', + description = 'function to detect informative substrate concentration variables in a time series. this function detects even variables which have less than the minimal state count in each time step, but different values from time step to time step. the output is a json file with an entry of all non-coordinate column names that, at least in one of the time steps or in between time steps, reach the given minimal value count. key is the column name, mapped is a list of all values (bool, str, and, if allvalues is True, int and float) or a list with minimum and maximum values (int, float).', epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', ) @@ -366,7 +393,7 @@ def get_conc_df(): 'path', nargs = '?', default = '.', - help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .', + help = 'path to the PhysiCell output directory. default is . .', ) # TimeSeries output_path '.' # TimeSeries custom_data_type nop @@ -380,7 +407,7 @@ def get_conc_df(): default = 'true', help = 'setting verbose to False for less text output, while processing. default is True.', ) - # get_conc_df values + # get_conc_attribute values parser.add_argument( 'values', nargs = '?', @@ -388,25 +415,25 @@ def get_conc_df(): type = int, help = 'minimal number of values a variable has to have in any of the mcds time steps to be outputted. variables that have only 1 state carry no information. None is a state too. default is 1.', ) - # get_conc_df drop + # get_conc_attribute drop parser.add_argument( '--drop', nargs = '*', default = [], help = "set of column labels to be dropped for the dataframe. don't worry: essential columns like ID, coordinates and time will never be dropped. Attention: when the keep parameter is given, then the drop parameter has to be an empty string! default is an empty string.", ) - # get_conc_df keep + # get_conc_attribute keep parser.add_argument( '--keep', nargs = '*', default = [], help = "set of column labels to be kept in the dataframe. set values=1 to be sure that all variables are kept. don't worry: essential columns like ID, coordinates and time will always be kept. default is an empty string.", ) - # get_conc_df collapse + # get_conc_attribute allvalues parser.add_argument( - '--collapse', - default = 'true', - help = 'should all mcds time steps from the time series be collapsed into one big csv, or a many csv, one for each time step? default is True.' + '--allvalues', + default = 'false', + help = 'for numeric data, should only the min and max values or all values be returned? default is false.', ) # parse arguments @@ -425,61 +452,34 @@ def get_conc_df(): else: s_path = '/'.join(s_path.split('/')[:-1]) if not os.path.exists(s_pathfile): - sys.exit(f'Error @ pcdl_get_conc_df : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') + sys.exit(f'Error @ pcdl_get_conc_attribute : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') # run - if os.path.isfile(args.path): - mcds = pcdl.TimeStep( - xmlfile = s_pathfile, - output_path = '.', - #custom_data_type, - microenv = True, - graph = False, - physiboss = False, - settingxml = None, - verbose = False if args.verbose.lower().startswith('f') else True - ) - df_conc = mcds.get_conc_df( - values = args.values, - drop = set(args.drop), - keep = set(args.keep), - ) - # going home - s_opathfile = s_pathfile.replace('.xml','_conc.csv') - df_conc.to_csv(s_opathfile) - print(s_opathfile) - - else: - mcdsts = pcdl.TimeSeries( - output_path = s_path, - #custom_data_type, - load = True, - microenv = True, - graph = False, - physiboss = False, - settingxml = None, - verbose = False if args.verbose.lower().startswith('f') else True, - ) - # handle collaps - b_collapse = False if args.collapse.lower().startswith('f') else True - ldf_conc = mcdsts.get_conc_df( - values = args.values, - drop = set(args.drop), - keep = set(args.keep), - collapse = b_collapse, - ) - # going home - if b_collapse: - s_opathfile = f'{s_path}/timeseries_conc.csv' - ldf_conc.to_csv(s_opathfile) - print(s_opathfile) - else: - ls_opathfile = [f"{s_path}/{s_xmlfile.replace('.xml','_conc.csv')}" for s_xmlfile in mcdsts.get_xmlfile_list()] - for i, df_conc in enumerate(ldf_conc): - df_conc.to_csv(ls_opathfile[i]) - print(ls_opathfile) - + mcdsts = pcdl.TimeSeries( + output_path = s_path, + #custom_data_type, + load = True, + microenv = True, + graph = False, + physiboss = False, + settingxml = None, + verbose = False if args.verbose.lower().startswith('f') else True, + ) + s_values = 'minmax' + b_allvalues = True if args.allvalues.lower().startswith('t') else False + if b_allvalues: + s_values = 'all' + dl_variable = mcdsts.get_conc_attribute( + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + allvalues = b_allvalues, + ) + s_ofile = f"timeseries_conc_attribute_{s_values.replace(' ','_')}.json" + s_opathfile = s_path + '/' + s_ofile + json.dump(dl_variable, open(s_opathfile, 'w'), sort_keys=True) # going home + print(s_opathfile) return 0 @@ -832,6 +832,7 @@ def make_conc_vtk(): # going home return 0 + ############################################ # cell agent relatd command line functions # ############################################ @@ -903,11 +904,11 @@ def get_celltype_list(): return 0 -def get_cell_attribute_list(): +def get_cell_df(): # argv parser = argparse.ArgumentParser( - prog = 'pcdl_get_cell_attribute_list', - description = 'this function is returns a list with all cell attribute labels, alphabetically ordered.', + prog = 'pcdl_get_cell_df', + description = 'this function extracts dataframes with a cell centric view of the simulation and saves them as csv files.', epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', ) @@ -916,22 +917,22 @@ def get_cell_attribute_list(): 'path', nargs = '?', default = '.', - help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .', + help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .' ) # TimeSeries output_path '.' - # TimeSeries custom_data_type nop + # TimeSeries custom_data_type nop (because datafarme is straightaway saved as csv) # TimeSeries microenv parser.add_argument( '--microenv', default = 'true', - help = 'should the microenvironment data be loaded? setting microenv to False will use less memory and speed up processing. default is True.', + help = 'should the microenvironment data be loaded? setting microenv to False will use less memory and speed up processing. default is True.' ) # TimeSeries graph False # TimeSeries physiboss parser.add_argument( '--physiboss', default = 'true', - help = 'if found, should physiboss state data be extracted and loaded into df_cell dataframe? default is True.' + help = 'if found, should physiboss state data be extracted and loaded into the df_cell dataframe? default is True.' ) # TimeSeries settingxml parser.add_argument( @@ -942,8 +943,36 @@ def get_cell_attribute_list(): # TimeSeries verbose parser.add_argument( '-v', '--verbose', - default = 'false', - help = 'setting verbose to True for more text output, while processing. default is False.', + default = 'true', + help = 'setting verbose to False for less text output, while processing. default is True.' + ) + # get_cell_df values + parser.add_argument( + 'values', + nargs = '?', + default = 1, + type = int, + help = 'minimal number of values a variable has to have in any of the mcds time steps to be outputted. variables that have only 1 state carry no information. None is a state too. default is 1.' + ) + # get_cell_df drop + parser.add_argument( + '--drop', + nargs = '*', + default = [], + help = "set of column labels to be dropped for the dataframe. don't worry: essential columns like ID, coordinates and time will never be dropped. Attention: when the keep parameter is given, then the drop parameter has to be an empty string! default is an empty string." + ) + # get_cell_df keep + parser.add_argument( + '--keep', + nargs = '*', + default = [], + help = "set of column labels to be kept in the dataframe. set values=1 to be sure that all variables are kept. don't worry: essential columns like ID, coordinates and time will always be kept. default is an empty string." + ) + # get_cell_df collapse + parser.add_argument( + '--collapse', + default = 'true', + help = 'should all mcds time steps from the time series be collapsed into one big csv, or a many csv, one csv for each time step?, default is True.' ) # parse arguments @@ -962,22 +991,62 @@ def get_cell_attribute_list(): else: s_path = '/'.join(s_path.split('/')[:-1]) if not os.path.exists(s_pathfile): - sys.exit(f'Error @ pcdl_get_cell_attribute_list : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') + sys.exit(f'Error @ pcdl_get_cell_df : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') # run - mcds = pcdl.TimeStep( - xmlfile = s_pathfile, - output_path = '.', - #custom_data_type, - microenv = False if args.microenv.lower().startswith('f') else True, - graph = False, - physiboss = False if args.physiboss.lower().startswith('f') else True, - settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, - verbose = True if args.verbose.lower().startswith('t') else False - ) + if os.path.isfile(args.path): + mcds = pcdl.TimeStep( + xmlfile = s_pathfile, + output_path = '.', + #custom_data_type, + microenv = False if args.microenv.lower().startswith('f') else True, + graph = False, + physiboss = False if args.physiboss.lower().startswith('f') else True, + settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, + verbose = False if args.verbose.lower().startswith('f') else True + ) + df_cell = mcds.get_cell_df( + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + ) + # going home + s_opathfile = s_pathfile.replace('.xml','_cell.csv') + df_cell.to_csv(s_opathfile) + print(s_opathfile) + + else: + mcdsts = pcdl.TimeSeries( + output_path = s_path, + #custom_data_type, + load = True, + microenv = False if args.microenv.lower().startswith('f') else True, + graph = False, + physiboss = False if args.physiboss.lower().startswith('f') else True, + settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, + verbose = False if args.verbose.lower().startswith('f') else True, + ) + # handle collaps + b_collapse = False if args.collapse.lower().startswith('f') else True + ldf_cell = mcdsts.get_cell_df( + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + collapse = b_collapse, + ) + # going home + if b_collapse: + s_opathfile = f'{s_path}/timeseries_cell.csv' + ldf_cell.to_csv(s_opathfile) + print(s_opathfile) + else: + ls_opathfile = [f"{s_path}/{s_xmlfile.replace('.xml','_cell.csv')}" for s_xmlfile in mcdsts.get_xmlfile_list()] + for i, df_cell in enumerate(ldf_cell): + df_cell.to_csv(ls_opathfile[i]) + print(ls_opathfile) # going home - print(mcds.get_cell_attribute_list()) + return 0 def get_cell_attribute(): @@ -1137,11 +1206,11 @@ def get_cell_attribute(): return 0 -def get_cell_df(): +def get_cell_attribute_list(): # argv parser = argparse.ArgumentParser( - prog = 'pcdl_get_cell_df', - description = 'this function extracts dataframes with a cell centric view of the simulation and saves them as csv files.', + prog = 'pcdl_get_cell_attribute_list', + description = 'this function is returns a list with all cell attribute labels, alphabetically ordered.', epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', ) @@ -1150,22 +1219,22 @@ def get_cell_df(): 'path', nargs = '?', default = '.', - help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .' + help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .', ) # TimeSeries output_path '.' - # TimeSeries custom_data_type nop (because datafarme is straightaway saved as csv) + # TimeSeries custom_data_type nop # TimeSeries microenv parser.add_argument( '--microenv', default = 'true', - help = 'should the microenvironment data be loaded? setting microenv to False will use less memory and speed up processing. default is True.' + help = 'should the microenvironment data be loaded? setting microenv to False will use less memory and speed up processing. default is True.', ) # TimeSeries graph False # TimeSeries physiboss parser.add_argument( '--physiboss', default = 'true', - help = 'if found, should physiboss state data be extracted and loaded into the df_cell dataframe? default is True.' + help = 'if found, should physiboss state data be extracted and loaded into df_cell dataframe? default is True.' ) # TimeSeries settingxml parser.add_argument( @@ -1176,198 +1245,8 @@ def get_cell_df(): # TimeSeries verbose parser.add_argument( '-v', '--verbose', - default = 'true', - help = 'setting verbose to False for less text output, while processing. default is True.' - ) - # get_cell_df values - parser.add_argument( - 'values', - nargs = '?', - default = 1, - type = int, - help = 'minimal number of values a variable has to have in any of the mcds time steps to be outputted. variables that have only 1 state carry no information. None is a state too. default is 1.' - ) - # get_cell_df drop - parser.add_argument( - '--drop', - nargs = '*', - default = [], - help = "set of column labels to be dropped for the dataframe. don't worry: essential columns like ID, coordinates and time will never be dropped. Attention: when the keep parameter is given, then the drop parameter has to be an empty string! default is an empty string." - ) - # get_cell_df keep - parser.add_argument( - '--keep', - nargs = '*', - default = [], - help = "set of column labels to be kept in the dataframe. set values=1 to be sure that all variables are kept. don't worry: essential columns like ID, coordinates and time will always be kept. default is an empty string." - ) - # get_cell_df collapse - parser.add_argument( - '--collapse', - default = 'true', - help = 'should all mcds time steps from the time series be collapsed into one big csv, or a many csv, one csv for each time step?, default is True.' - ) - - # parse arguments - args = parser.parse_args() - print(args) - - # process arguments - s_path = args.path.replace('\\','/') - while (s_path.find('//') > -1): - s_path = s_path.replace('//','/') - if (s_path.endswith('/')) and (len(s_path) > 1): - s_path = s_path[:-1] - s_pathfile = s_path - if not s_pathfile.endswith('.xml'): - s_pathfile = s_pathfile + '/initial.xml' - else: - s_path = '/'.join(s_path.split('/')[:-1]) - if not os.path.exists(s_pathfile): - sys.exit(f'Error @ pcdl_get_cell_df : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') - - # run - if os.path.isfile(args.path): - mcds = pcdl.TimeStep( - xmlfile = s_pathfile, - output_path = '.', - #custom_data_type, - microenv = False if args.microenv.lower().startswith('f') else True, - graph = False, - physiboss = False if args.physiboss.lower().startswith('f') else True, - settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, - verbose = False if args.verbose.lower().startswith('f') else True - ) - df_cell = mcds.get_cell_df( - values = args.values, - drop = set(args.drop), - keep = set(args.keep), - ) - # going home - s_opathfile = s_pathfile.replace('.xml','_cell.csv') - df_cell.to_csv(s_opathfile) - print(s_opathfile) - - else: - mcdsts = pcdl.TimeSeries( - output_path = s_path, - #custom_data_type, - load = True, - microenv = False if args.microenv.lower().startswith('f') else True, - graph = False, - physiboss = False if args.physiboss.lower().startswith('f') else True, - settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, - verbose = False if args.verbose.lower().startswith('f') else True, - ) - # handle collaps - b_collapse = False if args.collapse.lower().startswith('f') else True - ldf_cell = mcdsts.get_cell_df( - values = args.values, - drop = set(args.drop), - keep = set(args.keep), - collapse = b_collapse, - ) - # going home - if b_collapse: - s_opathfile = f'{s_path}/timeseries_cell.csv' - ldf_cell.to_csv(s_opathfile) - print(s_opathfile) - else: - ls_opathfile = [f"{s_path}/{s_xmlfile.replace('.xml','_cell.csv')}" for s_xmlfile in mcdsts.get_xmlfile_list()] - for i, df_cell in enumerate(ldf_cell): - df_cell.to_csv(ls_opathfile[i]) - print(ls_opathfile) - - # going home - return 0 - - -def get_anndata(): - # argv - parser = argparse.ArgumentParser( - prog = 'pcdl_get_anndata', - description = 'function to transform mcds time steps into one or many anndata objects for downstream analysis.', - epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', - ) - - # TimeSeries path - parser.add_argument( - 'path', - nargs = '?', - default = '.', - help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .' - ) - # TimeSeries output_path '.' - # TimeSeries custom_data_type - parser.add_argument( - '--custom_data_type', - nargs = '*', - default = [], - help = 'parameter to specify custom_data variable types other than float (namely: int, bool, str) like this var:dtype myint:int mybool:bool mystr:str . downstream float and int will be handled as numeric, bool as Boolean, and str as categorical data. default is an empty string.', - ) - # TimeSeries microenv - parser.add_argument( - '--microenv', - default = 'true', - help = 'should the microenvironment be extracted and loaded into the anndata object? setting microenv to False will use less memory and speed up processing. default is True.' - ) - # TimeSeries graph - parser.add_argument( - '--graph', - default = 'true', - help = 'should neighbor graph, attach graph, and attached spring graph be extracted and loaded into the anndata object? default is True.' - ) - # TimeSeries physiboss - parser.add_argument( - '--physiboss', - default = 'true', - help = 'if found, should physiboss state data be extracted and loaded into the anndata object? default is True.' - ) - # TimeSeries settingxml - parser.add_argument( - '--settingxml', default = 'false', - help = 'the settings.xml that is loaded, from which the cell type ID label mapping, is extracted, if this information is not found in the output xml file. set to None or False if the xml file is missing! default is False.', - ) - # TimeSeries verbose - parser.add_argument( - '-v', '--verbose', - default = 'true', - help = 'setting verbose to False for less text output, while processing. default is True.', - ) - # get_anndata values - parser.add_argument( - 'values', - nargs = '?', - default = 1, - type = int, - help = 'minimal number of values a variable has to have in any of the mcds time steps to be outputted. variables that have only 1 state carry no information. None is a state too. default is 1.' - ) - # get_anndata drop - parser.add_argument( - '--drop', - nargs = '*', - default = [], - help = "set of column labels to be dropped for the dataframe. don't worry: essential columns like ID, coordinates and time will never be dropped. Attention: when the keep parameter is given, then the drop parameter has to be an empty string! default is an empty string." - ) - # get_anndata keep - parser.add_argument( - '--keep', - nargs = '*', - default = [], - help = "set of column labels to be kept in the dataframe. set values=1 to be sure that all variables are kept. don't worry: essential columns like ID, coordinates and time will always be kept. default is an empty string." - ) - # get_anndata scale - parser.add_argument( - '--scale', - default = 'maxabs', - help = "specify how the data should be scaled. possible values are None, maxabs, minmax, std. None: no scaling. set scale to None if you would like to have raw data or entirely scale, transform, and normalize the data later. maxabs: maximum absolute value distance scaler will linearly map all values into a [-1, 1] interval. if the original data has no negative values, the result will be the same as with the minmax scaler (except with attributes with only one value). if the attribute has only zeros, the value will be set to 0. minmax: minimum maximum distance scaler will map all values linearly into a [0, 1] interval. if the attribute has only one value, the value will be set to 0. std: standard deviation scaler will result in sigmas. each attribute will be mean centered around 0. ddof delta degree of freedom is set to 1 because it is assumed that the values are samples out of the population and not the entire population. it is incomprehensible to me that the equivalent sklearn method has ddof set to 0. if the attribute has only one value, the value will be set to 0. default is maxabs" - ) - # get_anndata collapse - parser.add_argument( - '--collapse', - default = 'true', - help = 'should all mcds time steps from the time series be collapsed into one big anndata h5ad file, or a many h5ad, one h5ad for each time step?, default is True.' + help = 'setting verbose to True for more text output, while processing. default is False.', ) # parse arguments @@ -1384,85 +1263,31 @@ def get_anndata(): if not s_pathfile.endswith('.xml'): s_pathfile = s_pathfile + '/initial.xml' else: - s_path = '/'.join(s_path.split('/')[:-1]) - if not os.path.exists(s_pathfile): - sys.exit(f'Error @ pcdl_get_anndata : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') - - # custom_data_type - d_vartype = {} - for vartype in args.custom_data_type: - s_var, s_type = vartype.split(':') - if s_type in {'bool'}: o_type = bool - elif s_type in {'int'}: o_type = int - elif s_type in {'float'}: o_type = float - elif s_type in {'str'}: o_type = str - else: - sys.exit(f'Error @ pcdl_get_anndata : {s_var} {s_type} has an unknowen data type. knowen are bool, int, float, str.') - d_vartype.update({s_var : o_type}) - - # run - if os.path.isfile(args.path): - mcds = pcdl.TimeStep( - xmlfile = s_pathfile, - output_path = '.', - custom_data_type = d_vartype, - microenv = False if args.microenv.lower().startswith('f') else True, - graph = False if args.graph.lower().startswith('f') else True, - physiboss = False if args.physiboss.lower().startswith('f') else True, - settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, - verbose = False if args.verbose.lower().startswith('f') else True - ) - ann_mcds = mcds.get_anndata( - values = args.values, - drop = set(args.drop), - keep = set(args.keep), - scale = None if (args.scale.lower() == 'none') else args.scale, - ) - # going home - s_opathfile = s_pathfile.replace('.xml', f'_cell_{args.scale}.h5ad') - ann_mcds.write_h5ad(s_opathfile) - print(s_opathfile) - - else: - mcdsts = pcdl.TimeSeries( - output_path = s_path, - custom_data_type = d_vartype, - load = True, - microenv = False if args.microenv.lower().startswith('f') else True, - graph = False, - physiboss = False if args.physiboss.lower().startswith('f') else True, - settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, - verbose = False if args.verbose.lower().startswith('f') else True, - ) - # handle collaps - b_collapse = False if args.collapse.lower().startswith('f') else True - ann_mcdsts = mcdsts.get_anndata( - values = args.values, - drop = set(args.drop), - keep = set(args.keep), - scale = None if (args.scale.lower() == 'none') else args.scale, - collapse = b_collapse, - ) - # going home - if b_collapse : - s_opathfile = f'{s_path}/timeseries_cell_{args.scale}.h5ad' - ann_mcdsts.write_h5ad(s_opathfile) - print(s_opathfile) - else: - ls_opathfile = [f"{s_path}/{s_xmlfile.replace('.xml', '_cell_{}.h5ad'.format(args.scale))}" for s_xmlfile in mcdsts.get_xmlfile_list()] - for i, ann_mcds in enumerate(ann_mcdsts): - ann_mcds.write_h5ad(ls_opathfile[i]) - print(ls_opathfile) + s_path = '/'.join(s_path.split('/')[:-1]) + if not os.path.exists(s_pathfile): + sys.exit(f'Error @ pcdl_get_cell_attribute_list : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') + + # run + mcds = pcdl.TimeStep( + xmlfile = s_pathfile, + output_path = '.', + #custom_data_type, + microenv = False if args.microenv.lower().startswith('f') else True, + graph = False, + physiboss = False if args.physiboss.lower().startswith('f') else True, + settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, + verbose = True if args.verbose.lower().startswith('t') else False + ) # going home - return 0 + print(mcds.get_cell_attribute_list()) -def make_graph_gml(): +def get_anndata(): # argv parser = argparse.ArgumentParser( - prog = 'pcdl_make_graph_gml', - description = 'function to generate graph files in the gml graph modelling language standard format. gml was the outcome of an initiative that started at the international symposium on graph drawing 1995 in Passau and ended at Graph Drawing 1996 in Berkeley. the networkx python library (https://networkx.org/) and igraph C and python libraries (https://igraph.org/) for graph analysis are gml compatible and can as such read and write this file format.', + prog = 'pcdl_get_anndata', + description = 'function to transform mcds time steps into one or many anndata objects for downstream analysis.', epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', ) @@ -1471,7 +1296,7 @@ def make_graph_gml(): 'path', nargs = '?', default = '.', - help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .', + help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .' ) # TimeSeries output_path '.' # TimeSeries custom_data_type @@ -1485,14 +1310,19 @@ def make_graph_gml(): parser.add_argument( '--microenv', default = 'true', - help = 'should the microenvironment data be loaded? setting microenv to False will use less memory and speed up processing. default is True.' + help = 'should the microenvironment be extracted and loaded into the anndata object? setting microenv to False will use less memory and speed up processing. default is True.' + ) + # TimeSeries graph + parser.add_argument( + '--graph', + default = 'true', + help = 'should neighbor graph, attach graph, and attached spring graph be extracted and loaded into the anndata object? default is True.' ) - # TimeSeries graph True # TimeSeries physiboss parser.add_argument( '--physiboss', default = 'true', - help = 'if found, should physiboss state data be extracted and loaded into the df_cell dataframe? default is True.' + help = 'if found, should physiboss state data be extracted and loaded into the anndata object? default is True.' ) # TimeSeries settingxml parser.add_argument( @@ -1506,24 +1336,39 @@ def make_graph_gml(): default = 'true', help = 'setting verbose to False for less text output, while processing. default is True.', ) - # make_graph_gml graph_type + # get_anndata values parser.add_argument( - 'graph_type', + 'values', nargs = '?', - help = 'to specify which physicell output data should be processed. attached: processes mcds.get_attached_graph_dict dictionary. neighbor: processes mcds.get_neighbor_graph_dict dictionary spring: processes mcds.get_spring_graph_dict dictionary.', + default = 1, + type = int, + help = 'minimal number of values a variable has to have in any of the mcds time steps to be outputted. variables that have only 1 state carry no information. None is a state too. default is 1.' ) - # make_graph_gml edge_attribute + # get_anndata drop parser.add_argument( - '--edge_attribute', - default = 'true', - help = 'specifies if the spatial Euclidean distance is used for edge attribute, to generate a weighted graph. default is True.', + '--drop', + nargs = '*', + default = [], + help = "set of column labels to be dropped for the dataframe. don't worry: essential columns like ID, coordinates and time will never be dropped. Attention: when the keep parameter is given, then the drop parameter has to be an empty string! default is an empty string." ) - # make_graph_gml node_attrributes + # get_anndata keep parser.add_argument( - '--node_attribute', + '--keep', nargs = '*', default = [], - help = 'listing of mcds.get_cell_df dataframe columns, used for node attributes. default is and empty list.', + help = "set of column labels to be kept in the dataframe. set values=1 to be sure that all variables are kept. don't worry: essential columns like ID, coordinates and time will always be kept. default is an empty string." + ) + # get_anndata scale + parser.add_argument( + '--scale', + default = 'maxabs', + help = "specify how the data should be scaled. possible values are None, maxabs, minmax, std. None: no scaling. set scale to None if you would like to have raw data or entirely scale, transform, and normalize the data later. maxabs: maximum absolute value distance scaler will linearly map all values into a [-1, 1] interval. if the original data has no negative values, the result will be the same as with the minmax scaler (except with attributes with only one value). if the attribute has only zeros, the value will be set to 0. minmax: minimum maximum distance scaler will map all values linearly into a [0, 1] interval. if the attribute has only one value, the value will be set to 0. std: standard deviation scaler will result in sigmas. each attribute will be mean centered around 0. ddof delta degree of freedom is set to 1 because it is assumed that the values are samples out of the population and not the entire population. it is incomprehensible to me that the equivalent sklearn method has ddof set to 0. if the attribute has only one value, the value will be set to 0. default is maxabs" + ) + # get_anndata collapse + parser.add_argument( + '--collapse', + default = 'true', + help = 'should all mcds time steps from the time series be collapsed into one big anndata h5ad file, or a many h5ad, one h5ad for each time step?, default is True.' ) # parse arguments @@ -1542,7 +1387,7 @@ def make_graph_gml(): else: s_path = '/'.join(s_path.split('/')[:-1]) if not os.path.exists(s_pathfile): - sys.exit(f'Error @ pcdl_make_graph_gml : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') + sys.exit(f'Error @ pcdl_get_anndata : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') # custom_data_type d_vartype = {} @@ -1553,7 +1398,7 @@ def make_graph_gml(): elif s_type in {'float'}: o_type = float elif s_type in {'str'}: o_type = str else: - sys.exit(f'Error @ pcdl_make_graph_gml : {s_var} {s_type} has an unknowen data type. knowen are bool, int, float, str.') + sys.exit(f'Error @ pcdl_get_anndata : {s_var} {s_type} has an unknowen data type. knowen are bool, int, float, str.') d_vartype.update({s_var : o_type}) # run @@ -1563,17 +1408,20 @@ def make_graph_gml(): output_path = '.', custom_data_type = d_vartype, microenv = False if args.microenv.lower().startswith('f') else True, - graph = True, + graph = False if args.graph.lower().startswith('f') else True, physiboss = False if args.physiboss.lower().startswith('f') else True, settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, verbose = False if args.verbose.lower().startswith('f') else True ) - s_opathfile = mcds.make_graph_gml( - graph_type = args.graph_type, - edge_attribute = False if args.edge_attribute.lower().startswith('f') else True, - node_attribute = args.node_attribute, + ann_mcds = mcds.get_anndata( + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + scale = None if (args.scale.lower() == 'none') else args.scale, ) # going home + s_opathfile = s_pathfile.replace('.xml', f'_cell_{args.scale}.h5ad') + ann_mcds.write_h5ad(s_opathfile) print(s_opathfile) else: @@ -1582,18 +1430,30 @@ def make_graph_gml(): custom_data_type = d_vartype, load = True, microenv = False if args.microenv.lower().startswith('f') else True, - graph = True, + graph = False, physiboss = False if args.physiboss.lower().startswith('f') else True, settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, verbose = False if args.verbose.lower().startswith('f') else True, ) - ls_opathfile = mcdsts.make_graph_gml( - graph_type = args.graph_type, - edge_attribute = False if args.edge_attribute.lower().startswith('f') else True, - node_attribute = args.node_attribute, + # handle collaps + b_collapse = False if args.collapse.lower().startswith('f') else True + ann_mcdsts = mcdsts.get_anndata( + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + scale = None if (args.scale.lower() == 'none') else args.scale, + collapse = b_collapse, ) # going home - print(ls_opathfile) + if b_collapse : + s_opathfile = f'{s_path}/timeseries_cell_{args.scale}.h5ad' + ann_mcdsts.write_h5ad(s_opathfile) + print(s_opathfile) + else: + ls_opathfile = [f"{s_path}/{s_xmlfile.replace('.xml', '_cell_{}.h5ad'.format(args.scale))}" for s_xmlfile in mcdsts.get_xmlfile_list()] + for i, ann_mcds in enumerate(ann_mcdsts): + ann_mcds.write_h5ad(ls_opathfile[i]) + print(ls_opathfile) # going home return 0 @@ -2007,6 +1867,147 @@ def make_cell_vtk(): return 0 +def make_graph_gml(): + # argv + parser = argparse.ArgumentParser( + prog = 'pcdl_make_graph_gml', + description = 'function to generate graph files in the gml graph modelling language standard format. gml was the outcome of an initiative that started at the international symposium on graph drawing 1995 in Passau and ended at Graph Drawing 1996 in Berkeley. the networkx python library (https://networkx.org/) and igraph C and python libraries (https://igraph.org/) for graph analysis are gml compatible and can as such read and write this file format.', + epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', + ) + + # TimeSeries path + parser.add_argument( + 'path', + nargs = '?', + default = '.', + help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .', + ) + # TimeSeries output_path '.' + # TimeSeries custom_data_type + parser.add_argument( + '--custom_data_type', + nargs = '*', + default = [], + help = 'parameter to specify custom_data variable types other than float (namely: int, bool, str) like this var:dtype myint:int mybool:bool mystr:str . downstream float and int will be handled as numeric, bool as Boolean, and str as categorical data. default is an empty string.', + ) + # TimeSeries microenv + parser.add_argument( + '--microenv', + default = 'true', + help = 'should the microenvironment data be loaded? setting microenv to False will use less memory and speed up processing. default is True.' + ) + # TimeSeries graph True + # TimeSeries physiboss + parser.add_argument( + '--physiboss', + default = 'true', + help = 'if found, should physiboss state data be extracted and loaded into the df_cell dataframe? default is True.' + ) + # TimeSeries settingxml + parser.add_argument( + '--settingxml', + default = 'false', + help = 'the settings.xml that is loaded, from which the cell type ID label mapping, is extracted, if this information is not found in the output xml file. set to None or False if the xml file is missing! default is False.', + ) + # TimeSeries verbose + parser.add_argument( + '-v', '--verbose', + default = 'true', + help = 'setting verbose to False for less text output, while processing. default is True.', + ) + # make_graph_gml graph_type + parser.add_argument( + 'graph_type', + nargs = '?', + help = 'to specify which physicell output data should be processed. attached: processes mcds.get_attached_graph_dict dictionary. neighbor: processes mcds.get_neighbor_graph_dict dictionary spring: processes mcds.get_spring_graph_dict dictionary.', + ) + # make_graph_gml edge_attribute + parser.add_argument( + '--edge_attribute', + default = 'true', + help = 'specifies if the spatial Euclidean distance is used for edge attribute, to generate a weighted graph. default is True.', + ) + # make_graph_gml node_attrributes + parser.add_argument( + '--node_attribute', + nargs = '*', + default = [], + help = 'listing of mcds.get_cell_df dataframe columns, used for node attributes. default is and empty list.', + ) + + # parse arguments + args = parser.parse_args() + print(args) + + # process arguments + s_path = args.path.replace('\\','/') + while (s_path.find('//') > -1): + s_path = s_path.replace('//','/') + if (s_path.endswith('/')) and (len(s_path) > 1): + s_path = s_path[:-1] + s_pathfile = s_path + if not s_pathfile.endswith('.xml'): + s_pathfile = s_pathfile + '/initial.xml' + else: + s_path = '/'.join(s_path.split('/')[:-1]) + if not os.path.exists(s_pathfile): + sys.exit(f'Error @ pcdl_make_graph_gml : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') + + # custom_data_type + d_vartype = {} + for vartype in args.custom_data_type: + s_var, s_type = vartype.split(':') + if s_type in {'bool'}: o_type = bool + elif s_type in {'int'}: o_type = int + elif s_type in {'float'}: o_type = float + elif s_type in {'str'}: o_type = str + else: + sys.exit(f'Error @ pcdl_make_graph_gml : {s_var} {s_type} has an unknowen data type. knowen are bool, int, float, str.') + d_vartype.update({s_var : o_type}) + + # run + if os.path.isfile(args.path): + mcds = pcdl.TimeStep( + xmlfile = s_pathfile, + output_path = '.', + custom_data_type = d_vartype, + microenv = False if args.microenv.lower().startswith('f') else True, + graph = True, + physiboss = False if args.physiboss.lower().startswith('f') else True, + settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, + verbose = False if args.verbose.lower().startswith('f') else True + ) + s_opathfile = mcds.make_graph_gml( + graph_type = args.graph_type, + edge_attribute = False if args.edge_attribute.lower().startswith('f') else True, + node_attribute = args.node_attribute, + ) + # going home + print(s_opathfile) + + else: + mcdsts = pcdl.TimeSeries( + output_path = s_path, + custom_data_type = d_vartype, + load = True, + microenv = False if args.microenv.lower().startswith('f') else True, + graph = True, + physiboss = False if args.physiboss.lower().startswith('f') else True, + settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, + verbose = False if args.verbose.lower().startswith('f') else True, + ) + ls_opathfile = mcdsts.make_graph_gml( + graph_type = args.graph_type, + edge_attribute = False if args.edge_attribute.lower().startswith('f') else True, + node_attribute = args.node_attribute, + ) + # going home + print(ls_opathfile) + + # going home + return 0 + + def make_simularium(): # argv parser = argparse.ArgumentParser( @@ -2291,6 +2292,7 @@ def get_muspan(): # going home return 0 + def get_spatialdata(): # argv parser = argparse.ArgumentParser( @@ -2564,7 +2566,6 @@ def plot_timeseries(): default = 'cell', help = 'to specifies the data dataframe. cell: dataframe will be retrieved through the mcds.get_cell_df function. conc: dataframe will be retrieved through the mcds.get_conc_df function. default is cell.', ) - # plot_timeseries cat_drop parser.add_argument( '--cat_drop', @@ -2939,10 +2940,6 @@ def make_ome_tiff(): return 0 -####################### -# render neuroglancer # -####################### - def render_neuroglancer(): # argv parser = argparse.ArgumentParser( diff --git a/pcdl/timeseries.py b/pcdl/timeseries.py index 3853018..01e09fb 100644 --- a/pcdl/timeseries.py +++ b/pcdl/timeseries.py @@ -421,7 +421,7 @@ def get_mcds_list(self): return self.l_mcds - ## MICROENVIRONMENT RELATED FUNCTIONS ## + ## SUBSTRATE RELATED FUNCTIONS ## def get_conc_df(self, values=1, drop=set(), keep=set(), collapse=True): """ @@ -918,6 +918,160 @@ def get_cell_attribute(self, values=1, drop=set(), keep=set(), allvalues=False): return dl_variable_range + def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=True): + """ + input: + values: integer; default is 1 + minimal number of values a variable has to have to be outputted. + variables that have only 1 state carry no information. + None is a state too. + + drop: set of strings; default is an empty set + set of column labels to be dropped for the dataframe. + don't worry: essential columns like ID, coordinates + and time will never be dropped. + Attention: when the keep parameter is given, then + the drop parameter has to be an empty set! + + keep: set of strings; default is an empty set + set of column labels to be kept in the dataframe. + don't worry: essential columns like ID, coordinates + and time will always be kept. + + scale: string; default 'maxabs' + specify how the data should be scaled. + possible values are None, maxabs, minmax, std. + for more input, check out: help(pcdl.scaler) + + collapse: boole; default True + should all mcds time steps from the time series be collapsed + into one single anndata object, or a list of anndata objects + for each time step? + + keep_mcds: boole; default True + should the loaded original mcds be kept in memory + after transformation? + + output: + annmcds or self.l_annmcds: anndata object or list of anndata objects. + what is returned depends on the collapse setting. + + description: + function to transform mcds time steps into one or many + anndata objects for downstream analysis. + """ + # load optional dependency + ad = optional_import('anndata', s_caller='TimeSeries.get_anndata') + + # initialize vaiable + l_annmcds = [] + df_anncount = None + df_annobs = None + ar_annobsm = None + + # variable triage + if (values < 2): + ls_column = list(self.l_mcds[0].get_cell_df(drop=drop, keep=keep).columns) + else: + ls_column = sorted(es_coor_cell.difference({'ID'})) + ls_column.extend(sorted(self.get_cell_attribute(values=values, drop=drop, keep=keep, allvalues=False).keys())) + + # package collapse + if collapse: + + # warning + if self.verbose: + print('Warning @ mcdsts.get_anndata : only df_cell data, but not graph data, can be collapsed.') + + # extract + df_cell = self.get_cell_df(values=values, drop=drop, keep=keep, collapse=True) + df_count, df_obs, d_obsm, d_obsp, d_uns = _anndextract( + df_cell=df_cell, + scale = scale, + #graph_attached = {}, + #graph_neighbor = {}, + #graph_spring = {}, + #graph_method = s_physicellv, + ) + + # fuse to anndata object + ann_mcdsts = ad.AnnData( + X = df_count, + obs = df_obs, + obsm = d_obsm, + #obsp = d_obsp, # nop (graph) + #uns = d_uns, # nop (graph) + ) + + # mcds + if not keep_mcds: + self.l_mcds = [] + + # output + return ann_mcdsts + + # pack not collapsed + else: + # processing + lann_mcds = [] + i_mcds = len(self.l_mcds) + for i in range(i_mcds): + # fetch mcds + if keep_mcds: + mcds = self.l_mcds[i] + else: + mcds = self.l_mcds.pop(0) + + # extract physicell version + s_physicellv = mcds.get_physicell_version(), + + # extract time and dataframes + if self.verbose: + print(f'processing: {i+1}/{i_mcds} {mcds.get_time()}[min] mcds into anndata obj.') + df_cell = mcds.get_cell_df() + df_cell = df_cell.loc[:,ls_column] + + # extract + df_count, df_obs, d_obsm, d_obsp, d_uns = _anndextract( + df_cell=df_cell, + scale = scale, + graph_attached = mcds.get_attached_graph_dict(), + graph_neighbor = mcds.get_neighbor_graph_dict(), + graph_spring = mcds.get_spring_graph_dict(), + graph_method = s_physicellv, + ) + + # annmcds + ann_mcds = ad.AnnData( + X = df_count, + obs = df_obs, + obsm = d_obsm, + obsp = d_obsp, + uns = d_uns, + ) + lann_mcds.append(ann_mcds) + + # output + self.l_annmcds = lann_mcds + return self.l_annmcds + + + def get_annmcds_list(self): + """ + input: + self: TimeSeries class instance. + + output: + self.l_annmcds: list of chronologically ordered anndata mcds objects. + watch out, this is a pointer to the + self.l_annmcds list of anndata mcds objects, not a copy of self.l_annmcds! + + description: + function returns a binding to the self.l_annmcds list of anndata mcds objects. + """ + return self.l_annmcds + + def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slice=0.0, z_axis=None, alpha=1, cmap='viridis', title='', grid=True, legend_loc='lower left', xlim=None, ylim=None, xyequal=True, s=1.0, figsizepx=None, directory=None, ext='jpeg', figbgcolor=None, **kwargs): """ input: @@ -1093,235 +1247,418 @@ def make_cell_vtk(self, attribute=['cell_type'], ext='_cell.vtp'): return ls_vtkpathfile - ## OME TIFF RELATED FUNCTIONS ## - - def make_ome_tiff(self, cell_attribute='ID', conc_cutoff={}, focus=None, file=True, collapse=True): + def make_graph_gml(self, graph_type, edge_attribute=True, node_attribute=[]): """ input: - cell_attribute: strings; default is 'ID', which will result in a - cell segmentation mask. - column name within the cell dataframe. - the column data type has to be numeric (bool, int, float) - and cannot be string. - the result will be stored as 32 bit float. - - conc_cutoff: dictionary string to real; default is an empty dictionary. - if a contour from a substrate not should be cut by greater - than zero (shifted to integer 1), another cutoff value can be specified here. + self: TimeSeries class instance. - focus: set of strings; default is a None - set of substrate and cell_type names to specify what will be - translated into ome tiff format. - if None, all substrates and cell types will be processed. + graph_type: string + to specify which physicell output data should be processed. + attached, touch: processes mcds.get_attached_graph_dict dictionary. + neighbor: processes mcds.get_neighbor_graph_dict dictionary. + spring: processes mcds.get_spring_graph_dict dictionary. - file: boolean; default True - if True, an ome tiff file is the output. - if False, a numpy array with shape tczyx is the output. + edge_attribute: boolean; default True + specifies if the spatial Euclidean distance is used for + edge attribute, to generate a weighted graph. - collapse: boole; default True - should all mcds time steps from the time series be collapsed - into one ome tiff file (numpy array), - or an ome tiff file (numpy array) for each time step? + node_attribute: list of strings; default is empty list + list of mcds.get_cell_df dataframe columns, used for + node attributes. output: - a_tczyx_img: numpy array or ome tiff file. - + gml file for each time step. + path and filenames are printed to the standard output. description: - function to transform chosen mcdsts output into an 1[um] spaced - tczyx (time, channel, z-axis, y-axis, x-axis) ome tiff file or numpy array, - one substrate or cell_type per channel. - a ome tiff file is more or less: - a numpy array, containing the image information - and a xml, containing the microscopy metadata information, - like the channel labels. - the ome tiff file format can for example be read by the napari - or fiji (imagej) software. + function to generate graph files in the gml graph modelling language + standard format. - https://napari.org/stable/ - https://fiji.sc/ + gml was the outcome of an initiative that started at + the international symposium on graph drawing 1995 in Passau + and ended at Graph Drawing 1996 in Berkeley. the networkx python + and igraph C and python libraries for graph analysis are + gml compatible and can as such read and write this file format. + + https://en.wikipedia.org/wiki/Graph_Modelling_Language + https://github.com/elmbeech/physicelldataloader/blob/master/man/publication/himsolt1996gml_a_portable_graph_file_format.pdf + https://networkx.org/ + https://igraph.org/ """ - # for each time step - l_tczyx_img = [] + # processing + ls_pathfile = [] for mcds in self.get_mcds_list(): - # processing - b_file = True # 10 - if (not file and not collapse) or (not file and collapse) or (file and collapse): # 00, 01, 11 - b_file = False - o_tczyx_img = mcds.make_ome_tiff( - cell_attribute = cell_attribute, - conc_cutoff = conc_cutoff, - focus = focus, - file = b_file + s_pathfile = mcds.make_graph_gml( + graph_type = graph_type, + edge_attribute = edge_attribute, + node_attribute = node_attribute, ) - l_tczyx_img.append(o_tczyx_img) - - # handle channels - ls_substrate = mcds.get_substrate_list() - ls_celltype = mcds.get_celltype_list() + ls_pathfile.append(s_pathfile) - if not (focus is None): - ls_substrate = [s_substrate for s_substrate in ls_substrate if s_substrate in set(focus)] - ls_celltype = [s_celltype for s_celltype in ls_celltype if s_celltype in set(focus)] - if (set(focus) != set(ls_substrate).union(set(ls_celltype))): - sys.exit(f'Error : {focus} not found in {ls_substrate} {ls_celltype}') + # outout + return ls_pathfile - # output 00 list of numpy arrays - if (not file and not collapse): # 00 - if self.verbose: - print(f'la_tczyx_img shape: {len(l_tczyx_img)} * {l_tczyx_img[0].shape}') - return l_tczyx_img - # output 01 numpy array - elif (not file and collapse): # 01 - # numpy array - a_tczyx_img = np.array(l_tczyx_img) - if self.verbose: - print('a_tczyx_img shape:', a_tczyx_img.shape) - return a_tczyx_img - - # output 10 list of pathfile strings - elif (file and not collapse): # 10 - return l_tczyx_img - - # output 11 ometiff file - elif (file and collapse): # 11 - # load optional dependency - OmeTiffWriter = optional_import('bioio.writers', s_attr='OmeTiffWriter', s_pip='bioio', s_caller='TimeSeries.make_ome_tiff') - bioio_base = optional_import('bioio_base', s_pip='bioio', s_caller='TimeSeries.make_ome_tiff') - - # numpy array - a_tczyx_img = np.array(l_tczyx_img) - if self.verbose: - print('a_tczyx_img shape:', a_tczyx_img.shape) + def make_simularium(self, focus_cat=['cell_type','current_phase'], trajectory_title='timeseries', scale_factor=None, camera_defaults=None, model_meta_data=None): + """ + input: + focus_cat: list of 1 or 2 string; default is ['cell_type','current_phase'] + specify 1 or 2 categorical column labels, to be found + in mcdsts.get_cell_df(). - # generate filename - s_channel = '' - for s_substrate in ls_substrate: - try: - r_value = conc_cutoff[s_substrate] - s_channel += f'_{s_substrate}{r_value}' - except KeyError: - s_channel += f'_{s_substrate}' - for s_celltype in ls_celltype: - s_channel += f'_{s_celltype}' - if len(ls_celltype) > 0: - s_channel += f'_{cell_attribute}' - s_tifffile = f"timeseries{s_channel.replace(' ','_')}.ome.tiff" - if (len(s_tifffile) > 255): - print(f"Warning: filename {len(s_tifffile)} > 255 character.") - s_tifffile = 'timeseries_channels.ome.tiff' - print(f"file name adjusted to {s_tifffile}.") - s_tiffpathfile = self.path + '/' + s_tifffile + trajectory_title: string; default 'timeseries' + the trajectory_title will be used as + .simularium file name and displayed + in the simulation. - # save to file - OmeTiffWriter.save( - a_tczyx_img, - s_tiffpathfile, - dim_order = 'TCZYX', - #ome_xml=x_img, - channel_names = ls_substrate + ls_celltype, - image_names = [f'timeseries_{cell_attribute}'], - physical_pixel_sizes = bioio_base.types.PhysicalPixelSizes(mcds.get_voxel_spacing()[2], 1.0, 1.0), # z,y,x [um] - #channel_colors=, - #fs_kwargs={}, - ) - return s_tiffpathfile + scale_factor: float number; default is None + A multiplier for the scene, use if visualization is + too large or small. If None is provided, one will + be calculated based on the position data. + bue 20260822: does not seem to work in current simularium 1.13.0. - # error case - else: - sys.exit(f'Error @ make_ome_tiff : {file} {collapse} strange file collapse combination.') + camera_defaults: simulariumio.CameraData object; default is None + camera's initial settings which it also returns to + when reset. + model_meta_data: simulariumio.ModelMetaData; default is None + Metadata for the model that produced this + trajectory. - ## TIME SERIES RELATED FUNCTIONS ## + output: + .simularium file - def plot_timeseries(self, focus_cat=None, focus_num=None, aggregate_num=np.nanmean, frame='cell', cat_drop=set(), cat_keep=set(), z_slice=None, logy=False, ylim=None, secondary_y=None, subplots=False, sharex=False, sharey=False, linestyle='-', linewidth=None, cmap=None, color=None, grid=True, legend=True, yunit=None, title=None, ax=None, figsizepx=[640, 480], ext=None, figbgcolor=None, **kwargs): + description: + function returns a simularium trajectory file that can be run with the + online Simularium Viewer. + + https://simularium.allencell.org/ """ - input: - self: TimeSeries class instance + # library + sim = optional_import(s_module='simulariumio', s_caller='TimeSeries.make_simularium') - focus_cat: string; default is None - categorical or boolean data column within dataframe specified under frame. - default is None, which is total, which is all agents or voxels, no categories. + # handle input + model_meta_data=sim.ModelMetaData() if model_meta_data is None else model_meta_data + camera_defaults=sim.CameraData() if camera_defaults is None else camera_defaults - focus_num: string; default is None - numerical data column within dataframe specified under frame. - default is None, which is count, agent or voxel count. + # here we go + if self.verbose: + print(f'generating {trajectory_title}.simularium file ...') - aggregate_num: function; default np.nanmean - aggregation function for focus_num data. + # extract box from mcdsts + ltr_domain = self.get_mcds_list()[0].get_mesh_mnp_range() + lr_space = self.get_mcds_list()[0].get_mesh_spacing() + lr_box = np.array([ + ltr_domain[0][1] - ltr_domain[0][0], + ltr_domain[1][1] - ltr_domain[1][0], + ltr_domain[2][1] - ltr_domain[2][0], + ]) + lr_box[0] = lr_box[0] if lr_box[0] != 0.0 else lr_space[0] + lr_box[1] = lr_box[1] if lr_box[1] != 0.0 else lr_space[1] + lr_box[2] = lr_box[2] if lr_box[2] != 0.0 else lr_space[2] + ar_box = np.array(lr_box) - frame: string; default is cell_df - to specifies the data dataframe. - cell: dataframe will be retrieved through the mcds.get_cell_df function. - conc: dataframe will be retrieved through the mcds.get_conc_df function. + # extract cell dataframe from mcdsts + df_cell = self.get_cell_df() - cat_drop: set of strings; default is an empty set - if focus is a categorical attribute, - set of category labels to be dropped for the dataframe. - Attention: when the cat_keep parameter is given, then - the cat_drop parameter has to be an empty set! + # handle agent annotation + se_type = df_cell.loc[:, focus_cat].astype(str).agg('#'.join, axis=1) - cat_keep: set of strings; default is an empty set - if focus is a categorical attribute, - set of category labels to be kept in the dataframe. + # extract units from mcdsts + ds_unit = self.get_mcds_list()[0].get_unit_dict() - z_slice: floating point number; default is None - z-axis position to slice a 2D xy-plain out of the 3D mesh. - if z_slice position numeric but not an exact mesh center coordinate, - then z_slice will be adjusted to the nearest mesh center value, - the smaller one, if the coordinate lies on a saddle point. - if set to None, the whole domain is taken. + # generate simularium dataframe + with pd.option_context('future.infer_string', False): + df_sim = pd.DataFrame({ + 'time': df_cell.loc[:, 'time'].to_numpy(dtype=float), + 'unique_id': df_cell.loc[:, 'ID'].to_numpy(dtype=int), + 'type': se_type.to_numpy(dtype=object), + 'positionX': df_cell.loc[:, 'position_x'].to_numpy(dtype=float), + 'positionY': df_cell.loc[:, 'position_y'].to_numpy(dtype=float), + 'positionZ': df_cell.loc[:, 'position_z'].to_numpy(dtype=float), + 'radius': df_cell.loc[:, 'radius'].to_numpy(dtype=float), + 'rotationX': np.zeros(df_cell.shape[0], dtype=float), + 'rotationY': np.zeros(df_cell.shape[0], dtype=float), + 'rotationZ': np.zeros(df_cell.shape[0], dtype=float), + }) + df_sim.sort_values(['time', 'unique_id'], inplace=True) + # SimulariumIO 1.13.0 AgentData.from_dataframe expects traj.loc[0, ...] + # to select the whole trajectory, so all rows need the same index label. + df_sim.index = np.zeros(df_sim.shape[0], dtype=int) - logy: bool; default False - if True, then y axis is natural log scaled. + # generate simmularium trajectorydata object + o_sim = sim.TrajectoryData( + meta_data = sim.MetaData( + box_size=ar_box, + camera_defaults=camera_defaults, + scale_factor=scale_factor, + trajectory_title=trajectory_title, + model_meta_data=model_meta_data, + ), + agent_data = sim.AgentData.from_dataframe(df_sim), + time_units = sim.UnitData(ds_unit['time']), + spatial_units = sim.UnitData(ds_unit['spatial_unit']), + #plots=, + ) - ylim: tuple of two floats; default is None - y axis min and max value. - default is None, which automatically detects min and max value. + # transform data and save trajectorydata object to simularium file + s_save = self.path + '/' + trajectory_title + s_pathfile = s_save + '.simularium' + sim.TrajectoryConverter(o_sim).save(s_save) + if self.verbose: + print(f'simularium viewer at: https://simularium.allencell.org/') - secondary_y: bool or list of strings; default False - whether to plot on the secondary y-axis. - if a list, which columns to plot on the secondary y-axis. + # return error code + return s_pathfile - subplots: bool or sequence of iterable, default False - whether to group columns into subplots. - a sequence of iterable of column labels - will create a subplot for each group of columns. - sharex: bool, default False - in case subplots is True, share x-axis by - setting some x-axis labels to invisible. + ## SUBSTRATE AND CELL RELATED FUNCTIONS ## - sharey: bool, default False - in case subplots is True, share y-axis range and possibly - setting some y-axis labels to invisible. + def get_muspan(self, z_slice=None, values=1, drop=set(), keep=set()): + """ + input: + z_slice: floating point number; default is None + z-axis position to slice a 2D xy-plain out of the + 3D mesh. if None the whole 3D mesh will be returned. - linestyle: string or list of strings, default '-' - matplotlib line style {'-', '--', '-.', ':', ''}, - over all or per column. + values: integer; default is 1 + minimal number of values a variable has to have to be outputted. + variables that have only 1 state carry no information. + None is a state too. - linewidth: float or list of float, default None - line width in points. + drop: set of strings; default is an empty set + set of column labels to be dropped for the dataframe. + don't worry: essential columns like ID, coordinates + and time will never be dropped. + Attention: when the keep parameter is given, then + the drop parameter has to be an empty set! - cmap: string; default None - matplotlib colormap string. - https://matplotlib.org/stable/tutorials/colors/colormaps.html - achtung: if cmap is given, color will be disregarded. + keep: set of strings; default is an empty set + set of column labels to be kept in the dataframe. + set values=1 to be sure that all variables are kept. + don't worry: essential columns like ID, coordinates + and time will always be kept. - color: string or list of string or dictionary; default None - color string referred to by name, RGB or RGBA code. - achtung: if cmap is given, color will be disregarded. + output: + do_domain: dictionary of muspa domains, one for each time step z-layer. - grid: boolean; default True - plot axis grid lines. + description: + function returns a dictionary of muspa domains, containg a + cell and subs collection with disrcete and continuous labels + and all the graph as networks. + + https://www.muspan.co.uk + + https://docs.muspan.co.uk/latest/Documentation.html + """ + # variable triage + es_keep = set(self.get_cell_attribute(values=values, drop=drop, keep=keep, allvalues=False).keys()) - legend: bool or 'reverse'; default True - if True or reverse, place legend on axis subplots. + # processing + do_domain = {} + for mcds in self.get_mcds_list(): + do_domain.update( + mcds.get_muspan( + z_slice = z_slice, + #values = 1, + #drop = set(), + keep = es_keep, + ) + ) - yunit: string; default None - string to specify y-axis unit. - None will not print a unit on the y-axis. + # output + return do_domain + + + def get_spatialdata(self, images={'subs'}, labels=set(), points={'subs'}, shapes={'cell'}, values=1, drop=set(), keep=set(), scale='maxabs', keep_mcds=True): + """ + input: + images: set of string; default {'subs'} + specify if from the subs or cell dataset + a multichannel image should be generate. + so far, only the subs image element is implemented. + + labels: set of strings; default is an empty set + specify if from the subs or cell dataset + a label element should be generated. + so far, neither subs nor cell label elements are implemented. + + points: set of string; default {'subs'} + specify if from the subs or cell dataset + a points element should be generated. + both, subs and cell point elements, are implemented. + + shapes: set of string; default {'cell'} + specify if from the subs or cell dataset + a shape element should be generated. + so far, only the cell shape element is implemented. + + values: integer; default is 1 + minimal number of values a variable has to have to be outputted. + variables that have only 1 state carry no information. + None is a state too. + + drop: set of strings; default is an empty set + set of column labels to be dropped for the dataframe. + don't worry: essential columns like ID, coordinates + and time will never be dropped. + Attention: when the keep parameter is given, then + the drop parameter has to be an empty set! + + keep: set of strings; default is an empty set + set of column labels to be kept in the dataframe. + don't worry: essential columns like ID, coordinates + and time will always be kept. + + scale: string; default 'maxabs' + specify how the data should be scaled. + possible values are None, maxabs, minmax, std. + for more input, check out: help(pcdl.scaler) + + keep_mcds: boole; default True + should the loaded original mcds be kept in memory + after transformation? + + output: + self.l_sdmcds: list of spatialdata objects. + + description: + function to transform mcds time steps into + spatialdata objects for downstream analysis. + """ + # variable triage + es_keep = set(self.get_cell_attribute(values=values, drop=drop, keep=keep, allvalues=False).keys()) + + # processing + lsd_mcds = [] + i_mcds = len(self.l_mcds) + for i in range(i_mcds): + # fetch mcds + if keep_mcds: + mcds = self.l_mcds[i] + else: + mcds = self.l_mcds.pop(0) + + # extract time and dataframes + if self.verbose: + print(f'\nprocessing: {i+1}/{i_mcds} {mcds.get_time()}[min] mcds into spatialdata obj.') + + # get spatialdata object + sd_mcds = mcds.get_spatialdata( + points = points, + shapes = shapes, + #values = 1, + #drop = set(), + keep = es_keep, + scale = scale, + ) + lsd_mcds.append(sd_mcds) + + # output + self.l_sdmcds = lsd_mcds + return self.l_sdmcds + + + def get_sdmcds_list(self): + """ + input: + self: TimeSeries class instance. + + output: + self.l_sdmcds: list of chronologically ordered spatialdata mcds objects. + watch out, this is a pointer to the + self.l_sdmcds list of spdata mcds objects, not a copy of self.l_sdmcds! + + description: + function returns a binding to the self.l_sdmcds list of spdata mcds objects. + """ + return self.l_sdmcds + + + def plot_timeseries(self, focus_cat=None, focus_num=None, aggregate_num=np.nanmean, frame='cell', cat_drop=set(), cat_keep=set(), z_slice=None, logy=False, ylim=None, secondary_y=None, subplots=False, sharex=False, sharey=False, linestyle='-', linewidth=None, cmap=None, color=None, grid=True, legend=True, yunit=None, title=None, ax=None, figsizepx=[640, 480], ext=None, figbgcolor=None, **kwargs): + """ + input: + self: TimeSeries class instance + + focus_cat: string; default is None + categorical or boolean data column within dataframe specified under frame. + default is None, which is total, which is all agents or voxels, no categories. + + focus_num: string; default is None + numerical data column within dataframe specified under frame. + default is None, which is count, agent or voxel count. + + aggregate_num: function; default np.nanmean + aggregation function for focus_num data. + + frame: string; default is cell_df + to specifies the data dataframe. + cell: dataframe will be retrieved through the mcds.get_cell_df function. + conc: dataframe will be retrieved through the mcds.get_conc_df function. + + cat_drop: set of strings; default is an empty set + if focus is a categorical attribute, + set of category labels to be dropped for the dataframe. + Attention: when the cat_keep parameter is given, then + the cat_drop parameter has to be an empty set! + + cat_keep: set of strings; default is an empty set + if focus is a categorical attribute, + set of category labels to be kept in the dataframe. + + z_slice: floating point number; default is None + z-axis position to slice a 2D xy-plain out of the 3D mesh. + if z_slice position numeric but not an exact mesh center coordinate, + then z_slice will be adjusted to the nearest mesh center value, + the smaller one, if the coordinate lies on a saddle point. + if set to None, the whole domain is taken. + + logy: bool; default False + if True, then y axis is natural log scaled. + + ylim: tuple of two floats; default is None + y axis min and max value. + default is None, which automatically detects min and max value. + + secondary_y: bool or list of strings; default False + whether to plot on the secondary y-axis. + if a list, which columns to plot on the secondary y-axis. + + subplots: bool or sequence of iterable, default False + whether to group columns into subplots. + a sequence of iterable of column labels + will create a subplot for each group of columns. + + sharex: bool, default False + in case subplots is True, share x-axis by + setting some x-axis labels to invisible. + + sharey: bool, default False + in case subplots is True, share y-axis range and possibly + setting some y-axis labels to invisible. + + linestyle: string or list of strings, default '-' + matplotlib line style {'-', '--', '-.', ':', ''}, + over all or per column. + + linewidth: float or list of float, default None + line width in points. + + cmap: string; default None + matplotlib colormap string. + https://matplotlib.org/stable/tutorials/colors/colormaps.html + achtung: if cmap is given, color will be disregarded. + + color: string or list of string or dictionary; default None + color string referred to by name, RGB or RGBA code. + achtung: if cmap is given, color will be disregarded. + + grid: boolean; default True + plot axis grid lines. + + legend: bool or 'reverse'; default True + if True or reverse, place legend on axis subplots. + + yunit: string; default None + string to specify y-axis unit. + None will not print a unit on the y-axis. title: string or list; default None title to use for the plot or subplots. @@ -1578,486 +1915,139 @@ def plot_timeseries(self, focus_cat=None, focus_num=None, aggregate_num=np.nanme return fig - ## GRAPH RELATED FUNCTIONS ## - - def make_graph_gml(self, graph_type, edge_attribute=True, node_attribute=[]): + def make_ome_tiff(self, cell_attribute='ID', conc_cutoff={}, focus=None, file=True, collapse=True): """ input: - self: TimeSeries class instance. + cell_attribute: strings; default is 'ID', which will result in a + cell segmentation mask. + column name within the cell dataframe. + the column data type has to be numeric (bool, int, float) + and cannot be string. + the result will be stored as 32 bit float. - graph_type: string - to specify which physicell output data should be processed. - attached, touch: processes mcds.get_attached_graph_dict dictionary. - neighbor: processes mcds.get_neighbor_graph_dict dictionary. - spring: processes mcds.get_spring_graph_dict dictionary. + conc_cutoff: dictionary string to real; default is an empty dictionary. + if a contour from a substrate not should be cut by greater + than zero (shifted to integer 1), another cutoff value can be specified here. - edge_attribute: boolean; default True - specifies if the spatial Euclidean distance is used for - edge attribute, to generate a weighted graph. + focus: set of strings; default is a None + set of substrate and cell_type names to specify what will be + translated into ome tiff format. + if None, all substrates and cell types will be processed. - node_attribute: list of strings; default is empty list - list of mcds.get_cell_df dataframe columns, used for - node attributes. + file: boolean; default True + if True, an ome tiff file is the output. + if False, a numpy array with shape tczyx is the output. + + collapse: boole; default True + should all mcds time steps from the time series be collapsed + into one ome tiff file (numpy array), + or an ome tiff file (numpy array) for each time step? output: - gml file for each time step. - path and filenames are printed to the standard output. + a_tczyx_img: numpy array or ome tiff file. - description: - function to generate graph files in the gml graph modelling language - standard format. - gml was the outcome of an initiative that started at - the international symposium on graph drawing 1995 in Passau - and ended at Graph Drawing 1996 in Berkeley. the networkx python - and igraph C and python libraries for graph analysis are - gml compatible and can as such read and write this file format. + description: + function to transform chosen mcdsts output into an 1[um] spaced + tczyx (time, channel, z-axis, y-axis, x-axis) ome tiff file or numpy array, + one substrate or cell_type per channel. + a ome tiff file is more or less: + a numpy array, containing the image information + and a xml, containing the microscopy metadata information, + like the channel labels. + the ome tiff file format can for example be read by the napari + or fiji (imagej) software. - https://en.wikipedia.org/wiki/Graph_Modelling_Language - https://github.com/elmbeech/physicelldataloader/blob/master/man/publication/himsolt1996gml_a_portable_graph_file_format.pdf - https://networkx.org/ - https://igraph.org/ - """ - # processing - ls_pathfile = [] + https://napari.org/stable/ + https://fiji.sc/ + """ + # for each time step + l_tczyx_img = [] for mcds in self.get_mcds_list(): - s_pathfile = mcds.make_graph_gml( - graph_type = graph_type, - edge_attribute = edge_attribute, - node_attribute = node_attribute, + # processing + b_file = True # 10 + if (not file and not collapse) or (not file and collapse) or (file and collapse): # 00, 01, 11 + b_file = False + o_tczyx_img = mcds.make_ome_tiff( + cell_attribute = cell_attribute, + conc_cutoff = conc_cutoff, + focus = focus, + file = b_file ) - ls_pathfile.append(s_pathfile) - - # outout - return ls_pathfile - - - ## ANNDATA RELATED FUNCTIONS ## - - def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=True): - """ - input: - values: integer; default is 1 - minimal number of values a variable has to have to be outputted. - variables that have only 1 state carry no information. - None is a state too. - - drop: set of strings; default is an empty set - set of column labels to be dropped for the dataframe. - don't worry: essential columns like ID, coordinates - and time will never be dropped. - Attention: when the keep parameter is given, then - the drop parameter has to be an empty set! - - keep: set of strings; default is an empty set - set of column labels to be kept in the dataframe. - don't worry: essential columns like ID, coordinates - and time will always be kept. - - scale: string; default 'maxabs' - specify how the data should be scaled. - possible values are None, maxabs, minmax, std. - for more input, check out: help(pcdl.scaler) - - collapse: boole; default True - should all mcds time steps from the time series be collapsed - into one single anndata object, or a list of anndata objects - for each time step? - - keep_mcds: boole; default True - should the loaded original mcds be kept in memory - after transformation? - - output: - annmcds or self.l_annmcds: anndata object or list of anndata objects. - what is returned depends on the collapse setting. - - description: - function to transform mcds time steps into one or many - anndata objects for downstream analysis. - """ - # load optional dependency - ad = optional_import('anndata', s_caller='TimeSeries.get_anndata') - - # initialize vaiable - l_annmcds = [] - df_anncount = None - df_annobs = None - ar_annobsm = None + l_tczyx_img.append(o_tczyx_img) - # variable triage - if (values < 2): - ls_column = list(self.l_mcds[0].get_cell_df(drop=drop, keep=keep).columns) - else: - ls_column = sorted(es_coor_cell.difference({'ID'})) - ls_column.extend(sorted(self.get_cell_attribute(values=values, drop=drop, keep=keep, allvalues=False).keys())) + # handle channels + ls_substrate = mcds.get_substrate_list() + ls_celltype = mcds.get_celltype_list() - # package collapse - if collapse: + if not (focus is None): + ls_substrate = [s_substrate for s_substrate in ls_substrate if s_substrate in set(focus)] + ls_celltype = [s_celltype for s_celltype in ls_celltype if s_celltype in set(focus)] + if (set(focus) != set(ls_substrate).union(set(ls_celltype))): + sys.exit(f'Error : {focus} not found in {ls_substrate} {ls_celltype}') - # warning + # output 00 list of numpy arrays + if (not file and not collapse): # 00 if self.verbose: - print('Warning @ mcdsts.get_anndata : only df_cell data, but not graph data, can be collapsed.') - - # extract - df_cell = self.get_cell_df(values=values, drop=drop, keep=keep, collapse=True) - df_count, df_obs, d_obsm, d_obsp, d_uns = _anndextract( - df_cell=df_cell, - scale = scale, - #graph_attached = {}, - #graph_neighbor = {}, - #graph_spring = {}, - #graph_method = s_physicellv, - ) - - # fuse to anndata object - ann_mcdsts = ad.AnnData( - X = df_count, - obs = df_obs, - obsm = d_obsm, - #obsp = d_obsp, # nop (graph) - #uns = d_uns, # nop (graph) - ) - - # mcds - if not keep_mcds: - self.l_mcds = [] - - # output - return ann_mcdsts - - # pack not collapsed - else: - # processing - lann_mcds = [] - i_mcds = len(self.l_mcds) - for i in range(i_mcds): - # fetch mcds - if keep_mcds: - mcds = self.l_mcds[i] - else: - mcds = self.l_mcds.pop(0) - - # extract physicell version - s_physicellv = mcds.get_physicell_version(), - - # extract time and dataframes - if self.verbose: - print(f'processing: {i+1}/{i_mcds} {mcds.get_time()}[min] mcds into anndata obj.') - df_cell = mcds.get_cell_df() - df_cell = df_cell.loc[:,ls_column] - - # extract - df_count, df_obs, d_obsm, d_obsp, d_uns = _anndextract( - df_cell=df_cell, - scale = scale, - graph_attached = mcds.get_attached_graph_dict(), - graph_neighbor = mcds.get_neighbor_graph_dict(), - graph_spring = mcds.get_spring_graph_dict(), - graph_method = s_physicellv, - ) - - # annmcds - ann_mcds = ad.AnnData( - X = df_count, - obs = df_obs, - obsm = d_obsm, - obsp = d_obsp, - uns = d_uns, - ) - lann_mcds.append(ann_mcds) - - # output - self.l_annmcds = lann_mcds - return self.l_annmcds - - def get_annmcds_list(self): - """ - input: - self: TimeSeries class instance. - - output: - self.l_annmcds: list of chronologically ordered anndata mcds objects. - watch out, this is a pointer to the - self.l_annmcds list of anndata mcds objects, not a copy of self.l_annmcds! - - description: - function returns a binding to the self.l_annmcds list of anndata mcds objects. - """ - return self.l_annmcds - - - def get_spatialdata(self, images={'subs'}, labels=set(), points={'subs'}, shapes={'cell'}, values=1, drop=set(), keep=set(), scale='maxabs', keep_mcds=True): - """ - input: - images: set of string; default {'subs'} - specify if from the subs or cell dataset - a multichannel image should be generate. - so far, only the subs image element is implemented. - - labels: set of strings; default is an empty set - specify if from the subs or cell dataset - a label element should be generated. - so far, neither subs nor cell label elements are implemented. - - points: set of string; default {'subs'} - specify if from the subs or cell dataset - a points element should be generated. - both, subs and cell point elements, are implemented. - - shapes: set of string; default {'cell'} - specify if from the subs or cell dataset - a shape element should be generated. - so far, only the cell shape element is implemented. - - values: integer; default is 1 - minimal number of values a variable has to have to be outputted. - variables that have only 1 state carry no information. - None is a state too. - - drop: set of strings; default is an empty set - set of column labels to be dropped for the dataframe. - don't worry: essential columns like ID, coordinates - and time will never be dropped. - Attention: when the keep parameter is given, then - the drop parameter has to be an empty set! - - keep: set of strings; default is an empty set - set of column labels to be kept in the dataframe. - don't worry: essential columns like ID, coordinates - and time will always be kept. - - scale: string; default 'maxabs' - specify how the data should be scaled. - possible values are None, maxabs, minmax, std. - for more input, check out: help(pcdl.scaler) - - keep_mcds: boole; default True - should the loaded original mcds be kept in memory - after transformation? - - output: - self.l_sdmcds: list of spatialdata objects. - - description: - function to transform mcds time steps into - spatialdata objects for downstream analysis. - """ - # variable triage - es_keep = set(self.get_cell_attribute(values=values, drop=drop, keep=keep, allvalues=False).keys()) - - # processing - lsd_mcds = [] - i_mcds = len(self.l_mcds) - for i in range(i_mcds): - # fetch mcds - if keep_mcds: - mcds = self.l_mcds[i] - else: - mcds = self.l_mcds.pop(0) + print(f'la_tczyx_img shape: {len(l_tczyx_img)} * {l_tczyx_img[0].shape}') + return l_tczyx_img - # extract time and dataframes + # output 01 numpy array + elif (not file and collapse): # 01 + # numpy array + a_tczyx_img = np.array(l_tczyx_img) if self.verbose: - print(f'\nprocessing: {i+1}/{i_mcds} {mcds.get_time()}[min] mcds into spatialdata obj.') - - # get spatialdata object - sd_mcds = mcds.get_spatialdata( - points = points, - shapes = shapes, - #values = 1, - #drop = set(), - keep = es_keep, - scale = scale, - ) - lsd_mcds.append(sd_mcds) - - # output - self.l_sdmcds = lsd_mcds - return self.l_sdmcds - - - def get_sdmcds_list(self): - """ - input: - self: TimeSeries class instance. - - output: - self.l_sdmcds: list of chronologically ordered spatialdata mcds objects. - watch out, this is a pointer to the - self.l_sdmcds list of spdata mcds objects, not a copy of self.l_sdmcds! - - description: - function returns a binding to the self.l_sdmcds list of spdata mcds objects. - """ - return self.l_sdmcds - - - - ## MUSPAN RELATED FUNCTIONS ## - - def get_muspan(self, z_slice=None, values=1, drop=set(), keep=set()): - """ - input: - z_slice: floating point number; default is None - z-axis position to slice a 2D xy-plain out of the - 3D mesh. if None the whole 3D mesh will be returned. - - values: integer; default is 1 - minimal number of values a variable has to have to be outputted. - variables that have only 1 state carry no information. - None is a state too. + print('a_tczyx_img shape:', a_tczyx_img.shape) + return a_tczyx_img - drop: set of strings; default is an empty set - set of column labels to be dropped for the dataframe. - don't worry: essential columns like ID, coordinates - and time will never be dropped. - Attention: when the keep parameter is given, then - the drop parameter has to be an empty set! + # output 10 list of pathfile strings + elif (file and not collapse): # 10 + return l_tczyx_img - keep: set of strings; default is an empty set - set of column labels to be kept in the dataframe. - set values=1 to be sure that all variables are kept. - don't worry: essential columns like ID, coordinates - and time will always be kept. + # output 11 ometiff file + elif (file and collapse): # 11 + # load optional dependency + OmeTiffWriter = optional_import('bioio.writers', s_attr='OmeTiffWriter', s_pip='bioio', s_caller='TimeSeries.make_ome_tiff') + bioio_base = optional_import('bioio_base', s_pip='bioio', s_caller='TimeSeries.make_ome_tiff') - output: - do_domain: dictionary of muspa domains, one for each time step z-layer. + # numpy array + a_tczyx_img = np.array(l_tczyx_img) + if self.verbose: + print('a_tczyx_img shape:', a_tczyx_img.shape) - description: - function returns a dictionary of muspa domains, containg a - cell and subs collection with disrcete and continuous labels - and all the graph as networks. - + https://www.muspan.co.uk - + https://docs.muspan.co.uk/latest/Documentation.html - """ - # variable triage - es_keep = set(self.get_cell_attribute(values=values, drop=drop, keep=keep, allvalues=False).keys()) + # generate filename + s_channel = '' + for s_substrate in ls_substrate: + try: + r_value = conc_cutoff[s_substrate] + s_channel += f'_{s_substrate}{r_value}' + except KeyError: + s_channel += f'_{s_substrate}' + for s_celltype in ls_celltype: + s_channel += f'_{s_celltype}' + if len(ls_celltype) > 0: + s_channel += f'_{cell_attribute}' + s_tifffile = f"timeseries{s_channel.replace(' ','_')}.ome.tiff" + if (len(s_tifffile) > 255): + print(f"Warning: filename {len(s_tifffile)} > 255 character.") + s_tifffile = 'timeseries_channels.ome.tiff' + print(f"file name adjusted to {s_tifffile}.") + s_tiffpathfile = self.path + '/' + s_tifffile - # processing - do_domain = {} - for mcds in self.get_mcds_list(): - do_domain.update( - mcds.get_muspan( - z_slice = z_slice, - #values = 1, - #drop = set(), - keep = es_keep, - ) + # save to file + OmeTiffWriter.save( + a_tczyx_img, + s_tiffpathfile, + dim_order = 'TCZYX', + #ome_xml=x_img, + channel_names = ls_substrate + ls_celltype, + image_names = [f'timeseries_{cell_attribute}'], + physical_pixel_sizes = bioio_base.types.PhysicalPixelSizes(mcds.get_voxel_spacing()[2], 1.0, 1.0), # z,y,x [um] + #channel_colors=, + #fs_kwargs={}, ) + return s_tiffpathfile - # output - return do_domain - - - ## SIMULARIUM RELATED FUNCTIONS ## - - def make_simularium(self, focus_cat=['cell_type','current_phase'], trajectory_title='timeseries', scale_factor=None, camera_defaults=None, model_meta_data=None): - """ - input: - focus_cat: list of 1 or 2 string; default is ['cell_type','current_phase'] - specify 1 or 2 categorical column labels, to be found - in mcdsts.get_cell_df(). - - trajectory_title: string; default 'timeseries' - the trajectory_title will be used as - .simularium file name and displayed - in the simulation. - - scale_factor: float number; default is None - A multiplier for the scene, use if visualization is - too large or small. If None is provided, one will - be calculated based on the position data. - bue 20260822: does not seem to work in current simularium 1.13.0. - - camera_defaults: simulariumio.CameraData object; default is None - camera's initial settings which it also returns to - when reset. - - model_meta_data: simulariumio.ModelMetaData; default is None - Metadata for the model that produced this - trajectory. - - output: - .simularium file - - description: - function returns a simularium trajectory file that can be run with the - online Simularium Viewer. - + https://simularium.allencell.org/ - """ - # library - sim = optional_import(s_module='simulariumio', s_caller='TimeSeries.make_simularium') - - # handle input - model_meta_data=sim.ModelMetaData() if model_meta_data is None else model_meta_data - camera_defaults=sim.CameraData() if camera_defaults is None else camera_defaults - - # here we go - if self.verbose: - print(f'generating {trajectory_title}.simularium file ...') - - # extract box from mcdsts - ltr_domain = self.get_mcds_list()[0].get_mesh_mnp_range() - lr_space = self.get_mcds_list()[0].get_mesh_spacing() - lr_box = np.array([ - ltr_domain[0][1] - ltr_domain[0][0], - ltr_domain[1][1] - ltr_domain[1][0], - ltr_domain[2][1] - ltr_domain[2][0], - ]) - lr_box[0] = lr_box[0] if lr_box[0] != 0.0 else lr_space[0] - lr_box[1] = lr_box[1] if lr_box[1] != 0.0 else lr_space[1] - lr_box[2] = lr_box[2] if lr_box[2] != 0.0 else lr_space[2] - ar_box = np.array(lr_box) - - # extract cell dataframe from mcdsts - df_cell = self.get_cell_df() - - # handle agent annotation - se_type = df_cell.loc[:, focus_cat].astype(str).agg('#'.join, axis=1) - - # extract units from mcdsts - ds_unit = self.get_mcds_list()[0].get_unit_dict() - - # generate simularium dataframe - with pd.option_context('future.infer_string', False): - df_sim = pd.DataFrame({ - 'time': df_cell.loc[:, 'time'].to_numpy(dtype=float), - 'unique_id': df_cell.loc[:, 'ID'].to_numpy(dtype=int), - 'type': se_type.to_numpy(dtype=object), - 'positionX': df_cell.loc[:, 'position_x'].to_numpy(dtype=float), - 'positionY': df_cell.loc[:, 'position_y'].to_numpy(dtype=float), - 'positionZ': df_cell.loc[:, 'position_z'].to_numpy(dtype=float), - 'radius': df_cell.loc[:, 'radius'].to_numpy(dtype=float), - 'rotationX': np.zeros(df_cell.shape[0], dtype=float), - 'rotationY': np.zeros(df_cell.shape[0], dtype=float), - 'rotationZ': np.zeros(df_cell.shape[0], dtype=float), - }) - df_sim.sort_values(['time', 'unique_id'], inplace=True) - # SimulariumIO 1.13.0 AgentData.from_dataframe expects traj.loc[0, ...] - # to select the whole trajectory, so all rows need the same index label. - df_sim.index = np.zeros(df_sim.shape[0], dtype=int) - - # generate simmularium trajectorydata object - o_sim = sim.TrajectoryData( - meta_data = sim.MetaData( - box_size=ar_box, - camera_defaults=camera_defaults, - scale_factor=scale_factor, - trajectory_title=trajectory_title, - model_meta_data=model_meta_data, - ), - agent_data = sim.AgentData.from_dataframe(df_sim), - time_units = sim.UnitData(ds_unit['time']), - spatial_units = sim.UnitData(ds_unit['spatial_unit']), - #plots=, - ) - - # transform data and save trajectorydata object to simularium file - s_save = self.path + '/' + trajectory_title - s_pathfile = s_save + '.simularium' - sim.TrajectoryConverter(o_sim).save(s_save) - if self.verbose: - print(f'simularium viewer at: https://simularium.allencell.org/') - - # return error code - return s_pathfile + # error case + else: + sys.exit(f'Error @ make_ome_tiff : {file} {collapse} strange file collapse combination.') diff --git a/pcdl/timestep.py b/pcdl/timestep.py index b318c18..e54f682 100644 --- a/pcdl/timestep.py +++ b/pcdl/timestep.py @@ -1053,7 +1053,7 @@ def get_voxel_ijk(self, x, y, z, is_in_mesh=True): return li_ijk - ## MICROENVIRONMENT RELATED FUNCTIONS ## + ## SUBSTRATE RELATED FUNCTIONS ## def get_substrate_list(self): """ @@ -1602,6 +1602,108 @@ def get_cell_attribute_list(self): return self.data['cell']['ls_cellattr'].copy() + def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs'): + """ + input: + values: integer; default is 1 + minimal number of values a variable has to have to be outputted. + variables that have only 1 state carry no information. + None is a state too. + + drop: set of strings; default is an empty set + set of column labels to be dropped for the dataframe. + don't worry: essential columns like ID, coordinates + and time will never be dropped. + Attention: when the keep parameter is given, then + the drop parameter has to be an empty set! + + keep: set of strings; default is an empty set + set of column labels to be kept in the dataframe. + set values=1 to be sure that all variables are kept. + don't worry: essential columns like ID, coordinates + and time will always be kept. + + scale: string; default 'maxabs' + specify how the data should be scaled. + possible values are None, maxabs, minmax, std. + for more input, check out: help(pcdl.scaler) + + output: + annmcds: anndata object + for this one time step. + + description: + function to transform a mcds time step into an anndata object + for downstream analysis. + """ + # load optional dependency + ad = optional_import('anndata', s_caller='TimeStep.get_anndata') + + # processing + if self.verbose: + print(f'processing: 1/1 {round(self.get_time(),9)}[min] mcds into anndata obj.') + df_cell = self.get_cell_df(values=values, drop=drop, keep=keep) + df_count, df_obs, d_obsm, d_obsp, d_uns = _anndextract( + df_cell = df_cell, + scale = scale, + graph_attached = self.get_attached_graph_dict(), + graph_neighbor = self.get_neighbor_graph_dict(), + graph_spring = self.get_spring_graph_dict(), + graph_method = self.get_physicell_version(), + ) + annmcds = ad.AnnData( + X = df_count, + obs = df_obs, + obsm = d_obsm, + obsp = d_obsp, + uns = d_uns + ) + # output + return annmcds + + + def get_attached_graph_dict(self): + """ + input: + + output: + dei_graph: dictionary of sets of integers + maps each cell ID to the attached connected cell IDs. + + description: + function returns the attached cell graph as a dictionary object. + """ + return self.data['cell']['dei_graph']['attached_cells'].copy() + + + def get_neighbor_graph_dict(self): + """ + input: + + output: + dei_graph: dictionary of sets of integers + maps each cell ID to the connected neighbor cell IDs. + + description: + function returns the cell neighbor graph as a dictionary object. + """ + return self.data['cell']['dei_graph']['neighbor_cells'].copy() + + + def get_spring_graph_dict(self): + """ + input: + + output: + dei_graph: dictionary of sets of integers + maps each cell ID to the attached connected cell IDs. + + description: + function returns the attached spring cell graph as a dictionary object. + """ + return self.data['cell']['dei_graph']['spring_attached_cells'].copy() + + def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slice=0.0, z_axis=None, alpha=1, cmap='viridis', title=None, grid=True, legend_loc='lower left', xlim=None, ylim=None, xyequal=True, s=1.0, ax=None, figsizepx=None, directory=None, ext=None, figbgcolor=None, **kwargs): """ input: @@ -2014,396 +2116,112 @@ def make_cell_vtk(self, attribute=['cell_type'], ext='_cell.vtp'): return s_vtkpathfile - ## MICROENVIRONMENT AND CELL AGENT RELATED FUNCTIONS ## - - def make_ome_tiff(self, cell_attribute='ID', conc_cutoff={}, focus=None, file=True): + def make_graph_gml(self, graph_type, edge_attribute=True, node_attribute=[]): """ input: - cell_attribute: strings; default is 'ID', which will result in a - cell segmentation mask. - column name within the cell dataframe. - the column data type has to be numeric (bool, int, float) - and cannot be string. - the result will be stored as 32 bit float. - - conc_cutoff: dictionary string to real; default is an empty dictionary. - if a contour from a substrate not should be cut by greater - than zero (shifted to integer 1), another cutoff value can be - specified here. + graph_type: string + to specify which physicell output data should be processed. + neighbor, touch: processes mcds.get_neighbor_graph_dict dictionary. + attached: processes mcds.get_attached_graph_dict dictionary. + spring: processes mcds.get_spring_graph_dict dictionary. - focus: set of strings; default is a None - set of substrate and cell_type names to specify what will be - translated into ome tiff format. - if None, all substrates and cell types will be processed. + edge_attribute: boolean; default True + specifies if the spatial Euclidean distance is used for + edge attribute, to generate a weighted graph. - file: boolean; default True - if True, an ome tiff file is the output. - if False, a numpy array with shape czyx is the output. + node_attribute: list of strings; default is empty list + list of mcds.get_cell_df dataframe columns, used for + node attributes. output: - a_tczyx_img: numpy array or ome tiff file. + gml file, generated under the returned path. description: - function to transform chosen mcds output into an 1[um] spaced - czyx (channel, z-axis, y-axis, x-axis) ome tiff file or numpy array, - one substrate or cell_type per channel. - an ome tiff file is more or less: - a numpy array, containing the image information - and a xml, containing the microscopy metadata information, - like the channel labels. - the ome tiff file format can for example be read by the napari - or fiji (imagej) software. + function to generate graph files in the gml graph modelling language + standard format. - https://napari.org/stable/ - https://fiji.sc/ + gml was the outcome of an initiative that started at + the international symposium on graph drawing 1995 in Passau + and ended at Graph Drawing 1996 in Berkeley. the networkx python + and igraph C and python libraries for graph analysis are + gml compatible and can as such read and write this file format. + + https://en.wikipedia.org/wiki/Graph_Modelling_Language + https://github.com/elmbeech/physicelldataloader/blob/master/man/publication/himsolt1996gml_a_portable_graph_file_format.pdf + https://networkx.org/ + https://igraph.org/ """ - # load optional dependencies - if file: - OmeTiffWriter = optional_import('bioio.writers', s_attr='OmeTiffWriter', s_pip='bioio', s_caller='TimeStep.make_ome_tiff') - bioio_base = optional_import('bioio_base', s_pip='bioio', s_caller='TimeStep.make_ome_tiff') + # load dataframe for celltype information + df_cell = self.get_cell_df() + ds_unit = self.get_unit_dict() + s_unit_simtime = ds_unit["time"] + r_simtime = self.get_time() + if (graph_type in {'attached'}): + dei_graph = self.get_attached_graph_dict() + elif (graph_type in {'neighbor', 'touch'}): + dei_graph = self.get_neighbor_graph_dict() + elif (graph_type in {'spring'}): + dei_graph = self.get_spring_graph_dict() + #elif (graph_type in {'evo','devo','lineage'}): + # dei_graph = self.get_lineage_graph_dict() + else: + sys.exit(f'Erro @ make_graph_gml : unknown graph_type {graph_type}. known are attached, neighbor, spring, and touch.') - # handle channels - ls_substrate = self.get_substrate_list() - ls_celltype = self.get_celltype_list() + # generate filename + s_gmlpathfile = self.path + '/' + self.xmlfile.replace('.xml',f'_{graph_type}.gml') - if not (focus is None): - ls_substrate = [s_substrate for s_substrate in ls_substrate if s_substrate in set(focus)] - ls_celltype = [s_celltype for s_celltype in ls_celltype if s_celltype in set(focus)] - if (set(focus) != set(ls_substrate).union(set(ls_celltype))): - sys.exit(f'Error : {focus} not found in {ls_substrate} {ls_celltype}') + # open result gml file + f = open(s_gmlpathfile, 'w') + f.write(f'Creator "pcdl_v{__version__}"\ngraph [\n') + f.write(f' id {int(r_simtime)}\n comment "time_{s_unit_simtime}"\n label "{graph_type}_graph"\n directed 0\n') + for i_src, ei_dst in dei_graph.items(): + #print(f'{i_src} {sorted(ei_dst)}') + # node + f.write(f' node [\n id {i_src}\n label "node_{i_src}"\n') + # node attributes + for s_attribute in node_attribute: + o_attribute = df_cell.loc[i_src, s_attribute] + if (type(o_attribute) == str) or (o_attribute.dtype == np.object_): #in {str, np.str_, np.object_}): + f.write(f' {s_attribute} "{o_attribute}"\n') + elif (o_attribute.dtype == bool) or (o_attribute.dtype == int): #in {bool, np.bool_, np.bool, int, np.int_, np.int8, np.int16, np.int32, np.int64}): + f.write(f' {s_attribute} {int(o_attribute)}\n') + elif (o_attribute.dtype == float): #in {float, np.float16, np.float32, np.float64, np.float128}): + f.write(f' {s_attribute} {o_attribute}\n') + else: + sys.exit(f'Error @ TimeStep.make_graph_gml : attribute {o_attribute}; type {o_attribute.dtype}; type seems not to be bool, int, float, or string.') + f.write(f' ]\n') + # edge + for i_dst in ei_dst: + if (i_src < i_dst): + f.write(f' edge [\n source {i_src}\n target {i_dst}\n label "edge_{i_src}_{i_dst}"\n') + if (edge_attribute): + # edge distance attribute + x = df_cell.loc[i_src, 'position_x'] - df_cell.loc[i_dst, 'position_x'] + y = df_cell.loc[i_src, 'position_y'] - df_cell.loc[i_dst, 'position_y'] + z = df_cell.loc[i_src, 'position_z'] - df_cell.loc[i_dst, 'position_z'] + r_distance = (x**2 + y**2 + z**2)**(1/2) + f.write(f' distance_{ds_unit["position_y"]} {round(r_distance)}\n') + f.write(f' ]\n') + # development + #if (i_src > 16): + # break + # close result gml file + f.write(']\n') + f.close() - # const - ls_coor_mnp = ['mesh_center_m', 'mesh_center_n', 'mesh_center_p'] # xyz - ls_coor_xyz = ['position_x', 'position_y', 'position_z'] # xyz - ls_coor = ['voxel_x', 'voxel_y', 'voxel_z'] + # output + return s_gmlpathfile - # time step tensor - i_time = int(self.get_time()) - # get xy coordinate dataframe - lr_axis_z = list(self.get_mesh_mnp_axis()[2] - self.get_voxel_spacing()[2] / 2) - lr_axis_z.append(self.get_mesh_mnp_axis()[2][-1] + self.get_voxel_spacing()[2] / 2) - lll_coor = [] - for i_x in range(int(round(self.get_voxel_ijk_range()[0][1] * self.get_voxel_spacing()[0]))): - for i_y in range(int(round(self.get_voxel_ijk_range()[1][1] * self.get_voxel_spacing()[1]))): - lll_coor.append([i_x, i_y]) - df_coor = pd.DataFrame(lll_coor, columns=ls_coor[:2]) - lr_axis_z[-1] += 1 + ## SUBSTRATE AND CELL AGENT RELATED FUNCTIONS ## - # extract voxel radius - di_grow = {} - for s_substarte in ls_substrate: - di_grow.update({ - s_substarte : int(np.round(np.mean(self.get_voxel_spacing()[:2])) - 1) - }) + def get_muspan(self, z_slice=None, values=1, drop=set(), keep=set()): + """ + input: + z_slice: floating point number; default is None + z-axis position to slice a 2D xy-plain out of the + 3D mesh. if None the whole 3D mesh will be returned. - # get and shift substrate xy data - df_conc = self.get_conc_df() - df_conc = df_conc.loc[:, ls_coor_mnp + ls_substrate] - df_conc.loc[:, 'mesh_center_m'] = (df_conc.loc[:, 'mesh_center_m'] - self.get_xyz_range()[0][0]).round() - df_conc.loc[:, 'mesh_center_n'] = (df_conc.loc[:, 'mesh_center_n'] - self.get_xyz_range()[1][0]).round() - df_conc.rename({'mesh_center_m':'voxel_x', 'mesh_center_n':'voxel_y', 'mesh_center_p':'voxel_z'}, axis=1, inplace=True) - df_conc = df_conc.astype({'voxel_x': int, 'voxel_y': int, 'voxel_z': float}) - # level the cake - for s_channel in conc_cutoff.keys(): - try: - df_conc.loc[:, s_channel] = df_conc.loc[:, s_channel] - conc_cutoff[s_channel] + 1 # positive values starting at > 0 - df_conc.loc[(df_conc.loc[:, s_channel] <= conc_cutoff[s_channel]), s_channel] = 0 - except KeyError: - pass - - # get cell data - df_cell = self.get_cell_df().reset_index() - - # extract cell radius - for s_celltype in ls_celltype: - try: - i_cell_grow = int(round(df_cell.loc[(df_cell.cell_type == s_celltype), 'radius'].mean()) - 1) - except: - i_cell_grow = 0 - di_grow.update({s_celltype : i_cell_grow}) - - # filter and shift - df_cell = df_cell.loc[:, ls_coor_xyz + ['cell_type', cell_attribute]] - if (cell_attribute == 'cell_type'): - sys.exit(f'Error @ TimeStep.make_ome_tiff : cell_attribute cannot be cell_type.') - elif (df_cell.loc[:, cell_attribute].dtype == str) or (df_cell.loc[:, cell_attribute].dtype == np.object_): # in {str, np.str_, np.object_}): - sys.exit(f'Error @ TimeStep.make_ome_tiff : {cell_attribute} {df_cell.loc[:, cell_attribute].dtype} cell_attribute cannot be string or object. cell_attribute has to be boolean, integer, or float.') - elif (df_cell.loc[:, cell_attribute].dtype == bool): # in {bool, np.bool_, np.bool}): - df_cell = df_cell.astype({cell_attribute: int}) - df_cell.loc[:, 'position_x'] = (df_cell.loc[:, 'position_x'] - self.get_xyz_range()[0][0]).round() - df_cell.loc[:, 'position_y'] = (df_cell.loc[:, 'position_y'] - self.get_xyz_range()[1][0]).round() - df_cell.rename({'position_x':'voxel_x', 'position_y':'voxel_y', 'position_z':'voxel_z'}, axis=1, inplace=True) - df_cell = df_cell.astype({'voxel_x': int, 'voxel_y': int, 'voxel_z': float}) - # level the cake - df_cell.loc[:, cell_attribute] = df_cell.loc[:, cell_attribute] - df_cell.loc[:, cell_attribute].min() + 1 # positive values starting at > 0 - - # check for duplicates: two cell at exactelly the same xyz position. - #if self.verbose and df_cell.loc[:,['voxel_x', 'voxel_y', 'voxel_z']].duplicated().any(): - # df_duplicate = df_cell.loc[(df_cell.loc[:, ['voxel_x', 'voxel_y', 'voxel_z']].duplicated()), :] - # sys.exit(f"Error @ TimeStep.make_ome_tiff : {df_duplicate} cells at exactely the same xyz voxel position detected. cannot pivot!") - - # pivot cell_type - df_cell = df_cell.pivot_table(index=ls_coor, columns='cell_type', values=cell_attribute, aggfunc='sum').reset_index() # fill_value is na - for s_celltype in ls_celltype: - if not s_celltype in set(df_cell.columns): - df_cell[s_celltype] = 0 - - # each C channel - time step tensors - la_czyx_img = [] - ls_channel = ls_substrate + ls_celltype - for s_channel in ls_channel: - - # get channel dataframe - if s_channel in set(ls_substrate): - df_channel = df_conc.loc[:, ls_coor + [s_channel]] - elif s_channel in set(ls_celltype): - df_channel = df_cell.loc[:, ls_coor + [s_channel]] - else: - sys.exit(f'Error @ TimeStep.make_ome_tiff : {s_channel} unknown channel detected. not in substrate and cell type list {ls_substrate} {ls_celltype}!') - - # each z axis - la_zyx_img = [] - for i_zaxis in range(len(lr_axis_z)): - if (i_zaxis < (len(lr_axis_z) - 1)): - print(f'processing: {i_time} [min] {s_channel} [channel] {i_zaxis} [z_axis] ...') - # extract z layer - df_yxchannel = df_channel.loc[ - ((df_channel.loc[:, ls_coor[2]] >= lr_axis_z[i_zaxis]) & (df_channel.loc[:, ls_coor[2]] < lr_axis_z[i_zaxis + 1])), - ls_coor[:2] + [s_channel] - ] - - # drop row with na and duplicate entries - df_yxchannel = df_yxchannel.dropna(axis=0) - df_yxchannel = df_yxchannel.drop_duplicates() - - # merge with coooridnates and get image - # bue 20240811: df_coor left side merge will cut off reset cell that are out of the xyz domain range, which is what we want. - df_yxchannel = pd.merge(df_coor, df_yxchannel, on=ls_coor[:2], how='left').replace({np.nan: 0}) - try: - df_yxchannel = df_yxchannel.pivot(columns=ls_coor[0], index=ls_coor[1], values=s_channel) - except ValueError: # two cells from the same cell type very close to each other detetced. - if self.verbose: - df_duplicate = df_cell.loc[(df_yxchannel.loc[:, ['voxel_x', 'voxel_y']].duplicated()), :] - print(f'Warning: {s_channel} {df_duplicate} cells within 1[um] distance form each detected. cannot pivot. erase cell type from this timestep.') - df_yxchannel.loc[:,s_channel] = 0 # erase cells - df_yxchannel = df_yxchannel.drop_duplicates() - df_yxchannel = df_yxchannel.pivot(columns=ls_coor[0], index=ls_coor[1], values=s_channel) - a_yx_img = df_yxchannel.values - - # grow - a_yx_img = imagine.grow_seed(a_yx_img, i_step=di_grow[s_channel], b_verbose=False) - - # update output - la_zyx_img.append(a_yx_img) - a_zyx_img = np.array(la_zyx_img, np.float32) - la_czyx_img.append(np.array(a_zyx_img, np.float32)) - - # output - a_czyx_img = np.array(la_czyx_img, dtype=np.float32) - - # numpy array - if not file: - return a_czyx_img - - # write to file - else: - if self.verbose: - print('a_czyx_img shape:', a_czyx_img.shape) - # generate filename - s_channel = '' - for s_substrate in ls_substrate: - try: - r_value = conc_cutoff[s_substrate] - s_channel += f'_{s_substrate}{r_value}' - except KeyError: - s_channel += f'_{s_substrate}' - for s_celltype in ls_celltype: - s_channel += f'_{s_celltype}' - if len(ls_celltype) > 0: - s_channel += f'_{cell_attribute}' - s_tifffile = self.xmlfile.replace('.xml', f'{s_channel}.ome.tiff') - s_tifffile = s_tifffile.replace(' ','_') - if (len(s_tifffile) > 255): - print(f"Warning: filename {len(s_tifffile)} > 255 character.") - s_tifffile = self.xmlfile.replace('.xml', f'_channels.ome.tiff') - print(f"file name adjusted to {s_tifffile}.") - s_tiffpathfile = self.path + '/' + s_tifffile - - # save to file - OmeTiffWriter.save( - a_czyx_img, - s_tiffpathfile, - dim_order = 'CZYX', - #ome_xml=x_img, - channel_names = ls_channel, - image_names = [s_tifffile.replace('.ome.tiff','')], - physical_pixel_sizes = bioio_base.types.PhysicalPixelSizes(self.get_voxel_spacing()[2], 1.0, 1.0), # z,y,x [um] - #channel_colors=, - #fs_kwargs={}, - ) - return s_tiffpathfile - - - def render_neuroglancer(self, tiffpathfile, timestep=0, intensity_cmap='gray'): - """ - help(pcdl.render_neuroglancer) - try: mcds.render_neuroglancer(mcds.make_ome_tiff()) - """ - o_viewer = render_neuroglancer( - tiffpathfile = tiffpathfile, - timestep = timestep, - intensity_cmap = intensity_cmap, - ) - return o_viewer - - - ## GRAPH RELATED FUNCTIONS ## - - def get_attached_graph_dict(self): - """ - input: - - output: - dei_graph: dictionary of sets of integers - maps each cell ID to the attached connected cell IDs. - - description: - function returns the attached cell graph as a dictionary object. - """ - return self.data['cell']['dei_graph']['attached_cells'].copy() - - - def get_neighbor_graph_dict(self): - """ - input: - - output: - dei_graph: dictionary of sets of integers - maps each cell ID to the connected neighbor cell IDs. - - description: - function returns the cell neighbor graph as a dictionary object. - """ - return self.data['cell']['dei_graph']['neighbor_cells'].copy() - - - def get_spring_graph_dict(self): - """ - input: - - output: - dei_graph: dictionary of sets of integers - maps each cell ID to the attached connected cell IDs. - - description: - function returns the attached spring cell graph as a dictionary object. - """ - return self.data['cell']['dei_graph']['spring_attached_cells'].copy() - - - def make_graph_gml(self, graph_type, edge_attribute=True, node_attribute=[]): - """ - input: - graph_type: string - to specify which physicell output data should be processed. - neighbor, touch: processes mcds.get_neighbor_graph_dict dictionary. - attached: processes mcds.get_attached_graph_dict dictionary. - spring: processes mcds.get_spring_graph_dict dictionary. - - edge_attribute: boolean; default True - specifies if the spatial Euclidean distance is used for - edge attribute, to generate a weighted graph. - - node_attribute: list of strings; default is empty list - list of mcds.get_cell_df dataframe columns, used for - node attributes. - - output: - gml file, generated under the returned path. - - description: - function to generate graph files in the gml graph modelling language - standard format. - - gml was the outcome of an initiative that started at - the international symposium on graph drawing 1995 in Passau - and ended at Graph Drawing 1996 in Berkeley. the networkx python - and igraph C and python libraries for graph analysis are - gml compatible and can as such read and write this file format. - - https://en.wikipedia.org/wiki/Graph_Modelling_Language - https://github.com/elmbeech/physicelldataloader/blob/master/man/publication/himsolt1996gml_a_portable_graph_file_format.pdf - https://networkx.org/ - https://igraph.org/ - """ - # load dataframe for celltype information - df_cell = self.get_cell_df() - ds_unit = self.get_unit_dict() - s_unit_simtime = ds_unit["time"] - r_simtime = self.get_time() - if (graph_type in {'attached'}): - dei_graph = self.get_attached_graph_dict() - elif (graph_type in {'neighbor', 'touch'}): - dei_graph = self.get_neighbor_graph_dict() - elif (graph_type in {'spring'}): - dei_graph = self.get_spring_graph_dict() - #elif (graph_type in {'evo','devo','lineage'}): - # dei_graph = self.get_lineage_graph_dict() - else: - sys.exit(f'Erro @ make_graph_gml : unknown graph_type {graph_type}. known are attached, neighbor, spring, and touch.') - - # generate filename - s_gmlpathfile = self.path + '/' + self.xmlfile.replace('.xml',f'_{graph_type}.gml') - - # open result gml file - f = open(s_gmlpathfile, 'w') - f.write(f'Creator "pcdl_v{__version__}"\ngraph [\n') - f.write(f' id {int(r_simtime)}\n comment "time_{s_unit_simtime}"\n label "{graph_type}_graph"\n directed 0\n') - for i_src, ei_dst in dei_graph.items(): - #print(f'{i_src} {sorted(ei_dst)}') - # node - f.write(f' node [\n id {i_src}\n label "node_{i_src}"\n') - # node attributes - for s_attribute in node_attribute: - o_attribute = df_cell.loc[i_src, s_attribute] - if (type(o_attribute) == str) or (o_attribute.dtype == np.object_): #in {str, np.str_, np.object_}): - f.write(f' {s_attribute} "{o_attribute}"\n') - elif (o_attribute.dtype == bool) or (o_attribute.dtype == int): #in {bool, np.bool_, np.bool, int, np.int_, np.int8, np.int16, np.int32, np.int64}): - f.write(f' {s_attribute} {int(o_attribute)}\n') - elif (o_attribute.dtype == float): #in {float, np.float16, np.float32, np.float64, np.float128}): - f.write(f' {s_attribute} {o_attribute}\n') - else: - sys.exit(f'Error @ TimeStep.make_graph_gml : attribute {o_attribute}; type {o_attribute.dtype}; type seems not to be bool, int, float, or string.') - f.write(f' ]\n') - # edge - for i_dst in ei_dst: - if (i_src < i_dst): - f.write(f' edge [\n source {i_src}\n target {i_dst}\n label "edge_{i_src}_{i_dst}"\n') - if (edge_attribute): - # edge distance attribute - x = df_cell.loc[i_src, 'position_x'] - df_cell.loc[i_dst, 'position_x'] - y = df_cell.loc[i_src, 'position_y'] - df_cell.loc[i_dst, 'position_y'] - z = df_cell.loc[i_src, 'position_z'] - df_cell.loc[i_dst, 'position_z'] - r_distance = (x**2 + y**2 + z**2)**(1/2) - f.write(f' distance_{ds_unit["position_y"]} {round(r_distance)}\n') - f.write(f' ]\n') - # development - #if (i_src > 16): - # break - # close result gml file - f.write(']\n') - f.close() - - # output - return s_gmlpathfile - - - ## ANNDATA RELATED FUNCTIONS ## - - def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs'): - """ - input: values: integer; default is 1 minimal number of values a variable has to have to be outputted. variables that have only 1 state carry no information. @@ -2422,43 +2240,157 @@ def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs'): don't worry: essential columns like ID, coordinates and time will always be kept. - scale: string; default 'maxabs' - specify how the data should be scaled. - possible values are None, maxabs, minmax, std. - for more input, check out: help(pcdl.scaler) + output: + do_domain: dictionary of muspa domains, one for each z-layer. + + description: + function returns a dictionary of muspa domains, containg a + cell and subs collection with disrcete and continuous labels + and all the graph as networks. + + https://www.muspan.co.uk + + https://docs.muspan.co.uk/latest/Documentation.html + """ + # check if muspan library is installed + if (ms is None) or (ms.__file__ is None): + sys.exit(f'Error @ TimeStep.get_muspa : the muspan Multi Spatial Analysis python3 library is not installed!\nfor instructions check out : https://www.muspan.co.uk/') + + # load optional dependency + nx = optional_import('networkx', s_caller='TimeStep.get_muspan') + + # get conc and cell dataframe + df_conc = self.get_conc_df(values=values, drop=drop, keep=keep) + df_cell = self.get_cell_df(values=values, drop=drop, keep=keep) + i_kmax = df_conc.voxel_k.max() + i_kdigit = len(str(i_kmax)) + if (z_slice is None): + li_klayer = sorted(df_conc.voxel_k.unique()) + else: + li_klayer = [self.get_voxel_ijk(x=0,y=0, z=z_slice)[2]] + + # for each z layer generate a muspa domain + do_domain = {} + for i_klayer in li_klayer: + + # processing + if self.verbose: + print(f'processing: {self.xmlfile} mcds {i_klayer + 1}/{i_kmax + 1} z-stack layer to muspan obj.') + + ## generate muspan domain + s_domain = f"{self.xmlfile.replace('.xml','')}_z{str(i_klayer).zfill(i_kdigit)}" + o_domain = ms.domain( + name = s_domain, + unit_of_length = 'um', + ) + + ## handle subs collection + df_zconc = df_conc.loc[df_conc.voxel_k == i_klayer,:] + o_domain.add_points( + points = df_zconc.loc[:,['mesh_center_m','mesh_center_n']].values, + collection_name = 'subs' + ) + # drop this data + es_drop = set(df_zconc.columns).intersection({ + 'voxel_i', 'voxel_j', 'voxel_k', + 'mesh_center_m', 'mesh_center_n', 'mesh_center_p', + 'time', 'runtime', 'xmlfile', + }) + df_zconc = df_zconc.drop(es_drop, axis=1) + # add numerical data (no scaling) + for s_num in sorted(df_zconc.columns): + o_domain.add_labels( + label_name = s_num, + labels = df_zconc.loc[:,s_num], + add_labels_to = 'subs', + label_type = 'continuous', + ) + + ## handle cell collection + df_zcell = df_cell.loc[df_cell.voxel_k == i_klayer,:] + o_domain.add_points( + points = df_zcell.loc[:,['position_x','position_y']].values, + collection_name = 'cell' + ) + # get a physicell cell_id to muspan object id mapping + df_coor = df_zcell.loc[:,['position_x', 'position_y','position_z']] + df_coor['muspan_id'] = o_domain.collections['cell']['objects'] + di_cellid = df_coor['muspan_id'].to_dict() + # drop this data + es_drop = set(df_zcell.columns).intersection({ + 'voxel_i', 'voxel_j', 'voxel_k', + 'mesh_center_m', 'mesh_center_n', 'mesh_center_p', + 'position_x', 'position_y','position_z', + 'time', 'runtime', 'xmlfile', + }) + df_zcell = df_zcell.drop(es_drop, axis=1) + # dectect variable types + des_type = {'float': set(), 'int': set(), 'bool': set(), 'str': set()} + for _, se_zcell in df_zcell.items(): + if str(se_zcell.dtype).startswith('float'): + des_type['float'].add(se_zcell.name) + elif str(se_zcell.dtype).startswith('int'): + des_type['int'].add(se_zcell.name) + elif str(se_zcell.dtype).startswith('bool'): + des_type['bool'].add(se_zcell.name) + elif str(se_zcell.dtype).startswith('object') in str(se_zcell.dtype).startswith('str'): + des_type['str'].add(se_zcell.name) + else: + sys.exit(f'Error @ TimeStep.get_muspa : column {se_zcell.name} detected with unknown dtype {str(se_zcell.dtype)}.') + # add categorical data + for s_cat in sorted(des_type['str'].union(des_type['bool'])): + o_domain.add_labels( + label_name = s_cat, + labels = df_zcell.loc[:,s_cat], + add_labels_to = 'cell', + label_type = 'categorical', + ) + # add numerical data (no scaling) + for s_num in sorted(des_type['float'].union(des_type['int'])): + o_domain.add_labels( + label_name = s_num, + labels = df_zcell.loc[:,s_num], + add_labels_to = 'cell', + label_type = 'continuous', + ) + ## add graphs + ei_pccellid = set(df_zcell.index) + for s_graph, dei_graph in [ + ('neighbor', self.get_neighbor_graph_dict()), + ('attached', self.get_attached_graph_dict()), + ('spring', self.get_spring_graph_dict()), + ]: + # transform graph dict into weighted edge list + lt_wedge = [] + for i_src, ei_dst in sorted(dei_graph.items()): + for i_dst in ei_dst: + if (i_src in ei_pccellid) and (i_dst in ei_pccellid): + r_distance = ((df_coor.loc[i_src, ['position_x','position_y','position_z']].values - df_coor.loc[i_dst, ['position_x','position_y','position_z']].values)**2).sum()**(1/2) + lt_wedge.append((di_cellid[i_src], di_cellid[i_dst], r_distance)) + # generate graph + G = nx.Graph() + # dump the edges into the network + G.add_weighted_edges_from(lt_wedge, weight='Distance') + G.add_weighted_edges_from(lt_wedge, weight='Inverse Distance') + # add the network to the dictionary of networks + o_domain.networks[s_graph] = G + # clean up the domain + ms.helpers.clean_up(o_domain) - output: - annmcds: anndata object - for this one time step. + ## set domain boundary (have to be done last!) + o_domain.estimate_boundary( + method='specify', + specify_boundary_coords=( + (self.get_xyz_range()[0][0], self.get_xyz_range()[1][0]), + (self.get_xyz_range()[0][0], self.get_xyz_range()[1][1]), + (self.get_xyz_range()[0][1], self.get_xyz_range()[1][1]), + (self.get_xyz_range()[0][1], self.get_xyz_range()[1][0]) + ) + ) - description: - function to transform a mcds time step into an anndata object - for downstream analysis. - """ - # load optional dependency - ad = optional_import('anndata', s_caller='TimeStep.get_anndata') + # update output + do_domain.update({s_domain : o_domain}) - # processing - if self.verbose: - print(f'processing: 1/1 {round(self.get_time(),9)}[min] mcds into anndata obj.') - df_cell = self.get_cell_df(values=values, drop=drop, keep=keep) - df_count, df_obs, d_obsm, d_obsp, d_uns = _anndextract( - df_cell = df_cell, - scale = scale, - graph_attached = self.get_attached_graph_dict(), - graph_neighbor = self.get_neighbor_graph_dict(), - graph_spring = self.get_spring_graph_dict(), - graph_method = self.get_physicell_version(), - ) - annmcds = ad.AnnData( - X = df_count, - obs = df_obs, - obsm = d_obsm, - obsp = d_obsp, - uns = d_uns - ) # output - return annmcds + return do_domain def get_spatialdata(self, images={'subs'}, labels={}, points={'subs'}, shapes={'cell'}, values=1, drop=set(), keep=set(), scale='maxabs'): @@ -2722,184 +2654,246 @@ def get_spatialdata(self, images={'subs'}, labels={}, points={'subs'}, shapes={' return sdata - ## MUSPAN RELATED FUNCTIONS ## + def make_ome_tiff(self, cell_attribute='ID', conc_cutoff={}, focus=None, file=True): + """ + input: + cell_attribute: strings; default is 'ID', which will result in a + cell segmentation mask. + column name within the cell dataframe. + the column data type has to be numeric (bool, int, float) + and cannot be string. + the result will be stored as 32 bit float. + + conc_cutoff: dictionary string to real; default is an empty dictionary. + if a contour from a substrate not should be cut by greater + than zero (shifted to integer 1), another cutoff value can be + specified here. + + focus: set of strings; default is a None + set of substrate and cell_type names to specify what will be + translated into ome tiff format. + if None, all substrates and cell types will be processed. + + file: boolean; default True + if True, an ome tiff file is the output. + if False, a numpy array with shape czyx is the output. + + output: + a_tczyx_img: numpy array or ome tiff file. + + description: + function to transform chosen mcds output into an 1[um] spaced + czyx (channel, z-axis, y-axis, x-axis) ome tiff file or numpy array, + one substrate or cell_type per channel. + an ome tiff file is more or less: + a numpy array, containing the image information + and a xml, containing the microscopy metadata information, + like the channel labels. + the ome tiff file format can for example be read by the napari + or fiji (imagej) software. + + https://napari.org/stable/ + https://fiji.sc/ + """ + # load optional dependencies + if file: + OmeTiffWriter = optional_import('bioio.writers', s_attr='OmeTiffWriter', s_pip='bioio', s_caller='TimeStep.make_ome_tiff') + bioio_base = optional_import('bioio_base', s_pip='bioio', s_caller='TimeStep.make_ome_tiff') + + # handle channels + ls_substrate = self.get_substrate_list() + ls_celltype = self.get_celltype_list() + + if not (focus is None): + ls_substrate = [s_substrate for s_substrate in ls_substrate if s_substrate in set(focus)] + ls_celltype = [s_celltype for s_celltype in ls_celltype if s_celltype in set(focus)] + if (set(focus) != set(ls_substrate).union(set(ls_celltype))): + sys.exit(f'Error : {focus} not found in {ls_substrate} {ls_celltype}') + + # const + ls_coor_mnp = ['mesh_center_m', 'mesh_center_n', 'mesh_center_p'] # xyz + ls_coor_xyz = ['position_x', 'position_y', 'position_z'] # xyz + ls_coor = ['voxel_x', 'voxel_y', 'voxel_z'] + + # time step tensor + i_time = int(self.get_time()) + + # get xy coordinate dataframe + lr_axis_z = list(self.get_mesh_mnp_axis()[2] - self.get_voxel_spacing()[2] / 2) + lr_axis_z.append(self.get_mesh_mnp_axis()[2][-1] + self.get_voxel_spacing()[2] / 2) + lll_coor = [] + for i_x in range(int(round(self.get_voxel_ijk_range()[0][1] * self.get_voxel_spacing()[0]))): + for i_y in range(int(round(self.get_voxel_ijk_range()[1][1] * self.get_voxel_spacing()[1]))): + lll_coor.append([i_x, i_y]) + df_coor = pd.DataFrame(lll_coor, columns=ls_coor[:2]) + lr_axis_z[-1] += 1 + + # extract voxel radius + di_grow = {} + for s_substarte in ls_substrate: + di_grow.update({ + s_substarte : int(np.round(np.mean(self.get_voxel_spacing()[:2])) - 1) + }) + + # get and shift substrate xy data + df_conc = self.get_conc_df() + df_conc = df_conc.loc[:, ls_coor_mnp + ls_substrate] + df_conc.loc[:, 'mesh_center_m'] = (df_conc.loc[:, 'mesh_center_m'] - self.get_xyz_range()[0][0]).round() + df_conc.loc[:, 'mesh_center_n'] = (df_conc.loc[:, 'mesh_center_n'] - self.get_xyz_range()[1][0]).round() + df_conc.rename({'mesh_center_m':'voxel_x', 'mesh_center_n':'voxel_y', 'mesh_center_p':'voxel_z'}, axis=1, inplace=True) + df_conc = df_conc.astype({'voxel_x': int, 'voxel_y': int, 'voxel_z': float}) + # level the cake + for s_channel in conc_cutoff.keys(): + try: + df_conc.loc[:, s_channel] = df_conc.loc[:, s_channel] - conc_cutoff[s_channel] + 1 # positive values starting at > 0 + df_conc.loc[(df_conc.loc[:, s_channel] <= conc_cutoff[s_channel]), s_channel] = 0 + except KeyError: + pass + + # get cell data + df_cell = self.get_cell_df().reset_index() + + # extract cell radius + for s_celltype in ls_celltype: + try: + i_cell_grow = int(round(df_cell.loc[(df_cell.cell_type == s_celltype), 'radius'].mean()) - 1) + except: + i_cell_grow = 0 + di_grow.update({s_celltype : i_cell_grow}) + + # filter and shift + df_cell = df_cell.loc[:, ls_coor_xyz + ['cell_type', cell_attribute]] + if (cell_attribute == 'cell_type'): + sys.exit(f'Error @ TimeStep.make_ome_tiff : cell_attribute cannot be cell_type.') + elif (df_cell.loc[:, cell_attribute].dtype == str) or (df_cell.loc[:, cell_attribute].dtype == np.object_): # in {str, np.str_, np.object_}): + sys.exit(f'Error @ TimeStep.make_ome_tiff : {cell_attribute} {df_cell.loc[:, cell_attribute].dtype} cell_attribute cannot be string or object. cell_attribute has to be boolean, integer, or float.') + elif (df_cell.loc[:, cell_attribute].dtype == bool): # in {bool, np.bool_, np.bool}): + df_cell = df_cell.astype({cell_attribute: int}) + df_cell.loc[:, 'position_x'] = (df_cell.loc[:, 'position_x'] - self.get_xyz_range()[0][0]).round() + df_cell.loc[:, 'position_y'] = (df_cell.loc[:, 'position_y'] - self.get_xyz_range()[1][0]).round() + df_cell.rename({'position_x':'voxel_x', 'position_y':'voxel_y', 'position_z':'voxel_z'}, axis=1, inplace=True) + df_cell = df_cell.astype({'voxel_x': int, 'voxel_y': int, 'voxel_z': float}) + # level the cake + df_cell.loc[:, cell_attribute] = df_cell.loc[:, cell_attribute] - df_cell.loc[:, cell_attribute].min() + 1 # positive values starting at > 0 + + # check for duplicates: two cell at exactelly the same xyz position. + #if self.verbose and df_cell.loc[:,['voxel_x', 'voxel_y', 'voxel_z']].duplicated().any(): + # df_duplicate = df_cell.loc[(df_cell.loc[:, ['voxel_x', 'voxel_y', 'voxel_z']].duplicated()), :] + # sys.exit(f"Error @ TimeStep.make_ome_tiff : {df_duplicate} cells at exactely the same xyz voxel position detected. cannot pivot!") + + # pivot cell_type + df_cell = df_cell.pivot_table(index=ls_coor, columns='cell_type', values=cell_attribute, aggfunc='sum').reset_index() # fill_value is na + for s_celltype in ls_celltype: + if not s_celltype in set(df_cell.columns): + df_cell[s_celltype] = 0 + + # each C channel - time step tensors + la_czyx_img = [] + ls_channel = ls_substrate + ls_celltype + for s_channel in ls_channel: + + # get channel dataframe + if s_channel in set(ls_substrate): + df_channel = df_conc.loc[:, ls_coor + [s_channel]] + elif s_channel in set(ls_celltype): + df_channel = df_cell.loc[:, ls_coor + [s_channel]] + else: + sys.exit(f'Error @ TimeStep.make_ome_tiff : {s_channel} unknown channel detected. not in substrate and cell type list {ls_substrate} {ls_celltype}!') - def get_muspan(self, z_slice=None, values=1, drop=set(), keep=set()): - """ - input: - z_slice: floating point number; default is None - z-axis position to slice a 2D xy-plain out of the - 3D mesh. if None the whole 3D mesh will be returned. + # each z axis + la_zyx_img = [] + for i_zaxis in range(len(lr_axis_z)): + if (i_zaxis < (len(lr_axis_z) - 1)): + print(f'processing: {i_time} [min] {s_channel} [channel] {i_zaxis} [z_axis] ...') + # extract z layer + df_yxchannel = df_channel.loc[ + ((df_channel.loc[:, ls_coor[2]] >= lr_axis_z[i_zaxis]) & (df_channel.loc[:, ls_coor[2]] < lr_axis_z[i_zaxis + 1])), + ls_coor[:2] + [s_channel] + ] - values: integer; default is 1 - minimal number of values a variable has to have to be outputted. - variables that have only 1 state carry no information. - None is a state too. + # drop row with na and duplicate entries + df_yxchannel = df_yxchannel.dropna(axis=0) + df_yxchannel = df_yxchannel.drop_duplicates() - drop: set of strings; default is an empty set - set of column labels to be dropped for the dataframe. - don't worry: essential columns like ID, coordinates - and time will never be dropped. - Attention: when the keep parameter is given, then - the drop parameter has to be an empty set! + # merge with coooridnates and get image + # bue 20240811: df_coor left side merge will cut off reset cell that are out of the xyz domain range, which is what we want. + df_yxchannel = pd.merge(df_coor, df_yxchannel, on=ls_coor[:2], how='left').replace({np.nan: 0}) + try: + df_yxchannel = df_yxchannel.pivot(columns=ls_coor[0], index=ls_coor[1], values=s_channel) + except ValueError: # two cells from the same cell type very close to each other detetced. + if self.verbose: + df_duplicate = df_cell.loc[(df_yxchannel.loc[:, ['voxel_x', 'voxel_y']].duplicated()), :] + print(f'Warning: {s_channel} {df_duplicate} cells within 1[um] distance form each detected. cannot pivot. erase cell type from this timestep.') + df_yxchannel.loc[:,s_channel] = 0 # erase cells + df_yxchannel = df_yxchannel.drop_duplicates() + df_yxchannel = df_yxchannel.pivot(columns=ls_coor[0], index=ls_coor[1], values=s_channel) + a_yx_img = df_yxchannel.values - keep: set of strings; default is an empty set - set of column labels to be kept in the dataframe. - set values=1 to be sure that all variables are kept. - don't worry: essential columns like ID, coordinates - and time will always be kept. + # grow + a_yx_img = imagine.grow_seed(a_yx_img, i_step=di_grow[s_channel], b_verbose=False) - output: - do_domain: dictionary of muspa domains, one for each z-layer. + # update output + la_zyx_img.append(a_yx_img) + a_zyx_img = np.array(la_zyx_img, np.float32) + la_czyx_img.append(np.array(a_zyx_img, np.float32)) - description: - function returns a dictionary of muspa domains, containg a - cell and subs collection with disrcete and continuous labels - and all the graph as networks. - + https://www.muspan.co.uk - + https://docs.muspan.co.uk/latest/Documentation.html - """ - # check if muspan library is installed - if (ms is None) or (ms.__file__ is None): - sys.exit(f'Error @ TimeStep.get_muspa : the muspan Multi Spatial Analysis python3 library is not installed!\nfor instructions check out : https://www.muspan.co.uk/') + # output + a_czyx_img = np.array(la_czyx_img, dtype=np.float32) - # load optional dependency - nx = optional_import('networkx', s_caller='TimeStep.get_muspan') + # numpy array + if not file: + return a_czyx_img - # get conc and cell dataframe - df_conc = self.get_conc_df(values=values, drop=drop, keep=keep) - df_cell = self.get_cell_df(values=values, drop=drop, keep=keep) - i_kmax = df_conc.voxel_k.max() - i_kdigit = len(str(i_kmax)) - if (z_slice is None): - li_klayer = sorted(df_conc.voxel_k.unique()) + # write to file else: - li_klayer = [self.get_voxel_ijk(x=0,y=0, z=z_slice)[2]] - - # for each z layer generate a muspa domain - do_domain = {} - for i_klayer in li_klayer: - - # processing if self.verbose: - print(f'processing: {self.xmlfile} mcds {i_klayer + 1}/{i_kmax + 1} z-stack layer to muspan obj.') - - ## generate muspan domain - s_domain = f"{self.xmlfile.replace('.xml','')}_z{str(i_klayer).zfill(i_kdigit)}" - o_domain = ms.domain( - name = s_domain, - unit_of_length = 'um', - ) - - ## handle subs collection - df_zconc = df_conc.loc[df_conc.voxel_k == i_klayer,:] - o_domain.add_points( - points = df_zconc.loc[:,['mesh_center_m','mesh_center_n']].values, - collection_name = 'subs' - ) - # drop this data - es_drop = set(df_zconc.columns).intersection({ - 'voxel_i', 'voxel_j', 'voxel_k', - 'mesh_center_m', 'mesh_center_n', 'mesh_center_p', - 'time', 'runtime', 'xmlfile', - }) - df_zconc = df_zconc.drop(es_drop, axis=1) - # add numerical data (no scaling) - for s_num in sorted(df_zconc.columns): - o_domain.add_labels( - label_name = s_num, - labels = df_zconc.loc[:,s_num], - add_labels_to = 'subs', - label_type = 'continuous', - ) - - ## handle cell collection - df_zcell = df_cell.loc[df_cell.voxel_k == i_klayer,:] - o_domain.add_points( - points = df_zcell.loc[:,['position_x','position_y']].values, - collection_name = 'cell' - ) - # get a physicell cell_id to muspan object id mapping - df_coor = df_zcell.loc[:,['position_x', 'position_y','position_z']] - df_coor['muspan_id'] = o_domain.collections['cell']['objects'] - di_cellid = df_coor['muspan_id'].to_dict() - # drop this data - es_drop = set(df_zcell.columns).intersection({ - 'voxel_i', 'voxel_j', 'voxel_k', - 'mesh_center_m', 'mesh_center_n', 'mesh_center_p', - 'position_x', 'position_y','position_z', - 'time', 'runtime', 'xmlfile', - }) - df_zcell = df_zcell.drop(es_drop, axis=1) - # dectect variable types - des_type = {'float': set(), 'int': set(), 'bool': set(), 'str': set()} - for _, se_zcell in df_zcell.items(): - if str(se_zcell.dtype).startswith('float'): - des_type['float'].add(se_zcell.name) - elif str(se_zcell.dtype).startswith('int'): - des_type['int'].add(se_zcell.name) - elif str(se_zcell.dtype).startswith('bool'): - des_type['bool'].add(se_zcell.name) - elif str(se_zcell.dtype).startswith('object') in str(se_zcell.dtype).startswith('str'): - des_type['str'].add(se_zcell.name) - else: - sys.exit(f'Error @ TimeStep.get_muspa : column {se_zcell.name} detected with unknown dtype {str(se_zcell.dtype)}.') - # add categorical data - for s_cat in sorted(des_type['str'].union(des_type['bool'])): - o_domain.add_labels( - label_name = s_cat, - labels = df_zcell.loc[:,s_cat], - add_labels_to = 'cell', - label_type = 'categorical', - ) - # add numerical data (no scaling) - for s_num in sorted(des_type['float'].union(des_type['int'])): - o_domain.add_labels( - label_name = s_num, - labels = df_zcell.loc[:,s_num], - add_labels_to = 'cell', - label_type = 'continuous', - ) - ## add graphs - ei_pccellid = set(df_zcell.index) - for s_graph, dei_graph in [ - ('neighbor', self.get_neighbor_graph_dict()), - ('attached', self.get_attached_graph_dict()), - ('spring', self.get_spring_graph_dict()), - ]: - # transform graph dict into weighted edge list - lt_wedge = [] - for i_src, ei_dst in sorted(dei_graph.items()): - for i_dst in ei_dst: - if (i_src in ei_pccellid) and (i_dst in ei_pccellid): - r_distance = ((df_coor.loc[i_src, ['position_x','position_y','position_z']].values - df_coor.loc[i_dst, ['position_x','position_y','position_z']].values)**2).sum()**(1/2) - lt_wedge.append((di_cellid[i_src], di_cellid[i_dst], r_distance)) - # generate graph - G = nx.Graph() - # dump the edges into the network - G.add_weighted_edges_from(lt_wedge, weight='Distance') - G.add_weighted_edges_from(lt_wedge, weight='Inverse Distance') - # add the network to the dictionary of networks - o_domain.networks[s_graph] = G - # clean up the domain - ms.helpers.clean_up(o_domain) + print('a_czyx_img shape:', a_czyx_img.shape) + # generate filename + s_channel = '' + for s_substrate in ls_substrate: + try: + r_value = conc_cutoff[s_substrate] + s_channel += f'_{s_substrate}{r_value}' + except KeyError: + s_channel += f'_{s_substrate}' + for s_celltype in ls_celltype: + s_channel += f'_{s_celltype}' + if len(ls_celltype) > 0: + s_channel += f'_{cell_attribute}' + s_tifffile = self.xmlfile.replace('.xml', f'{s_channel}.ome.tiff') + s_tifffile = s_tifffile.replace(' ','_') + if (len(s_tifffile) > 255): + print(f"Warning: filename {len(s_tifffile)} > 255 character.") + s_tifffile = self.xmlfile.replace('.xml', f'_channels.ome.tiff') + print(f"file name adjusted to {s_tifffile}.") + s_tiffpathfile = self.path + '/' + s_tifffile - ## set domain boundary (have to be done last!) - o_domain.estimate_boundary( - method='specify', - specify_boundary_coords=( - (self.get_xyz_range()[0][0], self.get_xyz_range()[1][0]), - (self.get_xyz_range()[0][0], self.get_xyz_range()[1][1]), - (self.get_xyz_range()[0][1], self.get_xyz_range()[1][1]), - (self.get_xyz_range()[0][1], self.get_xyz_range()[1][0]) - ) + # save to file + OmeTiffWriter.save( + a_czyx_img, + s_tiffpathfile, + dim_order = 'CZYX', + #ome_xml=x_img, + channel_names = ls_channel, + image_names = [s_tifffile.replace('.ome.tiff','')], + physical_pixel_sizes = bioio_base.types.PhysicalPixelSizes(self.get_voxel_spacing()[2], 1.0, 1.0), # z,y,x [um] + #channel_colors=, + #fs_kwargs={}, ) + return s_tiffpathfile - # update output - do_domain.update({s_domain : o_domain}) - # output - return do_domain + def render_neuroglancer(self, tiffpathfile, timestep=0, intensity_cmap='gray'): + """ + help(pcdl.render_neuroglancer) + try: mcds.render_neuroglancer(mcds.make_ome_tiff()) + """ + o_viewer = render_neuroglancer( + tiffpathfile = tiffpathfile, + timestep = timestep, + intensity_cmap = intensity_cmap, + ) + return o_viewer ## LOAD DATA ## diff --git a/pyproject.toml b/pyproject.toml index 7384b24..e6c800b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,24 +124,25 @@ full = [ [project.scripts] # special thanks to Miguel Ponce-de-Leon who introduced me to entry point scripts! +# initialize # metadata pcdl_get_version = "pcdl.commandline:get_version" pcdl_get_unit_dict = "pcdl.commandline:get_unit_dict" # substrate pcdl_get_substrate_list = "pcdl.commandline:get_substrate_list" -pcdl_get_conc_attribute = "pcdl.commandline:get_conc_attribute" pcdl_get_conc_df = "pcdl.commandline:get_conc_df" +pcdl_get_conc_attribute = "pcdl.commandline:get_conc_attribute" pcdl_plot_contour = "pcdl.commandline:plot_contour" pcdl_make_conc_vtk = "pcdl.commandline:make_conc_vtk" # cell agent pcdl_get_celltype_list = "pcdl.commandline:get_celltype_list" -pcdl_get_cell_attribute_list = "pcdl.commandline:get_cell_attribute_list" -pcdl_get_cell_attribute = "pcdl.commandline:get_cell_attribute" pcdl_get_cell_df = "pcdl.commandline:get_cell_df" +pcdl_get_cell_attribute = "pcdl.commandline:get_cell_attribute" +pcdl_get_cell_attribute_list = "pcdl.commandline:get_cell_attribute_list" pcdl_get_anndata = "pcdl.commandline:get_anndata" -pcdl_make_graph_gml = "pcdl.commandline:make_graph_gml" pcdl_plot_scatter = "pcdl.commandline:plot_scatter" pcdl_make_cell_vtk = "pcdl.commandline:make_cell_vtk" +pcdl_make_graph_gml = "pcdl.commandline:make_graph_gml" pcdl_make_simularium = "pcdl.commandline:make_simularium" # substrate and cell agent pcdl_get_muspan = "pcdl.commandline:get_muspan" From ca58c77aa4626707ac4fd32fdafde98c93d7024e Mon Sep 17 00:00:00 2001 From: bue Date: Mon, 31 Aug 2026 00:33:46 -0400 Subject: [PATCH 08/15] @ pcdl : in the middle of getting things in order. --- README.md | 17 +- man/TUTORIAL_commandline.md | 147 ++-- man/TUTORIAL_galaxy.md | 54 +- man/TUTORIAL_python3_muspan.md | 4 +- man/TUTORIAL_python3_pandas.md | 4 +- man/TUTORIAL_python3_timeseries.md | 51 +- man/TUTORIAL_python3_timestep.md | 92 +- man/TUTORIAL_simularium.md | 10 +- pcdl/commandline.py | 843 +++++++++--------- pcdl/timeseries.py | 1280 ++++++++++++++-------------- pcdl/timestep.py | 1136 ++++++++++++------------ pyproject.toml | 9 +- 12 files changed, 1852 insertions(+), 1795 deletions(-) diff --git a/README.md b/README.md index 7b9b075..e454953 100644 --- a/README.md +++ b/README.md @@ -48,15 +48,16 @@ Basics Tutorials: 1. [pcdl from the command line](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_commandline.md) Extras tutorials python3 language: + ++ [pcdl and python3 and graphs](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_graph.md) + [pcdl and python3 and json](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_json.md) ++ [pcdl and python3 and matplotlib](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_matplotlib.md) ++ [pcdl and python3 and muspan](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_muspan.md) ++ [pcdl and python3 and napari](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_napari.md) ++ [pcdl and python3 and ome.tiff, tiff, png, and jpeg](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_ometiff.md) + [pcdl and python3 and pandas](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_pandas.md) + [pcdl and python3 and scanpy and squidpy](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_scverse.md) -+ [pcdl and python3 and muspan](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_muspan.md) -+ [pcdl and python3 and graphs](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_graph.md) -+ [pcdl and python3 and matplotlib](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_matplotlib.md) + [pcdl and python3 and vtk](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_vtk.md) -+ [pcdl and python3 and ome.tiff, tiff, png, and jpeg](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_ometiff.md) -+ [pcdl and python3 and napari](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_napari.md) Extras tutorials for other languages than python3: @@ -67,10 +68,13 @@ Extras tutorials for other languages than python3: Extras tutorials for GUI software: + [pcdl and paraview](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_paraview.md) + + [pcdl and blender](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_blender.md) -+ [pcdl and napari](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_napari.md) + [pcdl and fiji imagej, icy, qupath](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_fijiimagej.md) ++ [pcdl and galaxy](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_galaxy.md) ++ [pcdl and napari](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_napari.md) + [pcdl and neuroglancer](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_neuroglancer.md) ++ [pcdl and simularium](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_simularium.md) Slides: @@ -127,6 +131,7 @@ Developers, please make pull requests to the https://github.com/elmbeech/physice ## Road Map: ++ downcast, to\file, and read function. + evt generate lineage tree graph output files. diff --git a/man/TUTORIAL_commandline.md b/man/TUTORIAL_commandline.md index c82d3c9..45b600a 100644 --- a/man/TUTORIAL_commandline.md +++ b/man/TUTORIAL_commandline.md @@ -77,51 +77,51 @@ pcdl_get_substrate_list output/output00000000.xml pcdl_get_substrate_list -h ``` -### ✨ pcdl\_get\_conc\_attribute +### ✨ pcdl\_get\_conc\_df -Generate a [json](https://en.wikipedia.org/wiki/JSON) file, that lists all substrate attributes. -For each such attribute the min and the max value are listed. +Generate a dataframe [csv](https://en.wikipedia.org/wiki/Comma-separated_values) file that lists one voxel per row, +all substrate concentrations. -In the example below: -+ all substrates attributes are listed, that over the whole time series have at least 2 diffrent values. -+ all substrates attributes are listed, that in this particular time step have at least 2 different values. +In the example below, the generated csv contains: ++ all substrate concentration values over the whole time series. ++ all substrate that within this particular time step have more than 2 different concentration values. ```bash -pcdl_get_conc_attribute output 2 +pcdl_get_conc_df output ``` ```bash -pcdl_get_conc_attribute output/output00000000.xml 2 +pcdl_get_conc_df output/output00000000.xml 2 ``` ```bash -pcdl_get_conc_attribute -h +pcdl_get_conc_df -h ``` Further readings: -+ [TUTORIAL_python3_json.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_json.md) ++ [TUTORIAL_python3_pandas.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_pandas.md) + [TUTORIAL_r.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_r.md) + [TUTORIAL_julia.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_julia.md) -### ✨ pcdl\_get\_conc\_df +### ✨ pcdl\_get\_conc\_attribute -Generate a dataframe [csv](https://en.wikipedia.org/wiki/Comma-separated_values) file that lists one voxel per row, -all substrate concentrations. +Generate a [json](https://en.wikipedia.org/wiki/JSON) file, that lists all substrate attributes. +For each such attribute the min and the max value are listed. -In the example below, the generated csv contains: -+ all substrate concentration values over the whole time series. -+ all substrate that within this particular time step have more than 2 different concentration values. +In the example below: ++ all substrates attributes are listed, that over the whole time series have at least 2 diffrent values. ++ all substrates attributes are listed, that in this particular time step have at least 2 different values. ```bash -pcdl_get_conc_df output +pcdl_get_conc_attribute output 2 ``` ```bash -pcdl_get_conc_df output/output00000000.xml 2 +pcdl_get_conc_attribute output/output00000000.xml 2 ``` ```bash -pcdl_get_conc_df -h +pcdl_get_conc_attribute -h ``` Further readings: -+ [TUTORIAL_python3_pandas.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_pandas.md) ++ [TUTORIAL_python3_json.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_json.md) + [TUTORIAL_r.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_r.md) + [TUTORIAL_julia.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_julia.md) @@ -183,20 +183,30 @@ pcdl_get_celltype_list output/output00000000.xml pcdl_get_celltype_list -h ``` -### ✨ pcdl\_get\_cell\_attrribute\_list +### ✨ pcdl\_get\_cell\_df -Output all recorded cell attributes. +Generate a dataframe [csv](https://en.wikipedia.org/wiki/Comma-separated_values) file that lists one cell per row, +all attributes. + +In the example below, the generated csv contains: ++ all cell attributes, that over the whole time series have more than 2 different values. ++ from that particular time step all cell attributes available. ```bash -pcdl_get_cell_attribute_list output +pcdl_get_cell_df output 2 ``` ```bash -pcdl_get_cell_attribute_list output/output00000000.xml +pcdl_get_cell_df output/output00000000.xml ``` ```bash -pcdl_get_cell_attribute_list -h +pcdl_get_cell_df -h ``` +Further readings: ++ [TUTORIAL_python3_pandas.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_pandas.md) ++ [TUTORIAL_r.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_r.md) ++ [TUTORIAL_julia.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_julia.md) + ### ✨ pcdl\_get\_cell\_attribute Generate a [json](https://en.wikipedia.org/wiki/JSON) file, that lists all cell attributes. @@ -221,30 +231,20 @@ Further readings: + [TUTORIAL_r.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_r.md) + [TUTORIAL_julia.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_julia.md) -### ✨ pcdl\_get\_cell\_df - -Generate a dataframe [csv](https://en.wikipedia.org/wiki/Comma-separated_values) file that lists one cell per row, -all attributes. +### ✨ pcdl\_get\_cell\_attrribute\_list -In the example below, the generated csv contains: -+ all cell attributes, that over the whole time series have more than 2 different values. -+ from that particular time step all cell attributes available. +Output all recorded cell attributes. ```bash -pcdl_get_cell_df output 2 +pcdl_get_cell_attribute_list output ``` ```bash -pcdl_get_cell_df output/output00000000.xml +pcdl_get_cell_attribute_list output/output00000000.xml ``` ```bash -pcdl_get_cell_df -h +pcdl_get_cell_attribute_list -h ``` -Further readings: -+ [TUTORIAL_python3_pandas.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_pandas.md) -+ [TUTORIAL_r.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_r.md) -+ [TUTORIAL_julia.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_julia.md) - ### ✨ pcdl\_get\_anndata From the whole time series or from a single time step, generate h5ad [anndata](https://anndata.readthedocs.io/en/latest/) [hd5](https://en.wikipedia.org/wiki/Hierarchical_Data_Format) files. @@ -267,28 +267,6 @@ Further readings: + [TUTORIAL_r.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_r.md) + [TUTORIAL_julia.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_julia.md) -### ✨ pcdl\_make\_graph\_gml - -Generate [gml](https://github.com/elmbeech/physicelldataloader/blob/master/man/publication/himsolt1996gml_a_portable_graph_file_format.pdf) files. -One gml file per time step. - -Gml files can be read by graph analysis libraries like [networkx](https://networkx.org/) and [igraph](https://igraph.org/). - -```bash -pcdl_make_graph_gml output/output00000000.xml --node_attribute cell_type dead oxygen pressure -``` -```bash -pcdl_make_graph_gml output -``` -```bash -pcdl_make_graph_gml -h -``` - -Further readings: -+ [TUTORIAL_python3_graph.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_graph.md) -+ [TUTORIAL_r.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_r.md) -+ [TUTORIAL_julia.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_julia.md) - ### ✨ pcdl\_plot\_scatter Generate a [jpeg](https://en.wikipedia.org/wiki/JPEG) file that displaying all cells. @@ -334,6 +312,51 @@ Further readings: + [TUTORIAL_python3_vtk.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_vtk.md) + [TUTORIAL_julia.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_julia.md) +### ✨ pcdl\_make\_graph\_gml + +Generate [gml](https://github.com/elmbeech/physicelldataloader/blob/master/man/publication/himsolt1996gml_a_portable_graph_file_format.pdf) files. +One gml file per time step. + +Gml files can be read by graph analysis libraries like [networkx](https://networkx.org/) and [igraph](https://igraph.org/). + +```bash +pcdl_make_graph_gml output/output00000000.xml --node_attribute cell_type dead oxygen pressure +``` +```bash +pcdl_make_graph_gml output +``` +```bash +pcdl_make_graph_gml -h +``` + +Further readings: ++ [TUTORIAL_python3_graph.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_graph.md) ++ [TUTORIAL_r.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_r.md) ++ [TUTORIAL_julia.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_julia.md) + +### ✨ pcdl\_make\_simularium + +Generate [simularium](https://simularium.allencell.org/) trajectory viewer files from time series, +with categorical information from up to two attributes for the attributes listed. +The default attributes listed is cell\_type and current\_phase. + +These simularium files can be analyzed with the online simularium viewer +as described in the extra tutorials. ++ https://simularium.allencell.org/ + +```bash +pcdl_make_simularium output +``` +```bash +pcdl_make_simularium output cell_type dead --tt helloworld +``` +```bash +pcdl_make_simularium -h +``` + +Further readings: ++ [TUTORIAL_simularium.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_simularium.md) + ## Microenvironment and cell agent related commands diff --git a/man/TUTORIAL_galaxy.md b/man/TUTORIAL_galaxy.md index a3033d6..fc1d591 100644 --- a/man/TUTORIAL_galaxy.md +++ b/man/TUTORIAL_galaxy.md @@ -11,45 +11,45 @@ Special thanks to Björn Grüning, who helped me to port pcdl into Galaxy! ## Run PhysiCell Studio on Galaxy 1. In your web browser, open the European instance of Galaxy: https://usegalaxy.eu/ -2. Optionally, log into your user account. Create one if you do not already have one. -3. Near the top left corner, click "Tools". -4. Near the left-side top corner, in the "Tools" search bar, type "PhysiCell Studio". -5. Choose "PhysiCell Studio". -6. Choose "Run Tool" (center panel or top right). -7. How to load, develop, or run a PhysiCell model in the studio can be learned here: +1. Optionally, log into your user account. Create one if you do not already have one. +1. Near the top left corner, click "Tools". +1. Near the left-side top corner, in the "Tools" search bar, type "PhysiCell Studio". +1. Choose "PhysiCell Studio". +1. Choose "Run Tool" (center panel or top right). +1. How to load, develop, or run a PhysiCell model in the studio can be learned here: + https://physicell-studio.readthedocs.io/en/latest/index.html + https://github.com/PhysiCell-Tools/PhysiCell-Studio -8. [TKBue] +1. [TKBue] ## Upload local PhysiCell output to Galaxy 1. In your web browser, open the European instance of Galaxy: https://usegalaxy.eu/ -2. Optionally, log into your user account. Create one if you do not already have one. -3. Near the top left corner, click "Upload" (New upload Beta). -4. Near the top left corner, choose "Upload from Computer". -5. In the center panel click "Brows Files". -6. Click your self through to the PhysiCell output folder. -7. Click on one file in the output folder, then on your keyboard press Ctrl + A to choose all files. -8. Click "Open". After a while the filenames will appear in the central panel. -9. On the center panel bottom, flip the switch "Create a collection from these files" on. -10. "Enter a collection name" (e.g. output) and choose "Collection Type" "List". -11. Click "Start". -12. After a while, the uploaded data collection should appear in the history in the right-side panel. +1. Optionally, log into your user account. Create one if you do not already have one. +1. Near the top left corner, click "Upload" (New upload Beta). +1. Near the top left corner, choose "Upload from Computer". +1. In the center panel click "Brows Files". +1. Click your self through to the PhysiCell output folder. +1. Click on one file in the output folder, then on your keyboard press Ctrl + A to choose all files. +1. Click "Open". After a while the filenames will appear in the central panel. +1. On the center panel bottom, flip the switch "Create a collection from these files" on. +1. "Enter a collection name" (e.g. output) and choose "Collection Type" "List". +1. Click "Start". +1. After a while, the uploaded data collection should appear in the history in the right-side panel. ## The pcdl tools on Galaxy 1. In your web browser, open the European instance of Galaxy: https://usegalaxy.eu/ -2. Optionally, log into your user account. Create one if you do not already have one. -3. Upload or generate PhysiCell output as described above. -4. Near the top left corner, click "Tools". -5. Near the left-side top corner, in the "Tools" search bar, type "pcdl". -6. Choose one of the pcdl_ tools (e.g. pcdl\_get\_version). -7. Mouse drag and drop the uploaded or generated PhysiCell output data collection in the left-side history panel into the "essential:" "data collection" field in the middle panel. -8. Optionally, tweak the "essential" and "advanced:" parameters. -9. Click "Run Tool". -10. After a while, the result file(s) should appear in the left-side history panel. +1. Optionally, log into your user account. Create one if you do not already have one. +1. Upload or generate PhysiCell output as described above. +1. Near the top left corner, click "Tools". +1. Near the left-side top corner, in the "Tools" search bar, type "pcdl". +1. Choose one of the pcdl_ tools (e.g. pcdl\_get\_version). +1. Mouse drag and drop the uploaded or generated PhysiCell output data collection in the left-side history panel into the "essential:" "data collection" field in the middle panel. +1. Optionally, tweak the "essential" and "advanced:" parameters. +1. Click "Run Tool". +1. After a while, the result file(s) should appear in the left-side history panel. ### ✨ Further readings diff --git a/man/TUTORIAL_python3_muspan.md b/man/TUTORIAL_python3_muspan.md index 5ab0054..ad76fc7 100644 --- a/man/TUTORIAL_python3_muspan.md +++ b/man/TUTORIAL_python3_muspan.md @@ -21,9 +21,9 @@ import muspan as ms mcdsts = pcdl.TimeSeries('output/') do_domain = mcdsts.get_muspan() # translate the mcds time seris into a dictionary of muspan domain objects. -ls_domain = sorted(do_domain.keys()) # generate an orderes list of domain names +ls_domain = sorted(do_domain.keys()) # generate an orderes list of domain names -print(ls_domain) # print a list of domain names +print(ls_domain) # print a list of domain names print(do_domain[ls_domain[0]]) # take a look at the first domain in the ls_domain list. ``` diff --git a/man/TUTORIAL_python3_pandas.md b/man/TUTORIAL_python3_pandas.md index 3d9d354..06bde9d 100644 --- a/man/TUTORIAL_python3_pandas.md +++ b/man/TUTORIAL_python3_pandas.md @@ -19,8 +19,8 @@ I recommend working through the pandas cookbook from Julia Evens: The pandas library as such is very well documented. Please spend some time to familiarize yourself with the homepage, the users guides, and the API reference. + https://pandas.pydata.org/ -+ http://pandas.pydata.org/pandas-docs/stable/user_guide/index.html -+ http://pandas.pydata.org/pandas-docs/stable/reference/index.html ++ https://pandas.pydata.org/pandas-docs/stable/user_guide/index.html ++ https://pandas.pydata.org/pandas-docs/stable/reference/index.html ## Dump pcdl data construct from the command line into a csv file diff --git a/man/TUTORIAL_python3_timeseries.md b/man/TUTORIAL_python3_timeseries.md index 7184acf..6dab2ed 100644 --- a/man/TUTORIAL_python3_timeseries.md +++ b/man/TUTORIAL_python3_timeseries.md @@ -6,7 +6,7 @@ And perhaps, work thorough the [TUTORIAL_python3_timestep.md](https://github.com An exciting thing about modeling is to have time series data. -Pcdl's TimeSeries class is here to make the handling of a time series of MCD snapshots easy. +Pcdl's TimeSeries class is here to make the handling of a time series of MCDS snapshots easy. All analysis functions available for TimeStep are available for TimeSeries too, and we will not further discuss them here. @@ -38,17 +38,23 @@ mcdsts.get_cell_df() mcdsts.get_anndata() ``` ```python -mcdsts.make_graph_gml() -``` -```python mcdsts.plot_scatter() ``` ```python mcdsts.make_cell_vtk() ``` +```python +mcdsts.make_graph_gml() +``` For microenvironment and cell data, these are the functions: ```python +mcdsts.get_muspan() +``` +```python +mcdsts.get_spatialdata() +``` +```python mcdsts.make_ome_tiff() ``` ```python @@ -69,13 +75,15 @@ For microenvironment data, this is the function: For cell data, this is the function: + `mcdsts.get_cell_attribute()` ++ `mcdsts.make_simularium()` For microenvironment and cell data, this is the function: ++ `mcdsts.get_sdmcds_list()` + `mcdsts.plot_timeseries()` Besides, there are functions to render a set of jpeg, png, or tiff images into a movie. -+ `mcdsts.make_movie()` and `pcdl.make_movie()` + `mcdsts.make_gif()` and `pcdl.make_gif()` ++ `mcdsts.make_movie()` and `pcdl.make_movie()` @@ -102,8 +110,8 @@ python3 -c"import pathlib, pcdl, shutil; pcdl.install_data(); s_ipath=str(pathli Like in the TimeStep class, for memory consumption and processing speed control, -we can specify if we want to load microenvironment data and graph data from the snapshots we later on analyze. -Additionally, we can specify, if for first even want to load data at all, +we can specify if we want to load microenvironment data, graph data, and possible physiboss data from the snapshots we later on analyze. +Additionally, we can specify, if we for first even want to load data at all, or if we only would like to load the output xml file list, which we will see, can be manipulated before actual data is loaded. By default, all data from all snapshots is loaded. @@ -117,13 +125,13 @@ mcdsts = pcdl.TimeSeries('output/') ``` Fine tuned what data from a time step will be loaded -Here we only load cell data, not even information about cell type ID:label mapping. +Here we only load cell data. ```python import pcdl # the physicell data loader library, print('pcdl version:', pcdl.__version__) # it is easy to figure out which pcdl version you run -mcdsts = pcdl.TimeSeries(s_path, custom_data_type={}, load=True, microenv=False, graph=False, settingxml=None) +mcdsts = pcdl.TimeSeries(s_path, custom_data_type={}, load=True, microenv=False, graph=False, physiboss=False) ``` Fine tuned which time steps are loaded. @@ -167,7 +175,7 @@ mcdsts.read_mcds(ls_xml_even) len(mcdsts.get_mcds_list()) # 13 ``` -Single snapshots can now be accessed by indexing. +Single snapshots can be accessed by indexing. With a single snapshot, you may work exactly in the same way as with an object loaded by TimeStep. ```python @@ -181,14 +189,18 @@ for mcds in mcdsts.get_mcds_list(): print(mcds.get_mcds_list().get_time()) ``` -If you translate your time series into [anndata]((https://anndata.readthedocs.io/en/latest/) Objects, -using the collapse=False argument, you will endup with a similar list for anndata objects. +If you translate your time series into [anndata]((https://anndata.readthedocs.io/en/latest/) objects, +using the collapse=False argument, +or into [spatialdata](https://spatialdata.scverse.org/en/stable/) objects, +you will endup with a similar list for anndata or spatialdata objects, respective. ```python mcdsts.get_anndata(values=2, scale='maxabs', collapse=False) mcdsts.get_annmcds_list() # [AnnData object with n_obs × n_vars ..., ..., ...] ``` - +```python +mcdsts.get_spatialdata(values=2, scale='maxabs', collapse=False) +mcdsts.get_sdmcds_list() # [SpatialData object ..., ..., ...] ## Microenvironment Data Related Functions (Continuum Variables) @@ -269,6 +281,19 @@ help(mcdsts.get_cell_attribute) ``` +### ✨ Cell Data Analysis with [Simularium](https://simularium.allencell.org/) + +For cell agent trajectory visualization in 3D a **simularium file** can be retrieved.\ +This file can be analyzed with the online simularium viewer. ++ https://simularium.allencell.org/ + +```python +mcdsts.make_simularium() +``` + +Please have a look at [TUTORIAL_simularium.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_simularium.md) to learn more. + + ## Microenvironment and Cell Data Related Functions diff --git a/man/TUTORIAL_python3_timestep.md b/man/TUTORIAL_python3_timestep.md index 184233c..0a2c279 100644 --- a/man/TUTORIAL_python3_timestep.md +++ b/man/TUTORIAL_python3_timestep.md @@ -32,7 +32,7 @@ python3 -c"import pathlib, pcdl, shutil; pcdl.install_data(); s_ipath=str(pathli By default, all data related to the snapshot is loaded. For speed and less memory usage, it is however possible to only load the essential (output xml and cell mat data), -and exclude microenvironment, graph data, PhysiBoss data, and the PhysiCell\_settings.xml cell type ID label mapping. \ +and exclude microenvironment data, graph data, and possible PhysiBoss data. \ For custom\_data variables it is possible to specify data types, apart from the generic float type, namely: int, bool, and str. This can be done too, after the data is loaded, using the mcds.custom\_data\_astype function. \ For paths, in general, unix (slash) and windows (backslash) notation will work. @@ -134,7 +134,7 @@ mcds.get_runtime() # will return a float value like 15.596373 mcds.get_timestamp() # will return a sting like 2022-10-19T01:12:01Z ``` -Finally, it is possible to retrieve a dictionary that lists all units from all tracked variables, from metadata, mesh, continuum\_variables, and discrete\_cells. +Finally, it is possible to retrieve a dictionary that lists all units from all tracked variables, from metadata, mesh, substrate, and cells. ```python mcds.get_unit_dict() # will return a dictionary, which maps the variables to the units specified in the setting.xml. @@ -232,7 +232,7 @@ This file can be analyzed, for example with the [Paraview](https://en.wikipedia. mcds.make_conc_vtk() ``` -Please have a look at [TUTORIAL_paraview.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_paraview.md) to learn more. +Please have a look at [pcdl and python3 and vtk](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_vtk.md) and [TUTORIAL_paraview.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_paraview.md) to learn more. @@ -352,6 +352,44 @@ And we have cell neighbor graph infromation (obsp, uns). Please have a look at [TUTORIAL_python3_scverse.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_scverse.md) to learn more. +### ✨ Cell Data Analysis with [Matplotlib](https://matplotlib.org/) + +For cell agent visualization **matplotlib scatter plots**, +for any cell attribute, through any z\_slice can be retrieved. \ +The mcds.plot\_scatter function has many parameters to fine tune the plot. +Please have a look at its docstring to learn more. + +```python +fig = mcds.plot_scatter() +fig.show() +``` +```python +fig = mcds.plot_scatter('pressure') +fig.show() +``` +```python +help(mcds.plot_scatter) +``` + +The mcds.plot\_contour function output can combine with the mcds.plot\_contour output. \ +Please have a look at [TUTORIAL_python3_matplotlib.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_matplotlib.md) to learn more. + + +### ✨ Cell Data Analysis with [Vtk](https://vtk.org/) + +For cell agent visualization in 3D **glyph vtk files** can be retrieved.\ +This files can be analyzed, for example with the [Paraview](https://en.wikipedia.org/wiki/ParaView) software. + +```python +mcds.make_cell_vtk() +``` +```python +help(mcds.make_cell_vtk) +``` + +Please have a look at [pcdl and python3 and vtk](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_vtk.md) and [TUTORIAL_paraview.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_paraview.md) to learn more. + + ### ✨ Cell Data Analysis within the [Networkx](https://networkx.org/) and [Igraph](https://igraph.org/) Fore each cell, PhysiCell tracks the cell neighborhood (if cells touch each other, if cells are attached to each other). @@ -381,51 +419,35 @@ Cell lineage tree ```python mcds.make_graph_gml('lineage') ``` + +BUE 20260831 missing: +```python +mcds.get_attached_graph_dict() +mcds.get_neighbor_graph_dict() +mcds.get_spring_graph_dict() +``` --> Please have a look at [TUTORIAL_python3_graph.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_graph.md) to learn more. -### ✨ Cell Data Analysis with [Matplotlib](https://matplotlib.org/) - -For cell agent visualization **matplotlib scatter plots**, -for any cell attribute, through any z\_slice can be retrieved. \ -The mcds.plot\_scatter function has many parameters to fine tune the plot. -Please have a look at its docstring to learn more. +## Microenvironment and Cell Data Related Functions + -The mcds.plot\_contour function output can combine with the mcds.plot\_contour output. \ -Please have a look at [TUTORIAL_python3_matplotlib.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_matplotlib.md) to learn more. - - -### ✨ Cell Data Analysis with [Vtk](https://vtk.org/) - -For cell agent visualization in 3D **glyph vtk files** can be retrieved.\ -This files can be analyzed, for example with the [Paraview](https://en.wikipedia.org/wiki/ParaView) software. + -Please have a look at [TUTORIAL_paraview.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_paraview.md) to learn more. - - - -## Microenvironment and Cell Data Related Functions ### ✨ PhysiCell Data Analysis with [Napari](https://napari.org/stable/), [Fiji Imagej](https://fiji.sc/), [Neuroglancer](https://research.google/blog/an-interactive-automated-3d-reconstruction-of-a-fly-brain/), and similar software. diff --git a/man/TUTORIAL_simularium.md b/man/TUTORIAL_simularium.md index 82968e8..bde5bda 100644 --- a/man/TUTORIAL_simularium.md +++ b/man/TUTORIAL_simularium.md @@ -47,11 +47,11 @@ help(mcdsts.make_simularium()) ### ✨ simularium viewer 1. Open the simularium viewer web page: https://simularium.allencell.org/ -2. Near the top right corner, click the "Load models" dropdown menu and choose "Simularium file". -3. In the popup window choose "From your device" "Select File". -4. Select the simularium file and click "Open". -5. Click "Load". -6. Explore the loaded file. +1. Near the top right corner, click the "Load models" dropdown menu and choose "Simularium file". +1. In the popup window choose "From your device" "Select File". +1. Select the simularium file and click "Open". +1. Click "Load". +1. Explore the loaded file. ### ✨ Further readings' diff --git a/pcdl/commandline.py b/pcdl/commandline.py index 195e09a..18cca4f 100644 --- a/pcdl/commandline.py +++ b/pcdl/commandline.py @@ -250,11 +250,11 @@ def get_substrate_list(): return 0 -def get_conc_attribute(): +def get_conc_df(): # argv parser = argparse.ArgumentParser( - prog = 'pcdl_get_conc_attribute', - description = 'function to detect informative substrate concentration variables in a time series. this function detects even variables which have less than the minimal state count in each time step, but different values from time step to time step. the output is a json file with an entry of all non-coordinate column names that, at least in one of the time steps or in between time steps, reach the given minimal value count. key is the column name, mapped is a list of all values (bool, str, and, if allvalues is True, int and float) or a list with minimum and maximum values (int, float).', + prog = 'pcdl_get_conc_df', + description = 'this function extracts dataframes with concentration values for all chemical species in all voxels and saves them as csv files. additionally, this dataframe lists voxel and mesh center coordinates.', epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', ) @@ -263,7 +263,7 @@ def get_conc_attribute(): 'path', nargs = '?', default = '.', - help = 'path to the PhysiCell output directory. default is . .', + help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .', ) # TimeSeries output_path '.' # TimeSeries custom_data_type nop @@ -277,7 +277,7 @@ def get_conc_attribute(): default = 'true', help = 'setting verbose to False for less text output, while processing. default is True.', ) - # get_conc_attribute values + # get_conc_df values parser.add_argument( 'values', nargs = '?', @@ -285,25 +285,25 @@ def get_conc_attribute(): type = int, help = 'minimal number of values a variable has to have in any of the mcds time steps to be outputted. variables that have only 1 state carry no information. None is a state too. default is 1.', ) - # get_conc_attribute drop + # get_conc_df drop parser.add_argument( '--drop', nargs = '*', default = [], help = "set of column labels to be dropped for the dataframe. don't worry: essential columns like ID, coordinates and time will never be dropped. Attention: when the keep parameter is given, then the drop parameter has to be an empty string! default is an empty string.", ) - # get_conc_attribute keep + # get_conc_df keep parser.add_argument( '--keep', nargs = '*', default = [], help = "set of column labels to be kept in the dataframe. set values=1 to be sure that all variables are kept. don't worry: essential columns like ID, coordinates and time will always be kept. default is an empty string.", ) - # get_conc_attribute allvalues + # get_conc_df collapse parser.add_argument( - '--allvalues', - default = 'false', - help = 'for numeric data, should only the min and max values or all values be returned? default is false.', + '--collapse', + default = 'true', + help = 'should all mcds time steps from the time series be collapsed into one big csv, or a many csv, one for each time step? default is True.' ) # parse arguments @@ -322,42 +322,69 @@ def get_conc_attribute(): else: s_path = '/'.join(s_path.split('/')[:-1]) if not os.path.exists(s_pathfile): - sys.exit(f'Error @ pcdl_get_conc_attribute : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') + sys.exit(f'Error @ pcdl_get_conc_df : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') # run - mcdsts = pcdl.TimeSeries( - output_path = s_path, - #custom_data_type, - load = True, - microenv = True, - graph = False, - physiboss = False, - settingxml = None, - verbose = False if args.verbose.lower().startswith('f') else True, - ) - s_values = 'minmax' - b_allvalues = True if args.allvalues.lower().startswith('t') else False - if b_allvalues: - s_values = 'all' - dl_variable = mcdsts.get_conc_attribute( - values = args.values, - drop = set(args.drop), - keep = set(args.keep), - allvalues = b_allvalues, - ) - s_ofile = f"timeseries_conc_attribute_{s_values.replace(' ','_')}.json" - s_opathfile = s_path + '/' + s_ofile - json.dump(dl_variable, open(s_opathfile, 'w'), sort_keys=True) + if os.path.isfile(args.path): + mcds = pcdl.TimeStep( + xmlfile = s_pathfile, + output_path = '.', + #custom_data_type, + microenv = True, + graph = False, + physiboss = False, + settingxml = None, + verbose = False if args.verbose.lower().startswith('f') else True + ) + df_conc = mcds.get_conc_df( + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + ) + # going home + s_opathfile = s_pathfile.replace('.xml','_conc.csv') + df_conc.to_csv(s_opathfile) + print(s_opathfile) + + else: + mcdsts = pcdl.TimeSeries( + output_path = s_path, + #custom_data_type, + load = True, + microenv = True, + graph = False, + physiboss = False, + settingxml = None, + verbose = False if args.verbose.lower().startswith('f') else True, + ) + # handle collaps + b_collapse = False if args.collapse.lower().startswith('f') else True + ldf_conc = mcdsts.get_conc_df( + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + collapse = b_collapse, + ) + # going home + if b_collapse: + s_opathfile = f'{s_path}/timeseries_conc.csv' + ldf_conc.to_csv(s_opathfile) + print(s_opathfile) + else: + ls_opathfile = [f"{s_path}/{s_xmlfile.replace('.xml','_conc.csv')}" for s_xmlfile in mcdsts.get_xmlfile_list()] + for i, df_conc in enumerate(ldf_conc): + df_conc.to_csv(ls_opathfile[i]) + print(ls_opathfile) + # going home - print(s_opathfile) return 0 -def get_conc_df(): +def get_conc_attribute(): # argv parser = argparse.ArgumentParser( - prog = 'pcdl_get_conc_df', - description = 'this function extracts dataframes with concentration values for all chemical species in all voxels and saves them as csv files. additionally, this dataframe lists voxel and mesh center coordinates.', + prog = 'pcdl_get_conc_attribute', + description = 'function to detect informative substrate concentration variables in a time series. this function detects even variables which have less than the minimal state count in each time step, but different values from time step to time step. the output is a json file with an entry of all non-coordinate column names that, at least in one of the time steps or in between time steps, reach the given minimal value count. key is the column name, mapped is a list of all values (bool, str, and, if allvalues is True, int and float) or a list with minimum and maximum values (int, float).', epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', ) @@ -366,7 +393,7 @@ def get_conc_df(): 'path', nargs = '?', default = '.', - help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .', + help = 'path to the PhysiCell output directory. default is . .', ) # TimeSeries output_path '.' # TimeSeries custom_data_type nop @@ -380,7 +407,7 @@ def get_conc_df(): default = 'true', help = 'setting verbose to False for less text output, while processing. default is True.', ) - # get_conc_df values + # get_conc_attribute values parser.add_argument( 'values', nargs = '?', @@ -388,25 +415,25 @@ def get_conc_df(): type = int, help = 'minimal number of values a variable has to have in any of the mcds time steps to be outputted. variables that have only 1 state carry no information. None is a state too. default is 1.', ) - # get_conc_df drop + # get_conc_attribute drop parser.add_argument( '--drop', nargs = '*', default = [], help = "set of column labels to be dropped for the dataframe. don't worry: essential columns like ID, coordinates and time will never be dropped. Attention: when the keep parameter is given, then the drop parameter has to be an empty string! default is an empty string.", ) - # get_conc_df keep + # get_conc_attribute keep parser.add_argument( '--keep', nargs = '*', default = [], help = "set of column labels to be kept in the dataframe. set values=1 to be sure that all variables are kept. don't worry: essential columns like ID, coordinates and time will always be kept. default is an empty string.", ) - # get_conc_df collapse + # get_conc_attribute allvalues parser.add_argument( - '--collapse', - default = 'true', - help = 'should all mcds time steps from the time series be collapsed into one big csv, or a many csv, one for each time step? default is True.' + '--allvalues', + default = 'false', + help = 'for numeric data, should only the min and max values or all values be returned? default is false.', ) # parse arguments @@ -425,61 +452,34 @@ def get_conc_df(): else: s_path = '/'.join(s_path.split('/')[:-1]) if not os.path.exists(s_pathfile): - sys.exit(f'Error @ pcdl_get_conc_df : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') + sys.exit(f'Error @ pcdl_get_conc_attribute : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') # run - if os.path.isfile(args.path): - mcds = pcdl.TimeStep( - xmlfile = s_pathfile, - output_path = '.', - #custom_data_type, - microenv = True, - graph = False, - physiboss = False, - settingxml = None, - verbose = False if args.verbose.lower().startswith('f') else True - ) - df_conc = mcds.get_conc_df( - values = args.values, - drop = set(args.drop), - keep = set(args.keep), - ) - # going home - s_opathfile = s_pathfile.replace('.xml','_conc.csv') - df_conc.to_csv(s_opathfile) - print(s_opathfile) - - else: - mcdsts = pcdl.TimeSeries( - output_path = s_path, - #custom_data_type, - load = True, - microenv = True, - graph = False, - physiboss = False, - settingxml = None, - verbose = False if args.verbose.lower().startswith('f') else True, - ) - # handle collaps - b_collapse = False if args.collapse.lower().startswith('f') else True - ldf_conc = mcdsts.get_conc_df( - values = args.values, - drop = set(args.drop), - keep = set(args.keep), - collapse = b_collapse, - ) - # going home - if b_collapse: - s_opathfile = f'{s_path}/timeseries_conc.csv' - ldf_conc.to_csv(s_opathfile) - print(s_opathfile) - else: - ls_opathfile = [f"{s_path}/{s_xmlfile.replace('.xml','_conc.csv')}" for s_xmlfile in mcdsts.get_xmlfile_list()] - for i, df_conc in enumerate(ldf_conc): - df_conc.to_csv(ls_opathfile[i]) - print(ls_opathfile) - + mcdsts = pcdl.TimeSeries( + output_path = s_path, + #custom_data_type, + load = True, + microenv = True, + graph = False, + physiboss = False, + settingxml = None, + verbose = False if args.verbose.lower().startswith('f') else True, + ) + s_values = 'minmax' + b_allvalues = True if args.allvalues.lower().startswith('t') else False + if b_allvalues: + s_values = 'all' + dl_variable = mcdsts.get_conc_attribute( + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + allvalues = b_allvalues, + ) + s_ofile = f"timeseries_conc_attribute_{s_values.replace(' ','_')}.json" + s_opathfile = s_path + '/' + s_ofile + json.dump(dl_variable, open(s_opathfile, 'w'), sort_keys=True) # going home + print(s_opathfile) return 0 @@ -832,6 +832,7 @@ def make_conc_vtk(): # going home return 0 + ############################################ # cell agent relatd command line functions # ############################################ @@ -903,11 +904,11 @@ def get_celltype_list(): return 0 -def get_cell_attribute_list(): +def get_cell_df(): # argv parser = argparse.ArgumentParser( - prog = 'pcdl_get_cell_attribute_list', - description = 'this function is returns a list with all cell attribute labels, alphabetically ordered.', + prog = 'pcdl_get_cell_df', + description = 'this function extracts dataframes with a cell centric view of the simulation and saves them as csv files.', epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', ) @@ -916,22 +917,22 @@ def get_cell_attribute_list(): 'path', nargs = '?', default = '.', - help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .', + help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .' ) # TimeSeries output_path '.' - # TimeSeries custom_data_type nop + # TimeSeries custom_data_type nop (because datafarme is straightaway saved as csv) # TimeSeries microenv parser.add_argument( '--microenv', default = 'true', - help = 'should the microenvironment data be loaded? setting microenv to False will use less memory and speed up processing. default is True.', + help = 'should the microenvironment data be loaded? setting microenv to False will use less memory and speed up processing. default is True.' ) # TimeSeries graph False # TimeSeries physiboss parser.add_argument( '--physiboss', default = 'true', - help = 'if found, should physiboss state data be extracted and loaded into df_cell dataframe? default is True.' + help = 'if found, should physiboss state data be extracted and loaded into the df_cell dataframe? default is True.' ) # TimeSeries settingxml parser.add_argument( @@ -942,8 +943,36 @@ def get_cell_attribute_list(): # TimeSeries verbose parser.add_argument( '-v', '--verbose', - default = 'false', - help = 'setting verbose to True for more text output, while processing. default is False.', + default = 'true', + help = 'setting verbose to False for less text output, while processing. default is True.' + ) + # get_cell_df values + parser.add_argument( + 'values', + nargs = '?', + default = 1, + type = int, + help = 'minimal number of values a variable has to have in any of the mcds time steps to be outputted. variables that have only 1 state carry no information. None is a state too. default is 1.' + ) + # get_cell_df drop + parser.add_argument( + '--drop', + nargs = '*', + default = [], + help = "set of column labels to be dropped for the dataframe. don't worry: essential columns like ID, coordinates and time will never be dropped. Attention: when the keep parameter is given, then the drop parameter has to be an empty string! default is an empty string." + ) + # get_cell_df keep + parser.add_argument( + '--keep', + nargs = '*', + default = [], + help = "set of column labels to be kept in the dataframe. set values=1 to be sure that all variables are kept. don't worry: essential columns like ID, coordinates and time will always be kept. default is an empty string." + ) + # get_cell_df collapse + parser.add_argument( + '--collapse', + default = 'true', + help = 'should all mcds time steps from the time series be collapsed into one big csv, or a many csv, one csv for each time step?, default is True.' ) # parse arguments @@ -962,22 +991,62 @@ def get_cell_attribute_list(): else: s_path = '/'.join(s_path.split('/')[:-1]) if not os.path.exists(s_pathfile): - sys.exit(f'Error @ pcdl_get_cell_attribute_list : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') + sys.exit(f'Error @ pcdl_get_cell_df : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') # run - mcds = pcdl.TimeStep( - xmlfile = s_pathfile, - output_path = '.', - #custom_data_type, - microenv = False if args.microenv.lower().startswith('f') else True, - graph = False, - physiboss = False if args.physiboss.lower().startswith('f') else True, - settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, - verbose = True if args.verbose.lower().startswith('t') else False - ) + if os.path.isfile(args.path): + mcds = pcdl.TimeStep( + xmlfile = s_pathfile, + output_path = '.', + #custom_data_type, + microenv = False if args.microenv.lower().startswith('f') else True, + graph = False, + physiboss = False if args.physiboss.lower().startswith('f') else True, + settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, + verbose = False if args.verbose.lower().startswith('f') else True + ) + df_cell = mcds.get_cell_df( + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + ) + # going home + s_opathfile = s_pathfile.replace('.xml','_cell.csv') + df_cell.to_csv(s_opathfile) + print(s_opathfile) + + else: + mcdsts = pcdl.TimeSeries( + output_path = s_path, + #custom_data_type, + load = True, + microenv = False if args.microenv.lower().startswith('f') else True, + graph = False, + physiboss = False if args.physiboss.lower().startswith('f') else True, + settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, + verbose = False if args.verbose.lower().startswith('f') else True, + ) + # handle collaps + b_collapse = False if args.collapse.lower().startswith('f') else True + ldf_cell = mcdsts.get_cell_df( + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + collapse = b_collapse, + ) + # going home + if b_collapse: + s_opathfile = f'{s_path}/timeseries_cell.csv' + ldf_cell.to_csv(s_opathfile) + print(s_opathfile) + else: + ls_opathfile = [f"{s_path}/{s_xmlfile.replace('.xml','_cell.csv')}" for s_xmlfile in mcdsts.get_xmlfile_list()] + for i, df_cell in enumerate(ldf_cell): + df_cell.to_csv(ls_opathfile[i]) + print(ls_opathfile) # going home - print(mcds.get_cell_attribute_list()) + return 0 def get_cell_attribute(): @@ -1137,11 +1206,11 @@ def get_cell_attribute(): return 0 -def get_cell_df(): +def get_cell_attribute_list(): # argv parser = argparse.ArgumentParser( - prog = 'pcdl_get_cell_df', - description = 'this function extracts dataframes with a cell centric view of the simulation and saves them as csv files.', + prog = 'pcdl_get_cell_attribute_list', + description = 'this function is returns a list with all cell attribute labels, alphabetically ordered.', epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', ) @@ -1150,22 +1219,22 @@ def get_cell_df(): 'path', nargs = '?', default = '.', - help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .' + help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .', ) # TimeSeries output_path '.' - # TimeSeries custom_data_type nop (because datafarme is straightaway saved as csv) + # TimeSeries custom_data_type nop # TimeSeries microenv parser.add_argument( '--microenv', default = 'true', - help = 'should the microenvironment data be loaded? setting microenv to False will use less memory and speed up processing. default is True.' + help = 'should the microenvironment data be loaded? setting microenv to False will use less memory and speed up processing. default is True.', ) # TimeSeries graph False # TimeSeries physiboss parser.add_argument( '--physiboss', default = 'true', - help = 'if found, should physiboss state data be extracted and loaded into the df_cell dataframe? default is True.' + help = 'if found, should physiboss state data be extracted and loaded into df_cell dataframe? default is True.' ) # TimeSeries settingxml parser.add_argument( @@ -1176,198 +1245,8 @@ def get_cell_df(): # TimeSeries verbose parser.add_argument( '-v', '--verbose', - default = 'true', - help = 'setting verbose to False for less text output, while processing. default is True.' - ) - # get_cell_df values - parser.add_argument( - 'values', - nargs = '?', - default = 1, - type = int, - help = 'minimal number of values a variable has to have in any of the mcds time steps to be outputted. variables that have only 1 state carry no information. None is a state too. default is 1.' - ) - # get_cell_df drop - parser.add_argument( - '--drop', - nargs = '*', - default = [], - help = "set of column labels to be dropped for the dataframe. don't worry: essential columns like ID, coordinates and time will never be dropped. Attention: when the keep parameter is given, then the drop parameter has to be an empty string! default is an empty string." - ) - # get_cell_df keep - parser.add_argument( - '--keep', - nargs = '*', - default = [], - help = "set of column labels to be kept in the dataframe. set values=1 to be sure that all variables are kept. don't worry: essential columns like ID, coordinates and time will always be kept. default is an empty string." - ) - # get_cell_df collapse - parser.add_argument( - '--collapse', - default = 'true', - help = 'should all mcds time steps from the time series be collapsed into one big csv, or a many csv, one csv for each time step?, default is True.' - ) - - # parse arguments - args = parser.parse_args() - print(args) - - # process arguments - s_path = args.path.replace('\\','/') - while (s_path.find('//') > -1): - s_path = s_path.replace('//','/') - if (s_path.endswith('/')) and (len(s_path) > 1): - s_path = s_path[:-1] - s_pathfile = s_path - if not s_pathfile.endswith('.xml'): - s_pathfile = s_pathfile + '/initial.xml' - else: - s_path = '/'.join(s_path.split('/')[:-1]) - if not os.path.exists(s_pathfile): - sys.exit(f'Error @ pcdl_get_cell_df : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') - - # run - if os.path.isfile(args.path): - mcds = pcdl.TimeStep( - xmlfile = s_pathfile, - output_path = '.', - #custom_data_type, - microenv = False if args.microenv.lower().startswith('f') else True, - graph = False, - physiboss = False if args.physiboss.lower().startswith('f') else True, - settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, - verbose = False if args.verbose.lower().startswith('f') else True - ) - df_cell = mcds.get_cell_df( - values = args.values, - drop = set(args.drop), - keep = set(args.keep), - ) - # going home - s_opathfile = s_pathfile.replace('.xml','_cell.csv') - df_cell.to_csv(s_opathfile) - print(s_opathfile) - - else: - mcdsts = pcdl.TimeSeries( - output_path = s_path, - #custom_data_type, - load = True, - microenv = False if args.microenv.lower().startswith('f') else True, - graph = False, - physiboss = False if args.physiboss.lower().startswith('f') else True, - settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, - verbose = False if args.verbose.lower().startswith('f') else True, - ) - # handle collaps - b_collapse = False if args.collapse.lower().startswith('f') else True - ldf_cell = mcdsts.get_cell_df( - values = args.values, - drop = set(args.drop), - keep = set(args.keep), - collapse = b_collapse, - ) - # going home - if b_collapse: - s_opathfile = f'{s_path}/timeseries_cell.csv' - ldf_cell.to_csv(s_opathfile) - print(s_opathfile) - else: - ls_opathfile = [f"{s_path}/{s_xmlfile.replace('.xml','_cell.csv')}" for s_xmlfile in mcdsts.get_xmlfile_list()] - for i, df_cell in enumerate(ldf_cell): - df_cell.to_csv(ls_opathfile[i]) - print(ls_opathfile) - - # going home - return 0 - - -def get_anndata(): - # argv - parser = argparse.ArgumentParser( - prog = 'pcdl_get_anndata', - description = 'function to transform mcds time steps into one or many anndata objects for downstream analysis.', - epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', - ) - - # TimeSeries path - parser.add_argument( - 'path', - nargs = '?', - default = '.', - help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .' - ) - # TimeSeries output_path '.' - # TimeSeries custom_data_type - parser.add_argument( - '--custom_data_type', - nargs = '*', - default = [], - help = 'parameter to specify custom_data variable types other than float (namely: int, bool, str) like this var:dtype myint:int mybool:bool mystr:str . downstream float and int will be handled as numeric, bool as Boolean, and str as categorical data. default is an empty string.', - ) - # TimeSeries microenv - parser.add_argument( - '--microenv', - default = 'true', - help = 'should the microenvironment be extracted and loaded into the anndata object? setting microenv to False will use less memory and speed up processing. default is True.' - ) - # TimeSeries graph - parser.add_argument( - '--graph', - default = 'true', - help = 'should neighbor graph, attach graph, and attached spring graph be extracted and loaded into the anndata object? default is True.' - ) - # TimeSeries physiboss - parser.add_argument( - '--physiboss', - default = 'true', - help = 'if found, should physiboss state data be extracted and loaded into the anndata object? default is True.' - ) - # TimeSeries settingxml - parser.add_argument( - '--settingxml', default = 'false', - help = 'the settings.xml that is loaded, from which the cell type ID label mapping, is extracted, if this information is not found in the output xml file. set to None or False if the xml file is missing! default is False.', - ) - # TimeSeries verbose - parser.add_argument( - '-v', '--verbose', - default = 'true', - help = 'setting verbose to False for less text output, while processing. default is True.', - ) - # get_anndata values - parser.add_argument( - 'values', - nargs = '?', - default = 1, - type = int, - help = 'minimal number of values a variable has to have in any of the mcds time steps to be outputted. variables that have only 1 state carry no information. None is a state too. default is 1.' - ) - # get_anndata drop - parser.add_argument( - '--drop', - nargs = '*', - default = [], - help = "set of column labels to be dropped for the dataframe. don't worry: essential columns like ID, coordinates and time will never be dropped. Attention: when the keep parameter is given, then the drop parameter has to be an empty string! default is an empty string." - ) - # get_anndata keep - parser.add_argument( - '--keep', - nargs = '*', - default = [], - help = "set of column labels to be kept in the dataframe. set values=1 to be sure that all variables are kept. don't worry: essential columns like ID, coordinates and time will always be kept. default is an empty string." - ) - # get_anndata scale - parser.add_argument( - '--scale', - default = 'maxabs', - help = "specify how the data should be scaled. possible values are None, maxabs, minmax, std. None: no scaling. set scale to None if you would like to have raw data or entirely scale, transform, and normalize the data later. maxabs: maximum absolute value distance scaler will linearly map all values into a [-1, 1] interval. if the original data has no negative values, the result will be the same as with the minmax scaler (except with attributes with only one value). if the attribute has only zeros, the value will be set to 0. minmax: minimum maximum distance scaler will map all values linearly into a [0, 1] interval. if the attribute has only one value, the value will be set to 0. std: standard deviation scaler will result in sigmas. each attribute will be mean centered around 0. ddof delta degree of freedom is set to 1 because it is assumed that the values are samples out of the population and not the entire population. it is incomprehensible to me that the equivalent sklearn method has ddof set to 0. if the attribute has only one value, the value will be set to 0. default is maxabs" - ) - # get_anndata collapse - parser.add_argument( - '--collapse', - default = 'true', - help = 'should all mcds time steps from the time series be collapsed into one big anndata h5ad file, or a many h5ad, one h5ad for each time step?, default is True.' + help = 'setting verbose to True for more text output, while processing. default is False.', ) # parse arguments @@ -1384,85 +1263,31 @@ def get_anndata(): if not s_pathfile.endswith('.xml'): s_pathfile = s_pathfile + '/initial.xml' else: - s_path = '/'.join(s_path.split('/')[:-1]) - if not os.path.exists(s_pathfile): - sys.exit(f'Error @ pcdl_get_anndata : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') - - # custom_data_type - d_vartype = {} - for vartype in args.custom_data_type: - s_var, s_type = vartype.split(':') - if s_type in {'bool'}: o_type = bool - elif s_type in {'int'}: o_type = int - elif s_type in {'float'}: o_type = float - elif s_type in {'str'}: o_type = str - else: - sys.exit(f'Error @ pcdl_get_anndata : {s_var} {s_type} has an unknowen data type. knowen are bool, int, float, str.') - d_vartype.update({s_var : o_type}) - - # run - if os.path.isfile(args.path): - mcds = pcdl.TimeStep( - xmlfile = s_pathfile, - output_path = '.', - custom_data_type = d_vartype, - microenv = False if args.microenv.lower().startswith('f') else True, - graph = False if args.graph.lower().startswith('f') else True, - physiboss = False if args.physiboss.lower().startswith('f') else True, - settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, - verbose = False if args.verbose.lower().startswith('f') else True - ) - ann_mcds = mcds.get_anndata( - values = args.values, - drop = set(args.drop), - keep = set(args.keep), - scale = None if (args.scale.lower() == 'none') else args.scale, - ) - # going home - s_opathfile = s_pathfile.replace('.xml', f'_cell_{args.scale}.h5ad') - ann_mcds.write_h5ad(s_opathfile) - print(s_opathfile) - - else: - mcdsts = pcdl.TimeSeries( - output_path = s_path, - custom_data_type = d_vartype, - load = True, - microenv = False if args.microenv.lower().startswith('f') else True, - graph = False, - physiboss = False if args.physiboss.lower().startswith('f') else True, - settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, - verbose = False if args.verbose.lower().startswith('f') else True, - ) - # handle collaps - b_collapse = False if args.collapse.lower().startswith('f') else True - ann_mcdsts = mcdsts.get_anndata( - values = args.values, - drop = set(args.drop), - keep = set(args.keep), - scale = None if (args.scale.lower() == 'none') else args.scale, - collapse = b_collapse, - ) - # going home - if b_collapse : - s_opathfile = f'{s_path}/timeseries_cell_{args.scale}.h5ad' - ann_mcdsts.write_h5ad(s_opathfile) - print(s_opathfile) - else: - ls_opathfile = [f"{s_path}/{s_xmlfile.replace('.xml', '_cell_{}.h5ad'.format(args.scale))}" for s_xmlfile in mcdsts.get_xmlfile_list()] - for i, ann_mcds in enumerate(ann_mcdsts): - ann_mcds.write_h5ad(ls_opathfile[i]) - print(ls_opathfile) + s_path = '/'.join(s_path.split('/')[:-1]) + if not os.path.exists(s_pathfile): + sys.exit(f'Error @ pcdl_get_cell_attribute_list : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') + + # run + mcds = pcdl.TimeStep( + xmlfile = s_pathfile, + output_path = '.', + #custom_data_type, + microenv = False if args.microenv.lower().startswith('f') else True, + graph = False, + physiboss = False if args.physiboss.lower().startswith('f') else True, + settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, + verbose = True if args.verbose.lower().startswith('t') else False + ) # going home - return 0 + print(mcds.get_cell_attribute_list()) -def make_graph_gml(): +def get_anndata(): # argv parser = argparse.ArgumentParser( - prog = 'pcdl_make_graph_gml', - description = 'function to generate graph files in the gml graph modelling language standard format. gml was the outcome of an initiative that started at the international symposium on graph drawing 1995 in Passau and ended at Graph Drawing 1996 in Berkeley. the networkx python library (https://networkx.org/) and igraph C and python libraries (https://igraph.org/) for graph analysis are gml compatible and can as such read and write this file format.', + prog = 'pcdl_get_anndata', + description = 'function to transform mcds time steps into one or many anndata objects for downstream analysis.', epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', ) @@ -1471,7 +1296,7 @@ def make_graph_gml(): 'path', nargs = '?', default = '.', - help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .', + help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .' ) # TimeSeries output_path '.' # TimeSeries custom_data_type @@ -1485,14 +1310,19 @@ def make_graph_gml(): parser.add_argument( '--microenv', default = 'true', - help = 'should the microenvironment data be loaded? setting microenv to False will use less memory and speed up processing. default is True.' + help = 'should the microenvironment be extracted and loaded into the anndata object? setting microenv to False will use less memory and speed up processing. default is True.' + ) + # TimeSeries graph + parser.add_argument( + '--graph', + default = 'true', + help = 'should neighbor graph, attach graph, and attached spring graph be extracted and loaded into the anndata object? default is True.' ) - # TimeSeries graph True # TimeSeries physiboss parser.add_argument( '--physiboss', default = 'true', - help = 'if found, should physiboss state data be extracted and loaded into the df_cell dataframe? default is True.' + help = 'if found, should physiboss state data be extracted and loaded into the anndata object? default is True.' ) # TimeSeries settingxml parser.add_argument( @@ -1506,24 +1336,39 @@ def make_graph_gml(): default = 'true', help = 'setting verbose to False for less text output, while processing. default is True.', ) - # make_graph_gml graph_type + # get_anndata values parser.add_argument( - 'graph_type', + 'values', nargs = '?', - help = 'to specify which physicell output data should be processed. attached: processes mcds.get_attached_graph_dict dictionary. neighbor: processes mcds.get_neighbor_graph_dict dictionary spring: processes mcds.get_spring_graph_dict dictionary.', + default = 1, + type = int, + help = 'minimal number of values a variable has to have in any of the mcds time steps to be outputted. variables that have only 1 state carry no information. None is a state too. default is 1.' ) - # make_graph_gml edge_attribute + # get_anndata drop parser.add_argument( - '--edge_attribute', - default = 'true', - help = 'specifies if the spatial Euclidean distance is used for edge attribute, to generate a weighted graph. default is True.', + '--drop', + nargs = '*', + default = [], + help = "set of column labels to be dropped for the dataframe. don't worry: essential columns like ID, coordinates and time will never be dropped. Attention: when the keep parameter is given, then the drop parameter has to be an empty string! default is an empty string." ) - # make_graph_gml node_attrributes + # get_anndata keep parser.add_argument( - '--node_attribute', + '--keep', nargs = '*', default = [], - help = 'listing of mcds.get_cell_df dataframe columns, used for node attributes. default is and empty list.', + help = "set of column labels to be kept in the dataframe. set values=1 to be sure that all variables are kept. don't worry: essential columns like ID, coordinates and time will always be kept. default is an empty string." + ) + # get_anndata scale + parser.add_argument( + '--scale', + default = 'maxabs', + help = "specify how the data should be scaled. possible values are None, maxabs, minmax, std. None: no scaling. set scale to None if you would like to have raw data or entirely scale, transform, and normalize the data later. maxabs: maximum absolute value distance scaler will linearly map all values into a [-1, 1] interval. if the original data has no negative values, the result will be the same as with the minmax scaler (except with attributes with only one value). if the attribute has only zeros, the value will be set to 0. minmax: minimum maximum distance scaler will map all values linearly into a [0, 1] interval. if the attribute has only one value, the value will be set to 0. std: standard deviation scaler will result in sigmas. each attribute will be mean centered around 0. ddof delta degree of freedom is set to 1 because it is assumed that the values are samples out of the population and not the entire population. it is incomprehensible to me that the equivalent sklearn method has ddof set to 0. if the attribute has only one value, the value will be set to 0. default is maxabs" + ) + # get_anndata collapse + parser.add_argument( + '--collapse', + default = 'true', + help = 'should all mcds time steps from the time series be collapsed into one big anndata h5ad file, or a many h5ad, one h5ad for each time step?, default is True.' ) # parse arguments @@ -1542,7 +1387,7 @@ def make_graph_gml(): else: s_path = '/'.join(s_path.split('/')[:-1]) if not os.path.exists(s_pathfile): - sys.exit(f'Error @ pcdl_make_graph_gml : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') + sys.exit(f'Error @ pcdl_get_anndata : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') # custom_data_type d_vartype = {} @@ -1553,7 +1398,7 @@ def make_graph_gml(): elif s_type in {'float'}: o_type = float elif s_type in {'str'}: o_type = str else: - sys.exit(f'Error @ pcdl_make_graph_gml : {s_var} {s_type} has an unknowen data type. knowen are bool, int, float, str.') + sys.exit(f'Error @ pcdl_get_anndata : {s_var} {s_type} has an unknowen data type. knowen are bool, int, float, str.') d_vartype.update({s_var : o_type}) # run @@ -1563,17 +1408,20 @@ def make_graph_gml(): output_path = '.', custom_data_type = d_vartype, microenv = False if args.microenv.lower().startswith('f') else True, - graph = True, + graph = False if args.graph.lower().startswith('f') else True, physiboss = False if args.physiboss.lower().startswith('f') else True, settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, verbose = False if args.verbose.lower().startswith('f') else True ) - s_opathfile = mcds.make_graph_gml( - graph_type = args.graph_type, - edge_attribute = False if args.edge_attribute.lower().startswith('f') else True, - node_attribute = args.node_attribute, + ann_mcds = mcds.get_anndata( + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + scale = None if (args.scale.lower() == 'none') else args.scale, ) # going home + s_opathfile = s_pathfile.replace('.xml', f'_cell_{args.scale}.h5ad') + ann_mcds.write_h5ad(s_opathfile) print(s_opathfile) else: @@ -1582,18 +1430,30 @@ def make_graph_gml(): custom_data_type = d_vartype, load = True, microenv = False if args.microenv.lower().startswith('f') else True, - graph = True, + graph = False, physiboss = False if args.physiboss.lower().startswith('f') else True, settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, verbose = False if args.verbose.lower().startswith('f') else True, ) - ls_opathfile = mcdsts.make_graph_gml( - graph_type = args.graph_type, - edge_attribute = False if args.edge_attribute.lower().startswith('f') else True, - node_attribute = args.node_attribute, + # handle collaps + b_collapse = False if args.collapse.lower().startswith('f') else True + ann_mcdsts = mcdsts.get_anndata( + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + scale = None if (args.scale.lower() == 'none') else args.scale, + collapse = b_collapse, ) # going home - print(ls_opathfile) + if b_collapse : + s_opathfile = f'{s_path}/timeseries_cell_{args.scale}.h5ad' + ann_mcdsts.write_h5ad(s_opathfile) + print(s_opathfile) + else: + ls_opathfile = [f"{s_path}/{s_xmlfile.replace('.xml', '_cell_{}.h5ad'.format(args.scale))}" for s_xmlfile in mcdsts.get_xmlfile_list()] + for i, ann_mcds in enumerate(ann_mcdsts): + ann_mcds.write_h5ad(ls_opathfile[i]) + print(ls_opathfile) # going home return 0 @@ -2007,6 +1867,147 @@ def make_cell_vtk(): return 0 +def make_graph_gml(): + # argv + parser = argparse.ArgumentParser( + prog = 'pcdl_make_graph_gml', + description = 'function to generate graph files in the gml graph modelling language standard format. gml was the outcome of an initiative that started at the international symposium on graph drawing 1995 in Passau and ended at Graph Drawing 1996 in Berkeley. the networkx python library (https://networkx.org/) and igraph C and python libraries (https://igraph.org/) for graph analysis are gml compatible and can as such read and write this file format.', + epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', + ) + + # TimeSeries path + parser.add_argument( + 'path', + nargs = '?', + default = '.', + help = 'path to the PhysiCell output directory or a outputnnnnnnnn.xml file. default is . .', + ) + # TimeSeries output_path '.' + # TimeSeries custom_data_type + parser.add_argument( + '--custom_data_type', + nargs = '*', + default = [], + help = 'parameter to specify custom_data variable types other than float (namely: int, bool, str) like this var:dtype myint:int mybool:bool mystr:str . downstream float and int will be handled as numeric, bool as Boolean, and str as categorical data. default is an empty string.', + ) + # TimeSeries microenv + parser.add_argument( + '--microenv', + default = 'true', + help = 'should the microenvironment data be loaded? setting microenv to False will use less memory and speed up processing. default is True.' + ) + # TimeSeries graph True + # TimeSeries physiboss + parser.add_argument( + '--physiboss', + default = 'true', + help = 'if found, should physiboss state data be extracted and loaded into the df_cell dataframe? default is True.' + ) + # TimeSeries settingxml + parser.add_argument( + '--settingxml', + default = 'false', + help = 'the settings.xml that is loaded, from which the cell type ID label mapping, is extracted, if this information is not found in the output xml file. set to None or False if the xml file is missing! default is False.', + ) + # TimeSeries verbose + parser.add_argument( + '-v', '--verbose', + default = 'true', + help = 'setting verbose to False for less text output, while processing. default is True.', + ) + # make_graph_gml graph_type + parser.add_argument( + 'graph_type', + nargs = '?', + help = 'to specify which physicell output data should be processed. attached: processes mcds.get_attached_graph_dict dictionary. neighbor: processes mcds.get_neighbor_graph_dict dictionary spring: processes mcds.get_spring_graph_dict dictionary.', + ) + # make_graph_gml edge_attribute + parser.add_argument( + '--edge_attribute', + default = 'true', + help = 'specifies if the spatial Euclidean distance is used for edge attribute, to generate a weighted graph. default is True.', + ) + # make_graph_gml node_attrributes + parser.add_argument( + '--node_attribute', + nargs = '*', + default = [], + help = 'listing of mcds.get_cell_df dataframe columns, used for node attributes. default is and empty list.', + ) + + # parse arguments + args = parser.parse_args() + print(args) + + # process arguments + s_path = args.path.replace('\\','/') + while (s_path.find('//') > -1): + s_path = s_path.replace('//','/') + if (s_path.endswith('/')) and (len(s_path) > 1): + s_path = s_path[:-1] + s_pathfile = s_path + if not s_pathfile.endswith('.xml'): + s_pathfile = s_pathfile + '/initial.xml' + else: + s_path = '/'.join(s_path.split('/')[:-1]) + if not os.path.exists(s_pathfile): + sys.exit(f'Error @ pcdl_make_graph_gml : {s_pathfile} path does not look like a outputnnnnnnnn.xml file or physicell output directory ({s_path}/initial.xml is missing).') + + # custom_data_type + d_vartype = {} + for vartype in args.custom_data_type: + s_var, s_type = vartype.split(':') + if s_type in {'bool'}: o_type = bool + elif s_type in {'int'}: o_type = int + elif s_type in {'float'}: o_type = float + elif s_type in {'str'}: o_type = str + else: + sys.exit(f'Error @ pcdl_make_graph_gml : {s_var} {s_type} has an unknowen data type. knowen are bool, int, float, str.') + d_vartype.update({s_var : o_type}) + + # run + if os.path.isfile(args.path): + mcds = pcdl.TimeStep( + xmlfile = s_pathfile, + output_path = '.', + custom_data_type = d_vartype, + microenv = False if args.microenv.lower().startswith('f') else True, + graph = True, + physiboss = False if args.physiboss.lower().startswith('f') else True, + settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, + verbose = False if args.verbose.lower().startswith('f') else True + ) + s_opathfile = mcds.make_graph_gml( + graph_type = args.graph_type, + edge_attribute = False if args.edge_attribute.lower().startswith('f') else True, + node_attribute = args.node_attribute, + ) + # going home + print(s_opathfile) + + else: + mcdsts = pcdl.TimeSeries( + output_path = s_path, + custom_data_type = d_vartype, + load = True, + microenv = False if args.microenv.lower().startswith('f') else True, + graph = True, + physiboss = False if args.physiboss.lower().startswith('f') else True, + settingxml = None if ((args.settingxml.lower() == 'none') or (args.settingxml.lower() == 'false')) else args.settingxml, + verbose = False if args.verbose.lower().startswith('f') else True, + ) + ls_opathfile = mcdsts.make_graph_gml( + graph_type = args.graph_type, + edge_attribute = False if args.edge_attribute.lower().startswith('f') else True, + node_attribute = args.node_attribute, + ) + # going home + print(ls_opathfile) + + # going home + return 0 + + def make_simularium(): # argv parser = argparse.ArgumentParser( @@ -2291,6 +2292,7 @@ def get_muspan(): # going home return 0 + def get_spatialdata(): # argv parser = argparse.ArgumentParser( @@ -2564,7 +2566,6 @@ def plot_timeseries(): default = 'cell', help = 'to specifies the data dataframe. cell: dataframe will be retrieved through the mcds.get_cell_df function. conc: dataframe will be retrieved through the mcds.get_conc_df function. default is cell.', ) - # plot_timeseries cat_drop parser.add_argument( '--cat_drop', @@ -2939,10 +2940,6 @@ def make_ome_tiff(): return 0 -####################### -# render neuroglancer # -####################### - def render_neuroglancer(): # argv parser = argparse.ArgumentParser( diff --git a/pcdl/timeseries.py b/pcdl/timeseries.py index 3853018..01e09fb 100644 --- a/pcdl/timeseries.py +++ b/pcdl/timeseries.py @@ -421,7 +421,7 @@ def get_mcds_list(self): return self.l_mcds - ## MICROENVIRONMENT RELATED FUNCTIONS ## + ## SUBSTRATE RELATED FUNCTIONS ## def get_conc_df(self, values=1, drop=set(), keep=set(), collapse=True): """ @@ -918,6 +918,160 @@ def get_cell_attribute(self, values=1, drop=set(), keep=set(), allvalues=False): return dl_variable_range + def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=True): + """ + input: + values: integer; default is 1 + minimal number of values a variable has to have to be outputted. + variables that have only 1 state carry no information. + None is a state too. + + drop: set of strings; default is an empty set + set of column labels to be dropped for the dataframe. + don't worry: essential columns like ID, coordinates + and time will never be dropped. + Attention: when the keep parameter is given, then + the drop parameter has to be an empty set! + + keep: set of strings; default is an empty set + set of column labels to be kept in the dataframe. + don't worry: essential columns like ID, coordinates + and time will always be kept. + + scale: string; default 'maxabs' + specify how the data should be scaled. + possible values are None, maxabs, minmax, std. + for more input, check out: help(pcdl.scaler) + + collapse: boole; default True + should all mcds time steps from the time series be collapsed + into one single anndata object, or a list of anndata objects + for each time step? + + keep_mcds: boole; default True + should the loaded original mcds be kept in memory + after transformation? + + output: + annmcds or self.l_annmcds: anndata object or list of anndata objects. + what is returned depends on the collapse setting. + + description: + function to transform mcds time steps into one or many + anndata objects for downstream analysis. + """ + # load optional dependency + ad = optional_import('anndata', s_caller='TimeSeries.get_anndata') + + # initialize vaiable + l_annmcds = [] + df_anncount = None + df_annobs = None + ar_annobsm = None + + # variable triage + if (values < 2): + ls_column = list(self.l_mcds[0].get_cell_df(drop=drop, keep=keep).columns) + else: + ls_column = sorted(es_coor_cell.difference({'ID'})) + ls_column.extend(sorted(self.get_cell_attribute(values=values, drop=drop, keep=keep, allvalues=False).keys())) + + # package collapse + if collapse: + + # warning + if self.verbose: + print('Warning @ mcdsts.get_anndata : only df_cell data, but not graph data, can be collapsed.') + + # extract + df_cell = self.get_cell_df(values=values, drop=drop, keep=keep, collapse=True) + df_count, df_obs, d_obsm, d_obsp, d_uns = _anndextract( + df_cell=df_cell, + scale = scale, + #graph_attached = {}, + #graph_neighbor = {}, + #graph_spring = {}, + #graph_method = s_physicellv, + ) + + # fuse to anndata object + ann_mcdsts = ad.AnnData( + X = df_count, + obs = df_obs, + obsm = d_obsm, + #obsp = d_obsp, # nop (graph) + #uns = d_uns, # nop (graph) + ) + + # mcds + if not keep_mcds: + self.l_mcds = [] + + # output + return ann_mcdsts + + # pack not collapsed + else: + # processing + lann_mcds = [] + i_mcds = len(self.l_mcds) + for i in range(i_mcds): + # fetch mcds + if keep_mcds: + mcds = self.l_mcds[i] + else: + mcds = self.l_mcds.pop(0) + + # extract physicell version + s_physicellv = mcds.get_physicell_version(), + + # extract time and dataframes + if self.verbose: + print(f'processing: {i+1}/{i_mcds} {mcds.get_time()}[min] mcds into anndata obj.') + df_cell = mcds.get_cell_df() + df_cell = df_cell.loc[:,ls_column] + + # extract + df_count, df_obs, d_obsm, d_obsp, d_uns = _anndextract( + df_cell=df_cell, + scale = scale, + graph_attached = mcds.get_attached_graph_dict(), + graph_neighbor = mcds.get_neighbor_graph_dict(), + graph_spring = mcds.get_spring_graph_dict(), + graph_method = s_physicellv, + ) + + # annmcds + ann_mcds = ad.AnnData( + X = df_count, + obs = df_obs, + obsm = d_obsm, + obsp = d_obsp, + uns = d_uns, + ) + lann_mcds.append(ann_mcds) + + # output + self.l_annmcds = lann_mcds + return self.l_annmcds + + + def get_annmcds_list(self): + """ + input: + self: TimeSeries class instance. + + output: + self.l_annmcds: list of chronologically ordered anndata mcds objects. + watch out, this is a pointer to the + self.l_annmcds list of anndata mcds objects, not a copy of self.l_annmcds! + + description: + function returns a binding to the self.l_annmcds list of anndata mcds objects. + """ + return self.l_annmcds + + def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slice=0.0, z_axis=None, alpha=1, cmap='viridis', title='', grid=True, legend_loc='lower left', xlim=None, ylim=None, xyequal=True, s=1.0, figsizepx=None, directory=None, ext='jpeg', figbgcolor=None, **kwargs): """ input: @@ -1093,235 +1247,418 @@ def make_cell_vtk(self, attribute=['cell_type'], ext='_cell.vtp'): return ls_vtkpathfile - ## OME TIFF RELATED FUNCTIONS ## - - def make_ome_tiff(self, cell_attribute='ID', conc_cutoff={}, focus=None, file=True, collapse=True): + def make_graph_gml(self, graph_type, edge_attribute=True, node_attribute=[]): """ input: - cell_attribute: strings; default is 'ID', which will result in a - cell segmentation mask. - column name within the cell dataframe. - the column data type has to be numeric (bool, int, float) - and cannot be string. - the result will be stored as 32 bit float. - - conc_cutoff: dictionary string to real; default is an empty dictionary. - if a contour from a substrate not should be cut by greater - than zero (shifted to integer 1), another cutoff value can be specified here. + self: TimeSeries class instance. - focus: set of strings; default is a None - set of substrate and cell_type names to specify what will be - translated into ome tiff format. - if None, all substrates and cell types will be processed. + graph_type: string + to specify which physicell output data should be processed. + attached, touch: processes mcds.get_attached_graph_dict dictionary. + neighbor: processes mcds.get_neighbor_graph_dict dictionary. + spring: processes mcds.get_spring_graph_dict dictionary. - file: boolean; default True - if True, an ome tiff file is the output. - if False, a numpy array with shape tczyx is the output. + edge_attribute: boolean; default True + specifies if the spatial Euclidean distance is used for + edge attribute, to generate a weighted graph. - collapse: boole; default True - should all mcds time steps from the time series be collapsed - into one ome tiff file (numpy array), - or an ome tiff file (numpy array) for each time step? + node_attribute: list of strings; default is empty list + list of mcds.get_cell_df dataframe columns, used for + node attributes. output: - a_tczyx_img: numpy array or ome tiff file. - + gml file for each time step. + path and filenames are printed to the standard output. description: - function to transform chosen mcdsts output into an 1[um] spaced - tczyx (time, channel, z-axis, y-axis, x-axis) ome tiff file or numpy array, - one substrate or cell_type per channel. - a ome tiff file is more or less: - a numpy array, containing the image information - and a xml, containing the microscopy metadata information, - like the channel labels. - the ome tiff file format can for example be read by the napari - or fiji (imagej) software. + function to generate graph files in the gml graph modelling language + standard format. - https://napari.org/stable/ - https://fiji.sc/ + gml was the outcome of an initiative that started at + the international symposium on graph drawing 1995 in Passau + and ended at Graph Drawing 1996 in Berkeley. the networkx python + and igraph C and python libraries for graph analysis are + gml compatible and can as such read and write this file format. + + https://en.wikipedia.org/wiki/Graph_Modelling_Language + https://github.com/elmbeech/physicelldataloader/blob/master/man/publication/himsolt1996gml_a_portable_graph_file_format.pdf + https://networkx.org/ + https://igraph.org/ """ - # for each time step - l_tczyx_img = [] + # processing + ls_pathfile = [] for mcds in self.get_mcds_list(): - # processing - b_file = True # 10 - if (not file and not collapse) or (not file and collapse) or (file and collapse): # 00, 01, 11 - b_file = False - o_tczyx_img = mcds.make_ome_tiff( - cell_attribute = cell_attribute, - conc_cutoff = conc_cutoff, - focus = focus, - file = b_file + s_pathfile = mcds.make_graph_gml( + graph_type = graph_type, + edge_attribute = edge_attribute, + node_attribute = node_attribute, ) - l_tczyx_img.append(o_tczyx_img) - - # handle channels - ls_substrate = mcds.get_substrate_list() - ls_celltype = mcds.get_celltype_list() + ls_pathfile.append(s_pathfile) - if not (focus is None): - ls_substrate = [s_substrate for s_substrate in ls_substrate if s_substrate in set(focus)] - ls_celltype = [s_celltype for s_celltype in ls_celltype if s_celltype in set(focus)] - if (set(focus) != set(ls_substrate).union(set(ls_celltype))): - sys.exit(f'Error : {focus} not found in {ls_substrate} {ls_celltype}') + # outout + return ls_pathfile - # output 00 list of numpy arrays - if (not file and not collapse): # 00 - if self.verbose: - print(f'la_tczyx_img shape: {len(l_tczyx_img)} * {l_tczyx_img[0].shape}') - return l_tczyx_img - # output 01 numpy array - elif (not file and collapse): # 01 - # numpy array - a_tczyx_img = np.array(l_tczyx_img) - if self.verbose: - print('a_tczyx_img shape:', a_tczyx_img.shape) - return a_tczyx_img - - # output 10 list of pathfile strings - elif (file and not collapse): # 10 - return l_tczyx_img - - # output 11 ometiff file - elif (file and collapse): # 11 - # load optional dependency - OmeTiffWriter = optional_import('bioio.writers', s_attr='OmeTiffWriter', s_pip='bioio', s_caller='TimeSeries.make_ome_tiff') - bioio_base = optional_import('bioio_base', s_pip='bioio', s_caller='TimeSeries.make_ome_tiff') - - # numpy array - a_tczyx_img = np.array(l_tczyx_img) - if self.verbose: - print('a_tczyx_img shape:', a_tczyx_img.shape) + def make_simularium(self, focus_cat=['cell_type','current_phase'], trajectory_title='timeseries', scale_factor=None, camera_defaults=None, model_meta_data=None): + """ + input: + focus_cat: list of 1 or 2 string; default is ['cell_type','current_phase'] + specify 1 or 2 categorical column labels, to be found + in mcdsts.get_cell_df(). - # generate filename - s_channel = '' - for s_substrate in ls_substrate: - try: - r_value = conc_cutoff[s_substrate] - s_channel += f'_{s_substrate}{r_value}' - except KeyError: - s_channel += f'_{s_substrate}' - for s_celltype in ls_celltype: - s_channel += f'_{s_celltype}' - if len(ls_celltype) > 0: - s_channel += f'_{cell_attribute}' - s_tifffile = f"timeseries{s_channel.replace(' ','_')}.ome.tiff" - if (len(s_tifffile) > 255): - print(f"Warning: filename {len(s_tifffile)} > 255 character.") - s_tifffile = 'timeseries_channels.ome.tiff' - print(f"file name adjusted to {s_tifffile}.") - s_tiffpathfile = self.path + '/' + s_tifffile + trajectory_title: string; default 'timeseries' + the trajectory_title will be used as + .simularium file name and displayed + in the simulation. - # save to file - OmeTiffWriter.save( - a_tczyx_img, - s_tiffpathfile, - dim_order = 'TCZYX', - #ome_xml=x_img, - channel_names = ls_substrate + ls_celltype, - image_names = [f'timeseries_{cell_attribute}'], - physical_pixel_sizes = bioio_base.types.PhysicalPixelSizes(mcds.get_voxel_spacing()[2], 1.0, 1.0), # z,y,x [um] - #channel_colors=, - #fs_kwargs={}, - ) - return s_tiffpathfile + scale_factor: float number; default is None + A multiplier for the scene, use if visualization is + too large or small. If None is provided, one will + be calculated based on the position data. + bue 20260822: does not seem to work in current simularium 1.13.0. - # error case - else: - sys.exit(f'Error @ make_ome_tiff : {file} {collapse} strange file collapse combination.') + camera_defaults: simulariumio.CameraData object; default is None + camera's initial settings which it also returns to + when reset. + model_meta_data: simulariumio.ModelMetaData; default is None + Metadata for the model that produced this + trajectory. - ## TIME SERIES RELATED FUNCTIONS ## + output: + .simularium file - def plot_timeseries(self, focus_cat=None, focus_num=None, aggregate_num=np.nanmean, frame='cell', cat_drop=set(), cat_keep=set(), z_slice=None, logy=False, ylim=None, secondary_y=None, subplots=False, sharex=False, sharey=False, linestyle='-', linewidth=None, cmap=None, color=None, grid=True, legend=True, yunit=None, title=None, ax=None, figsizepx=[640, 480], ext=None, figbgcolor=None, **kwargs): + description: + function returns a simularium trajectory file that can be run with the + online Simularium Viewer. + + https://simularium.allencell.org/ """ - input: - self: TimeSeries class instance + # library + sim = optional_import(s_module='simulariumio', s_caller='TimeSeries.make_simularium') - focus_cat: string; default is None - categorical or boolean data column within dataframe specified under frame. - default is None, which is total, which is all agents or voxels, no categories. + # handle input + model_meta_data=sim.ModelMetaData() if model_meta_data is None else model_meta_data + camera_defaults=sim.CameraData() if camera_defaults is None else camera_defaults - focus_num: string; default is None - numerical data column within dataframe specified under frame. - default is None, which is count, agent or voxel count. + # here we go + if self.verbose: + print(f'generating {trajectory_title}.simularium file ...') - aggregate_num: function; default np.nanmean - aggregation function for focus_num data. + # extract box from mcdsts + ltr_domain = self.get_mcds_list()[0].get_mesh_mnp_range() + lr_space = self.get_mcds_list()[0].get_mesh_spacing() + lr_box = np.array([ + ltr_domain[0][1] - ltr_domain[0][0], + ltr_domain[1][1] - ltr_domain[1][0], + ltr_domain[2][1] - ltr_domain[2][0], + ]) + lr_box[0] = lr_box[0] if lr_box[0] != 0.0 else lr_space[0] + lr_box[1] = lr_box[1] if lr_box[1] != 0.0 else lr_space[1] + lr_box[2] = lr_box[2] if lr_box[2] != 0.0 else lr_space[2] + ar_box = np.array(lr_box) - frame: string; default is cell_df - to specifies the data dataframe. - cell: dataframe will be retrieved through the mcds.get_cell_df function. - conc: dataframe will be retrieved through the mcds.get_conc_df function. + # extract cell dataframe from mcdsts + df_cell = self.get_cell_df() - cat_drop: set of strings; default is an empty set - if focus is a categorical attribute, - set of category labels to be dropped for the dataframe. - Attention: when the cat_keep parameter is given, then - the cat_drop parameter has to be an empty set! + # handle agent annotation + se_type = df_cell.loc[:, focus_cat].astype(str).agg('#'.join, axis=1) - cat_keep: set of strings; default is an empty set - if focus is a categorical attribute, - set of category labels to be kept in the dataframe. + # extract units from mcdsts + ds_unit = self.get_mcds_list()[0].get_unit_dict() - z_slice: floating point number; default is None - z-axis position to slice a 2D xy-plain out of the 3D mesh. - if z_slice position numeric but not an exact mesh center coordinate, - then z_slice will be adjusted to the nearest mesh center value, - the smaller one, if the coordinate lies on a saddle point. - if set to None, the whole domain is taken. + # generate simularium dataframe + with pd.option_context('future.infer_string', False): + df_sim = pd.DataFrame({ + 'time': df_cell.loc[:, 'time'].to_numpy(dtype=float), + 'unique_id': df_cell.loc[:, 'ID'].to_numpy(dtype=int), + 'type': se_type.to_numpy(dtype=object), + 'positionX': df_cell.loc[:, 'position_x'].to_numpy(dtype=float), + 'positionY': df_cell.loc[:, 'position_y'].to_numpy(dtype=float), + 'positionZ': df_cell.loc[:, 'position_z'].to_numpy(dtype=float), + 'radius': df_cell.loc[:, 'radius'].to_numpy(dtype=float), + 'rotationX': np.zeros(df_cell.shape[0], dtype=float), + 'rotationY': np.zeros(df_cell.shape[0], dtype=float), + 'rotationZ': np.zeros(df_cell.shape[0], dtype=float), + }) + df_sim.sort_values(['time', 'unique_id'], inplace=True) + # SimulariumIO 1.13.0 AgentData.from_dataframe expects traj.loc[0, ...] + # to select the whole trajectory, so all rows need the same index label. + df_sim.index = np.zeros(df_sim.shape[0], dtype=int) - logy: bool; default False - if True, then y axis is natural log scaled. + # generate simmularium trajectorydata object + o_sim = sim.TrajectoryData( + meta_data = sim.MetaData( + box_size=ar_box, + camera_defaults=camera_defaults, + scale_factor=scale_factor, + trajectory_title=trajectory_title, + model_meta_data=model_meta_data, + ), + agent_data = sim.AgentData.from_dataframe(df_sim), + time_units = sim.UnitData(ds_unit['time']), + spatial_units = sim.UnitData(ds_unit['spatial_unit']), + #plots=, + ) - ylim: tuple of two floats; default is None - y axis min and max value. - default is None, which automatically detects min and max value. + # transform data and save trajectorydata object to simularium file + s_save = self.path + '/' + trajectory_title + s_pathfile = s_save + '.simularium' + sim.TrajectoryConverter(o_sim).save(s_save) + if self.verbose: + print(f'simularium viewer at: https://simularium.allencell.org/') - secondary_y: bool or list of strings; default False - whether to plot on the secondary y-axis. - if a list, which columns to plot on the secondary y-axis. + # return error code + return s_pathfile - subplots: bool or sequence of iterable, default False - whether to group columns into subplots. - a sequence of iterable of column labels - will create a subplot for each group of columns. - sharex: bool, default False - in case subplots is True, share x-axis by - setting some x-axis labels to invisible. + ## SUBSTRATE AND CELL RELATED FUNCTIONS ## - sharey: bool, default False - in case subplots is True, share y-axis range and possibly - setting some y-axis labels to invisible. + def get_muspan(self, z_slice=None, values=1, drop=set(), keep=set()): + """ + input: + z_slice: floating point number; default is None + z-axis position to slice a 2D xy-plain out of the + 3D mesh. if None the whole 3D mesh will be returned. - linestyle: string or list of strings, default '-' - matplotlib line style {'-', '--', '-.', ':', ''}, - over all or per column. + values: integer; default is 1 + minimal number of values a variable has to have to be outputted. + variables that have only 1 state carry no information. + None is a state too. - linewidth: float or list of float, default None - line width in points. + drop: set of strings; default is an empty set + set of column labels to be dropped for the dataframe. + don't worry: essential columns like ID, coordinates + and time will never be dropped. + Attention: when the keep parameter is given, then + the drop parameter has to be an empty set! - cmap: string; default None - matplotlib colormap string. - https://matplotlib.org/stable/tutorials/colors/colormaps.html - achtung: if cmap is given, color will be disregarded. + keep: set of strings; default is an empty set + set of column labels to be kept in the dataframe. + set values=1 to be sure that all variables are kept. + don't worry: essential columns like ID, coordinates + and time will always be kept. - color: string or list of string or dictionary; default None - color string referred to by name, RGB or RGBA code. - achtung: if cmap is given, color will be disregarded. + output: + do_domain: dictionary of muspa domains, one for each time step z-layer. - grid: boolean; default True - plot axis grid lines. + description: + function returns a dictionary of muspa domains, containg a + cell and subs collection with disrcete and continuous labels + and all the graph as networks. + + https://www.muspan.co.uk + + https://docs.muspan.co.uk/latest/Documentation.html + """ + # variable triage + es_keep = set(self.get_cell_attribute(values=values, drop=drop, keep=keep, allvalues=False).keys()) - legend: bool or 'reverse'; default True - if True or reverse, place legend on axis subplots. + # processing + do_domain = {} + for mcds in self.get_mcds_list(): + do_domain.update( + mcds.get_muspan( + z_slice = z_slice, + #values = 1, + #drop = set(), + keep = es_keep, + ) + ) - yunit: string; default None - string to specify y-axis unit. - None will not print a unit on the y-axis. + # output + return do_domain + + + def get_spatialdata(self, images={'subs'}, labels=set(), points={'subs'}, shapes={'cell'}, values=1, drop=set(), keep=set(), scale='maxabs', keep_mcds=True): + """ + input: + images: set of string; default {'subs'} + specify if from the subs or cell dataset + a multichannel image should be generate. + so far, only the subs image element is implemented. + + labels: set of strings; default is an empty set + specify if from the subs or cell dataset + a label element should be generated. + so far, neither subs nor cell label elements are implemented. + + points: set of string; default {'subs'} + specify if from the subs or cell dataset + a points element should be generated. + both, subs and cell point elements, are implemented. + + shapes: set of string; default {'cell'} + specify if from the subs or cell dataset + a shape element should be generated. + so far, only the cell shape element is implemented. + + values: integer; default is 1 + minimal number of values a variable has to have to be outputted. + variables that have only 1 state carry no information. + None is a state too. + + drop: set of strings; default is an empty set + set of column labels to be dropped for the dataframe. + don't worry: essential columns like ID, coordinates + and time will never be dropped. + Attention: when the keep parameter is given, then + the drop parameter has to be an empty set! + + keep: set of strings; default is an empty set + set of column labels to be kept in the dataframe. + don't worry: essential columns like ID, coordinates + and time will always be kept. + + scale: string; default 'maxabs' + specify how the data should be scaled. + possible values are None, maxabs, minmax, std. + for more input, check out: help(pcdl.scaler) + + keep_mcds: boole; default True + should the loaded original mcds be kept in memory + after transformation? + + output: + self.l_sdmcds: list of spatialdata objects. + + description: + function to transform mcds time steps into + spatialdata objects for downstream analysis. + """ + # variable triage + es_keep = set(self.get_cell_attribute(values=values, drop=drop, keep=keep, allvalues=False).keys()) + + # processing + lsd_mcds = [] + i_mcds = len(self.l_mcds) + for i in range(i_mcds): + # fetch mcds + if keep_mcds: + mcds = self.l_mcds[i] + else: + mcds = self.l_mcds.pop(0) + + # extract time and dataframes + if self.verbose: + print(f'\nprocessing: {i+1}/{i_mcds} {mcds.get_time()}[min] mcds into spatialdata obj.') + + # get spatialdata object + sd_mcds = mcds.get_spatialdata( + points = points, + shapes = shapes, + #values = 1, + #drop = set(), + keep = es_keep, + scale = scale, + ) + lsd_mcds.append(sd_mcds) + + # output + self.l_sdmcds = lsd_mcds + return self.l_sdmcds + + + def get_sdmcds_list(self): + """ + input: + self: TimeSeries class instance. + + output: + self.l_sdmcds: list of chronologically ordered spatialdata mcds objects. + watch out, this is a pointer to the + self.l_sdmcds list of spdata mcds objects, not a copy of self.l_sdmcds! + + description: + function returns a binding to the self.l_sdmcds list of spdata mcds objects. + """ + return self.l_sdmcds + + + def plot_timeseries(self, focus_cat=None, focus_num=None, aggregate_num=np.nanmean, frame='cell', cat_drop=set(), cat_keep=set(), z_slice=None, logy=False, ylim=None, secondary_y=None, subplots=False, sharex=False, sharey=False, linestyle='-', linewidth=None, cmap=None, color=None, grid=True, legend=True, yunit=None, title=None, ax=None, figsizepx=[640, 480], ext=None, figbgcolor=None, **kwargs): + """ + input: + self: TimeSeries class instance + + focus_cat: string; default is None + categorical or boolean data column within dataframe specified under frame. + default is None, which is total, which is all agents or voxels, no categories. + + focus_num: string; default is None + numerical data column within dataframe specified under frame. + default is None, which is count, agent or voxel count. + + aggregate_num: function; default np.nanmean + aggregation function for focus_num data. + + frame: string; default is cell_df + to specifies the data dataframe. + cell: dataframe will be retrieved through the mcds.get_cell_df function. + conc: dataframe will be retrieved through the mcds.get_conc_df function. + + cat_drop: set of strings; default is an empty set + if focus is a categorical attribute, + set of category labels to be dropped for the dataframe. + Attention: when the cat_keep parameter is given, then + the cat_drop parameter has to be an empty set! + + cat_keep: set of strings; default is an empty set + if focus is a categorical attribute, + set of category labels to be kept in the dataframe. + + z_slice: floating point number; default is None + z-axis position to slice a 2D xy-plain out of the 3D mesh. + if z_slice position numeric but not an exact mesh center coordinate, + then z_slice will be adjusted to the nearest mesh center value, + the smaller one, if the coordinate lies on a saddle point. + if set to None, the whole domain is taken. + + logy: bool; default False + if True, then y axis is natural log scaled. + + ylim: tuple of two floats; default is None + y axis min and max value. + default is None, which automatically detects min and max value. + + secondary_y: bool or list of strings; default False + whether to plot on the secondary y-axis. + if a list, which columns to plot on the secondary y-axis. + + subplots: bool or sequence of iterable, default False + whether to group columns into subplots. + a sequence of iterable of column labels + will create a subplot for each group of columns. + + sharex: bool, default False + in case subplots is True, share x-axis by + setting some x-axis labels to invisible. + + sharey: bool, default False + in case subplots is True, share y-axis range and possibly + setting some y-axis labels to invisible. + + linestyle: string or list of strings, default '-' + matplotlib line style {'-', '--', '-.', ':', ''}, + over all or per column. + + linewidth: float or list of float, default None + line width in points. + + cmap: string; default None + matplotlib colormap string. + https://matplotlib.org/stable/tutorials/colors/colormaps.html + achtung: if cmap is given, color will be disregarded. + + color: string or list of string or dictionary; default None + color string referred to by name, RGB or RGBA code. + achtung: if cmap is given, color will be disregarded. + + grid: boolean; default True + plot axis grid lines. + + legend: bool or 'reverse'; default True + if True or reverse, place legend on axis subplots. + + yunit: string; default None + string to specify y-axis unit. + None will not print a unit on the y-axis. title: string or list; default None title to use for the plot or subplots. @@ -1578,486 +1915,139 @@ def plot_timeseries(self, focus_cat=None, focus_num=None, aggregate_num=np.nanme return fig - ## GRAPH RELATED FUNCTIONS ## - - def make_graph_gml(self, graph_type, edge_attribute=True, node_attribute=[]): + def make_ome_tiff(self, cell_attribute='ID', conc_cutoff={}, focus=None, file=True, collapse=True): """ input: - self: TimeSeries class instance. + cell_attribute: strings; default is 'ID', which will result in a + cell segmentation mask. + column name within the cell dataframe. + the column data type has to be numeric (bool, int, float) + and cannot be string. + the result will be stored as 32 bit float. - graph_type: string - to specify which physicell output data should be processed. - attached, touch: processes mcds.get_attached_graph_dict dictionary. - neighbor: processes mcds.get_neighbor_graph_dict dictionary. - spring: processes mcds.get_spring_graph_dict dictionary. + conc_cutoff: dictionary string to real; default is an empty dictionary. + if a contour from a substrate not should be cut by greater + than zero (shifted to integer 1), another cutoff value can be specified here. - edge_attribute: boolean; default True - specifies if the spatial Euclidean distance is used for - edge attribute, to generate a weighted graph. + focus: set of strings; default is a None + set of substrate and cell_type names to specify what will be + translated into ome tiff format. + if None, all substrates and cell types will be processed. - node_attribute: list of strings; default is empty list - list of mcds.get_cell_df dataframe columns, used for - node attributes. + file: boolean; default True + if True, an ome tiff file is the output. + if False, a numpy array with shape tczyx is the output. + + collapse: boole; default True + should all mcds time steps from the time series be collapsed + into one ome tiff file (numpy array), + or an ome tiff file (numpy array) for each time step? output: - gml file for each time step. - path and filenames are printed to the standard output. + a_tczyx_img: numpy array or ome tiff file. - description: - function to generate graph files in the gml graph modelling language - standard format. - gml was the outcome of an initiative that started at - the international symposium on graph drawing 1995 in Passau - and ended at Graph Drawing 1996 in Berkeley. the networkx python - and igraph C and python libraries for graph analysis are - gml compatible and can as such read and write this file format. + description: + function to transform chosen mcdsts output into an 1[um] spaced + tczyx (time, channel, z-axis, y-axis, x-axis) ome tiff file or numpy array, + one substrate or cell_type per channel. + a ome tiff file is more or less: + a numpy array, containing the image information + and a xml, containing the microscopy metadata information, + like the channel labels. + the ome tiff file format can for example be read by the napari + or fiji (imagej) software. - https://en.wikipedia.org/wiki/Graph_Modelling_Language - https://github.com/elmbeech/physicelldataloader/blob/master/man/publication/himsolt1996gml_a_portable_graph_file_format.pdf - https://networkx.org/ - https://igraph.org/ - """ - # processing - ls_pathfile = [] + https://napari.org/stable/ + https://fiji.sc/ + """ + # for each time step + l_tczyx_img = [] for mcds in self.get_mcds_list(): - s_pathfile = mcds.make_graph_gml( - graph_type = graph_type, - edge_attribute = edge_attribute, - node_attribute = node_attribute, + # processing + b_file = True # 10 + if (not file and not collapse) or (not file and collapse) or (file and collapse): # 00, 01, 11 + b_file = False + o_tczyx_img = mcds.make_ome_tiff( + cell_attribute = cell_attribute, + conc_cutoff = conc_cutoff, + focus = focus, + file = b_file ) - ls_pathfile.append(s_pathfile) - - # outout - return ls_pathfile - - - ## ANNDATA RELATED FUNCTIONS ## - - def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=True): - """ - input: - values: integer; default is 1 - minimal number of values a variable has to have to be outputted. - variables that have only 1 state carry no information. - None is a state too. - - drop: set of strings; default is an empty set - set of column labels to be dropped for the dataframe. - don't worry: essential columns like ID, coordinates - and time will never be dropped. - Attention: when the keep parameter is given, then - the drop parameter has to be an empty set! - - keep: set of strings; default is an empty set - set of column labels to be kept in the dataframe. - don't worry: essential columns like ID, coordinates - and time will always be kept. - - scale: string; default 'maxabs' - specify how the data should be scaled. - possible values are None, maxabs, minmax, std. - for more input, check out: help(pcdl.scaler) - - collapse: boole; default True - should all mcds time steps from the time series be collapsed - into one single anndata object, or a list of anndata objects - for each time step? - - keep_mcds: boole; default True - should the loaded original mcds be kept in memory - after transformation? - - output: - annmcds or self.l_annmcds: anndata object or list of anndata objects. - what is returned depends on the collapse setting. - - description: - function to transform mcds time steps into one or many - anndata objects for downstream analysis. - """ - # load optional dependency - ad = optional_import('anndata', s_caller='TimeSeries.get_anndata') - - # initialize vaiable - l_annmcds = [] - df_anncount = None - df_annobs = None - ar_annobsm = None + l_tczyx_img.append(o_tczyx_img) - # variable triage - if (values < 2): - ls_column = list(self.l_mcds[0].get_cell_df(drop=drop, keep=keep).columns) - else: - ls_column = sorted(es_coor_cell.difference({'ID'})) - ls_column.extend(sorted(self.get_cell_attribute(values=values, drop=drop, keep=keep, allvalues=False).keys())) + # handle channels + ls_substrate = mcds.get_substrate_list() + ls_celltype = mcds.get_celltype_list() - # package collapse - if collapse: + if not (focus is None): + ls_substrate = [s_substrate for s_substrate in ls_substrate if s_substrate in set(focus)] + ls_celltype = [s_celltype for s_celltype in ls_celltype if s_celltype in set(focus)] + if (set(focus) != set(ls_substrate).union(set(ls_celltype))): + sys.exit(f'Error : {focus} not found in {ls_substrate} {ls_celltype}') - # warning + # output 00 list of numpy arrays + if (not file and not collapse): # 00 if self.verbose: - print('Warning @ mcdsts.get_anndata : only df_cell data, but not graph data, can be collapsed.') - - # extract - df_cell = self.get_cell_df(values=values, drop=drop, keep=keep, collapse=True) - df_count, df_obs, d_obsm, d_obsp, d_uns = _anndextract( - df_cell=df_cell, - scale = scale, - #graph_attached = {}, - #graph_neighbor = {}, - #graph_spring = {}, - #graph_method = s_physicellv, - ) - - # fuse to anndata object - ann_mcdsts = ad.AnnData( - X = df_count, - obs = df_obs, - obsm = d_obsm, - #obsp = d_obsp, # nop (graph) - #uns = d_uns, # nop (graph) - ) - - # mcds - if not keep_mcds: - self.l_mcds = [] - - # output - return ann_mcdsts - - # pack not collapsed - else: - # processing - lann_mcds = [] - i_mcds = len(self.l_mcds) - for i in range(i_mcds): - # fetch mcds - if keep_mcds: - mcds = self.l_mcds[i] - else: - mcds = self.l_mcds.pop(0) - - # extract physicell version - s_physicellv = mcds.get_physicell_version(), - - # extract time and dataframes - if self.verbose: - print(f'processing: {i+1}/{i_mcds} {mcds.get_time()}[min] mcds into anndata obj.') - df_cell = mcds.get_cell_df() - df_cell = df_cell.loc[:,ls_column] - - # extract - df_count, df_obs, d_obsm, d_obsp, d_uns = _anndextract( - df_cell=df_cell, - scale = scale, - graph_attached = mcds.get_attached_graph_dict(), - graph_neighbor = mcds.get_neighbor_graph_dict(), - graph_spring = mcds.get_spring_graph_dict(), - graph_method = s_physicellv, - ) - - # annmcds - ann_mcds = ad.AnnData( - X = df_count, - obs = df_obs, - obsm = d_obsm, - obsp = d_obsp, - uns = d_uns, - ) - lann_mcds.append(ann_mcds) - - # output - self.l_annmcds = lann_mcds - return self.l_annmcds - - def get_annmcds_list(self): - """ - input: - self: TimeSeries class instance. - - output: - self.l_annmcds: list of chronologically ordered anndata mcds objects. - watch out, this is a pointer to the - self.l_annmcds list of anndata mcds objects, not a copy of self.l_annmcds! - - description: - function returns a binding to the self.l_annmcds list of anndata mcds objects. - """ - return self.l_annmcds - - - def get_spatialdata(self, images={'subs'}, labels=set(), points={'subs'}, shapes={'cell'}, values=1, drop=set(), keep=set(), scale='maxabs', keep_mcds=True): - """ - input: - images: set of string; default {'subs'} - specify if from the subs or cell dataset - a multichannel image should be generate. - so far, only the subs image element is implemented. - - labels: set of strings; default is an empty set - specify if from the subs or cell dataset - a label element should be generated. - so far, neither subs nor cell label elements are implemented. - - points: set of string; default {'subs'} - specify if from the subs or cell dataset - a points element should be generated. - both, subs and cell point elements, are implemented. - - shapes: set of string; default {'cell'} - specify if from the subs or cell dataset - a shape element should be generated. - so far, only the cell shape element is implemented. - - values: integer; default is 1 - minimal number of values a variable has to have to be outputted. - variables that have only 1 state carry no information. - None is a state too. - - drop: set of strings; default is an empty set - set of column labels to be dropped for the dataframe. - don't worry: essential columns like ID, coordinates - and time will never be dropped. - Attention: when the keep parameter is given, then - the drop parameter has to be an empty set! - - keep: set of strings; default is an empty set - set of column labels to be kept in the dataframe. - don't worry: essential columns like ID, coordinates - and time will always be kept. - - scale: string; default 'maxabs' - specify how the data should be scaled. - possible values are None, maxabs, minmax, std. - for more input, check out: help(pcdl.scaler) - - keep_mcds: boole; default True - should the loaded original mcds be kept in memory - after transformation? - - output: - self.l_sdmcds: list of spatialdata objects. - - description: - function to transform mcds time steps into - spatialdata objects for downstream analysis. - """ - # variable triage - es_keep = set(self.get_cell_attribute(values=values, drop=drop, keep=keep, allvalues=False).keys()) - - # processing - lsd_mcds = [] - i_mcds = len(self.l_mcds) - for i in range(i_mcds): - # fetch mcds - if keep_mcds: - mcds = self.l_mcds[i] - else: - mcds = self.l_mcds.pop(0) + print(f'la_tczyx_img shape: {len(l_tczyx_img)} * {l_tczyx_img[0].shape}') + return l_tczyx_img - # extract time and dataframes + # output 01 numpy array + elif (not file and collapse): # 01 + # numpy array + a_tczyx_img = np.array(l_tczyx_img) if self.verbose: - print(f'\nprocessing: {i+1}/{i_mcds} {mcds.get_time()}[min] mcds into spatialdata obj.') - - # get spatialdata object - sd_mcds = mcds.get_spatialdata( - points = points, - shapes = shapes, - #values = 1, - #drop = set(), - keep = es_keep, - scale = scale, - ) - lsd_mcds.append(sd_mcds) - - # output - self.l_sdmcds = lsd_mcds - return self.l_sdmcds - - - def get_sdmcds_list(self): - """ - input: - self: TimeSeries class instance. - - output: - self.l_sdmcds: list of chronologically ordered spatialdata mcds objects. - watch out, this is a pointer to the - self.l_sdmcds list of spdata mcds objects, not a copy of self.l_sdmcds! - - description: - function returns a binding to the self.l_sdmcds list of spdata mcds objects. - """ - return self.l_sdmcds - - - - ## MUSPAN RELATED FUNCTIONS ## - - def get_muspan(self, z_slice=None, values=1, drop=set(), keep=set()): - """ - input: - z_slice: floating point number; default is None - z-axis position to slice a 2D xy-plain out of the - 3D mesh. if None the whole 3D mesh will be returned. - - values: integer; default is 1 - minimal number of values a variable has to have to be outputted. - variables that have only 1 state carry no information. - None is a state too. + print('a_tczyx_img shape:', a_tczyx_img.shape) + return a_tczyx_img - drop: set of strings; default is an empty set - set of column labels to be dropped for the dataframe. - don't worry: essential columns like ID, coordinates - and time will never be dropped. - Attention: when the keep parameter is given, then - the drop parameter has to be an empty set! + # output 10 list of pathfile strings + elif (file and not collapse): # 10 + return l_tczyx_img - keep: set of strings; default is an empty set - set of column labels to be kept in the dataframe. - set values=1 to be sure that all variables are kept. - don't worry: essential columns like ID, coordinates - and time will always be kept. + # output 11 ometiff file + elif (file and collapse): # 11 + # load optional dependency + OmeTiffWriter = optional_import('bioio.writers', s_attr='OmeTiffWriter', s_pip='bioio', s_caller='TimeSeries.make_ome_tiff') + bioio_base = optional_import('bioio_base', s_pip='bioio', s_caller='TimeSeries.make_ome_tiff') - output: - do_domain: dictionary of muspa domains, one for each time step z-layer. + # numpy array + a_tczyx_img = np.array(l_tczyx_img) + if self.verbose: + print('a_tczyx_img shape:', a_tczyx_img.shape) - description: - function returns a dictionary of muspa domains, containg a - cell and subs collection with disrcete and continuous labels - and all the graph as networks. - + https://www.muspan.co.uk - + https://docs.muspan.co.uk/latest/Documentation.html - """ - # variable triage - es_keep = set(self.get_cell_attribute(values=values, drop=drop, keep=keep, allvalues=False).keys()) + # generate filename + s_channel = '' + for s_substrate in ls_substrate: + try: + r_value = conc_cutoff[s_substrate] + s_channel += f'_{s_substrate}{r_value}' + except KeyError: + s_channel += f'_{s_substrate}' + for s_celltype in ls_celltype: + s_channel += f'_{s_celltype}' + if len(ls_celltype) > 0: + s_channel += f'_{cell_attribute}' + s_tifffile = f"timeseries{s_channel.replace(' ','_')}.ome.tiff" + if (len(s_tifffile) > 255): + print(f"Warning: filename {len(s_tifffile)} > 255 character.") + s_tifffile = 'timeseries_channels.ome.tiff' + print(f"file name adjusted to {s_tifffile}.") + s_tiffpathfile = self.path + '/' + s_tifffile - # processing - do_domain = {} - for mcds in self.get_mcds_list(): - do_domain.update( - mcds.get_muspan( - z_slice = z_slice, - #values = 1, - #drop = set(), - keep = es_keep, - ) + # save to file + OmeTiffWriter.save( + a_tczyx_img, + s_tiffpathfile, + dim_order = 'TCZYX', + #ome_xml=x_img, + channel_names = ls_substrate + ls_celltype, + image_names = [f'timeseries_{cell_attribute}'], + physical_pixel_sizes = bioio_base.types.PhysicalPixelSizes(mcds.get_voxel_spacing()[2], 1.0, 1.0), # z,y,x [um] + #channel_colors=, + #fs_kwargs={}, ) + return s_tiffpathfile - # output - return do_domain - - - ## SIMULARIUM RELATED FUNCTIONS ## - - def make_simularium(self, focus_cat=['cell_type','current_phase'], trajectory_title='timeseries', scale_factor=None, camera_defaults=None, model_meta_data=None): - """ - input: - focus_cat: list of 1 or 2 string; default is ['cell_type','current_phase'] - specify 1 or 2 categorical column labels, to be found - in mcdsts.get_cell_df(). - - trajectory_title: string; default 'timeseries' - the trajectory_title will be used as - .simularium file name and displayed - in the simulation. - - scale_factor: float number; default is None - A multiplier for the scene, use if visualization is - too large or small. If None is provided, one will - be calculated based on the position data. - bue 20260822: does not seem to work in current simularium 1.13.0. - - camera_defaults: simulariumio.CameraData object; default is None - camera's initial settings which it also returns to - when reset. - - model_meta_data: simulariumio.ModelMetaData; default is None - Metadata for the model that produced this - trajectory. - - output: - .simularium file - - description: - function returns a simularium trajectory file that can be run with the - online Simularium Viewer. - + https://simularium.allencell.org/ - """ - # library - sim = optional_import(s_module='simulariumio', s_caller='TimeSeries.make_simularium') - - # handle input - model_meta_data=sim.ModelMetaData() if model_meta_data is None else model_meta_data - camera_defaults=sim.CameraData() if camera_defaults is None else camera_defaults - - # here we go - if self.verbose: - print(f'generating {trajectory_title}.simularium file ...') - - # extract box from mcdsts - ltr_domain = self.get_mcds_list()[0].get_mesh_mnp_range() - lr_space = self.get_mcds_list()[0].get_mesh_spacing() - lr_box = np.array([ - ltr_domain[0][1] - ltr_domain[0][0], - ltr_domain[1][1] - ltr_domain[1][0], - ltr_domain[2][1] - ltr_domain[2][0], - ]) - lr_box[0] = lr_box[0] if lr_box[0] != 0.0 else lr_space[0] - lr_box[1] = lr_box[1] if lr_box[1] != 0.0 else lr_space[1] - lr_box[2] = lr_box[2] if lr_box[2] != 0.0 else lr_space[2] - ar_box = np.array(lr_box) - - # extract cell dataframe from mcdsts - df_cell = self.get_cell_df() - - # handle agent annotation - se_type = df_cell.loc[:, focus_cat].astype(str).agg('#'.join, axis=1) - - # extract units from mcdsts - ds_unit = self.get_mcds_list()[0].get_unit_dict() - - # generate simularium dataframe - with pd.option_context('future.infer_string', False): - df_sim = pd.DataFrame({ - 'time': df_cell.loc[:, 'time'].to_numpy(dtype=float), - 'unique_id': df_cell.loc[:, 'ID'].to_numpy(dtype=int), - 'type': se_type.to_numpy(dtype=object), - 'positionX': df_cell.loc[:, 'position_x'].to_numpy(dtype=float), - 'positionY': df_cell.loc[:, 'position_y'].to_numpy(dtype=float), - 'positionZ': df_cell.loc[:, 'position_z'].to_numpy(dtype=float), - 'radius': df_cell.loc[:, 'radius'].to_numpy(dtype=float), - 'rotationX': np.zeros(df_cell.shape[0], dtype=float), - 'rotationY': np.zeros(df_cell.shape[0], dtype=float), - 'rotationZ': np.zeros(df_cell.shape[0], dtype=float), - }) - df_sim.sort_values(['time', 'unique_id'], inplace=True) - # SimulariumIO 1.13.0 AgentData.from_dataframe expects traj.loc[0, ...] - # to select the whole trajectory, so all rows need the same index label. - df_sim.index = np.zeros(df_sim.shape[0], dtype=int) - - # generate simmularium trajectorydata object - o_sim = sim.TrajectoryData( - meta_data = sim.MetaData( - box_size=ar_box, - camera_defaults=camera_defaults, - scale_factor=scale_factor, - trajectory_title=trajectory_title, - model_meta_data=model_meta_data, - ), - agent_data = sim.AgentData.from_dataframe(df_sim), - time_units = sim.UnitData(ds_unit['time']), - spatial_units = sim.UnitData(ds_unit['spatial_unit']), - #plots=, - ) - - # transform data and save trajectorydata object to simularium file - s_save = self.path + '/' + trajectory_title - s_pathfile = s_save + '.simularium' - sim.TrajectoryConverter(o_sim).save(s_save) - if self.verbose: - print(f'simularium viewer at: https://simularium.allencell.org/') - - # return error code - return s_pathfile + # error case + else: + sys.exit(f'Error @ make_ome_tiff : {file} {collapse} strange file collapse combination.') diff --git a/pcdl/timestep.py b/pcdl/timestep.py index b318c18..e54f682 100644 --- a/pcdl/timestep.py +++ b/pcdl/timestep.py @@ -1053,7 +1053,7 @@ def get_voxel_ijk(self, x, y, z, is_in_mesh=True): return li_ijk - ## MICROENVIRONMENT RELATED FUNCTIONS ## + ## SUBSTRATE RELATED FUNCTIONS ## def get_substrate_list(self): """ @@ -1602,6 +1602,108 @@ def get_cell_attribute_list(self): return self.data['cell']['ls_cellattr'].copy() + def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs'): + """ + input: + values: integer; default is 1 + minimal number of values a variable has to have to be outputted. + variables that have only 1 state carry no information. + None is a state too. + + drop: set of strings; default is an empty set + set of column labels to be dropped for the dataframe. + don't worry: essential columns like ID, coordinates + and time will never be dropped. + Attention: when the keep parameter is given, then + the drop parameter has to be an empty set! + + keep: set of strings; default is an empty set + set of column labels to be kept in the dataframe. + set values=1 to be sure that all variables are kept. + don't worry: essential columns like ID, coordinates + and time will always be kept. + + scale: string; default 'maxabs' + specify how the data should be scaled. + possible values are None, maxabs, minmax, std. + for more input, check out: help(pcdl.scaler) + + output: + annmcds: anndata object + for this one time step. + + description: + function to transform a mcds time step into an anndata object + for downstream analysis. + """ + # load optional dependency + ad = optional_import('anndata', s_caller='TimeStep.get_anndata') + + # processing + if self.verbose: + print(f'processing: 1/1 {round(self.get_time(),9)}[min] mcds into anndata obj.') + df_cell = self.get_cell_df(values=values, drop=drop, keep=keep) + df_count, df_obs, d_obsm, d_obsp, d_uns = _anndextract( + df_cell = df_cell, + scale = scale, + graph_attached = self.get_attached_graph_dict(), + graph_neighbor = self.get_neighbor_graph_dict(), + graph_spring = self.get_spring_graph_dict(), + graph_method = self.get_physicell_version(), + ) + annmcds = ad.AnnData( + X = df_count, + obs = df_obs, + obsm = d_obsm, + obsp = d_obsp, + uns = d_uns + ) + # output + return annmcds + + + def get_attached_graph_dict(self): + """ + input: + + output: + dei_graph: dictionary of sets of integers + maps each cell ID to the attached connected cell IDs. + + description: + function returns the attached cell graph as a dictionary object. + """ + return self.data['cell']['dei_graph']['attached_cells'].copy() + + + def get_neighbor_graph_dict(self): + """ + input: + + output: + dei_graph: dictionary of sets of integers + maps each cell ID to the connected neighbor cell IDs. + + description: + function returns the cell neighbor graph as a dictionary object. + """ + return self.data['cell']['dei_graph']['neighbor_cells'].copy() + + + def get_spring_graph_dict(self): + """ + input: + + output: + dei_graph: dictionary of sets of integers + maps each cell ID to the attached connected cell IDs. + + description: + function returns the attached spring cell graph as a dictionary object. + """ + return self.data['cell']['dei_graph']['spring_attached_cells'].copy() + + def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slice=0.0, z_axis=None, alpha=1, cmap='viridis', title=None, grid=True, legend_loc='lower left', xlim=None, ylim=None, xyequal=True, s=1.0, ax=None, figsizepx=None, directory=None, ext=None, figbgcolor=None, **kwargs): """ input: @@ -2014,396 +2116,112 @@ def make_cell_vtk(self, attribute=['cell_type'], ext='_cell.vtp'): return s_vtkpathfile - ## MICROENVIRONMENT AND CELL AGENT RELATED FUNCTIONS ## - - def make_ome_tiff(self, cell_attribute='ID', conc_cutoff={}, focus=None, file=True): + def make_graph_gml(self, graph_type, edge_attribute=True, node_attribute=[]): """ input: - cell_attribute: strings; default is 'ID', which will result in a - cell segmentation mask. - column name within the cell dataframe. - the column data type has to be numeric (bool, int, float) - and cannot be string. - the result will be stored as 32 bit float. - - conc_cutoff: dictionary string to real; default is an empty dictionary. - if a contour from a substrate not should be cut by greater - than zero (shifted to integer 1), another cutoff value can be - specified here. + graph_type: string + to specify which physicell output data should be processed. + neighbor, touch: processes mcds.get_neighbor_graph_dict dictionary. + attached: processes mcds.get_attached_graph_dict dictionary. + spring: processes mcds.get_spring_graph_dict dictionary. - focus: set of strings; default is a None - set of substrate and cell_type names to specify what will be - translated into ome tiff format. - if None, all substrates and cell types will be processed. + edge_attribute: boolean; default True + specifies if the spatial Euclidean distance is used for + edge attribute, to generate a weighted graph. - file: boolean; default True - if True, an ome tiff file is the output. - if False, a numpy array with shape czyx is the output. + node_attribute: list of strings; default is empty list + list of mcds.get_cell_df dataframe columns, used for + node attributes. output: - a_tczyx_img: numpy array or ome tiff file. + gml file, generated under the returned path. description: - function to transform chosen mcds output into an 1[um] spaced - czyx (channel, z-axis, y-axis, x-axis) ome tiff file or numpy array, - one substrate or cell_type per channel. - an ome tiff file is more or less: - a numpy array, containing the image information - and a xml, containing the microscopy metadata information, - like the channel labels. - the ome tiff file format can for example be read by the napari - or fiji (imagej) software. + function to generate graph files in the gml graph modelling language + standard format. - https://napari.org/stable/ - https://fiji.sc/ + gml was the outcome of an initiative that started at + the international symposium on graph drawing 1995 in Passau + and ended at Graph Drawing 1996 in Berkeley. the networkx python + and igraph C and python libraries for graph analysis are + gml compatible and can as such read and write this file format. + + https://en.wikipedia.org/wiki/Graph_Modelling_Language + https://github.com/elmbeech/physicelldataloader/blob/master/man/publication/himsolt1996gml_a_portable_graph_file_format.pdf + https://networkx.org/ + https://igraph.org/ """ - # load optional dependencies - if file: - OmeTiffWriter = optional_import('bioio.writers', s_attr='OmeTiffWriter', s_pip='bioio', s_caller='TimeStep.make_ome_tiff') - bioio_base = optional_import('bioio_base', s_pip='bioio', s_caller='TimeStep.make_ome_tiff') + # load dataframe for celltype information + df_cell = self.get_cell_df() + ds_unit = self.get_unit_dict() + s_unit_simtime = ds_unit["time"] + r_simtime = self.get_time() + if (graph_type in {'attached'}): + dei_graph = self.get_attached_graph_dict() + elif (graph_type in {'neighbor', 'touch'}): + dei_graph = self.get_neighbor_graph_dict() + elif (graph_type in {'spring'}): + dei_graph = self.get_spring_graph_dict() + #elif (graph_type in {'evo','devo','lineage'}): + # dei_graph = self.get_lineage_graph_dict() + else: + sys.exit(f'Erro @ make_graph_gml : unknown graph_type {graph_type}. known are attached, neighbor, spring, and touch.') - # handle channels - ls_substrate = self.get_substrate_list() - ls_celltype = self.get_celltype_list() + # generate filename + s_gmlpathfile = self.path + '/' + self.xmlfile.replace('.xml',f'_{graph_type}.gml') - if not (focus is None): - ls_substrate = [s_substrate for s_substrate in ls_substrate if s_substrate in set(focus)] - ls_celltype = [s_celltype for s_celltype in ls_celltype if s_celltype in set(focus)] - if (set(focus) != set(ls_substrate).union(set(ls_celltype))): - sys.exit(f'Error : {focus} not found in {ls_substrate} {ls_celltype}') + # open result gml file + f = open(s_gmlpathfile, 'w') + f.write(f'Creator "pcdl_v{__version__}"\ngraph [\n') + f.write(f' id {int(r_simtime)}\n comment "time_{s_unit_simtime}"\n label "{graph_type}_graph"\n directed 0\n') + for i_src, ei_dst in dei_graph.items(): + #print(f'{i_src} {sorted(ei_dst)}') + # node + f.write(f' node [\n id {i_src}\n label "node_{i_src}"\n') + # node attributes + for s_attribute in node_attribute: + o_attribute = df_cell.loc[i_src, s_attribute] + if (type(o_attribute) == str) or (o_attribute.dtype == np.object_): #in {str, np.str_, np.object_}): + f.write(f' {s_attribute} "{o_attribute}"\n') + elif (o_attribute.dtype == bool) or (o_attribute.dtype == int): #in {bool, np.bool_, np.bool, int, np.int_, np.int8, np.int16, np.int32, np.int64}): + f.write(f' {s_attribute} {int(o_attribute)}\n') + elif (o_attribute.dtype == float): #in {float, np.float16, np.float32, np.float64, np.float128}): + f.write(f' {s_attribute} {o_attribute}\n') + else: + sys.exit(f'Error @ TimeStep.make_graph_gml : attribute {o_attribute}; type {o_attribute.dtype}; type seems not to be bool, int, float, or string.') + f.write(f' ]\n') + # edge + for i_dst in ei_dst: + if (i_src < i_dst): + f.write(f' edge [\n source {i_src}\n target {i_dst}\n label "edge_{i_src}_{i_dst}"\n') + if (edge_attribute): + # edge distance attribute + x = df_cell.loc[i_src, 'position_x'] - df_cell.loc[i_dst, 'position_x'] + y = df_cell.loc[i_src, 'position_y'] - df_cell.loc[i_dst, 'position_y'] + z = df_cell.loc[i_src, 'position_z'] - df_cell.loc[i_dst, 'position_z'] + r_distance = (x**2 + y**2 + z**2)**(1/2) + f.write(f' distance_{ds_unit["position_y"]} {round(r_distance)}\n') + f.write(f' ]\n') + # development + #if (i_src > 16): + # break + # close result gml file + f.write(']\n') + f.close() - # const - ls_coor_mnp = ['mesh_center_m', 'mesh_center_n', 'mesh_center_p'] # xyz - ls_coor_xyz = ['position_x', 'position_y', 'position_z'] # xyz - ls_coor = ['voxel_x', 'voxel_y', 'voxel_z'] + # output + return s_gmlpathfile - # time step tensor - i_time = int(self.get_time()) - # get xy coordinate dataframe - lr_axis_z = list(self.get_mesh_mnp_axis()[2] - self.get_voxel_spacing()[2] / 2) - lr_axis_z.append(self.get_mesh_mnp_axis()[2][-1] + self.get_voxel_spacing()[2] / 2) - lll_coor = [] - for i_x in range(int(round(self.get_voxel_ijk_range()[0][1] * self.get_voxel_spacing()[0]))): - for i_y in range(int(round(self.get_voxel_ijk_range()[1][1] * self.get_voxel_spacing()[1]))): - lll_coor.append([i_x, i_y]) - df_coor = pd.DataFrame(lll_coor, columns=ls_coor[:2]) - lr_axis_z[-1] += 1 + ## SUBSTRATE AND CELL AGENT RELATED FUNCTIONS ## - # extract voxel radius - di_grow = {} - for s_substarte in ls_substrate: - di_grow.update({ - s_substarte : int(np.round(np.mean(self.get_voxel_spacing()[:2])) - 1) - }) + def get_muspan(self, z_slice=None, values=1, drop=set(), keep=set()): + """ + input: + z_slice: floating point number; default is None + z-axis position to slice a 2D xy-plain out of the + 3D mesh. if None the whole 3D mesh will be returned. - # get and shift substrate xy data - df_conc = self.get_conc_df() - df_conc = df_conc.loc[:, ls_coor_mnp + ls_substrate] - df_conc.loc[:, 'mesh_center_m'] = (df_conc.loc[:, 'mesh_center_m'] - self.get_xyz_range()[0][0]).round() - df_conc.loc[:, 'mesh_center_n'] = (df_conc.loc[:, 'mesh_center_n'] - self.get_xyz_range()[1][0]).round() - df_conc.rename({'mesh_center_m':'voxel_x', 'mesh_center_n':'voxel_y', 'mesh_center_p':'voxel_z'}, axis=1, inplace=True) - df_conc = df_conc.astype({'voxel_x': int, 'voxel_y': int, 'voxel_z': float}) - # level the cake - for s_channel in conc_cutoff.keys(): - try: - df_conc.loc[:, s_channel] = df_conc.loc[:, s_channel] - conc_cutoff[s_channel] + 1 # positive values starting at > 0 - df_conc.loc[(df_conc.loc[:, s_channel] <= conc_cutoff[s_channel]), s_channel] = 0 - except KeyError: - pass - - # get cell data - df_cell = self.get_cell_df().reset_index() - - # extract cell radius - for s_celltype in ls_celltype: - try: - i_cell_grow = int(round(df_cell.loc[(df_cell.cell_type == s_celltype), 'radius'].mean()) - 1) - except: - i_cell_grow = 0 - di_grow.update({s_celltype : i_cell_grow}) - - # filter and shift - df_cell = df_cell.loc[:, ls_coor_xyz + ['cell_type', cell_attribute]] - if (cell_attribute == 'cell_type'): - sys.exit(f'Error @ TimeStep.make_ome_tiff : cell_attribute cannot be cell_type.') - elif (df_cell.loc[:, cell_attribute].dtype == str) or (df_cell.loc[:, cell_attribute].dtype == np.object_): # in {str, np.str_, np.object_}): - sys.exit(f'Error @ TimeStep.make_ome_tiff : {cell_attribute} {df_cell.loc[:, cell_attribute].dtype} cell_attribute cannot be string or object. cell_attribute has to be boolean, integer, or float.') - elif (df_cell.loc[:, cell_attribute].dtype == bool): # in {bool, np.bool_, np.bool}): - df_cell = df_cell.astype({cell_attribute: int}) - df_cell.loc[:, 'position_x'] = (df_cell.loc[:, 'position_x'] - self.get_xyz_range()[0][0]).round() - df_cell.loc[:, 'position_y'] = (df_cell.loc[:, 'position_y'] - self.get_xyz_range()[1][0]).round() - df_cell.rename({'position_x':'voxel_x', 'position_y':'voxel_y', 'position_z':'voxel_z'}, axis=1, inplace=True) - df_cell = df_cell.astype({'voxel_x': int, 'voxel_y': int, 'voxel_z': float}) - # level the cake - df_cell.loc[:, cell_attribute] = df_cell.loc[:, cell_attribute] - df_cell.loc[:, cell_attribute].min() + 1 # positive values starting at > 0 - - # check for duplicates: two cell at exactelly the same xyz position. - #if self.verbose and df_cell.loc[:,['voxel_x', 'voxel_y', 'voxel_z']].duplicated().any(): - # df_duplicate = df_cell.loc[(df_cell.loc[:, ['voxel_x', 'voxel_y', 'voxel_z']].duplicated()), :] - # sys.exit(f"Error @ TimeStep.make_ome_tiff : {df_duplicate} cells at exactely the same xyz voxel position detected. cannot pivot!") - - # pivot cell_type - df_cell = df_cell.pivot_table(index=ls_coor, columns='cell_type', values=cell_attribute, aggfunc='sum').reset_index() # fill_value is na - for s_celltype in ls_celltype: - if not s_celltype in set(df_cell.columns): - df_cell[s_celltype] = 0 - - # each C channel - time step tensors - la_czyx_img = [] - ls_channel = ls_substrate + ls_celltype - for s_channel in ls_channel: - - # get channel dataframe - if s_channel in set(ls_substrate): - df_channel = df_conc.loc[:, ls_coor + [s_channel]] - elif s_channel in set(ls_celltype): - df_channel = df_cell.loc[:, ls_coor + [s_channel]] - else: - sys.exit(f'Error @ TimeStep.make_ome_tiff : {s_channel} unknown channel detected. not in substrate and cell type list {ls_substrate} {ls_celltype}!') - - # each z axis - la_zyx_img = [] - for i_zaxis in range(len(lr_axis_z)): - if (i_zaxis < (len(lr_axis_z) - 1)): - print(f'processing: {i_time} [min] {s_channel} [channel] {i_zaxis} [z_axis] ...') - # extract z layer - df_yxchannel = df_channel.loc[ - ((df_channel.loc[:, ls_coor[2]] >= lr_axis_z[i_zaxis]) & (df_channel.loc[:, ls_coor[2]] < lr_axis_z[i_zaxis + 1])), - ls_coor[:2] + [s_channel] - ] - - # drop row with na and duplicate entries - df_yxchannel = df_yxchannel.dropna(axis=0) - df_yxchannel = df_yxchannel.drop_duplicates() - - # merge with coooridnates and get image - # bue 20240811: df_coor left side merge will cut off reset cell that are out of the xyz domain range, which is what we want. - df_yxchannel = pd.merge(df_coor, df_yxchannel, on=ls_coor[:2], how='left').replace({np.nan: 0}) - try: - df_yxchannel = df_yxchannel.pivot(columns=ls_coor[0], index=ls_coor[1], values=s_channel) - except ValueError: # two cells from the same cell type very close to each other detetced. - if self.verbose: - df_duplicate = df_cell.loc[(df_yxchannel.loc[:, ['voxel_x', 'voxel_y']].duplicated()), :] - print(f'Warning: {s_channel} {df_duplicate} cells within 1[um] distance form each detected. cannot pivot. erase cell type from this timestep.') - df_yxchannel.loc[:,s_channel] = 0 # erase cells - df_yxchannel = df_yxchannel.drop_duplicates() - df_yxchannel = df_yxchannel.pivot(columns=ls_coor[0], index=ls_coor[1], values=s_channel) - a_yx_img = df_yxchannel.values - - # grow - a_yx_img = imagine.grow_seed(a_yx_img, i_step=di_grow[s_channel], b_verbose=False) - - # update output - la_zyx_img.append(a_yx_img) - a_zyx_img = np.array(la_zyx_img, np.float32) - la_czyx_img.append(np.array(a_zyx_img, np.float32)) - - # output - a_czyx_img = np.array(la_czyx_img, dtype=np.float32) - - # numpy array - if not file: - return a_czyx_img - - # write to file - else: - if self.verbose: - print('a_czyx_img shape:', a_czyx_img.shape) - # generate filename - s_channel = '' - for s_substrate in ls_substrate: - try: - r_value = conc_cutoff[s_substrate] - s_channel += f'_{s_substrate}{r_value}' - except KeyError: - s_channel += f'_{s_substrate}' - for s_celltype in ls_celltype: - s_channel += f'_{s_celltype}' - if len(ls_celltype) > 0: - s_channel += f'_{cell_attribute}' - s_tifffile = self.xmlfile.replace('.xml', f'{s_channel}.ome.tiff') - s_tifffile = s_tifffile.replace(' ','_') - if (len(s_tifffile) > 255): - print(f"Warning: filename {len(s_tifffile)} > 255 character.") - s_tifffile = self.xmlfile.replace('.xml', f'_channels.ome.tiff') - print(f"file name adjusted to {s_tifffile}.") - s_tiffpathfile = self.path + '/' + s_tifffile - - # save to file - OmeTiffWriter.save( - a_czyx_img, - s_tiffpathfile, - dim_order = 'CZYX', - #ome_xml=x_img, - channel_names = ls_channel, - image_names = [s_tifffile.replace('.ome.tiff','')], - physical_pixel_sizes = bioio_base.types.PhysicalPixelSizes(self.get_voxel_spacing()[2], 1.0, 1.0), # z,y,x [um] - #channel_colors=, - #fs_kwargs={}, - ) - return s_tiffpathfile - - - def render_neuroglancer(self, tiffpathfile, timestep=0, intensity_cmap='gray'): - """ - help(pcdl.render_neuroglancer) - try: mcds.render_neuroglancer(mcds.make_ome_tiff()) - """ - o_viewer = render_neuroglancer( - tiffpathfile = tiffpathfile, - timestep = timestep, - intensity_cmap = intensity_cmap, - ) - return o_viewer - - - ## GRAPH RELATED FUNCTIONS ## - - def get_attached_graph_dict(self): - """ - input: - - output: - dei_graph: dictionary of sets of integers - maps each cell ID to the attached connected cell IDs. - - description: - function returns the attached cell graph as a dictionary object. - """ - return self.data['cell']['dei_graph']['attached_cells'].copy() - - - def get_neighbor_graph_dict(self): - """ - input: - - output: - dei_graph: dictionary of sets of integers - maps each cell ID to the connected neighbor cell IDs. - - description: - function returns the cell neighbor graph as a dictionary object. - """ - return self.data['cell']['dei_graph']['neighbor_cells'].copy() - - - def get_spring_graph_dict(self): - """ - input: - - output: - dei_graph: dictionary of sets of integers - maps each cell ID to the attached connected cell IDs. - - description: - function returns the attached spring cell graph as a dictionary object. - """ - return self.data['cell']['dei_graph']['spring_attached_cells'].copy() - - - def make_graph_gml(self, graph_type, edge_attribute=True, node_attribute=[]): - """ - input: - graph_type: string - to specify which physicell output data should be processed. - neighbor, touch: processes mcds.get_neighbor_graph_dict dictionary. - attached: processes mcds.get_attached_graph_dict dictionary. - spring: processes mcds.get_spring_graph_dict dictionary. - - edge_attribute: boolean; default True - specifies if the spatial Euclidean distance is used for - edge attribute, to generate a weighted graph. - - node_attribute: list of strings; default is empty list - list of mcds.get_cell_df dataframe columns, used for - node attributes. - - output: - gml file, generated under the returned path. - - description: - function to generate graph files in the gml graph modelling language - standard format. - - gml was the outcome of an initiative that started at - the international symposium on graph drawing 1995 in Passau - and ended at Graph Drawing 1996 in Berkeley. the networkx python - and igraph C and python libraries for graph analysis are - gml compatible and can as such read and write this file format. - - https://en.wikipedia.org/wiki/Graph_Modelling_Language - https://github.com/elmbeech/physicelldataloader/blob/master/man/publication/himsolt1996gml_a_portable_graph_file_format.pdf - https://networkx.org/ - https://igraph.org/ - """ - # load dataframe for celltype information - df_cell = self.get_cell_df() - ds_unit = self.get_unit_dict() - s_unit_simtime = ds_unit["time"] - r_simtime = self.get_time() - if (graph_type in {'attached'}): - dei_graph = self.get_attached_graph_dict() - elif (graph_type in {'neighbor', 'touch'}): - dei_graph = self.get_neighbor_graph_dict() - elif (graph_type in {'spring'}): - dei_graph = self.get_spring_graph_dict() - #elif (graph_type in {'evo','devo','lineage'}): - # dei_graph = self.get_lineage_graph_dict() - else: - sys.exit(f'Erro @ make_graph_gml : unknown graph_type {graph_type}. known are attached, neighbor, spring, and touch.') - - # generate filename - s_gmlpathfile = self.path + '/' + self.xmlfile.replace('.xml',f'_{graph_type}.gml') - - # open result gml file - f = open(s_gmlpathfile, 'w') - f.write(f'Creator "pcdl_v{__version__}"\ngraph [\n') - f.write(f' id {int(r_simtime)}\n comment "time_{s_unit_simtime}"\n label "{graph_type}_graph"\n directed 0\n') - for i_src, ei_dst in dei_graph.items(): - #print(f'{i_src} {sorted(ei_dst)}') - # node - f.write(f' node [\n id {i_src}\n label "node_{i_src}"\n') - # node attributes - for s_attribute in node_attribute: - o_attribute = df_cell.loc[i_src, s_attribute] - if (type(o_attribute) == str) or (o_attribute.dtype == np.object_): #in {str, np.str_, np.object_}): - f.write(f' {s_attribute} "{o_attribute}"\n') - elif (o_attribute.dtype == bool) or (o_attribute.dtype == int): #in {bool, np.bool_, np.bool, int, np.int_, np.int8, np.int16, np.int32, np.int64}): - f.write(f' {s_attribute} {int(o_attribute)}\n') - elif (o_attribute.dtype == float): #in {float, np.float16, np.float32, np.float64, np.float128}): - f.write(f' {s_attribute} {o_attribute}\n') - else: - sys.exit(f'Error @ TimeStep.make_graph_gml : attribute {o_attribute}; type {o_attribute.dtype}; type seems not to be bool, int, float, or string.') - f.write(f' ]\n') - # edge - for i_dst in ei_dst: - if (i_src < i_dst): - f.write(f' edge [\n source {i_src}\n target {i_dst}\n label "edge_{i_src}_{i_dst}"\n') - if (edge_attribute): - # edge distance attribute - x = df_cell.loc[i_src, 'position_x'] - df_cell.loc[i_dst, 'position_x'] - y = df_cell.loc[i_src, 'position_y'] - df_cell.loc[i_dst, 'position_y'] - z = df_cell.loc[i_src, 'position_z'] - df_cell.loc[i_dst, 'position_z'] - r_distance = (x**2 + y**2 + z**2)**(1/2) - f.write(f' distance_{ds_unit["position_y"]} {round(r_distance)}\n') - f.write(f' ]\n') - # development - #if (i_src > 16): - # break - # close result gml file - f.write(']\n') - f.close() - - # output - return s_gmlpathfile - - - ## ANNDATA RELATED FUNCTIONS ## - - def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs'): - """ - input: values: integer; default is 1 minimal number of values a variable has to have to be outputted. variables that have only 1 state carry no information. @@ -2422,43 +2240,157 @@ def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs'): don't worry: essential columns like ID, coordinates and time will always be kept. - scale: string; default 'maxabs' - specify how the data should be scaled. - possible values are None, maxabs, minmax, std. - for more input, check out: help(pcdl.scaler) + output: + do_domain: dictionary of muspa domains, one for each z-layer. + + description: + function returns a dictionary of muspa domains, containg a + cell and subs collection with disrcete and continuous labels + and all the graph as networks. + + https://www.muspan.co.uk + + https://docs.muspan.co.uk/latest/Documentation.html + """ + # check if muspan library is installed + if (ms is None) or (ms.__file__ is None): + sys.exit(f'Error @ TimeStep.get_muspa : the muspan Multi Spatial Analysis python3 library is not installed!\nfor instructions check out : https://www.muspan.co.uk/') + + # load optional dependency + nx = optional_import('networkx', s_caller='TimeStep.get_muspan') + + # get conc and cell dataframe + df_conc = self.get_conc_df(values=values, drop=drop, keep=keep) + df_cell = self.get_cell_df(values=values, drop=drop, keep=keep) + i_kmax = df_conc.voxel_k.max() + i_kdigit = len(str(i_kmax)) + if (z_slice is None): + li_klayer = sorted(df_conc.voxel_k.unique()) + else: + li_klayer = [self.get_voxel_ijk(x=0,y=0, z=z_slice)[2]] + + # for each z layer generate a muspa domain + do_domain = {} + for i_klayer in li_klayer: + + # processing + if self.verbose: + print(f'processing: {self.xmlfile} mcds {i_klayer + 1}/{i_kmax + 1} z-stack layer to muspan obj.') + + ## generate muspan domain + s_domain = f"{self.xmlfile.replace('.xml','')}_z{str(i_klayer).zfill(i_kdigit)}" + o_domain = ms.domain( + name = s_domain, + unit_of_length = 'um', + ) + + ## handle subs collection + df_zconc = df_conc.loc[df_conc.voxel_k == i_klayer,:] + o_domain.add_points( + points = df_zconc.loc[:,['mesh_center_m','mesh_center_n']].values, + collection_name = 'subs' + ) + # drop this data + es_drop = set(df_zconc.columns).intersection({ + 'voxel_i', 'voxel_j', 'voxel_k', + 'mesh_center_m', 'mesh_center_n', 'mesh_center_p', + 'time', 'runtime', 'xmlfile', + }) + df_zconc = df_zconc.drop(es_drop, axis=1) + # add numerical data (no scaling) + for s_num in sorted(df_zconc.columns): + o_domain.add_labels( + label_name = s_num, + labels = df_zconc.loc[:,s_num], + add_labels_to = 'subs', + label_type = 'continuous', + ) + + ## handle cell collection + df_zcell = df_cell.loc[df_cell.voxel_k == i_klayer,:] + o_domain.add_points( + points = df_zcell.loc[:,['position_x','position_y']].values, + collection_name = 'cell' + ) + # get a physicell cell_id to muspan object id mapping + df_coor = df_zcell.loc[:,['position_x', 'position_y','position_z']] + df_coor['muspan_id'] = o_domain.collections['cell']['objects'] + di_cellid = df_coor['muspan_id'].to_dict() + # drop this data + es_drop = set(df_zcell.columns).intersection({ + 'voxel_i', 'voxel_j', 'voxel_k', + 'mesh_center_m', 'mesh_center_n', 'mesh_center_p', + 'position_x', 'position_y','position_z', + 'time', 'runtime', 'xmlfile', + }) + df_zcell = df_zcell.drop(es_drop, axis=1) + # dectect variable types + des_type = {'float': set(), 'int': set(), 'bool': set(), 'str': set()} + for _, se_zcell in df_zcell.items(): + if str(se_zcell.dtype).startswith('float'): + des_type['float'].add(se_zcell.name) + elif str(se_zcell.dtype).startswith('int'): + des_type['int'].add(se_zcell.name) + elif str(se_zcell.dtype).startswith('bool'): + des_type['bool'].add(se_zcell.name) + elif str(se_zcell.dtype).startswith('object') in str(se_zcell.dtype).startswith('str'): + des_type['str'].add(se_zcell.name) + else: + sys.exit(f'Error @ TimeStep.get_muspa : column {se_zcell.name} detected with unknown dtype {str(se_zcell.dtype)}.') + # add categorical data + for s_cat in sorted(des_type['str'].union(des_type['bool'])): + o_domain.add_labels( + label_name = s_cat, + labels = df_zcell.loc[:,s_cat], + add_labels_to = 'cell', + label_type = 'categorical', + ) + # add numerical data (no scaling) + for s_num in sorted(des_type['float'].union(des_type['int'])): + o_domain.add_labels( + label_name = s_num, + labels = df_zcell.loc[:,s_num], + add_labels_to = 'cell', + label_type = 'continuous', + ) + ## add graphs + ei_pccellid = set(df_zcell.index) + for s_graph, dei_graph in [ + ('neighbor', self.get_neighbor_graph_dict()), + ('attached', self.get_attached_graph_dict()), + ('spring', self.get_spring_graph_dict()), + ]: + # transform graph dict into weighted edge list + lt_wedge = [] + for i_src, ei_dst in sorted(dei_graph.items()): + for i_dst in ei_dst: + if (i_src in ei_pccellid) and (i_dst in ei_pccellid): + r_distance = ((df_coor.loc[i_src, ['position_x','position_y','position_z']].values - df_coor.loc[i_dst, ['position_x','position_y','position_z']].values)**2).sum()**(1/2) + lt_wedge.append((di_cellid[i_src], di_cellid[i_dst], r_distance)) + # generate graph + G = nx.Graph() + # dump the edges into the network + G.add_weighted_edges_from(lt_wedge, weight='Distance') + G.add_weighted_edges_from(lt_wedge, weight='Inverse Distance') + # add the network to the dictionary of networks + o_domain.networks[s_graph] = G + # clean up the domain + ms.helpers.clean_up(o_domain) - output: - annmcds: anndata object - for this one time step. + ## set domain boundary (have to be done last!) + o_domain.estimate_boundary( + method='specify', + specify_boundary_coords=( + (self.get_xyz_range()[0][0], self.get_xyz_range()[1][0]), + (self.get_xyz_range()[0][0], self.get_xyz_range()[1][1]), + (self.get_xyz_range()[0][1], self.get_xyz_range()[1][1]), + (self.get_xyz_range()[0][1], self.get_xyz_range()[1][0]) + ) + ) - description: - function to transform a mcds time step into an anndata object - for downstream analysis. - """ - # load optional dependency - ad = optional_import('anndata', s_caller='TimeStep.get_anndata') + # update output + do_domain.update({s_domain : o_domain}) - # processing - if self.verbose: - print(f'processing: 1/1 {round(self.get_time(),9)}[min] mcds into anndata obj.') - df_cell = self.get_cell_df(values=values, drop=drop, keep=keep) - df_count, df_obs, d_obsm, d_obsp, d_uns = _anndextract( - df_cell = df_cell, - scale = scale, - graph_attached = self.get_attached_graph_dict(), - graph_neighbor = self.get_neighbor_graph_dict(), - graph_spring = self.get_spring_graph_dict(), - graph_method = self.get_physicell_version(), - ) - annmcds = ad.AnnData( - X = df_count, - obs = df_obs, - obsm = d_obsm, - obsp = d_obsp, - uns = d_uns - ) # output - return annmcds + return do_domain def get_spatialdata(self, images={'subs'}, labels={}, points={'subs'}, shapes={'cell'}, values=1, drop=set(), keep=set(), scale='maxabs'): @@ -2722,184 +2654,246 @@ def get_spatialdata(self, images={'subs'}, labels={}, points={'subs'}, shapes={' return sdata - ## MUSPAN RELATED FUNCTIONS ## + def make_ome_tiff(self, cell_attribute='ID', conc_cutoff={}, focus=None, file=True): + """ + input: + cell_attribute: strings; default is 'ID', which will result in a + cell segmentation mask. + column name within the cell dataframe. + the column data type has to be numeric (bool, int, float) + and cannot be string. + the result will be stored as 32 bit float. + + conc_cutoff: dictionary string to real; default is an empty dictionary. + if a contour from a substrate not should be cut by greater + than zero (shifted to integer 1), another cutoff value can be + specified here. + + focus: set of strings; default is a None + set of substrate and cell_type names to specify what will be + translated into ome tiff format. + if None, all substrates and cell types will be processed. + + file: boolean; default True + if True, an ome tiff file is the output. + if False, a numpy array with shape czyx is the output. + + output: + a_tczyx_img: numpy array or ome tiff file. + + description: + function to transform chosen mcds output into an 1[um] spaced + czyx (channel, z-axis, y-axis, x-axis) ome tiff file or numpy array, + one substrate or cell_type per channel. + an ome tiff file is more or less: + a numpy array, containing the image information + and a xml, containing the microscopy metadata information, + like the channel labels. + the ome tiff file format can for example be read by the napari + or fiji (imagej) software. + + https://napari.org/stable/ + https://fiji.sc/ + """ + # load optional dependencies + if file: + OmeTiffWriter = optional_import('bioio.writers', s_attr='OmeTiffWriter', s_pip='bioio', s_caller='TimeStep.make_ome_tiff') + bioio_base = optional_import('bioio_base', s_pip='bioio', s_caller='TimeStep.make_ome_tiff') + + # handle channels + ls_substrate = self.get_substrate_list() + ls_celltype = self.get_celltype_list() + + if not (focus is None): + ls_substrate = [s_substrate for s_substrate in ls_substrate if s_substrate in set(focus)] + ls_celltype = [s_celltype for s_celltype in ls_celltype if s_celltype in set(focus)] + if (set(focus) != set(ls_substrate).union(set(ls_celltype))): + sys.exit(f'Error : {focus} not found in {ls_substrate} {ls_celltype}') + + # const + ls_coor_mnp = ['mesh_center_m', 'mesh_center_n', 'mesh_center_p'] # xyz + ls_coor_xyz = ['position_x', 'position_y', 'position_z'] # xyz + ls_coor = ['voxel_x', 'voxel_y', 'voxel_z'] + + # time step tensor + i_time = int(self.get_time()) + + # get xy coordinate dataframe + lr_axis_z = list(self.get_mesh_mnp_axis()[2] - self.get_voxel_spacing()[2] / 2) + lr_axis_z.append(self.get_mesh_mnp_axis()[2][-1] + self.get_voxel_spacing()[2] / 2) + lll_coor = [] + for i_x in range(int(round(self.get_voxel_ijk_range()[0][1] * self.get_voxel_spacing()[0]))): + for i_y in range(int(round(self.get_voxel_ijk_range()[1][1] * self.get_voxel_spacing()[1]))): + lll_coor.append([i_x, i_y]) + df_coor = pd.DataFrame(lll_coor, columns=ls_coor[:2]) + lr_axis_z[-1] += 1 + + # extract voxel radius + di_grow = {} + for s_substarte in ls_substrate: + di_grow.update({ + s_substarte : int(np.round(np.mean(self.get_voxel_spacing()[:2])) - 1) + }) + + # get and shift substrate xy data + df_conc = self.get_conc_df() + df_conc = df_conc.loc[:, ls_coor_mnp + ls_substrate] + df_conc.loc[:, 'mesh_center_m'] = (df_conc.loc[:, 'mesh_center_m'] - self.get_xyz_range()[0][0]).round() + df_conc.loc[:, 'mesh_center_n'] = (df_conc.loc[:, 'mesh_center_n'] - self.get_xyz_range()[1][0]).round() + df_conc.rename({'mesh_center_m':'voxel_x', 'mesh_center_n':'voxel_y', 'mesh_center_p':'voxel_z'}, axis=1, inplace=True) + df_conc = df_conc.astype({'voxel_x': int, 'voxel_y': int, 'voxel_z': float}) + # level the cake + for s_channel in conc_cutoff.keys(): + try: + df_conc.loc[:, s_channel] = df_conc.loc[:, s_channel] - conc_cutoff[s_channel] + 1 # positive values starting at > 0 + df_conc.loc[(df_conc.loc[:, s_channel] <= conc_cutoff[s_channel]), s_channel] = 0 + except KeyError: + pass + + # get cell data + df_cell = self.get_cell_df().reset_index() + + # extract cell radius + for s_celltype in ls_celltype: + try: + i_cell_grow = int(round(df_cell.loc[(df_cell.cell_type == s_celltype), 'radius'].mean()) - 1) + except: + i_cell_grow = 0 + di_grow.update({s_celltype : i_cell_grow}) + + # filter and shift + df_cell = df_cell.loc[:, ls_coor_xyz + ['cell_type', cell_attribute]] + if (cell_attribute == 'cell_type'): + sys.exit(f'Error @ TimeStep.make_ome_tiff : cell_attribute cannot be cell_type.') + elif (df_cell.loc[:, cell_attribute].dtype == str) or (df_cell.loc[:, cell_attribute].dtype == np.object_): # in {str, np.str_, np.object_}): + sys.exit(f'Error @ TimeStep.make_ome_tiff : {cell_attribute} {df_cell.loc[:, cell_attribute].dtype} cell_attribute cannot be string or object. cell_attribute has to be boolean, integer, or float.') + elif (df_cell.loc[:, cell_attribute].dtype == bool): # in {bool, np.bool_, np.bool}): + df_cell = df_cell.astype({cell_attribute: int}) + df_cell.loc[:, 'position_x'] = (df_cell.loc[:, 'position_x'] - self.get_xyz_range()[0][0]).round() + df_cell.loc[:, 'position_y'] = (df_cell.loc[:, 'position_y'] - self.get_xyz_range()[1][0]).round() + df_cell.rename({'position_x':'voxel_x', 'position_y':'voxel_y', 'position_z':'voxel_z'}, axis=1, inplace=True) + df_cell = df_cell.astype({'voxel_x': int, 'voxel_y': int, 'voxel_z': float}) + # level the cake + df_cell.loc[:, cell_attribute] = df_cell.loc[:, cell_attribute] - df_cell.loc[:, cell_attribute].min() + 1 # positive values starting at > 0 + + # check for duplicates: two cell at exactelly the same xyz position. + #if self.verbose and df_cell.loc[:,['voxel_x', 'voxel_y', 'voxel_z']].duplicated().any(): + # df_duplicate = df_cell.loc[(df_cell.loc[:, ['voxel_x', 'voxel_y', 'voxel_z']].duplicated()), :] + # sys.exit(f"Error @ TimeStep.make_ome_tiff : {df_duplicate} cells at exactely the same xyz voxel position detected. cannot pivot!") + + # pivot cell_type + df_cell = df_cell.pivot_table(index=ls_coor, columns='cell_type', values=cell_attribute, aggfunc='sum').reset_index() # fill_value is na + for s_celltype in ls_celltype: + if not s_celltype in set(df_cell.columns): + df_cell[s_celltype] = 0 + + # each C channel - time step tensors + la_czyx_img = [] + ls_channel = ls_substrate + ls_celltype + for s_channel in ls_channel: + + # get channel dataframe + if s_channel in set(ls_substrate): + df_channel = df_conc.loc[:, ls_coor + [s_channel]] + elif s_channel in set(ls_celltype): + df_channel = df_cell.loc[:, ls_coor + [s_channel]] + else: + sys.exit(f'Error @ TimeStep.make_ome_tiff : {s_channel} unknown channel detected. not in substrate and cell type list {ls_substrate} {ls_celltype}!') - def get_muspan(self, z_slice=None, values=1, drop=set(), keep=set()): - """ - input: - z_slice: floating point number; default is None - z-axis position to slice a 2D xy-plain out of the - 3D mesh. if None the whole 3D mesh will be returned. + # each z axis + la_zyx_img = [] + for i_zaxis in range(len(lr_axis_z)): + if (i_zaxis < (len(lr_axis_z) - 1)): + print(f'processing: {i_time} [min] {s_channel} [channel] {i_zaxis} [z_axis] ...') + # extract z layer + df_yxchannel = df_channel.loc[ + ((df_channel.loc[:, ls_coor[2]] >= lr_axis_z[i_zaxis]) & (df_channel.loc[:, ls_coor[2]] < lr_axis_z[i_zaxis + 1])), + ls_coor[:2] + [s_channel] + ] - values: integer; default is 1 - minimal number of values a variable has to have to be outputted. - variables that have only 1 state carry no information. - None is a state too. + # drop row with na and duplicate entries + df_yxchannel = df_yxchannel.dropna(axis=0) + df_yxchannel = df_yxchannel.drop_duplicates() - drop: set of strings; default is an empty set - set of column labels to be dropped for the dataframe. - don't worry: essential columns like ID, coordinates - and time will never be dropped. - Attention: when the keep parameter is given, then - the drop parameter has to be an empty set! + # merge with coooridnates and get image + # bue 20240811: df_coor left side merge will cut off reset cell that are out of the xyz domain range, which is what we want. + df_yxchannel = pd.merge(df_coor, df_yxchannel, on=ls_coor[:2], how='left').replace({np.nan: 0}) + try: + df_yxchannel = df_yxchannel.pivot(columns=ls_coor[0], index=ls_coor[1], values=s_channel) + except ValueError: # two cells from the same cell type very close to each other detetced. + if self.verbose: + df_duplicate = df_cell.loc[(df_yxchannel.loc[:, ['voxel_x', 'voxel_y']].duplicated()), :] + print(f'Warning: {s_channel} {df_duplicate} cells within 1[um] distance form each detected. cannot pivot. erase cell type from this timestep.') + df_yxchannel.loc[:,s_channel] = 0 # erase cells + df_yxchannel = df_yxchannel.drop_duplicates() + df_yxchannel = df_yxchannel.pivot(columns=ls_coor[0], index=ls_coor[1], values=s_channel) + a_yx_img = df_yxchannel.values - keep: set of strings; default is an empty set - set of column labels to be kept in the dataframe. - set values=1 to be sure that all variables are kept. - don't worry: essential columns like ID, coordinates - and time will always be kept. + # grow + a_yx_img = imagine.grow_seed(a_yx_img, i_step=di_grow[s_channel], b_verbose=False) - output: - do_domain: dictionary of muspa domains, one for each z-layer. + # update output + la_zyx_img.append(a_yx_img) + a_zyx_img = np.array(la_zyx_img, np.float32) + la_czyx_img.append(np.array(a_zyx_img, np.float32)) - description: - function returns a dictionary of muspa domains, containg a - cell and subs collection with disrcete and continuous labels - and all the graph as networks. - + https://www.muspan.co.uk - + https://docs.muspan.co.uk/latest/Documentation.html - """ - # check if muspan library is installed - if (ms is None) or (ms.__file__ is None): - sys.exit(f'Error @ TimeStep.get_muspa : the muspan Multi Spatial Analysis python3 library is not installed!\nfor instructions check out : https://www.muspan.co.uk/') + # output + a_czyx_img = np.array(la_czyx_img, dtype=np.float32) - # load optional dependency - nx = optional_import('networkx', s_caller='TimeStep.get_muspan') + # numpy array + if not file: + return a_czyx_img - # get conc and cell dataframe - df_conc = self.get_conc_df(values=values, drop=drop, keep=keep) - df_cell = self.get_cell_df(values=values, drop=drop, keep=keep) - i_kmax = df_conc.voxel_k.max() - i_kdigit = len(str(i_kmax)) - if (z_slice is None): - li_klayer = sorted(df_conc.voxel_k.unique()) + # write to file else: - li_klayer = [self.get_voxel_ijk(x=0,y=0, z=z_slice)[2]] - - # for each z layer generate a muspa domain - do_domain = {} - for i_klayer in li_klayer: - - # processing if self.verbose: - print(f'processing: {self.xmlfile} mcds {i_klayer + 1}/{i_kmax + 1} z-stack layer to muspan obj.') - - ## generate muspan domain - s_domain = f"{self.xmlfile.replace('.xml','')}_z{str(i_klayer).zfill(i_kdigit)}" - o_domain = ms.domain( - name = s_domain, - unit_of_length = 'um', - ) - - ## handle subs collection - df_zconc = df_conc.loc[df_conc.voxel_k == i_klayer,:] - o_domain.add_points( - points = df_zconc.loc[:,['mesh_center_m','mesh_center_n']].values, - collection_name = 'subs' - ) - # drop this data - es_drop = set(df_zconc.columns).intersection({ - 'voxel_i', 'voxel_j', 'voxel_k', - 'mesh_center_m', 'mesh_center_n', 'mesh_center_p', - 'time', 'runtime', 'xmlfile', - }) - df_zconc = df_zconc.drop(es_drop, axis=1) - # add numerical data (no scaling) - for s_num in sorted(df_zconc.columns): - o_domain.add_labels( - label_name = s_num, - labels = df_zconc.loc[:,s_num], - add_labels_to = 'subs', - label_type = 'continuous', - ) - - ## handle cell collection - df_zcell = df_cell.loc[df_cell.voxel_k == i_klayer,:] - o_domain.add_points( - points = df_zcell.loc[:,['position_x','position_y']].values, - collection_name = 'cell' - ) - # get a physicell cell_id to muspan object id mapping - df_coor = df_zcell.loc[:,['position_x', 'position_y','position_z']] - df_coor['muspan_id'] = o_domain.collections['cell']['objects'] - di_cellid = df_coor['muspan_id'].to_dict() - # drop this data - es_drop = set(df_zcell.columns).intersection({ - 'voxel_i', 'voxel_j', 'voxel_k', - 'mesh_center_m', 'mesh_center_n', 'mesh_center_p', - 'position_x', 'position_y','position_z', - 'time', 'runtime', 'xmlfile', - }) - df_zcell = df_zcell.drop(es_drop, axis=1) - # dectect variable types - des_type = {'float': set(), 'int': set(), 'bool': set(), 'str': set()} - for _, se_zcell in df_zcell.items(): - if str(se_zcell.dtype).startswith('float'): - des_type['float'].add(se_zcell.name) - elif str(se_zcell.dtype).startswith('int'): - des_type['int'].add(se_zcell.name) - elif str(se_zcell.dtype).startswith('bool'): - des_type['bool'].add(se_zcell.name) - elif str(se_zcell.dtype).startswith('object') in str(se_zcell.dtype).startswith('str'): - des_type['str'].add(se_zcell.name) - else: - sys.exit(f'Error @ TimeStep.get_muspa : column {se_zcell.name} detected with unknown dtype {str(se_zcell.dtype)}.') - # add categorical data - for s_cat in sorted(des_type['str'].union(des_type['bool'])): - o_domain.add_labels( - label_name = s_cat, - labels = df_zcell.loc[:,s_cat], - add_labels_to = 'cell', - label_type = 'categorical', - ) - # add numerical data (no scaling) - for s_num in sorted(des_type['float'].union(des_type['int'])): - o_domain.add_labels( - label_name = s_num, - labels = df_zcell.loc[:,s_num], - add_labels_to = 'cell', - label_type = 'continuous', - ) - ## add graphs - ei_pccellid = set(df_zcell.index) - for s_graph, dei_graph in [ - ('neighbor', self.get_neighbor_graph_dict()), - ('attached', self.get_attached_graph_dict()), - ('spring', self.get_spring_graph_dict()), - ]: - # transform graph dict into weighted edge list - lt_wedge = [] - for i_src, ei_dst in sorted(dei_graph.items()): - for i_dst in ei_dst: - if (i_src in ei_pccellid) and (i_dst in ei_pccellid): - r_distance = ((df_coor.loc[i_src, ['position_x','position_y','position_z']].values - df_coor.loc[i_dst, ['position_x','position_y','position_z']].values)**2).sum()**(1/2) - lt_wedge.append((di_cellid[i_src], di_cellid[i_dst], r_distance)) - # generate graph - G = nx.Graph() - # dump the edges into the network - G.add_weighted_edges_from(lt_wedge, weight='Distance') - G.add_weighted_edges_from(lt_wedge, weight='Inverse Distance') - # add the network to the dictionary of networks - o_domain.networks[s_graph] = G - # clean up the domain - ms.helpers.clean_up(o_domain) + print('a_czyx_img shape:', a_czyx_img.shape) + # generate filename + s_channel = '' + for s_substrate in ls_substrate: + try: + r_value = conc_cutoff[s_substrate] + s_channel += f'_{s_substrate}{r_value}' + except KeyError: + s_channel += f'_{s_substrate}' + for s_celltype in ls_celltype: + s_channel += f'_{s_celltype}' + if len(ls_celltype) > 0: + s_channel += f'_{cell_attribute}' + s_tifffile = self.xmlfile.replace('.xml', f'{s_channel}.ome.tiff') + s_tifffile = s_tifffile.replace(' ','_') + if (len(s_tifffile) > 255): + print(f"Warning: filename {len(s_tifffile)} > 255 character.") + s_tifffile = self.xmlfile.replace('.xml', f'_channels.ome.tiff') + print(f"file name adjusted to {s_tifffile}.") + s_tiffpathfile = self.path + '/' + s_tifffile - ## set domain boundary (have to be done last!) - o_domain.estimate_boundary( - method='specify', - specify_boundary_coords=( - (self.get_xyz_range()[0][0], self.get_xyz_range()[1][0]), - (self.get_xyz_range()[0][0], self.get_xyz_range()[1][1]), - (self.get_xyz_range()[0][1], self.get_xyz_range()[1][1]), - (self.get_xyz_range()[0][1], self.get_xyz_range()[1][0]) - ) + # save to file + OmeTiffWriter.save( + a_czyx_img, + s_tiffpathfile, + dim_order = 'CZYX', + #ome_xml=x_img, + channel_names = ls_channel, + image_names = [s_tifffile.replace('.ome.tiff','')], + physical_pixel_sizes = bioio_base.types.PhysicalPixelSizes(self.get_voxel_spacing()[2], 1.0, 1.0), # z,y,x [um] + #channel_colors=, + #fs_kwargs={}, ) + return s_tiffpathfile - # update output - do_domain.update({s_domain : o_domain}) - # output - return do_domain + def render_neuroglancer(self, tiffpathfile, timestep=0, intensity_cmap='gray'): + """ + help(pcdl.render_neuroglancer) + try: mcds.render_neuroglancer(mcds.make_ome_tiff()) + """ + o_viewer = render_neuroglancer( + tiffpathfile = tiffpathfile, + timestep = timestep, + intensity_cmap = intensity_cmap, + ) + return o_viewer ## LOAD DATA ## diff --git a/pyproject.toml b/pyproject.toml index 7384b24..e6c800b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,24 +124,25 @@ full = [ [project.scripts] # special thanks to Miguel Ponce-de-Leon who introduced me to entry point scripts! +# initialize # metadata pcdl_get_version = "pcdl.commandline:get_version" pcdl_get_unit_dict = "pcdl.commandline:get_unit_dict" # substrate pcdl_get_substrate_list = "pcdl.commandline:get_substrate_list" -pcdl_get_conc_attribute = "pcdl.commandline:get_conc_attribute" pcdl_get_conc_df = "pcdl.commandline:get_conc_df" +pcdl_get_conc_attribute = "pcdl.commandline:get_conc_attribute" pcdl_plot_contour = "pcdl.commandline:plot_contour" pcdl_make_conc_vtk = "pcdl.commandline:make_conc_vtk" # cell agent pcdl_get_celltype_list = "pcdl.commandline:get_celltype_list" -pcdl_get_cell_attribute_list = "pcdl.commandline:get_cell_attribute_list" -pcdl_get_cell_attribute = "pcdl.commandline:get_cell_attribute" pcdl_get_cell_df = "pcdl.commandline:get_cell_df" +pcdl_get_cell_attribute = "pcdl.commandline:get_cell_attribute" +pcdl_get_cell_attribute_list = "pcdl.commandline:get_cell_attribute_list" pcdl_get_anndata = "pcdl.commandline:get_anndata" -pcdl_make_graph_gml = "pcdl.commandline:make_graph_gml" pcdl_plot_scatter = "pcdl.commandline:plot_scatter" pcdl_make_cell_vtk = "pcdl.commandline:make_cell_vtk" +pcdl_make_graph_gml = "pcdl.commandline:make_graph_gml" pcdl_make_simularium = "pcdl.commandline:make_simularium" # substrate and cell agent pcdl_get_muspan = "pcdl.commandline:get_muspan" From 98ac7807f09c4b3d93c95d16d55a1feb12c510cf Mon Sep 17 00:00:00 2001 From: bue Date: Tue, 1 Sep 2026 17:14:15 -0400 Subject: [PATCH 09/15] @ pcdl : in the middle galaxy totorial writing. --- man/TUTORIAL_galaxy.md | 20 +++++++++++++------- man/TUTORIAL_simularium.md | 16 ++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/man/TUTORIAL_galaxy.md b/man/TUTORIAL_galaxy.md index fc1d591..493e2aa 100644 --- a/man/TUTORIAL_galaxy.md +++ b/man/TUTORIAL_galaxy.md @@ -8,21 +8,27 @@ We here focus on [the European instance of Galaxy](https://usegalaxy.eu/) since Special thanks to Björn Grüning, who helped me to port pcdl into Galaxy! -## Run PhysiCell Studio on Galaxy +## ✨ Run PhysiCell Studio on Galaxy 1. In your web browser, open the European instance of Galaxy: https://usegalaxy.eu/ -1. Optionally, log into your user account. Create one if you do not already have one. -1. Near the top left corner, click "Tools". +1. Required, log into your user account. Create one if you do not already have one. +1. One the left side pannel, click "Interactive Tools". 1. Near the left-side top corner, in the "Tools" search bar, type "PhysiCell Studio". 1. Choose "PhysiCell Studio". -1. Choose "Run Tool" (center panel or top right). +1. On the center pannel under additional option flip the switch "Email notification" to Yes. +1. On the center pannel click on one of the "Run Tool" buttons. +1. On the left-side pannel, after a while, "Active Interactive Tools" should pop up. +1. Under "Active Interactive Tools" click "PhysiCell Studio". 1. How to load, develop, or run a PhysiCell model in the studio can be learned here: + https://physicell-studio.readthedocs.io/en/latest/index.html + https://github.com/PhysiCell-Tools/PhysiCell-Studio +1. In the left side "History" pannel do NOT DELETE the "PhysiCell Studio on : all reults archive." +1. Run simmulation. 1. [TKBue] -## Upload local PhysiCell output to Galaxy + +## ✨ Upload local PhysiCell output to Galaxy 1. In your web browser, open the European instance of Galaxy: https://usegalaxy.eu/ 1. Optionally, log into your user account. Create one if you do not already have one. @@ -38,7 +44,7 @@ Special thanks to Björn Grüning, who helped me to port pcdl into Galaxy! 1. After a while, the uploaded data collection should appear in the history in the right-side panel. -## The pcdl tools on Galaxy +## ✨ The pcdl tools on Galaxy 1. In your web browser, open the European instance of Galaxy: https://usegalaxy.eu/ 1. Optionally, log into your user account. Create one if you do not already have one. @@ -52,7 +58,7 @@ Special thanks to Björn Grüning, who helped me to port pcdl into Galaxy! 1. After a while, the result file(s) should appear in the left-side history panel. -### ✨ Further readings +## ✨ Further readings You can learn more about Galaxy here: + https://galaxyproject.org/tutorials/g101/ diff --git a/man/TUTORIAL_simularium.md b/man/TUTORIAL_simularium.md index bde5bda..918a216 100644 --- a/man/TUTORIAL_simularium.md +++ b/man/TUTORIAL_simularium.md @@ -5,9 +5,9 @@ The [simularium](https://simularium.allencell.org/) viewer was developed to shar Simularium files can easily be generated from mcds time series. -### ✨ command line +## ✨ command line -#### command line time series +### command line time series Generate a simularium trajectory file. @@ -15,16 +15,16 @@ Generate a simularium trajectory file. pcdl_make_simularium output_2d/ ``` -#### command line man page +### command line man page ```bash pcdl_make_simularium -h ``` -### ✨ python +## ✨ python -#### python time series +### python time series Load a time series and generate a simularium trajectory file. @@ -35,7 +35,7 @@ mcdsts = pcdl.TimeSeries('output_2d/') mcdsts.make_simularium() ``` -#### python docstrings +### python docstrings ```python import pcdl @@ -44,7 +44,7 @@ mcdsts = pcdl.TimeSeries('output_2d/') help(mcdsts.make_simularium()) ``` -### ✨ simularium viewer +## ✨ simularium viewer 1. Open the simularium viewer web page: https://simularium.allencell.org/ 1. Near the top right corner, click the "Load models" dropdown menu and choose "Simularium file". @@ -54,7 +54,7 @@ help(mcdsts.make_simularium()) 1. Explore the loaded file. -### ✨ Further readings' +## ✨ Further readings' Please work through the official documentation to learn more about how simularium software. From b50f34415a03f7a763c97a4fbbb267ff22577e75 Mon Sep 17 00:00:00 2001 From: bue Date: Tue, 1 Sep 2026 17:14:15 -0400 Subject: [PATCH 10/15] @ pcdl : refernce manual and tutorials complete. --- man/REFERENCE.md | 103 ++++++++++++++++++------------- man/TUTORIAL_galaxy.md | 63 +++++++++++-------- man/TUTORIAL_python3_timestep.md | 36 ++++++----- man/TUTORIAL_simularium.md | 16 ++--- pcdl/commandline.py | 84 ++++++++++++------------- pyproject.toml | 2 +- 6 files changed, 169 insertions(+), 135 deletions(-) diff --git a/man/REFERENCE.md b/man/REFERENCE.md index ae2d265..2395a81 100644 --- a/man/REFERENCE.md +++ b/man/REFERENCE.md @@ -38,15 +38,18 @@ Basically, there are four types of functions: + plot_ : plot functions generate a matplotlib figure, or axis object, or file output (jpeg, png, tiff), depending on your parameter settings. ### TimeStep initialize + + [help(pcdl.TimeStep.\_\_init\_\_)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.__init__.md) #! workhorse function + [help(mcds.custom_data_astype)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.custom_data_astype.md) #! workhorse function + [help(mcds.set_verbose_false)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.set_verbose_false.md) #! workhorse function + [help(mcds.set_verbose_true)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.set_verbose_true.md) #! workhorse function ++ [help(mcds.render_neuroglancer)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.render_neuroglancer.md) ### TimeStep medata + *version* + [help(mcds.get_multicellds_version)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_multicellds_version.md) #! workhorse function -+ [help(mcds.get_pcdl_version)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_pcdl_version.md) #! workhorse function ++ [help(mcds.get_pcdl_version)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_pcdl_version.md) + [help(mcds.get_physicell_version)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_physicell_version.md) #! workhorse function *time* @@ -58,6 +61,7 @@ Basically, there are four types of functions: + [help(mcds.get_unit_dict)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_unit_dict.md) ### TimeStep mesh + *range and axis* + [help(mcds.get_voxel_ijk_range)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_voxel_ijk_range.md) + [help(mcds.get_mesh_mnp_range)](https://github.com/elmbeech/physicelldataloader/blob/master/man/docstring/mcds.get_mesh_mnp_range.md) @@ -78,7 +82,8 @@ Basically, there are four types of functions: + [help(mcds.get_voxel_volume)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_voxel_volume.md) + [help(mcds.get_voxel_ijk)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_voxel_ijk.md) -### TimeStep microenvironment +### TimeStep substarte + + [help(mcds.get_substrate_list)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_substrate_list.md) #! workhorse function + [help(mcds.get_substrate_dict)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_substrate_dict.md) + [help(mcds.get_substrate_df)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_substrate_df.md) @@ -87,30 +92,30 @@ Basically, there are four types of functions: + [help(mcds.make_conc_vtk)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.make_conc_vtk.md) #! workhorse function ### TimeStep cells + + [help(mcds.get_celltype_list)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_celltype_list.md) #! workhorse function + [help(mcds.get_celltype_dict)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_celltype_dict.md) + [help(mcds.get_cell_df)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_cell_df.md) #! workhorse function -+ [help(mcds.plot_scatter)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.plot_scatter.md) #! workhorse function -+ [help(mcds.make_cell_vtk)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.make_cell_vtk.md) #! workhorse function ++ [help(mcds.get_cell_attribute_list)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_cell_attribute_list.md) #! workhorse function + [help(mcds.get_anndata)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_anndata.md) #! workhorse function - -### TimeStep cell graph + [help(mcds.get_attached_graph_dict)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_attached_graph_dict.md) + [help(mcds.get_neighbor_graph_dict)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_neighbor_graph_dict.md) ++ [help(mcds.get_spring_graph_dict)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_spring_graph_dict.md) ++ [help(mcds.plot_scatter)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.plot_scatter.md) #! workhorse function ++ [help(mcds.make_cell_vtk)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.make_cell_vtk.md) #! workhorse function + [help(mcds.make_graph_gml)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.make_graph_gml.md) #! workhose function -### TimeStep microenvironment and cells +### TimeStep substarte and cells + + [help(mcds.get_muspan)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_muspan.md) #! workhose function + [help(mcds.get_spatialdata)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_spatialdata.md) #! workhose function + [help(mcds.make_ome_tiff)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.make_ome_tiff.md) #! workhose function -+ [help(mcds.make_neuroglancer)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.make_neuroglancer.md) #! workhose function ### TimeStep internal functions -+ [help(pcdl.scaler)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl.scaler.md) # anndata -+ [help(pcdl.graphfile_parser)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl.graphfile_parser.md) # mcds + ```python3 -help(pcdl.TimeStep._read_xml) help(pcdl.TimeStep._anndextract) +help(pcdl.TimeStep._read_xml) ``` @@ -124,59 +129,71 @@ Basically, there are four types of functions: ### TimeSeries initialization + [help(pcdl.TimeSeries.\_\_init\_\_)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.__init__.md) #! workhosefunction -+ [help(mcdsts.get_xmlfile_list)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_xmlfile_list.md) #! workhosefunction -+ [help(mcdsts.read_mcds)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.read_mcds.md) -+ [help(mcdsts.get_mcds_list)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_mcds_list.md) #! workhose function -+ [help(mcdsts.get_annmcds_list)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_annmcds_list.md) #! workhose function + [help(mcdsts.custom_data_astype)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.custom_data_astype.md) #! workhorse function + [help(mcdsts.set_verbose_false)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.set_verbose_false.md) #! workhorse function + [help(mcdsts.set_verbose_true)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.set_verbose_true.md) #! workhorse function ++ [help(mcdsts.render_neuroglancer)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.render_neuroglancer.md) + +### TimeSeries load data + ++ [help(mcdsts.get_xmlfile_list)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_xmlfile_list.md) #! workhosefunction ++ [help(mcdsts.read_mcds)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.read_mcds.md) ++ [help(mcdsts.get_mcds_list)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_mcds_list.md) #! workhose function + +### TimeSeries substrate -### TimeSeries microenvironment + [help(mcdsts.get_conc_df)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_conc_df.md) #! workhorse function + [help(mcdsts.get_conc_attribute)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_conc_attribute.md) #! workhorse function + [help(mcdsts.plot_contour)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.plot_contour.md) # !workhorse function + [help(mcdsts.make_conc_vtk)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.make_conc_vtk.md) #! workhorse function ### TimeSeries cells + + [help(mcdsts.get_cell_df)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_cell_df.md) #! workhorse function + [help(mcdsts.get_cell_attribute)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_cell_attribute.md) #! workhorse function ++ [help(mcdsts.get_anndata)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_anndata.md) #! workhorse function ++ [help(mcdsts.get_annmcds_list)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_annmcds_list.md) #! workhose function + [help(mcdsts.plot_scatter)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.plot_scatter.md) # !workhorse function + [help(mcdsts.make_cell_vtk)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.make_cell_vtk.md) #! workhorse function -+ [help(mcdsts.make_simularium)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.make_simularium.md) #! workhorse function -+ [help(mcdsts.get_anndata)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_anndata.md) #! workhorse function ++ [help(mcdsts.make_graph_gml)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.make_graph_gml.md) #! workhose function ++ [help(mcdsts.make_simularium)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.make_simularium.md) -### TimeSeries cell graph -+ [help(mcdsts.get_graph_gml)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.make_graph_gml.md) #! workhose function +### TimeSteries substrate and cells -### TimeSteries microenvironment and cells + [help(mcdsts.get_muspan)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_muspan.md) #! workhose function + [help(mcdsts.get_spatialdata)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_spatialdata.md) #! workhose function -+ [help(mcdsts.make_ome_tiff)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.make_ome_tiff.md) #! workhose function -+ [help(mcdsts.make_neuroglancer)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.make_neuroglancer.md) #! workhose function - -### Timeseries timeseries ++ [help(mcdsts.get_sdmcds_list)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.get_sdmcds_list.md) #! workhose function + [help(mcdsts.plot_timeseries)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.plot_timeseries.md) #! workhorse function ++ [help(mcdsts.make_ome_tiff)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.make_ome_tiff.md) #! workhose function ### TimeSeries making movies -+ [help(pcdl.make_gif)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl.make_gif.md) # ! workhorse function -+ [help(pcdl.make_movie)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl.make_movie.md) # ! workhorse function -+ [help(mcdsts.make_gif)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl.make_gif.md) # ! workhorse function -+ [help(mcdsts.make_movie)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl.make_movie.md) # ! workhorse function + ++ [help(mcdsts.make_gif)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl.make_gif.md) # physicell basics ++ [help(mcdsts.make_movie)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl.make_movie.md) # physicell basics ### TimeSeries internal functions -+ [help(pcdl.scaler)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl.scaler.md) # anndata ```python3 help(pcdl.TimeSeries._handle_magick) -help(pcdl.TimeSeries._anndextract) ``` +### pcdl module level ### + ++ pcdl.\_\_version\_\_ # loaded pcdl version ++ [help(pcdl.graphfile_parser)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl.graphfile_parser.md) # mcds ++ [help(pcdl.make_gif)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl.make_gif.md) # physicell basics ++ [help(pcdl.make_movie)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl.make_movie.md) # physicell basics ++ pcdl.pccmap # officical PhysiCell color map ++ [help(pcdl.render_neuroglancer)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/ocdl.render_neuroglancer.md) ++ [help(pcdl.scaler)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl.scaler.md) # anndata + + # Command line The command line interface functions mimic the name and parameter arguments as closely as possible to the related python3 functions. ### Command line metadata + *version* + [pcdl_get_version --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_version.md) #! workhorse function @@ -184,35 +201,35 @@ The command line interface functions mimic the name and parameter arguments as c + [pcdl_get_unit_dict --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_unit_dict.md) ### Command line microenvironment + + [pcdl_get_substrate_list --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_substrate_list.md) #! workhorse function -+ [pcdl_get_conc_attribute --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_conc_attribute.md) #! workhorse function + [pcdl_get_conc_df --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_conc_df.md) #! workhorse function ++ [pcdl_get_conc_attribute --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_conc_attribute.md) #! workhorse function + [pcdl_plot_contour --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_plot_contour.md) #! workhorse function + [pcdl_make_conc_vtk --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_conc_vtk.md) #! workhorse function ### Command line cells + + [pcdl_get_celltype_list --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_celltype_list.md) #! workhorse function -+ [pcdl_get_cell_attribute_list --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_cell_attribute_list.md) #! workhorse function -+ [pcdl_get_cell_attribute --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_cell_attribute.md) #! workhorse function + [pcdl_get_cell_df --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_cell_df.md) #! workhorse function ++ [pcdl_get_cell_attribute --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_cell_attribute.md) #! workhorse function ++ [pcdl_get_cell_attribute_list --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_cell_attribute_list.md) #! workhorse function + [pcdl_get_anndata --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_anndata.md) #! workhorse function + [pcdl_plot_scatter --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_plot_scatter.md) #! workhorse function + [pcdl_make_cell_vtk --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_cell_vtk.md) #! workhorse function -+ [pcdl_make_simularium --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_simularium.md) #! workhorse function - -### Command line cell graph + [pcdl_make_graph_gml --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_graph_gml.md) #! workhorse function ++ [pcdl_make_simularium --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_simularium.md) ### Command line cells and microenvironment + + [pcdl_get_muspan --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_muspan.md) #! workhorse function + [pcdl_get_spatialdata --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_get_spatialdata.md) #! workhorse function -+ [pcdl_make_ome_tiff --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_ome_tiff.md) #! workhorse function -+ [pcdl_make_neuroglancer --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_neuroglancer.md) #! workhorse function - -### Command line timeseries + [pcdl_plot_timeseries --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_plot_timeseries.md) #! workhorse function ++ [pcdl_make_ome_tiff --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_ome_tiff.md) #! workhorse function ### Command line making movies -+ [pcdl_make_gif --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_gif.md) #! workhorse function -+ [pcdl_make_movie --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_movie.md) #! workhorse function + ++ [pcdl_make_gif --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_gif.md) #! physicell basics ++ [pcdl_make_movie --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_movie.md) #! physicell basics ++ [pcdl_render_neuroglancer --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_render_neuroglancer.md) diff --git a/man/TUTORIAL_galaxy.md b/man/TUTORIAL_galaxy.md index fc1d591..3136da0 100644 --- a/man/TUTORIAL_galaxy.md +++ b/man/TUTORIAL_galaxy.md @@ -1,6 +1,6 @@ # PhysiCell Data Loader Tutorial: pcdl and Galaxy -[Galaxy](https://en.wikipedia.org/wiki/Galaxy_(computational_biology)) is an open source, web-based platform for bioinformatics anlysis. +[Galaxy](https://en.wikipedia.org/wiki/Galaxy_(computational_biology)) is an open-source, web-based platform for bioinformatics analysis. There a hand full of galaxy server instances around the world (USA, Europe, France, Australia, possibly others). We here focus on [the European instance of Galaxy](https://usegalaxy.eu/) since it has pcdl tools installed. @@ -8,53 +8,66 @@ We here focus on [the European instance of Galaxy](https://usegalaxy.eu/) since Special thanks to Björn Grüning, who helped me to port pcdl into Galaxy! -## Run PhysiCell Studio on Galaxy +## ✨ Run PhysiCell Studio on Galaxy and save the output -1. In your web browser, open the European instance of Galaxy: https://usegalaxy.eu/ -1. Optionally, log into your user account. Create one if you do not already have one. -1. Near the top left corner, click "Tools". -1. Near the left-side top corner, in the "Tools" search bar, type "PhysiCell Studio". +1. Point your web browser to the European instance of Galaxy: https://usegalaxy.eu/ +1. Required! Log into your user account. Create one if you do not already have one. +1. On the left-side panel, click "Interactive Tools". +1. Near the left-side top corner, in the "Tools" search bar, type "physicell studio". 1. Choose "PhysiCell Studio". -1. Choose "Run Tool" (center panel or top right). -1. How to load, develop, or run a PhysiCell model in the studio can be learned here: - + https://physicell-studio.readthedocs.io/en/latest/index.html - + https://github.com/PhysiCell-Tools/PhysiCell-Studio -1. [TKBue] +1. On the center panel, click on one of the "Run Tool" buttons. +1. On the left-side panel, after a while, "Active Interactive Tools" should pop up. +1. Under "Active Interactive Tools" click "PhysiCell Studio". +1. Run a simmulation: In brief, in the middle panel, click the "Run" tab, then click the "Run simmulation", then wait until on the screen output "Processed finish" shows up. +1. On the center panel menu bar, choose "Misc > put on History > all output.zip". +1. On the right-side History panel, after a while, an "all\_output.zip" file should pop up. + +### Unzip the zip folder +1. Near the top left corner, click "Tools". +1. Near the left-side top corner, in the "Tools" search bar, type "unzip". +1. Choose "Unzip Unzip a file". +1. Mouse drag and drop the "all\_output.zip" file from the right-side History panel into the tool's parameters "Input file" field in the middle panel. +1. On the center panel, click on one of the "Run Tool" buttons. +1. On the right-side History panel, after a while, an unzipped file collection should pop up. -## Upload local PhysiCell output to Galaxy +## ✨ Upload local PhysiCell output to Galaxy -1. In your web browser, open the European instance of Galaxy: https://usegalaxy.eu/ -1. Optionally, log into your user account. Create one if you do not already have one. -1. Near the top left corner, click "Upload" (New upload Beta). +1. Point your web browser to the European instance of Galaxy: https://usegalaxy.eu/ +1. Optionally: Log into your user account. +1. Near the top left corner, click "Upload (New upload Beta)". 1. Near the top left corner, choose "Upload from Computer". 1. In the center panel click "Brows Files". 1. Click your self through to the PhysiCell output folder. -1. Click on one file in the output folder, then on your keyboard press Ctrl + A to choose all files. +1. Click on one file in the output folder, then, on your keyboard, press Ctrl + A to choose all files. 1. Click "Open". After a while the filenames will appear in the central panel. 1. On the center panel bottom, flip the switch "Create a collection from these files" on. 1. "Enter a collection name" (e.g. output) and choose "Collection Type" "List". 1. Click "Start". -1. After a while, the uploaded data collection should appear in the history in the right-side panel. +1. After a while, the uploaded data collection should appear in the in the right-side History panel. -## The pcdl tools on Galaxy +## ✨ The pcdl tools on Galaxy -1. In your web browser, open the European instance of Galaxy: https://usegalaxy.eu/ -1. Optionally, log into your user account. Create one if you do not already have one. +1. Point your web browser to the European instance of Galaxy: https://usegalaxy.eu/ +1. Optionally: Log into your user account. 1. Upload or generate PhysiCell output as described above. 1. Near the top left corner, click "Tools". 1. Near the left-side top corner, in the "Tools" search bar, type "pcdl". -1. Choose one of the pcdl_ tools (e.g. pcdl\_get\_version). -1. Mouse drag and drop the uploaded or generated PhysiCell output data collection in the left-side history panel into the "essential:" "data collection" field in the middle panel. -1. Optionally, tweak the "essential" and "advanced:" parameters. +1. Choose one of the pcdl\_ tools (e.g. pcdl\_get\_version). +1. Mouse drag and drop the uploaded or generated PhysiCell output data collection in the left-side History panel into the "essential:" "data collection" field in the middle panel. +1. Optionally, tweak the "essential" and "advanced" parameters. 1. Click "Run Tool". -1. After a while, the result file(s) should appear in the left-side history panel. +1. After a while, the result file(s) should appear in the left-side History panel. -### ✨ Further readings +## ✨ Further readings You can learn more about Galaxy here: + https://galaxyproject.org/tutorials/g101/ +How to load, develop, or run a PhysiCell model in the studio can be learned here: ++ https://physicell-studio.readthedocs.io/en/latest/index.html ++ https://github.com/PhysiCell-Tools/PhysiCell-Studio + That's it. The rest is analysis within Galaxy! diff --git a/man/TUTORIAL_python3_timestep.md b/man/TUTORIAL_python3_timestep.md index 0a2c279..6e6a9ec 100644 --- a/man/TUTORIAL_python3_timestep.md +++ b/man/TUTORIAL_python3_timestep.md @@ -413,40 +413,45 @@ Cell neighbor spring attached graph ```python mcds.make_graph_gml('spring') ``` - -BUE 20260831 missing: +Furthermore, there are functions to recall the graph dictionary for each of the tracked graphs. ```python -mcds.get_attached_graph_dict() mcds.get_neighbor_graph_dict() +``` +```python +mcds.get_attached_graph_dict() +``` +```python mcds.get_spring_graph_dict() ``` ---> -Please have a look at [TUTORIAL_python3_graph.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_graph.md) to learn more. +Please have a look at +[TUTORIAL_python3_graph.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_graph.md) to learn more. + ## Microenvironment and Cell Data Related Functions - - - - + +Please have a look at +[TUTORIAL_python3_muspan.md](TUTORIAL_python3_muspan.md) +[TUTORIAL_python3_scverse.md](TUTORIAL_python3_scverse.md) +to learn more. ### ✨ PhysiCell Data Analysis with [Napari](https://napari.org/stable/), [Fiji Imagej](https://fiji.sc/), [Neuroglancer](https://research.google/blog/an-interactive-automated-3d-reconstruction-of-a-fly-brain/), and similar software. @@ -483,7 +488,6 @@ and [TUTORIAL_neuroglancer.md]((https://github.com/elmbeech/physicelldataloader/ to learn more. - ## Mesh Data Related Functions For data analysis, the functions related to the mesh are most probably the least one you have to deal with. diff --git a/man/TUTORIAL_simularium.md b/man/TUTORIAL_simularium.md index bde5bda..918a216 100644 --- a/man/TUTORIAL_simularium.md +++ b/man/TUTORIAL_simularium.md @@ -5,9 +5,9 @@ The [simularium](https://simularium.allencell.org/) viewer was developed to shar Simularium files can easily be generated from mcds time series. -### ✨ command line +## ✨ command line -#### command line time series +### command line time series Generate a simularium trajectory file. @@ -15,16 +15,16 @@ Generate a simularium trajectory file. pcdl_make_simularium output_2d/ ``` -#### command line man page +### command line man page ```bash pcdl_make_simularium -h ``` -### ✨ python +## ✨ python -#### python time series +### python time series Load a time series and generate a simularium trajectory file. @@ -35,7 +35,7 @@ mcdsts = pcdl.TimeSeries('output_2d/') mcdsts.make_simularium() ``` -#### python docstrings +### python docstrings ```python import pcdl @@ -44,7 +44,7 @@ mcdsts = pcdl.TimeSeries('output_2d/') help(mcdsts.make_simularium()) ``` -### ✨ simularium viewer +## ✨ simularium viewer 1. Open the simularium viewer web page: https://simularium.allencell.org/ 1. Near the top right corner, click the "Load models" dropdown menu and choose "Simularium file". @@ -54,7 +54,7 @@ help(mcdsts.make_simularium()) 1. Explore the loaded file. -### ✨ Further readings' +## ✨ Further readings' Please work through the official documentation to learn more about how simularium software. diff --git a/pcdl/commandline.py b/pcdl/commandline.py index 18cca4f..a9dae84 100644 --- a/pcdl/commandline.py +++ b/pcdl/commandline.py @@ -2940,48 +2940,6 @@ def make_ome_tiff(): return 0 -def render_neuroglancer(): - # argv - parser = argparse.ArgumentParser( - prog = 'pcdl_render_neuroglancer', - description = 'function to load a time step from an ome tiff files, generated with make_ome_tiff, into neuroglancer.', - epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', - ) - # ome tiff path file - parser.add_argument( - 'tiffpathfile', - nargs = '?', - default = '.', - help = 'path to ome tiff file.', - ) - # time step - parser.add_argument( - 'timestep', - nargs = '?', - default = 0, - type = int, - help = 'time step, within a possibly collapsed ome tiff file, to render. the default will work with single time step ome tiff files.', - ) - # intensity colormap - parser.add_argument( - '--intensity_cmap', - default = 'gray', - help = 'matlab color map label, used to display expression intensity values. if None, no intensity layers will be generated. https://matplotlib.org/stable/users/explain/colors/colormaps.html', - ) - - # parse arguments - args = parser.parse_args() - print(args) - - # process arguments - s_neuromancerpath = str(pathlib.Path(pcdl.__file__).parent).replace('\\','/') + '/' - s_tiffpathfile = args.tiffpathfile.replace('\\','/') - - # run - # bue 20250623: use subprocess to run python3 in interactive mode to run the neuromancer script, which is needed to keep the neuroglancer web gl server running. - subprocess.run(['python3', '-i', f'{s_neuromancerpath}neuromancer.py', s_tiffpathfile, '--timestep', str(args.timestep), '--intensity_cmap', args.intensity_cmap]) - - ################# # making movies # ################# @@ -3082,3 +3040,45 @@ def make_movie(): # going home print(s_opathfile) return 0 + + +def render_neuroglancer(): + # argv + parser = argparse.ArgumentParser( + prog = 'pcdl_render_neuroglancer', + description = 'function to load a time step from an ome tiff files, generated with make_ome_tiff, into neuroglancer.', + epilog = 'homepage: https://github.com/elmbeech/physicelldataloader', + ) + # ome tiff path file + parser.add_argument( + 'tiffpathfile', + nargs = '?', + default = '.', + help = 'path to ome tiff file.', + ) + # time step + parser.add_argument( + 'timestep', + nargs = '?', + default = 0, + type = int, + help = 'time step, within a possibly collapsed ome tiff file, to render. the default will work with single time step ome tiff files.', + ) + # intensity colormap + parser.add_argument( + '--intensity_cmap', + default = 'gray', + help = 'matlab color map label, used to display expression intensity values. if None, no intensity layers will be generated. https://matplotlib.org/stable/users/explain/colors/colormaps.html', + ) + + # parse arguments + args = parser.parse_args() + print(args) + + # process arguments + s_neuromancerpath = str(pathlib.Path(pcdl.__file__).parent).replace('\\','/') + '/' + s_tiffpathfile = args.tiffpathfile.replace('\\','/') + + # run + # bue 20250623: use subprocess to run python3 in interactive mode to run the neuromancer script, which is needed to keep the neuroglancer web gl server running. + subprocess.run(['python3', '-i', f'{s_neuromancerpath}neuromancer.py', s_tiffpathfile, '--timestep', str(args.timestep), '--intensity_cmap', args.intensity_cmap]) diff --git a/pyproject.toml b/pyproject.toml index e1e15a4..54773a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,9 +150,9 @@ pcdl_get_spatialdata = "pcdl.commandline:get_spatialdata" pcdl_plot_timeseries = "pcdl.commandline:plot_timeseries" pcdl_make_ome_tiff = "pcdl.commandline:make_ome_tiff" # making movies -pcdl_render_neuroglancer = "pcdl.commandline:render_neuroglancer" pcdl_make_gif = "pcdl.commandline:make_gif" pcdl_make_movie = "pcdl.commandline:make_movie" +pcdl_render_neuroglancer = "pcdl.commandline:render_neuroglancer" [project.urls] From 15d42b6ce4a8371058e4b03f9595776bf6306e65 Mon Sep 17 00:00:00 2001 From: bue Date: Thu, 3 Sep 2026 02:36:03 -0400 Subject: [PATCH 11/15] @ pcdl : things in order. --- test/test_commandline_2d.py | 750 ++++++++++++++++++------------------ test/test_timeseries_2d.py | 543 +++++++++++++------------- test/test_timeseries_3d.py | 414 ++++++++++---------- test/test_timestep_2d.py | 451 +++++++++++----------- test/test_timestep_3d.py | 120 +++--- 5 files changed, 1154 insertions(+), 1124 deletions(-) diff --git a/test/test_commandline_2d.py b/test/test_commandline_2d.py index a6333df..8c95a48 100644 --- a/test/test_commandline_2d.py +++ b/test/test_commandline_2d.py @@ -153,63 +153,6 @@ def test_pcdl_get_substrate_list_timestep(self): assert o_result.returncode == 0 -class TestCommandLineInterfaceConcDfAttribute(object): - ''' tests for one pcdl command line interface function. ''' - - # timeseries collapsed: - # + path (str) nop - # + verbose (true, _false_) nop - # + values (int) ok - # + drop (oxygen) ok - # + keep (oxygen) ok - # + allvalues (false _true_) ok - - def test_pcdl_get_conc_attribute_timeseries(self): - o_result = subprocess.run(['pcdl_get_conc_attribute', s_path_2d], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_conc_attribute_minmax.json') - assert o_result.returncode == 0 - - def test_pcdl_get_conc_attribute_timeseries_value(self): - o_result = subprocess.run(['pcdl_get_conc_attribute', s_path_2d, '2'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_conc_attribute_minmax.json') - assert o_result.returncode == 0 - - def test_pcdl_get_conc_attribute_timeseries_drop(self): - o_result = subprocess.run(['pcdl_get_conc_attribute', s_path_2d, '--drop', 'conc_type', 'oxygen'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_conc_attribute_minmax.json') - assert o_result.returncode == 0 - - def test_pcdl_get_conc_attribute_timeseries_keep(self): - o_result = subprocess.run(['pcdl_get_conc_attribute', s_path_2d, '--keep', 'conc_type', 'oxygen'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_conc_attribute_minmax.json') - assert o_result.returncode == 0 - - def test_pcdl_get_conc_attribute_timeseries_allvalues(self): - o_result = subprocess.run(['pcdl_get_conc_attribute', s_path_2d, '--allvalues', 'true'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_conc_attribute_all.json') - assert o_result.returncode == 0 - - class TestCommandLineInterfaceConcDf(object): ''' tests for one pcdl command line interface function. ''' @@ -305,6 +248,63 @@ def test_pcdl_get_conc_df_timestep_keep(self): assert o_result.returncode == 0 +class TestCommandLineInterfaceConcDfAttribute(object): + ''' tests for one pcdl command line interface function. ''' + + # timeseries collapsed: + # + path (str) nop + # + verbose (true, _false_) nop + # + values (int) ok + # + drop (oxygen) ok + # + keep (oxygen) ok + # + allvalues (false _true_) ok + + def test_pcdl_get_conc_attribute_timeseries(self): + o_result = subprocess.run(['pcdl_get_conc_attribute', s_path_2d], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries_conc_attribute_minmax.json') + assert o_result.returncode == 0 + + def test_pcdl_get_conc_attribute_timeseries_value(self): + o_result = subprocess.run(['pcdl_get_conc_attribute', s_path_2d, '2'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries_conc_attribute_minmax.json') + assert o_result.returncode == 0 + + def test_pcdl_get_conc_attribute_timeseries_drop(self): + o_result = subprocess.run(['pcdl_get_conc_attribute', s_path_2d, '--drop', 'conc_type', 'oxygen'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries_conc_attribute_minmax.json') + assert o_result.returncode == 0 + + def test_pcdl_get_conc_attribute_timeseries_keep(self): + o_result = subprocess.run(['pcdl_get_conc_attribute', s_path_2d, '--keep', 'conc_type', 'oxygen'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries_conc_attribute_minmax.json') + assert o_result.returncode == 0 + + def test_pcdl_get_conc_attribute_timeseries_allvalues(self): + o_result = subprocess.run(['pcdl_get_conc_attribute', s_path_2d, '--allvalues', 'true'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries_conc_attribute_all.json') + assert o_result.returncode == 0 + + class TestCommandLineInterfacePlotContour(object): ''' tests for one pcdl command line interface function. ''' @@ -473,332 +473,332 @@ def test_pcdl_get_celltype_list_timestep_settingxmlfalse(self): assert o_result.returncode == 0 -class TestCommandLineInterfaceCellAttributeList(object): +class TestCommandLineInterfaceCellDf(object): ''' tests for one pcdl command line interface function. ''' - # timeseries collapsed: - # + path (str) nop + # timeseries + # + collapse (true false) ok + + # timestep and timeseries: + # + path nop + # + customtype nop (because the dataframe is straightaway saved as csv) # + microenv (true, _false_) ok # + physiboss (true, _false_) ok # + settingxml (string, _none_, _false_) ok # + verbose (true, _false_) nop + # + values (int) ok + # + drop (cell_type oxygen) ok + # + keep (cell_type oxygen) ok - def test_pcdl_get_cell_attribute_list_timeseries(self): - o_result = subprocess.run(['pcdl_get_cell_attribute_list', s_path_2d], check=False, capture_output=True) + def test_pcdl_get_cell_df_timeseries(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_attribute_list_timeseries_microenv(self): - o_result = subprocess.run(['pcdl_get_cell_attribute_list', s_path_2d, '--microenv', 'false'], check=False, capture_output=True) + def test_pcdl_get_cell_df_timeseries_collapsed(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '--collapse', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_attribute_list_timeseries_physiboss(self): - o_result = subprocess.run(['pcdl_get_cell_attribute_list', s_path_2d, '--physiboss', 'false'], check=False, capture_output=True) + def test_pcdl_get_cell_df_timeseries_microenv(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '--microenv', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_attribute_list_timeseries_settingxmlfalse(self): - o_result = subprocess.run(['pcdl_get_cell_attribute_list', s_path_2d, '--settingxml', 'false'], check=False, capture_output=True) + def test_pcdl_get_cell_df_timeseries_physiboss(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '--physiboss', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_attribute_list_timeseries_settingxmlnone(self): - o_result = subprocess.run(['pcdl_get_cell_attribute_list', s_path_2d, '--settingxml', 'none'], check=False, capture_output=True) + def test_pcdl_get_cell_df_timeseries_settingxmlfalse(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '--settingxml', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries_cell.csv') assert o_result.returncode == 0 + def test_pcdl_get_cell_df_timeseries_settingxmlnone(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '--settingxml', 'none'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries_cell.csv') + assert o_result.returncode == 0 -class TestCommandLineInterfaceCellDfAttribute(object): - ''' tests for one pcdl command line interface function. ''' - - # timeseries collapsed: - # + path (str) nop - # + customtype ([], _sample:bool_) ok - # + microenv (true, _false_) ok - # + physiboss (true, _false_) - # + settingxml (string, _none_, _false_) ok - # + verbose (true, _false_) nop - # + values (int) ok - # + drop (cell_type oxygen) ok - # + keep (cell_type oxygen) ok - # + allvalues (false _true_) ok + def test_pcdl_get_cell_df_timeseries_value(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '2'], check=False, capture_output=True) + print(f'o_result: {o_result}\n') + print(f'o_result.returncode: {o_result.returncode}\n') + print(f'o_result.stdout: {o_result.stdout}\n') + print(f'o_result.stderr: {o_result.stderr}\n') + os.remove(f'{s_path_2d}/timeseries_cell.csv') + assert o_result.returncode == 0 - def test_pcdl_get_cell_attribute_timeseries(self): - o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d], check=False, capture_output=True) + def test_pcdl_get_cell_df_timeseries_drop(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '--drop', 'cell_type', 'oxygen'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') + os.remove(f'{s_path_2d}/timeseries_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_attribute_timeseries_customtype(self): - o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--custom_data_type', 'sample:bool'], check=False, capture_output=True) + def test_pcdl_get_cell_df_timeseries_keep(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '--keep', 'cell_type', 'oxygen'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') + os.remove(f'{s_path_2d}/timeseries_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_attribute_timeseries_microenv(self): - o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--microenv', 'false'], check=False, capture_output=True) + def test_pcdl_get_cell_df_timestep(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') + os.remove(f'{s_path_2d}/output00000024_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_attribute_timeseries_physiboss(self): - o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--physiboss', 'false'], check=False, capture_output=True) + def test_pcdl_get_cell_df_timestep_microenv(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d, '--microenv', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') + os.remove(f'{s_path_2d}/output00000024_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_attribute_timeseries_settingxmlfalse(self): - o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--settingxml', 'false'], check=False, capture_output=True) + def test_pcdl_get_cell_df_timestep_physiboss(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d, '--physiboss', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') + os.remove(f'{s_path_2d}/output00000024_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_attribute_timeseries_settingxmlnone(self): - o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--settingxml', 'none'], check=False, capture_output=True) + def test_pcdl_get_cell_df_timestep_settingxmlfalse(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d, '--settingxml', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') + os.remove(f'{s_path_2d}/output00000024_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_attribute_timeseries_value(self): - o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '2'], check=False, capture_output=True) + def test_pcdl_get_cell_df_timestep_settingxmlnone(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d, '--settingxml', 'none'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') + os.remove(f'{s_path_2d}/output00000024_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_attribute_timeseries_drop(self): - o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--drop', 'cell_type', 'oxygen'], check=False, capture_output=True) + def test_pcdl_get_cell_df_timestep_value(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d, '2'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') + os.remove(f'{s_path_2d}/output00000024_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_attribute_timeseries_keep(self): - o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--keep', 'cell_type', 'oxygen'], check=False, capture_output=True) + def test_pcdl_get_cell_df_timestep_drop(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d, '--drop', 'cell_type', 'oxygen'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') + os.remove(f'{s_path_2d}/output00000024_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_attribute_timeseries_allvalues(self): - o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--allvalues', 'true'], check=False, capture_output=True) + def test_pcdl_get_cell_df_timestep_keep(self): + o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d, '--keep', 'cell_type', 'oxygen'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell_attribute_all.json') + os.remove(f'{s_path_2d}/output00000024_cell.csv') assert o_result.returncode == 0 -class TestCommandLineInterfaceCellDf(object): +class TestCommandLineInterfaceCellDfAttribute(object): ''' tests for one pcdl command line interface function. ''' - # timeseries - # + collapse (true false) ok - - # timestep and timeseries: - # + path nop - # + customtype nop (because the dataframe is straightaway saved as csv) + # timeseries collapsed: + # + path (str) nop + # + customtype ([], _sample:bool_) ok # + microenv (true, _false_) ok - # + physiboss (true, _false_) ok + # + physiboss (true, _false_) # + settingxml (string, _none_, _false_) ok # + verbose (true, _false_) nop # + values (int) ok # + drop (cell_type oxygen) ok # + keep (cell_type oxygen) ok + # + allvalues (false _true_) ok - def test_pcdl_get_cell_df_timeseries(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d], check=False, capture_output=True) + def test_pcdl_get_cell_attribute_timeseries(self): + o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell.csv') + os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') assert o_result.returncode == 0 - def test_pcdl_get_cell_df_timeseries_collapsed(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '--collapse', 'false'], check=False, capture_output=True) + def test_pcdl_get_cell_attribute_timeseries_customtype(self): + o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--custom_data_type', 'sample:bool'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.csv') + os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') assert o_result.returncode == 0 - def test_pcdl_get_cell_df_timeseries_microenv(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '--microenv', 'false'], check=False, capture_output=True) + def test_pcdl_get_cell_attribute_timeseries_microenv(self): + o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--microenv', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell.csv') + os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') assert o_result.returncode == 0 - def test_pcdl_get_cell_df_timeseries_physiboss(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '--physiboss', 'false'], check=False, capture_output=True) + def test_pcdl_get_cell_attribute_timeseries_physiboss(self): + o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--physiboss', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell.csv') + os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') assert o_result.returncode == 0 - def test_pcdl_get_cell_df_timeseries_settingxmlfalse(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '--settingxml', 'false'], check=False, capture_output=True) + def test_pcdl_get_cell_attribute_timeseries_settingxmlfalse(self): + o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--settingxml', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell.csv') + os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') assert o_result.returncode == 0 - def test_pcdl_get_cell_df_timeseries_settingxmlnone(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '--settingxml', 'none'], check=False, capture_output=True) + def test_pcdl_get_cell_attribute_timeseries_settingxmlnone(self): + o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--settingxml', 'none'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell.csv') + os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') assert o_result.returncode == 0 - def test_pcdl_get_cell_df_timeseries_value(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '2'], check=False, capture_output=True) + def test_pcdl_get_cell_attribute_timeseries_value(self): + o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '2'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell.csv') + os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') assert o_result.returncode == 0 - def test_pcdl_get_cell_df_timeseries_drop(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '--drop', 'cell_type', 'oxygen'], check=False, capture_output=True) + def test_pcdl_get_cell_attribute_timeseries_drop(self): + o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--drop', 'cell_type', 'oxygen'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell.csv') + os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') assert o_result.returncode == 0 - def test_pcdl_get_cell_df_timeseries_keep(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_path_2d, '--keep', 'cell_type', 'oxygen'], check=False, capture_output=True) + def test_pcdl_get_cell_attribute_timeseries_keep(self): + o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--keep', 'cell_type', 'oxygen'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/timeseries_cell.csv') + os.remove(f'{s_path_2d}/timeseries_cell_attribute_minmax.json') assert o_result.returncode == 0 - def test_pcdl_get_cell_df_timestep(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d], check=False, capture_output=True) + def test_pcdl_get_cell_attribute_timeseries_allvalues(self): + o_result = subprocess.run(['pcdl_get_cell_attribute', s_path_2d, '--allvalues', 'true'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_cell.csv') + os.remove(f'{s_path_2d}/timeseries_cell_attribute_all.json') assert o_result.returncode == 0 - def test_pcdl_get_cell_df_timestep_microenv(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d, '--microenv', 'false'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_cell.csv') - assert o_result.returncode == 0 - def test_pcdl_get_cell_df_timestep_physiboss(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d, '--physiboss', 'false'], check=False, capture_output=True) - print(f'o_result: {o_result}\n') - print(f'o_result.returncode: {o_result.returncode}\n') - print(f'o_result.stdout: {o_result.stdout}\n') - print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_cell.csv') - assert o_result.returncode == 0 +class TestCommandLineInterfaceCellAttributeList(object): + ''' tests for one pcdl command line interface function. ''' - def test_pcdl_get_cell_df_timestep_settingxmlfalse(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d, '--settingxml', 'false'], check=False, capture_output=True) + # timeseries collapsed: + # + path (str) nop + # + microenv (true, _false_) ok + # + physiboss (true, _false_) ok + # + settingxml (string, _none_, _false_) ok + # + verbose (true, _false_) nop + + def test_pcdl_get_cell_attribute_list_timeseries(self): + o_result = subprocess.run(['pcdl_get_cell_attribute_list', s_path_2d], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_df_timestep_settingxmlnone(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d, '--settingxml', 'none'], check=False, capture_output=True) + def test_pcdl_get_cell_attribute_list_timeseries_microenv(self): + o_result = subprocess.run(['pcdl_get_cell_attribute_list', s_path_2d, '--microenv', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_df_timestep_value(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d, '2'], check=False, capture_output=True) + def test_pcdl_get_cell_attribute_list_timeseries_physiboss(self): + o_result = subprocess.run(['pcdl_get_cell_attribute_list', s_path_2d, '--physiboss', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_df_timestep_drop(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d, '--drop', 'cell_type', 'oxygen'], check=False, capture_output=True) + def test_pcdl_get_cell_attribute_list_timeseries_settingxmlfalse(self): + o_result = subprocess.run(['pcdl_get_cell_attribute_list', s_path_2d, '--settingxml', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_cell.csv') assert o_result.returncode == 0 - def test_pcdl_get_cell_df_timestep_keep(self): - o_result = subprocess.run(['pcdl_get_cell_df', s_pathfile_2d, '--keep', 'cell_type', 'oxygen'], check=False, capture_output=True) + def test_pcdl_get_cell_attribute_list_timeseries_settingxmlnone(self): + o_result = subprocess.run(['pcdl_get_cell_attribute_list', s_path_2d, '--settingxml', 'none'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_cell.csv') assert o_result.returncode == 0 @@ -1033,444 +1033,444 @@ def test_pcdl_get_anndata_timestep_scale(self): assert o_result.returncode == 0 -class TestCommandLineInterfaceGraphGml(object): +class TestCommandLineInterfacePlotScatter(object): ''' tests for one pcdl command line interface function. ''' - # timestep and timeseries: + # time series and time steps. # + path nop # + customtype ([], _sample:bool_) ok - # + microenv (true, false) ok + # + microenv (true, _false_) ok # + physiboss (true, _false_) ok - # + settingxml (string, _none_, _false_) ok + # + settingxml (PhysiCell_settings.xml, _false_) ok # + verbose (true, _false_) nop - # + graph_type (neighbor, _attached_) ok - # + edge_attribute (true, _false_) ok - # + node_attribute (cell_type oxygen) ok + # + focus (_oxygen_) ok + # + z_slize (0.0, _1.1_) ok + # + z_axis (none, _0.0_40.0_) ok + # + alpha (1.0, _0.5_) ok + # + cmap (viridis, _magma_) ok + # + title (, _abc_) ok + # + grid (true, _false_) ok + # + legend_loc ('lower left', _'upper right'_) ok + # + xlim (none, _-40_400_) ok + # + ylim (none, _-30_300_) ok + # + xyequal (true, _false_) ok + # + s ('none', '74') ok + # + figsizepx (none, _[641, 481]_) ok + # + ext (jpeg, _tiff_) ok + # + figbgcolor (none, _yellow_) ok - def test_pcdl_make_graph_gml_timeseries_default(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor'], check=False, capture_output=True) + def test_pcdl_plot_scatter_default(self): + o_result = subprocess.run([ + 'pcdl_plot_scatter', s_pathfile_2d, + ], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') + shutil.rmtree(f'{s_path_2d}/cell_cell_type_z0.0/') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timeseries_customtype_nodeattribute_one(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--custom_data_type', 'sample:bool', '--node_attribute', 'sample'], check=False, capture_output=True) + def test_pcdl_plot_scatter_set(self): + o_result = subprocess.run([ + 'pcdl_plot_scatter', s_pathfile_2d, 'oxygen', + '--custom_data_type', 'sample:bool', + '--microenv', 'True', + '--physiboss', 'false', + '--settingxml', 'false', + '--cat_drop', '', + '--cat_keep', '', + '--z_slice', '1.1', + '--z_axis', '0.0', '40.0', + '--alpha', '0.5', + '--cmap', 'magma', + '--title', 'abc', + '--grid', 'false', + '--legend_loc', 'upper right', + '--xlim', '-40', '400', + '--ylim', '-30', '300', + '--xyequal', 'false', + '--s', '74', + '--figsizepx', '842', '531', + '--directory', f'{s_path_2d}/jakku', + '--ext', 'tiff', + '--figbgcolor', 'yellow', + ], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') + shutil.rmtree(f'{s_path_2d}/jakku/') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timeseries_microenv(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--microenv', 'false'], check=False, capture_output=True) + +class TestCommandLineInterfaceCellVtk(object): + ''' tests for one pcdl command line interface function. ''' + + # timestep and timeseries: + # + path nop + # + customtype ([], _sample:bool_) ok + # + microenv (true, _false) ok + # + physiboss (true, _false_) ok + # + settingxml (string, _none_, _false_) ok + # + verbose (true, _false_) nop + # + attribute (cell_type oxygen, _empty_) ok + # + ext (_cell.vtp, _.vtp_) ok + + def test_pcdl_make_cell_vtk_timeseries_default(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timeseries_physiboss(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--physiboss', 'false'], check=False, capture_output=True) + def test_pcdl_make_cell_vtk_timeseries_customtype_attribute_one(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d, 'sample', '--custom_data_type', 'sample:bool'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timeseries_settingxmlfalse_nodeattribute_one(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--settingxml', 'false', '--node_attribute', 'default_fusion_rates'], check=False, capture_output=True) + def test_pcdl_make_cell_vtk_timeseries_microenv(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d, '--microenv', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timeseries_settingxmlnone_nodeattribute_one(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--settingxml', 'none', '--node_attribute', 'default_fusion_rates'], check=False, capture_output=True) + def test_pcdl_make_cell_vtk_timeseries_physiboss(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d, '--physiboss', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timeseries_graph_type(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'attached'], check=False, capture_output=True) + def test_pcdl_make_cell_vtk_timeseries_settingxmlfalse_attribute_one(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d, 'default_fusion_rates', '--settingxml', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_attached.gml') + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timeseries_edge_attribute(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--edge_attribute', 'false'], check=False, capture_output=True) + def test_pcdl_make_cell_vtk_timeseries_settingxmlnone_attribute_one(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d, 'default_fusion_rates', '--settingxml', 'none'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timeseries_nodeattribute_one(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--node_attribute', 'cell_type'], check=False, capture_output=True) + def test_pcdl_make_cell_vtk_timeseries_attribute_many(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d, 'cell_type', 'oxygen'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timeseries_nodeattribute_many(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--node_attribute', 'cell_type', 'oxygen'], check=False, capture_output=True) + def test_pcdl_make_cell_vtk_timeseries_ext(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d, '--ext', '.vtp'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timestep_default(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor'], check=False, capture_output=True) + def test_pcdl_make_cell_vtk_timestep_default(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_neighbor.gml') + os.remove(f'{s_path_2d}/output00000024_cell.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timestep_customtype_nodeattribute_one(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--custom_data_type', 'sample:bool', '--node_attribute', 'sample'], check=False, capture_output=True) + def test_pcdl_make_cell_vtk_timestep_customtype_attribute_one(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d, 'sample', '--custom_data_type', 'sample:bool'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_neighbor.gml') + os.remove(f'{s_path_2d}/output00000024_cell.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timestep_microenv(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--microenv', 'false'], check=False, capture_output=True) + def test_pcdl_make_cell_vtk_timestep_microenv(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d, '--microenv', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_neighbor.gml') + os.remove(f'{s_path_2d}/output00000024_cell.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timestep_physiboss(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--physiboss', 'false'], check=False, capture_output=True) + def test_pcdl_make_cell_vtk_timestep_physiboss(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d, '--physiboss', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_neighbor.gml') + os.remove(f'{s_path_2d}/output00000024_cell.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timestep_settingxmlfalse_nodeattribute_one(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--settingxml', 'false', '--node_attribute', 'default_fusion_rates'], check=False, capture_output=True) + def test_pcdl_make_cell_vtk_timestep_settingxmlfalse_attribute_one(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d, 'default_fusion_rates', '--settingxml', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_neighbor.gml') + os.remove(f'{s_path_2d}/output00000024_cell.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timestep_settingxmlnone_nodeattribute_one(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--settingxml', 'none', '--node_attribute', 'default_fusion_rates'], check=False, capture_output=True) + def test_pcdl_make_cell_vtk_timestep_settingxmlnone_attribute_one(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d, 'default_fusion_rates', '--settingxml', 'none'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_neighbor.gml') + os.remove(f'{s_path_2d}/output00000024_cell.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timestep_graph_type(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'attached'], check=False, capture_output=True) + def test_pcdl_make_cell_vtk_timestep_attribute_many(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d, 'cell_type', 'oxygen'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_attached.gml') + os.remove(f'{s_path_2d}/output00000024_cell.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timestep_edge_attribute(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--edge_attribute', 'false'], check=False, capture_output=True) + def test_pcdl_make_cell_vtk_timestep_ext(self): + o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d, '--ext', '.vtp'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_neighbor.gml') + os.remove(f'{s_path_2d}/output00000024.vtp') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timestep_node_attribute_one(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--node_attribute', 'cell_type'], check=False, capture_output=True) + +class TestCommandLineInterfaceGraphGml(object): + ''' tests for one pcdl command line interface function. ''' + + # timestep and timeseries: + # + path nop + # + customtype ([], _sample:bool_) ok + # + microenv (true, false) ok + # + physiboss (true, _false_) ok + # + settingxml (string, _none_, _false_) ok + # + verbose (true, _false_) nop + # + graph_type (neighbor, _attached_) ok + # + edge_attribute (true, _false_) ok + # + node_attribute (cell_type oxygen) ok + + def test_pcdl_make_graph_gml_timeseries_default(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_neighbor.gml') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') assert o_result.returncode == 0 - def test_pcdl_make_graph_gml_timestep_node_attribute_many(self): - o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--node_attribute', 'cell_type', 'oxygen'], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timeseries_customtype_nodeattribute_one(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--custom_data_type', 'sample:bool', '--node_attribute', 'sample'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_neighbor.gml') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') assert o_result.returncode == 0 - -class TestCommandLineInterfacePlotScatter(object): - ''' tests for one pcdl command line interface function. ''' - - # time series and time steps. - # + path nop - # + customtype ([], _sample:bool_) ok - # + microenv (true, _false_) ok - # + physiboss (true, _false_) ok - # + settingxml (PhysiCell_settings.xml, _false_) ok - # + verbose (true, _false_) nop - # + focus (_oxygen_) ok - # + z_slize (0.0, _1.1_) ok - # + z_axis (none, _0.0_40.0_) ok - # + alpha (1.0, _0.5_) ok - # + cmap (viridis, _magma_) ok - # + title (, _abc_) ok - # + grid (true, _false_) ok - # + legend_loc ('lower left', _'upper right'_) ok - # + xlim (none, _-40_400_) ok - # + ylim (none, _-30_300_) ok - # + xyequal (true, _false_) ok - # + s ('none', '74') ok - # + figsizepx (none, _[641, 481]_) ok - # + ext (jpeg, _tiff_) ok - # + figbgcolor (none, _yellow_) ok - - def test_pcdl_plot_scatter_default(self): - o_result = subprocess.run([ - 'pcdl_plot_scatter', s_pathfile_2d, - ], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timeseries_microenv(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--microenv', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - shutil.rmtree(f'{s_path_2d}/cell_cell_type_z0.0/') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') assert o_result.returncode == 0 - def test_pcdl_plot_scatter_set(self): - o_result = subprocess.run([ - 'pcdl_plot_scatter', s_pathfile_2d, 'oxygen', - '--custom_data_type', 'sample:bool', - '--microenv', 'True', - '--physiboss', 'false', - '--settingxml', 'false', - '--cat_drop', '', - '--cat_keep', '', - '--z_slice', '1.1', - '--z_axis', '0.0', '40.0', - '--alpha', '0.5', - '--cmap', 'magma', - '--title', 'abc', - '--grid', 'false', - '--legend_loc', 'upper right', - '--xlim', '-40', '400', - '--ylim', '-30', '300', - '--xyequal', 'false', - '--s', '74', - '--figsizepx', '842', '531', - '--directory', f'{s_path_2d}/jakku', - '--ext', 'tiff', - '--figbgcolor', 'yellow', - ], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timeseries_physiboss(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--physiboss', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - shutil.rmtree(f'{s_path_2d}/jakku/') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') assert o_result.returncode == 0 - -class TestCommandLineInterfaceCellVtk(object): - ''' tests for one pcdl command line interface function. ''' - - # timestep and timeseries: - # + path nop - # + customtype ([], _sample:bool_) ok - # + microenv (true, _false) ok - # + physiboss (true, _false_) ok - # + settingxml (string, _none_, _false_) ok - # + verbose (true, _false_) nop - # + attribute (cell_type oxygen, _empty_) ok - # + ext (_cell.vtp, _.vtp_) ok - - def test_pcdl_make_cell_vtk_timeseries_default(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timeseries_settingxmlfalse_nodeattribute_one(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--settingxml', 'false', '--node_attribute', 'default_fusion_rates'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.vtp') + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') assert o_result.returncode == 0 - def test_pcdl_make_cell_vtk_timeseries_customtype_attribute_one(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d, 'sample', '--custom_data_type', 'sample:bool'], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timeseries_settingxmlnone_nodeattribute_one(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--settingxml', 'none', '--node_attribute', 'default_fusion_rates'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.vtp') + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') assert o_result.returncode == 0 - def test_pcdl_make_cell_vtk_timeseries_microenv(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d, '--microenv', 'false'], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timeseries_graph_type(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'attached'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.vtp') + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_attached.gml') assert o_result.returncode == 0 - def test_pcdl_make_cell_vtk_timeseries_physiboss(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d, '--physiboss', 'false'], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timeseries_edge_attribute(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--edge_attribute', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.vtp') + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') assert o_result.returncode == 0 - def test_pcdl_make_cell_vtk_timeseries_settingxmlfalse_attribute_one(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d, 'default_fusion_rates', '--settingxml', 'false'], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timeseries_nodeattribute_one(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--node_attribute', 'cell_type'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.vtp') + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') assert o_result.returncode == 0 - def test_pcdl_make_cell_vtk_timeseries_settingxmlnone_attribute_one(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d, 'default_fusion_rates', '--settingxml', 'none'], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timeseries_nodeattribute_many(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_path_2d, 'neighbor', '--node_attribute', 'cell_type', 'oxygen'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.vtp') + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_neighbor.gml') assert o_result.returncode == 0 - def test_pcdl_make_cell_vtk_timeseries_attribute_many(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d, 'cell_type', 'oxygen'], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timestep_default(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.vtp') + os.remove(f'{s_path_2d}/output00000024_neighbor.gml') assert o_result.returncode == 0 - def test_pcdl_make_cell_vtk_timeseries_ext(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_path_2d, '--ext', '.vtp'], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timestep_customtype_nodeattribute_one(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--custom_data_type', 'sample:bool', '--node_attribute', 'sample'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - for i_step in range(25): - os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}.vtp') + os.remove(f'{s_path_2d}/output00000024_neighbor.gml') assert o_result.returncode == 0 - def test_pcdl_make_cell_vtk_timestep_default(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timestep_microenv(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--microenv', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_cell.vtp') + os.remove(f'{s_path_2d}/output00000024_neighbor.gml') assert o_result.returncode == 0 - def test_pcdl_make_cell_vtk_timestep_customtype_attribute_one(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d, 'sample', '--custom_data_type', 'sample:bool'], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timestep_physiboss(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--physiboss', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_cell.vtp') + os.remove(f'{s_path_2d}/output00000024_neighbor.gml') assert o_result.returncode == 0 - def test_pcdl_make_cell_vtk_timestep_microenv(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d, '--microenv', 'false'], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timestep_settingxmlfalse_nodeattribute_one(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--settingxml', 'false', '--node_attribute', 'default_fusion_rates'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_cell.vtp') + os.remove(f'{s_path_2d}/output00000024_neighbor.gml') assert o_result.returncode == 0 - def test_pcdl_make_cell_vtk_timestep_physiboss(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d, '--physiboss', 'false'], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timestep_settingxmlnone_nodeattribute_one(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--settingxml', 'none', '--node_attribute', 'default_fusion_rates'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_cell.vtp') + os.remove(f'{s_path_2d}/output00000024_neighbor.gml') assert o_result.returncode == 0 - def test_pcdl_make_cell_vtk_timestep_settingxmlfalse_attribute_one(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d, 'default_fusion_rates', '--settingxml', 'false'], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timestep_graph_type(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'attached'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_cell.vtp') + os.remove(f'{s_path_2d}/output00000024_attached.gml') assert o_result.returncode == 0 - def test_pcdl_make_cell_vtk_timestep_settingxmlnone_attribute_one(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d, 'default_fusion_rates', '--settingxml', 'none'], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timestep_edge_attribute(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--edge_attribute', 'false'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_cell.vtp') + os.remove(f'{s_path_2d}/output00000024_neighbor.gml') assert o_result.returncode == 0 - def test_pcdl_make_cell_vtk_timestep_attribute_many(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d, 'cell_type', 'oxygen'], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timestep_node_attribute_one(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--node_attribute', 'cell_type'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024_cell.vtp') + os.remove(f'{s_path_2d}/output00000024_neighbor.gml') assert o_result.returncode == 0 - def test_pcdl_make_cell_vtk_timestep_ext(self): - o_result = subprocess.run(['pcdl_make_cell_vtk', s_pathfile_2d, '--ext', '.vtp'], check=False, capture_output=True) + def test_pcdl_make_graph_gml_timestep_node_attribute_many(self): + o_result = subprocess.run(['pcdl_make_graph_gml', s_pathfile_2d, 'neighbor', '--node_attribute', 'cell_type', 'oxygen'], check=False, capture_output=True) print(f'o_result: {o_result}\n') print(f'o_result.returncode: {o_result.returncode}\n') print(f'o_result.stdout: {o_result.stdout}\n') print(f'o_result.stderr: {o_result.stderr}\n') - os.remove(f'{s_path_2d}/output00000024.vtp') + os.remove(f'{s_path_2d}/output00000024_neighbor.gml') assert o_result.returncode == 0 diff --git a/test/test_timeseries_2d.py b/test/test_timeseries_2d.py index 992583b..6218214 100644 --- a/test/test_timeseries_2d.py +++ b/test/test_timeseries_2d.py @@ -39,7 +39,7 @@ ## making movies related functions ## -class TestTimeSeriesMovies(object): +class TestTimeSeries2dMovies(object): ''' tests for loading a pcdl.TimeSeries data set. ''' mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) @@ -114,9 +114,48 @@ def test_mcdsts_make_movie_jpeg6(self, mcdsts=mcdsts): shutil.rmtree(s_opath) +class TestTimeSeries2dNeuroglancer(object): + ''' tests for loading a pcdl.TimeSeries data set. ''' + mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) + + ## make_gif and magick ommand ## + def test_mcdsts_render_neuroglancer_default(self, mcdsts=mcdsts): + s_tiffpathfile = mcdsts.make_ome_tiff() + o_viewer = mcdsts.render_neuroglancer( + tiffpathfile = s_tiffpathfile, + #timestep = 0, + #intensity_cmap='gray', + ) + assert(str(type(o_viewer)) == "") and \ + (str(o_viewer).startswith('http://127.0.0.1:')) + os.remove(s_tiffpathfile) + + def test_mcdsts_render_neuroglancer_timestep(self, mcdsts=mcdsts): + s_tiffpathfile = mcdsts.make_ome_tiff() + o_viewer = mcdsts.render_neuroglancer( + tiffpathfile = s_tiffpathfile, + timestep = 12, + intensity_cmap='gray', + ) + assert(str(type(o_viewer)) == "") and \ + (str(o_viewer).startswith('http://127.0.0.1:')) + os.remove(s_tiffpathfile) + + def test_mcdsts_render_neuroglancer_cmap(self, mcdsts=mcdsts): + s_tiffpathfile = mcdsts.make_ome_tiff() + o_viewer = mcdsts.render_neuroglancer( + tiffpathfile = s_tiffpathfile, + timestep = 0, + intensity_cmap='magma', + ) + assert(str(type(o_viewer)) == "") and \ + (str(o_viewer).startswith('http://127.0.0.1:')) + os.remove(s_tiffpathfile) + + ## data loading related functions ## -class TestTimeSeriesInit(object): +class TestTimeSeries2dInit(object): ''' tests for loading a pcdl.TimeSeries data set. ''' def test_mcdsts_custom_data_astype(self): @@ -137,6 +176,10 @@ def test_mcdsts_set_verbose_false(self): assert(str(type(mcdsts)) == "") and \ (not mcdsts.verbose) + # test_make_gif double command + # test_make_movie double command + # test_render_neuroglancer double command + ## get_xmlfile and read_mcds command and get_mcds_list ## def test_mcdsts_get_xmlfile_list(self): mcdsts = pcdl.TimeSeries(s_path_2d, load=False, verbose=True) @@ -146,28 +189,29 @@ def test_mcdsts_get_xmlfile_list(self): (ls_xmlfile[-1] == 'output00000024.xml') and \ (len(ls_xmlfile) == 25) - def test_mcdsts_get_mcds_list(self): - mcdsts = pcdl.TimeSeries(s_path_2d, load=True, verbose=True) - l_mcds = mcdsts.get_mcds_list() + def test_mcdsts_read_mcds(self): + mcdsts = pcdl.TimeSeries(s_path_2d, load=False, verbose=True) + l_mcds_loadfalse = mcdsts.get_mcds_list() + mcdsts.read_mcds() assert(str(type(mcdsts)) == "") and \ (str(type(mcdsts.l_mcds[0])) == "") and \ (str(type(mcdsts.l_mcds[-1])) == "") and \ (mcdsts.l_mcds[0].get_time() == 0) and \ (mcdsts.l_mcds[-1].get_time() == 1440) and \ (len(mcdsts.l_mcds) == 25) and \ - (mcdsts.l_mcds == l_mcds) + (l_mcds_loadfalse is None) - def test_mcdsts_read_mcds(self): - mcdsts = pcdl.TimeSeries(s_path_2d, load=False, verbose=True) - l_mcds_loadfalse = mcdsts.get_mcds_list() - mcdsts.read_mcds() + def test_mcdsts_get_mcds_list(self): + mcdsts = pcdl.TimeSeries(s_path_2d, load=True, verbose=True) + l_mcds = mcdsts.get_mcds_list() assert(str(type(mcdsts)) == "") and \ (str(type(mcdsts.l_mcds[0])) == "") and \ (str(type(mcdsts.l_mcds[-1])) == "") and \ (mcdsts.l_mcds[0].get_time() == 0) and \ (mcdsts.l_mcds[-1].get_time() == 1440) and \ (len(mcdsts.l_mcds) == 25) and \ - (l_mcds_loadfalse is None) + (mcdsts.l_mcds == l_mcds) + def test_mcdsts_read_mcds_xmlfilelist(self): mcdsts = pcdl.TimeSeries(s_path_2d, load=False, verbose=True) @@ -192,7 +236,7 @@ def test_mcdsts_form_list_of_mcds(self): ## micro environment related functions ## -class TestTimeSeriesMicroenv(object): +class TestTimeSeries2dMicroenv(object): ''' tests for pcdl.TimeSeriesmicro environment related functions. ''' mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) @@ -313,7 +357,7 @@ def test_mcdsts_make_conc_vtk(self, mcdsts=mcdsts): ## cell related functions ## -class TestTimeSeriesCell(object): +class TestTimeSeries2dCell(object): ''' tests for pcdl.TimeSeries cell related functions. ''' mcdsts = pcdl.TimeSeries(s_path_2d, verbose=False) @@ -454,9 +498,99 @@ def test_mcdsts_make_cell_vtk(self, mcdsts=mcdsts): os.remove(s_pathfile) +## anndata time series related functions ## + +class TestTimeSeries2dAnnData(object): + ''' test for pcdl.TestSeries class. ''' + + # get_anndata + # get_annmcds_list {integrated} + # value {1, _2_} + # collaps {True, _False_} + # keep_mcds {True, _False_} + + ## get_anndata command ## + def test_mcdsts_get_anndata(self): + mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) + ann = mcdsts.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=True) + l_annmcds = mcdsts.get_annmcds_list() + assert(str(type(mcdsts)) == "") and \ + (len(mcdsts.l_mcds) == 25) and \ + (l_annmcds == mcdsts.l_annmcds) and \ + (mcdsts.l_annmcds is None) and \ + (str(type(ann)) == "") and \ + (ann.X.shape[0] > 9) and \ + (ann.X.shape[1] == 105) and \ + (ann.obs.shape[0] > 9) and \ + (ann.obs.shape[1] == 8) and \ + (ann.obsm['spatial'].shape[0] > 9) and \ + (ann.obsm['spatial'].shape[1] == 2) and \ + (len(ann.obsp) == 0) and \ + (ann.var.shape == (105, 0)) and \ + (len(ann.uns) == 0) + + def test_mcdsts_get_anndata_value(self): + mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) + ann = mcdsts.get_anndata(values=2, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=True) + l_annmcds = mcdsts.get_annmcds_list() + assert(str(type(mcdsts)) == "") and \ + (len(mcdsts.l_mcds) == 25) and \ + (l_annmcds == mcdsts.l_annmcds) and \ + (mcdsts.l_annmcds is None) and \ + (str(type(ann)) == "") and \ + (ann.X.shape[0] > 9) and \ + (ann.X.shape[1] == 50) and \ + (ann.obs.shape[0] > 9) and \ + (ann.obs.shape[1] == 7) and \ + (ann.obsm['spatial'].shape[0] > 9) and \ + (ann.obsm['spatial'].shape[1] == 2) and \ + (len(ann.obsp) == 0) and \ + (ann.var.shape == (50, 0)) and \ + (len(ann.uns) == 0) + + def test_mcdsts_get_anndata_collapsefalse(self): + mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) + ann = mcdsts.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs', collapse=False, keep_mcds=True) + l_annmcds = mcdsts.get_annmcds_list() + assert(str(type(mcdsts)) == "") and \ + (len(mcdsts.l_mcds) == 25) and \ + (l_annmcds == mcdsts.l_annmcds) and \ + (str(type(mcdsts.l_annmcds)) == "") and \ + (len(mcdsts.l_annmcds) == 25) and \ + (all([str(type(ann)) == "" for ann in mcdsts.l_annmcds])) and \ + (mcdsts.l_annmcds[24].X.shape[0] > 9) and \ + (mcdsts.l_annmcds[24].X.shape[1] == 105) and \ + (mcdsts.l_annmcds[24].obs.shape[0] > 9) and \ + (mcdsts.l_annmcds[24].obs.shape[1] == 8) and \ + (mcdsts.l_annmcds[24].obsm['spatial'].shape[0] > 9) and \ + (mcdsts.l_annmcds[24].obsm['spatial'].shape[1] == 2) and \ + (len(mcdsts.l_annmcds[24].obsp) == 4) and \ + (mcdsts.l_annmcds[24].var.shape == (105, 0)) and \ + (len(mcdsts.l_annmcds[24].uns) == 2) + + def test_mcdsts_get_anndata_keepmcdsfalse(self): + mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) + ann = mcdsts.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=False) + l_annmcds = mcdsts.get_annmcds_list() + assert(str(type(mcdsts)) == "") and \ + (len(mcdsts.l_mcds) == 0) and \ + (l_annmcds == mcdsts.l_annmcds) and \ + (mcdsts.l_annmcds is None) and \ + (str(type(ann)) == "") and \ + (ann.X.shape[0] > 9) and \ + (ann.X.shape[1] == 105) and \ + (ann.obs.shape[0] > 9) and \ + (ann.obs.shape[1] == 8) and \ + (ann.obsm['spatial'].shape[0] > 9) and \ + (ann.obsm['spatial'].shape[1] == 2) and \ + (len(ann.obsp) == 0) and \ + (ann.var.shape == (105, 0)) and \ + (len(ann.uns) == 0) + + ## graph related functions ## -class TestTimeSeriesGraph(object): +class TestTimeSeries2dGraph(object): ''' tests for pcdl.TimeSeries graph related functions. ''' mcdsts = pcdl.TimeSeries(s_path_2d, verbose=False) @@ -495,96 +629,113 @@ def test_mcdsts_get_graph_gml_neighbor_allattr(self, mcdsts=mcdsts): os.remove(s_pathfile) -## ome tiff related functions ## +## simularium time series related functions ## -class TestTimeSeriesOmeTiff(object): - ''' tests for pcdl.TimeSeries ome tiff related functions. ''' - mcdsts = pcdl.TimeSeries(s_path_2d, verbose=False) +class TestTimeSeries2dSimularium(object): + ''' test for pcdl.TimeSeries class. ''' - ## ome tiff related functions ## - def test_mcdsts_make_ome_tiff_defaultattr_00(self, mcdsts=mcdsts): - la_ometiff = mcdsts.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=False, collapse=False) + ## get_simularium command ## + def test_mcdsts_get_simularium_default(self): + import simulariumio as sim + mcdsts = pcdl.TimeSeries(s_path_2d, verbose=False) + s_pathfile = mcdsts.make_simularium(focus_cat=['cell_type','current_phase'], trajectory_title='timeseries', scale_factor=None, camera_defaults=None, model_meta_data=None) assert(str(type(mcdsts)) == "") and \ - (type(la_ometiff) is list) and \ - (type(la_ometiff[0]) is np.ndarray) and \ - (type(la_ometiff[-1]) is np.ndarray) and \ - (la_ometiff[0].dtype == np.float32) and \ - (la_ometiff[-1].dtype == np.float32) and \ - (la_ometiff[0].shape == (4, 1, 200, 300)) and \ - (la_ometiff[-1].shape == (4, 1, 200, 300)) and \ - (len(la_ometiff) == 25) + (s_pathfile.endswith('/pcdl/output_2d/timeseries.simularium')) and \ + (os.path.exists(s_pathfile)) + os.remove(s_pathfile) - def test_mcdsts_make_ome_tiff_defaultattr_01(self, mcdsts=mcdsts): - a_ometiff = mcdsts.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=False, collapse=True) + def test_mcdsts_get_simularium_nondefault(self): + import simulariumio as sim + mcdsts = pcdl.TimeSeries(s_path_2d, verbose=False) + s_pathfile = mcdsts.make_simularium(focus_cat=['current_phase', 'dead'], trajectory_title='zeitreihe', scale_factor=0.1, camera_defaults=sim.CameraData(), model_meta_data=sim.ModelMetaData()) assert(str(type(mcdsts)) == "") and \ - (type(a_ometiff) is np.ndarray) and \ - (a_ometiff.dtype == np.float32) and \ - (a_ometiff.shape == (25, 4, 1, 200, 300)) + (s_pathfile.endswith('/pcdl/output_2d/zeitreihe.simularium')) and \ + (os.path.exists(s_pathfile)) + os.remove(s_pathfile) - def test_mcdsts_make_ome_tiff_defaultattr_10(self, mcdsts=mcdsts): - ls_pathfile = mcdsts.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=True, collapse=False) - assert(str(type(mcdsts)) == "") and \ - (ls_pathfile[0].endswith('pcdl/output_2d/output00000000_oxygen_water_default_blood_cells_ID.ome.tiff')) and \ - (ls_pathfile[-1].endswith('pcdl/output_2d/output00000024_oxygen_water_default_blood_cells_ID.ome.tiff')) and \ - (os.path.exists(ls_pathfile[0])) and \ - (os.path.exists(ls_pathfile[-1])) and \ - (os.path.getsize(ls_pathfile[0]) > 2**10) and\ - (os.path.getsize(ls_pathfile[-1]) > 2**10) and\ - (len(ls_pathfile) == 25) - for s_pathfile in ls_pathfile: - os.remove(s_pathfile) - def test_mcdsts_make_ome_tiff_defaultattr_11(self, mcdsts=mcdsts): - s_pathfile = mcdsts.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=True, collapse=True) - assert(str(type(mcdsts)) == "") and \ - (s_pathfile.endswith('pcdl/output_2d/timeseries_oxygen_water_default_blood_cells_ID.ome.tiff')) and \ - (os.path.exists(s_pathfile)) and \ - (os.path.getsize(s_pathfile) > 2**10 ) - os.remove(s_pathfile) +## muspan time series related functions ## +class TestTimeSeries2dMuspan(object): + ''' test for pcdl.TimeSeries class. ''' + ## get_muspan command ## + def test_mcdsts_get_muspan_default(self): + try: + import muspan as ms + mcdsts = pcdl.TimeSeries(s_path_2d, verbose=False) + do_domain = mcdsts.get_muspan(z_slice=None, values=1, drop=set(), keep=set()) + s_key = sorted(do_domain.keys())[-1] + assert(str(type(mcdsts)) == "") and \ + (type(do_domain) == dict) and \ + (len(do_domain) == 25) and \ + (str(type(do_domain[s_key])) == "") and \ + (len(do_domain[s_key].collections) == 2) and \ + (len(do_domain[s_key].networks) == 3) and \ + (len(do_domain[s_key].objects) > 9) + except ModuleNotFoundError: + print('Warning @ pytest TestTimeSeries2dMuspan : muspan module not installed.') + assert True + except SystemExit: + print('Warning @ pytest TestTimeSeries2dMuspan : muspan module not installed.') + assert True -class TestTimeSeriesNeuroglancer(object): - ''' tests for loading a pcdl.TimeSeries data set. ''' - mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) + def test_mcdsts_get_muspan_zslice(self): + try: + import muspan as ms + mcdsts = pcdl.TimeSeries(s_path_2d, verbose=False) + do_domain = mcdsts.get_muspan(z_slice=0.0, values=1, drop=set(), keep=set()) + s_key = sorted(do_domain.keys())[-1] + assert(str(type(mcdsts)) == "") and \ + (type(do_domain) == dict) and \ + (len(do_domain) == 25) and \ + (str(type(do_domain[s_key])) == "") and \ + (len(do_domain[s_key].collections) == 2) and \ + (len(do_domain[s_key].networks) == 3) and \ + (len(do_domain[s_key].objects) > 9) + except ModuleNotFoundError: + print('Warning @ pytest TestTimeSeries2dMuspan : muspan module not installed.') + assert True + except SystemExit: + print('Warning @ pytest TestTimeSeries2dMuspan : muspan module not installed.') + assert True - ## make_gif and magick ommand ## - def test_mcdsts_render_neuroglancer_default(self, mcdsts=mcdsts): - s_tiffpathfile = mcdsts.make_ome_tiff() - o_viewer = mcdsts.render_neuroglancer( - tiffpathfile = s_tiffpathfile, - #timestep = 0, - #intensity_cmap='gray', - ) - assert(str(type(o_viewer)) == "") and \ - (str(o_viewer).startswith('http://127.0.0.1:')) - os.remove(s_tiffpathfile) - def test_mcdsts_render_neuroglancer_timestep(self, mcdsts=mcdsts): - s_tiffpathfile = mcdsts.make_ome_tiff() - o_viewer = mcdsts.render_neuroglancer( - tiffpathfile = s_tiffpathfile, - timestep = 12, - intensity_cmap='gray', - ) - assert(str(type(o_viewer)) == "") and \ - (str(o_viewer).startswith('http://127.0.0.1:')) - os.remove(s_tiffpathfile) +## spatialdata time seris related functions ## - def test_mcdsts_render_neuroglancer_cmap(self, mcdsts=mcdsts): - s_tiffpathfile = mcdsts.make_ome_tiff() - o_viewer = mcdsts.render_neuroglancer( - tiffpathfile = s_tiffpathfile, - timestep = 0, - intensity_cmap='magma', - ) - assert(str(type(o_viewer)) == "") and \ - (str(o_viewer).startswith('http://127.0.0.1:')) - os.remove(s_tiffpathfile) +class TestTimeSeries2dSpatialData(object): + ''' test for pcdl.TestSeries class. ''' + + # get_sdmcds_list {integrated} + # get_cell_attributes ok + # get_get_spatialdata ok + # keep_mcds {True, _False_} + + def test_mcdsts_get_spatialdata_default(self): + mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) + lo_sdmcds_output = mcdsts.get_spatialdata(images={'subs'}, labels=set(), points={'subs'}, shapes={'cell'}, values=1, drop=set(), keep=set(), scale='maxabs', keep_mcds=True) + lo_sdmcds_memory = mcdsts.get_sdmcds_list() + assert(str(type(mcdsts)) == "") and \ + (len(mcdsts.l_mcds) == 25) and \ + (len(mcdsts.l_sdmcds) == 25) and \ + (lo_sdmcds_output == mcdsts.l_sdmcds) and \ + (lo_sdmcds_output == lo_sdmcds_memory) and \ + (str(type(lo_sdmcds_output[8])) == "") + + def test_mcdsts_get_spatialdata_keepmcdsfalse(self): + mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) + lo_sdmcds_output = mcdsts.get_spatialdata(images={'subs'}, labels=set(), points={'subs'}, shapes={'cell'}, values=1, drop=set(), keep=set(), scale='maxabs', keep_mcds=False) + lo_sdmcds_memory = mcdsts.get_sdmcds_list() + assert(str(type(mcdsts)) == "") and \ + (len(mcdsts.l_mcds) == 0) and \ + (len(mcdsts.l_sdmcds) == 25) and \ + (lo_sdmcds_output == mcdsts.l_sdmcds) and \ + (lo_sdmcds_output == lo_sdmcds_memory) and \ + (str(type(lo_sdmcds_output[8])) == "") ## time series related functions ## -class TestTimeSeriesTimeseries(object): +class TestTimeSeries2dTimeseries(object): ''' tests for pcdl.TimeSeries graph related functions. ''' mcdsts = pcdl.TimeSeries(s_path_2d, verbose=False) @@ -917,193 +1068,49 @@ def test_mcdsts_plot_timeseries_none_none_yunit_conc_fig(self, mcdsts=mcdsts): plt.close() -## anndata time series related functions ## - -class TestTimeSeriesAnnData(object): - ''' test for pcdl.TestSeries class. ''' - - # get_anndata - # get_annmcds_list {integrated} - # value {1, _2_} - # collaps {True, _False_} - # keep_mcds {True, _False_} - - ## get_anndata command ## - def test_mcdsts_get_anndata(self): - mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) - ann = mcdsts.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=True) - l_annmcds = mcdsts.get_annmcds_list() - assert(str(type(mcdsts)) == "") and \ - (len(mcdsts.l_mcds) == 25) and \ - (l_annmcds == mcdsts.l_annmcds) and \ - (mcdsts.l_annmcds is None) and \ - (str(type(ann)) == "") and \ - (ann.X.shape[0] > 9) and \ - (ann.X.shape[1] == 105) and \ - (ann.obs.shape[0] > 9) and \ - (ann.obs.shape[1] == 8) and \ - (ann.obsm['spatial'].shape[0] > 9) and \ - (ann.obsm['spatial'].shape[1] == 2) and \ - (len(ann.obsp) == 0) and \ - (ann.var.shape == (105, 0)) and \ - (len(ann.uns) == 0) - - def test_mcdsts_get_anndata_value(self): - mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) - ann = mcdsts.get_anndata(values=2, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=True) - l_annmcds = mcdsts.get_annmcds_list() - assert(str(type(mcdsts)) == "") and \ - (len(mcdsts.l_mcds) == 25) and \ - (l_annmcds == mcdsts.l_annmcds) and \ - (mcdsts.l_annmcds is None) and \ - (str(type(ann)) == "") and \ - (ann.X.shape[0] > 9) and \ - (ann.X.shape[1] == 50) and \ - (ann.obs.shape[0] > 9) and \ - (ann.obs.shape[1] == 7) and \ - (ann.obsm['spatial'].shape[0] > 9) and \ - (ann.obsm['spatial'].shape[1] == 2) and \ - (len(ann.obsp) == 0) and \ - (ann.var.shape == (50, 0)) and \ - (len(ann.uns) == 0) - - def test_mcdsts_get_anndata_collapsefalse(self): - mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) - ann = mcdsts.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs', collapse=False, keep_mcds=True) - l_annmcds = mcdsts.get_annmcds_list() - assert(str(type(mcdsts)) == "") and \ - (len(mcdsts.l_mcds) == 25) and \ - (l_annmcds == mcdsts.l_annmcds) and \ - (str(type(mcdsts.l_annmcds)) == "") and \ - (len(mcdsts.l_annmcds) == 25) and \ - (all([str(type(ann)) == "" for ann in mcdsts.l_annmcds])) and \ - (mcdsts.l_annmcds[24].X.shape[0] > 9) and \ - (mcdsts.l_annmcds[24].X.shape[1] == 105) and \ - (mcdsts.l_annmcds[24].obs.shape[0] > 9) and \ - (mcdsts.l_annmcds[24].obs.shape[1] == 8) and \ - (mcdsts.l_annmcds[24].obsm['spatial'].shape[0] > 9) and \ - (mcdsts.l_annmcds[24].obsm['spatial'].shape[1] == 2) and \ - (len(mcdsts.l_annmcds[24].obsp) == 4) and \ - (mcdsts.l_annmcds[24].var.shape == (105, 0)) and \ - (len(mcdsts.l_annmcds[24].uns) == 2) - - def test_mcdsts_get_anndata_keepmcdsfalse(self): - mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) - ann = mcdsts.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=False) - l_annmcds = mcdsts.get_annmcds_list() - assert(str(type(mcdsts)) == "") and \ - (len(mcdsts.l_mcds) == 0) and \ - (l_annmcds == mcdsts.l_annmcds) and \ - (mcdsts.l_annmcds is None) and \ - (str(type(ann)) == "") and \ - (ann.X.shape[0] > 9) and \ - (ann.X.shape[1] == 105) and \ - (ann.obs.shape[0] > 9) and \ - (ann.obs.shape[1] == 8) and \ - (ann.obsm['spatial'].shape[0] > 9) and \ - (ann.obsm['spatial'].shape[1] == 2) and \ - (len(ann.obsp) == 0) and \ - (ann.var.shape == (105, 0)) and \ - (len(ann.uns) == 0) - - -## muspan time series related functions ## -class TestTimeSeriesMuspan(object): - ''' test for pcdl.TimeSeries class. ''' - - ## get_muspan command ## - def test_mcdsts_get_muspan_default(self): - try: - import muspan as ms - mcdsts = pcdl.TimeSeries(s_path_2d, verbose=False) - do_domain = mcdsts.get_muspan(z_slice=None, values=1, drop=set(), keep=set()) - s_key = sorted(do_domain.keys())[-1] - assert(str(type(mcdsts)) == "") and \ - (type(do_domain) == dict) and \ - (len(do_domain) == 25) and \ - (str(type(do_domain[s_key])) == "") and \ - (len(do_domain[s_key].collections) == 2) and \ - (len(do_domain[s_key].networks) == 3) and \ - (len(do_domain[s_key].objects) > 9) - except ModuleNotFoundError: - print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') - assert True - except SystemExit: - print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') - assert True - - def test_mcdsts_get_muspan_zslice(self): - try: - import muspan as ms - mcdsts = pcdl.TimeSeries(s_path_2d, verbose=False) - do_domain = mcdsts.get_muspan(z_slice=0.0, values=1, drop=set(), keep=set()) - s_key = sorted(do_domain.keys())[-1] - assert(str(type(mcdsts)) == "") and \ - (type(do_domain) == dict) and \ - (len(do_domain) == 25) and \ - (str(type(do_domain[s_key])) == "") and \ - (len(do_domain[s_key].collections) == 2) and \ - (len(do_domain[s_key].networks) == 3) and \ - (len(do_domain[s_key].objects) > 9) - except ModuleNotFoundError: - print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') - assert True - except SystemExit: - print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') - assert True - - -## spatialdata time seris related functions ## -class TestTimeSeriesSpatialData(object): - ''' test for pcdl.TestSeries class. ''' +## ome tiff related functions ## - # get_sdmcds_list {integrated} - # get_cell_attributes ok - # get_get_spatialdata ok - # keep_mcds {True, _False_} +class TestTimeSeries2dOmeTiff(object): + ''' tests for pcdl.TimeSeries ome tiff related functions. ''' + mcdsts = pcdl.TimeSeries(s_path_2d, verbose=False) - def test_mcdsts_get_spatialdata_default(self): - mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) - lo_sdmcds_output = mcdsts.get_spatialdata(images={'subs'}, labels=set(), points={'subs'}, shapes={'cell'}, values=1, drop=set(), keep=set(), scale='maxabs', keep_mcds=True) - lo_sdmcds_memory = mcdsts.get_sdmcds_list() + ## ome tiff related functions ## + def test_mcdsts_make_ome_tiff_defaultattr_00(self, mcdsts=mcdsts): + la_ometiff = mcdsts.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=False, collapse=False) assert(str(type(mcdsts)) == "") and \ - (len(mcdsts.l_mcds) == 25) and \ - (len(mcdsts.l_sdmcds) == 25) and \ - (lo_sdmcds_output == mcdsts.l_sdmcds) and \ - (lo_sdmcds_output == lo_sdmcds_memory) and \ - (str(type(lo_sdmcds_output[8])) == "") + (type(la_ometiff) is list) and \ + (type(la_ometiff[0]) is np.ndarray) and \ + (type(la_ometiff[-1]) is np.ndarray) and \ + (la_ometiff[0].dtype == np.float32) and \ + (la_ometiff[-1].dtype == np.float32) and \ + (la_ometiff[0].shape == (4, 1, 200, 300)) and \ + (la_ometiff[-1].shape == (4, 1, 200, 300)) and \ + (len(la_ometiff) == 25) - def test_mcdsts_get_spatialdata_keepmcdsfalse(self): - mcdsts = pcdl.TimeSeries(s_path_2d, verbose=True) - lo_sdmcds_output = mcdsts.get_spatialdata(images={'subs'}, labels=set(), points={'subs'}, shapes={'cell'}, values=1, drop=set(), keep=set(), scale='maxabs', keep_mcds=False) - lo_sdmcds_memory = mcdsts.get_sdmcds_list() + def test_mcdsts_make_ome_tiff_defaultattr_01(self, mcdsts=mcdsts): + a_ometiff = mcdsts.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=False, collapse=True) assert(str(type(mcdsts)) == "") and \ - (len(mcdsts.l_mcds) == 0) and \ - (len(mcdsts.l_sdmcds) == 25) and \ - (lo_sdmcds_output == mcdsts.l_sdmcds) and \ - (lo_sdmcds_output == lo_sdmcds_memory) and \ - (str(type(lo_sdmcds_output[8])) == "") - - -## simularium time series related functions ## -class TestTimeSeriesSimularium(object): - ''' test for pcdl.TimeSeries class. ''' + (type(a_ometiff) is np.ndarray) and \ + (a_ometiff.dtype == np.float32) and \ + (a_ometiff.shape == (25, 4, 1, 200, 300)) - ## get_simularium command ## - def test_mcdsts_get_simularium_default(self): - import simulariumio as sim - mcdsts = pcdl.TimeSeries(s_path_2d, verbose=False) - s_pathfile = mcdsts.make_simularium(focus_cat=['cell_type','current_phase'], trajectory_title='timeseries', scale_factor=None, camera_defaults=None, model_meta_data=None) + def test_mcdsts_make_ome_tiff_defaultattr_10(self, mcdsts=mcdsts): + ls_pathfile = mcdsts.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=True, collapse=False) assert(str(type(mcdsts)) == "") and \ - (s_pathfile.endswith('/pcdl/output_2d/timeseries.simularium')) and \ - (os.path.exists(s_pathfile)) - os.remove(s_pathfile) + (ls_pathfile[0].endswith('pcdl/output_2d/output00000000_oxygen_water_default_blood_cells_ID.ome.tiff')) and \ + (ls_pathfile[-1].endswith('pcdl/output_2d/output00000024_oxygen_water_default_blood_cells_ID.ome.tiff')) and \ + (os.path.exists(ls_pathfile[0])) and \ + (os.path.exists(ls_pathfile[-1])) and \ + (os.path.getsize(ls_pathfile[0]) > 2**10) and\ + (os.path.getsize(ls_pathfile[-1]) > 2**10) and\ + (len(ls_pathfile) == 25) + for s_pathfile in ls_pathfile: + os.remove(s_pathfile) - def test_mcdsts_get_simularium_nondefault(self): - import simulariumio as sim - mcdsts = pcdl.TimeSeries(s_path_2d, verbose=False) - s_pathfile = mcdsts.make_simularium(focus_cat=['current_phase', 'dead'], trajectory_title='zeitreihe', scale_factor=0.1, camera_defaults=sim.CameraData(), model_meta_data=sim.ModelMetaData()) + def test_mcdsts_make_ome_tiff_defaultattr_11(self, mcdsts=mcdsts): + s_pathfile = mcdsts.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=True, collapse=True) assert(str(type(mcdsts)) == "") and \ - (s_pathfile.endswith('/pcdl/output_2d/zeitreihe.simularium')) and \ - (os.path.exists(s_pathfile)) + (s_pathfile.endswith('pcdl/output_2d/timeseries_oxygen_water_default_blood_cells_ID.ome.tiff')) and \ + (os.path.exists(s_pathfile)) and \ + (os.path.getsize(s_pathfile) > 2**10 ) os.remove(s_pathfile) diff --git a/test/test_timeseries_3d.py b/test/test_timeseries_3d.py index 94801e4..73bdb8d 100644 --- a/test/test_timeseries_3d.py +++ b/test/test_timeseries_3d.py @@ -380,7 +380,97 @@ def test_mcdsts_make_cell_vtk(self, mcdsts=mcdsts): os.remove(s_pathfile) +## anndata time series related functions ## +class TestTimeSeries3dAnnData(object): + ''' test for pcdl.TestSeries class. ''' + + # get_anndata + # get_annmcds_list {integrated} + # value {1, _2_} + # collaps {True, _False_} + # keep_mcds {True, _False_} + + ## get_anndata command ## + def test_mcdsts_get_anndata(self): + mcdsts = pcdl.TimeSeries(s_path_3d, verbose=True) + ann = mcdsts.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=True) + l_annmcds = mcdsts.get_annmcds_list() + assert(str(type(mcdsts)) == "") and \ + (len(mcdsts.l_mcds) == 25) and \ + (l_annmcds == mcdsts.l_annmcds) and \ + (mcdsts.l_annmcds is None) and \ + (str(type(ann)) == "") and \ + (ann.X.shape[0] > 9) and \ + (ann.X.shape[1] == 105) and \ + (ann.obs.shape[0] > 9) and \ + (ann.obs.shape[1] == 8) and \ + (ann.obsm['spatial'].shape[0] > 9) and \ + (ann.obsm['spatial'].shape[1] == 3) and \ + (len(ann.obsp) == 0) and \ + (ann.var.shape == (105, 0)) and \ + (len(ann.uns) == 0) + + def test_mcdsts_get_anndata_value(self): + mcdsts = pcdl.TimeSeries(s_path_3d, verbose=True) + ann = mcdsts.get_anndata(values=2, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=True) + l_annmcds = mcdsts.get_annmcds_list() + assert(str(type(mcdsts)) == "") and \ + (len(mcdsts.l_mcds) == 25) and \ + (l_annmcds == mcdsts.l_annmcds) and \ + (mcdsts.l_annmcds is None) and \ + (str(type(ann)) == "") and \ + (ann.X.shape[0] > 9) and \ + (ann.X.shape[1] == 56) and \ + (ann.obs.shape[0] > 9) and \ + (ann.obs.shape[1] == 7) and \ + (ann.obsm['spatial'].shape[0] > 9) and \ + (ann.obsm['spatial'].shape[1] == 3) and \ + (len(ann.obsp) == 0) and \ + (ann.var.shape == (56, 0)) and \ + (len(ann.uns) == 0) + + def test_mcdsts_get_anndata_collapsefalse(self): + mcdsts = pcdl.TimeSeries(s_path_3d, verbose=True) + ann = mcdsts.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs', collapse=False, keep_mcds=True) + l_annmcds = mcdsts.get_annmcds_list() + assert(str(type(mcdsts)) == "") and \ + (len(mcdsts.l_mcds) == 25) and \ + (l_annmcds == mcdsts.l_annmcds) and \ + (str(type(mcdsts.l_annmcds)) == "") and \ + (len(mcdsts.l_annmcds) == 25) and \ + (all([str(type(ann)) == "" for ann in mcdsts.l_annmcds])) and \ + (mcdsts.l_annmcds[24].X.shape[0] > 9) and \ + (mcdsts.l_annmcds[24].X.shape[1] == 105) and \ + (mcdsts.l_annmcds[24].obs.shape[0] > 9) and \ + (mcdsts.l_annmcds[24].obs.shape[1] == 8) and \ + (mcdsts.l_annmcds[24].obsm['spatial'].shape[0] > 9) and \ + (mcdsts.l_annmcds[24].obsm['spatial'].shape[1] == 3) and \ + (len(mcdsts.l_annmcds[24].obsp) == 4) and \ + (mcdsts.l_annmcds[24].var.shape == (105, 0)) and \ + (len(mcdsts.l_annmcds[24].uns) == 2) + + def test_mcdsts_get_anndata_keepmcdsfalse(self): + mcdsts = pcdl.TimeSeries(s_path_3d, verbose=True) + ann = mcdsts.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=False) + l_annmcds = mcdsts.get_annmcds_list() + assert(str(type(mcdsts)) == "") and \ + (len(mcdsts.l_mcds) == 0) and \ + (l_annmcds == mcdsts.l_annmcds) and \ + (mcdsts.l_annmcds is None) and \ + (str(type(ann)) == "") and \ + (ann.X.shape[0] > 9) and \ + (ann.X.shape[1] == 105) and \ + (ann.obs.shape[0] > 9) and \ + (ann.obs.shape[1] == 8) and \ + (ann.obsm['spatial'].shape[0] > 9) and \ + (ann.obsm['spatial'].shape[1] == 3) and \ + (len(ann.obsp) == 0) and \ + (ann.var.shape == (105, 0)) and \ + (len(ann.uns) == 0) + + ## graph related functions ## + class TestTimeSeries3dGraph(object): ''' tests for pcdl.TimeStep graph related functions. ''' mcdsts = pcdl.TimeSeries(s_path_3d, verbose=False) @@ -419,30 +509,109 @@ def test_mcdsts_get_graph_gml_neighbor_allattr(self, mcdsts=mcdsts): for s_pathfile in ls_pathfile: os.remove(s_pathfile) -## ome.tiff related functions ## -class TestTimeSeries3dOmeTiff(object): - ''' tests for pcdl.TimeStep graph related functions. ''' - mcdsts = pcdl.TimeSeries(s_path_3d, verbose=False) - ## graph related functions ## - def test_mcdsts_make_ome_tiff_defaultattr_00(self, mcdsts=mcdsts): - la_ometiff = mcdsts.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=False, collapse=False) +## simularium time series related functions ## + +class TestTimeSeries3dSimularium(object): + ''' test for pcdl.TimeSeries class. ''' + + ## get_simularium command ## + def test_mcdsts_get_simularium_default(self): + import simulariumio as sim + mcdsts = pcdl.TimeSeries(s_path_3d, verbose=False) + s_pathfile = mcdsts.make_simularium(focus_cat=['cell_type','current_phase'], trajectory_title='timeseries', scale_factor=None, camera_defaults=None, model_meta_data=None) assert(str(type(mcdsts)) == "") and \ - (type(la_ometiff) is list) and \ - (type(la_ometiff[0]) is np.ndarray) and \ - (type(la_ometiff[-1]) is np.ndarray) and \ - (la_ometiff[0].dtype == np.float32) and \ - (la_ometiff[-1].dtype == np.float32) and \ - (la_ometiff[0].shape == (4, 11, 200, 300)) and \ - (la_ometiff[-1].shape == (4, 11, 200, 300)) and \ - (len(la_ometiff) == 25) + (s_pathfile.endswith('/pcdl/output_3d/timeseries.simularium')) and \ + (os.path.exists(s_pathfile)) + os.remove(s_pathfile) - def test_mcdsts_make_ome_tiff_defaultattr_01(self, mcdsts=mcdsts): - a_ometiff = mcdsts.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=False, collapse=True) + def test_mcdsts_get_simularium_nondefault(self): + import simulariumio as sim + mcdsts = pcdl.TimeSeries(s_path_3d, verbose=False) + s_pathfile = mcdsts.make_simularium(focus_cat=['current_phase', 'dead'], trajectory_title='zeitreihe', scale_factor=0.1, camera_defaults=sim.CameraData(), model_meta_data=sim.ModelMetaData()) assert(str(type(mcdsts)) == "") and \ - (type(a_ometiff) is np.ndarray) and \ - (a_ometiff.dtype == np.float32) and \ - (a_ometiff.shape == (25, 4, 11, 200, 300)) + (s_pathfile.endswith('/pcdl/output_3d/zeitreihe.simularium')) and \ + (os.path.exists(s_pathfile)) + os.remove(s_pathfile) + + +## muspan time series related functions ## + +class TestTimeSeries3dMuspan(object): + ''' test for pcdl.TimeSeries class. ''' + + ## get_muspan command ## + def test_mcdsts_get_muspan_default(self): + try: + import muspan as ms + mcdsts = pcdl.TimeSeries(s_path_3d, verbose=False) + do_domain = mcdsts.get_muspan(z_slice=None, values=1, drop=set(), keep=set()) + s_key = sorted(do_domain.keys())[-1] + assert(str(type(mcdsts)) == "") and \ + (type(do_domain) == dict) and \ + (len(do_domain) == 275) and \ + (str(type(do_domain[s_key])) == "") and \ + (len(do_domain[s_key].collections) == 2) and \ + (len(do_domain[s_key].networks) == 3) and \ + (len(do_domain[s_key].objects) > 9) + except ModuleNotFoundError: + print('Warning @ pytest TestTimeSeries3dMuspan : muspan module not installed.') + assert True + except SystemExit: + print('Warning @ pytest TestTimeSeries3dMuspan : muspan module not installed.') + assert True + + def test_mcdsts_get_muspan_zslice(self): + try: + import muspan as ms + mcdsts = pcdl.TimeSeries(s_path_3d, verbose=False) + do_domain = mcdsts.get_muspan(z_slice=0.0, values=1, drop=set(), keep=set()) + s_key = sorted(do_domain.keys())[-1] + assert(str(type(mcdsts)) == "") and \ + (type(do_domain) == dict) and \ + (len(do_domain) == 25) and \ + (str(type(do_domain[s_key])) == "") and \ + (len(do_domain[s_key].collections) == 2) and \ + (len(do_domain[s_key].networks) == 3) and \ + (len(do_domain[s_key].objects) > 9) + except ModuleNotFoundError: + print('Warning @ pytest TestTimeSeries3dMuspan : muspan module not installed.') + assert True + except SystemExit: + print('Warning @ pytest TestTimeSeries3dMuspan : muspan module not installed.') + assert True + + +## spatialdata time seris related functions ## +class TestTimeSeries3dSpatialData(object): + ''' test for pcdl.TestSeries class. ''' + + # get_sdmcds_list {integrated} + # get_cell_attributes ok + # get_get_spatialdata ok + # keep_mcds {True, _False_} + + def test_mcdsts_get_spatialdata_default(self): + mcdsts = pcdl.TimeSeries(s_path_3d, verbose=True) + lo_sdmcds_output = mcdsts.get_spatialdata(images={'subs'}, labels=set(), points={'subs'}, shapes={'cell'}, values=1, drop=set(), keep=set(), scale='maxabs', keep_mcds=True) + lo_sdmcds_memory = mcdsts.get_sdmcds_list() + assert(str(type(mcdsts)) == "") and \ + (len(mcdsts.l_mcds) == 25) and \ + (len(mcdsts.l_sdmcds) == 25) and \ + (lo_sdmcds_output == mcdsts.l_sdmcds) and \ + (lo_sdmcds_output == lo_sdmcds_memory) and \ + (str(type(lo_sdmcds_output[8])) == "") + + def test_mcdsts_get_spatialdata_keepmcdsfalse(self): + mcdsts = pcdl.TimeSeries(s_path_3d, verbose=True) + lo_sdmcds_output = mcdsts.get_spatialdata(images={'subs'}, labels=set(), points={'subs'}, shapes={'cell'}, values=1, drop=set(), keep=set(), scale='maxabs', keep_mcds=False) + lo_sdmcds_memory = mcdsts.get_sdmcds_list() + assert(str(type(mcdsts)) == "") and \ + (len(mcdsts.l_mcds) == 0) and \ + (len(mcdsts.l_sdmcds) == 25) and \ + (lo_sdmcds_output == mcdsts.l_sdmcds) and \ + (lo_sdmcds_output == lo_sdmcds_memory) and \ + (str(type(lo_sdmcds_output[8])) == "") ## time series related functions ## @@ -780,192 +949,27 @@ def test_mcdsts_plot_timeseries_none_none_yunit_conc_fig(self, mcdsts=mcdsts): plt.close() -## anndata time series related functions ## -class TestTimeSeries3dAnnData(object): - ''' test for pcdl.TestSeries class. ''' - - # get_anndata - # get_annmcds_list {integrated} - # value {1, _2_} - # collaps {True, _False_} - # keep_mcds {True, _False_} - - ## get_anndata command ## - def test_mcdsts_get_anndata(self): - mcdsts = pcdl.TimeSeries(s_path_3d, verbose=True) - ann = mcdsts.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=True) - l_annmcds = mcdsts.get_annmcds_list() - assert(str(type(mcdsts)) == "") and \ - (len(mcdsts.l_mcds) == 25) and \ - (l_annmcds == mcdsts.l_annmcds) and \ - (mcdsts.l_annmcds is None) and \ - (str(type(ann)) == "") and \ - (ann.X.shape[0] > 9) and \ - (ann.X.shape[1] == 105) and \ - (ann.obs.shape[0] > 9) and \ - (ann.obs.shape[1] == 8) and \ - (ann.obsm['spatial'].shape[0] > 9) and \ - (ann.obsm['spatial'].shape[1] == 3) and \ - (len(ann.obsp) == 0) and \ - (ann.var.shape == (105, 0)) and \ - (len(ann.uns) == 0) - - def test_mcdsts_get_anndata_value(self): - mcdsts = pcdl.TimeSeries(s_path_3d, verbose=True) - ann = mcdsts.get_anndata(values=2, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=True) - l_annmcds = mcdsts.get_annmcds_list() - assert(str(type(mcdsts)) == "") and \ - (len(mcdsts.l_mcds) == 25) and \ - (l_annmcds == mcdsts.l_annmcds) and \ - (mcdsts.l_annmcds is None) and \ - (str(type(ann)) == "") and \ - (ann.X.shape[0] > 9) and \ - (ann.X.shape[1] == 56) and \ - (ann.obs.shape[0] > 9) and \ - (ann.obs.shape[1] == 7) and \ - (ann.obsm['spatial'].shape[0] > 9) and \ - (ann.obsm['spatial'].shape[1] == 3) and \ - (len(ann.obsp) == 0) and \ - (ann.var.shape == (56, 0)) and \ - (len(ann.uns) == 0) - - def test_mcdsts_get_anndata_collapsefalse(self): - mcdsts = pcdl.TimeSeries(s_path_3d, verbose=True) - ann = mcdsts.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs', collapse=False, keep_mcds=True) - l_annmcds = mcdsts.get_annmcds_list() - assert(str(type(mcdsts)) == "") and \ - (len(mcdsts.l_mcds) == 25) and \ - (l_annmcds == mcdsts.l_annmcds) and \ - (str(type(mcdsts.l_annmcds)) == "") and \ - (len(mcdsts.l_annmcds) == 25) and \ - (all([str(type(ann)) == "" for ann in mcdsts.l_annmcds])) and \ - (mcdsts.l_annmcds[24].X.shape[0] > 9) and \ - (mcdsts.l_annmcds[24].X.shape[1] == 105) and \ - (mcdsts.l_annmcds[24].obs.shape[0] > 9) and \ - (mcdsts.l_annmcds[24].obs.shape[1] == 8) and \ - (mcdsts.l_annmcds[24].obsm['spatial'].shape[0] > 9) and \ - (mcdsts.l_annmcds[24].obsm['spatial'].shape[1] == 3) and \ - (len(mcdsts.l_annmcds[24].obsp) == 4) and \ - (mcdsts.l_annmcds[24].var.shape == (105, 0)) and \ - (len(mcdsts.l_annmcds[24].uns) == 2) - - def test_mcdsts_get_anndata_keepmcdsfalse(self): - mcdsts = pcdl.TimeSeries(s_path_3d, verbose=True) - ann = mcdsts.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs', collapse=True, keep_mcds=False) - l_annmcds = mcdsts.get_annmcds_list() - assert(str(type(mcdsts)) == "") and \ - (len(mcdsts.l_mcds) == 0) and \ - (l_annmcds == mcdsts.l_annmcds) and \ - (mcdsts.l_annmcds is None) and \ - (str(type(ann)) == "") and \ - (ann.X.shape[0] > 9) and \ - (ann.X.shape[1] == 105) and \ - (ann.obs.shape[0] > 9) and \ - (ann.obs.shape[1] == 8) and \ - (ann.obsm['spatial'].shape[0] > 9) and \ - (ann.obsm['spatial'].shape[1] == 3) and \ - (len(ann.obsp) == 0) and \ - (ann.var.shape == (105, 0)) and \ - (len(ann.uns) == 0) - - -## muspan time series related functions ## -class TestTimeSeriesMuspan(object): - ''' test for pcdl.TimeSeries class. ''' - - ## get_muspan command ## - def test_mcdsts_get_muspan_default(self): - try: - import muspan as ms - mcdsts = pcdl.TimeSeries(s_path_3d, verbose=False) - do_domain = mcdsts.get_muspan(z_slice=None, values=1, drop=set(), keep=set()) - s_key = sorted(do_domain.keys())[-1] - assert(str(type(mcdsts)) == "") and \ - (type(do_domain) == dict) and \ - (len(do_domain) == 275) and \ - (str(type(do_domain[s_key])) == "") and \ - (len(do_domain[s_key].collections) == 2) and \ - (len(do_domain[s_key].networks) == 3) and \ - (len(do_domain[s_key].objects) > 9) - except ModuleNotFoundError: - print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') - assert True - except SystemExit: - print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') - assert True - - def test_mcdsts_get_muspan_zslice(self): - try: - import muspan as ms - mcdsts = pcdl.TimeSeries(s_path_3d, verbose=False) - do_domain = mcdsts.get_muspan(z_slice=0.0, values=1, drop=set(), keep=set()) - s_key = sorted(do_domain.keys())[-1] - assert(str(type(mcdsts)) == "") and \ - (type(do_domain) == dict) and \ - (len(do_domain) == 25) and \ - (str(type(do_domain[s_key])) == "") and \ - (len(do_domain[s_key].collections) == 2) and \ - (len(do_domain[s_key].networks) == 3) and \ - (len(do_domain[s_key].objects) > 9) - except ModuleNotFoundError: - print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') - assert True - except SystemExit: - print('Warning @ pytest TestTimeSeriesMuspan : muspan module not installed.') - assert True - - -## spatialdata time seris related functions ## -class TestTimeSeriesSpatialData(object): - ''' test for pcdl.TestSeries class. ''' - - # get_sdmcds_list {integrated} - # get_cell_attributes ok - # get_get_spatialdata ok - # keep_mcds {True, _False_} - - def test_mcdsts_get_spatialdata_default(self): - mcdsts = pcdl.TimeSeries(s_path_3d, verbose=True) - lo_sdmcds_output = mcdsts.get_spatialdata(images={'subs'}, labels=set(), points={'subs'}, shapes={'cell'}, values=1, drop=set(), keep=set(), scale='maxabs', keep_mcds=True) - lo_sdmcds_memory = mcdsts.get_sdmcds_list() - assert(str(type(mcdsts)) == "") and \ - (len(mcdsts.l_mcds) == 25) and \ - (len(mcdsts.l_sdmcds) == 25) and \ - (lo_sdmcds_output == mcdsts.l_sdmcds) and \ - (lo_sdmcds_output == lo_sdmcds_memory) and \ - (str(type(lo_sdmcds_output[8])) == "") - - def test_mcdsts_get_spatialdata_keepmcdsfalse(self): - mcdsts = pcdl.TimeSeries(s_path_3d, verbose=True) - lo_sdmcds_output = mcdsts.get_spatialdata(images={'subs'}, labels=set(), points={'subs'}, shapes={'cell'}, values=1, drop=set(), keep=set(), scale='maxabs', keep_mcds=False) - lo_sdmcds_memory = mcdsts.get_sdmcds_list() - assert(str(type(mcdsts)) == "") and \ - (len(mcdsts.l_mcds) == 0) and \ - (len(mcdsts.l_sdmcds) == 25) and \ - (lo_sdmcds_output == mcdsts.l_sdmcds) and \ - (lo_sdmcds_output == lo_sdmcds_memory) and \ - (str(type(lo_sdmcds_output[8])) == "") - - -## simularium time series related functions ## -class TestTimeSeriesSimularium(object): - ''' test for pcdl.TimeSeries class. ''' +## ome.tiff related functions ## +class TestTimeSeries3dOmeTiff(object): + ''' tests for pcdl.TimeStep graph related functions. ''' + mcdsts = pcdl.TimeSeries(s_path_3d, verbose=False) - ## get_simularium command ## - def test_mcdsts_get_simularium_default(self): - import simulariumio as sim - mcdsts = pcdl.TimeSeries(s_path_3d, verbose=False) - s_pathfile = mcdsts.make_simularium(focus_cat=['cell_type','current_phase'], trajectory_title='timeseries', scale_factor=None, camera_defaults=None, model_meta_data=None) + ## graph related functions ## + def test_mcdsts_make_ome_tiff_defaultattr_00(self, mcdsts=mcdsts): + la_ometiff = mcdsts.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=False, collapse=False) assert(str(type(mcdsts)) == "") and \ - (s_pathfile.endswith('/pcdl/output_3d/timeseries.simularium')) and \ - (os.path.exists(s_pathfile)) - os.remove(s_pathfile) + (type(la_ometiff) is list) and \ + (type(la_ometiff[0]) is np.ndarray) and \ + (type(la_ometiff[-1]) is np.ndarray) and \ + (la_ometiff[0].dtype == np.float32) and \ + (la_ometiff[-1].dtype == np.float32) and \ + (la_ometiff[0].shape == (4, 11, 200, 300)) and \ + (la_ometiff[-1].shape == (4, 11, 200, 300)) and \ + (len(la_ometiff) == 25) - def test_mcdsts_get_simularium_nondefault(self): - import simulariumio as sim - mcdsts = pcdl.TimeSeries(s_path_3d, verbose=False) - s_pathfile = mcdsts.make_simularium(focus_cat=['current_phase', 'dead'], trajectory_title='zeitreihe', scale_factor=0.1, camera_defaults=sim.CameraData(), model_meta_data=sim.ModelMetaData()) + def test_mcdsts_make_ome_tiff_defaultattr_01(self, mcdsts=mcdsts): + a_ometiff = mcdsts.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=False, collapse=True) assert(str(type(mcdsts)) == "") and \ - (s_pathfile.endswith('/pcdl/output_3d/zeitreihe.simularium')) and \ - (os.path.exists(s_pathfile)) - os.remove(s_pathfile) + (type(a_ometiff) is np.ndarray) and \ + (a_ometiff.dtype == np.float32) and \ + (a_ometiff.shape == (25, 4, 11, 200, 300)) diff --git a/test/test_timestep_2d.py b/test/test_timestep_2d.py index 6fa64e5..0183c90 100644 --- a/test/test_timestep_2d.py +++ b/test/test_timestep_2d.py @@ -38,9 +38,50 @@ pcdl.install_data() +## neuroglancer rendering realted function + +class TestTimeStep2dNeuroglancer(object): + ''' tests for loading a pcdl.TimeStep data set. ''' + mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) + + ## make_gif and magick ommand ## + def test_mcds_render_neuroglancer_default(self, mcds=mcds): + s_tiffpathfile = mcds.make_ome_tiff() + o_viewer = mcds.render_neuroglancer( + tiffpathfile = s_tiffpathfile, + #timestep = 0, + #intensity_cmap='gray', + ) + assert(str(type(o_viewer)) == "") and \ + (str(o_viewer).startswith('http://127.0.0.1:')) + os.remove(s_tiffpathfile) + + def test_mcds_render_neuroglancer_timestep(self, mcds=mcds): + s_tiffpathfile = mcds.make_ome_tiff() + o_viewer = mcds.render_neuroglancer( + tiffpathfile = s_tiffpathfile, + timestep = -1, + intensity_cmap='gray', + ) + assert(str(type(o_viewer)) == "") and \ + (str(o_viewer).startswith('http://127.0.0.1:')) + os.remove(s_tiffpathfile) + + def test_mcds_render_neuroglancer_cmap(self, mcds=mcds): + s_tiffpathfile = mcds.make_ome_tiff() + o_viewer = mcds.render_neuroglancer( + tiffpathfile = s_tiffpathfile, + timestep = 0, + intensity_cmap='magma', + ) + assert(str(type(o_viewer)) == "") and \ + (str(o_viewer).startswith('http://127.0.0.1:')) + os.remove(s_tiffpathfile) + + ## data loading related functions ## -class TestTimeStepInit(object): +class TestTimeStep2dInit(object): ''' tests for loading a pcdl.TimeStep data set. ''' mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) df_cell = mcds.get_cell_df() @@ -69,7 +110,17 @@ def test_mcds_init_settingxml(self, mcds=mcds, df_cell=df_cell): (df_cell.shape[1] == 122) -class TestTimeStepInitMicroenvFalse(object): +class TestTimeStep2dInitCustomdataastype(object): + ''' tests for loading a pcdl.TimeStep data and run custom_data_astype function. ''' + mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) + + def test_mcds_custom_data_astype(self, mcds=mcds): + mcds.custom_data_astype({'sample': bool}) + assert(str(type(mcds)) == "") and \ + (mcds.data['cell']['df_cell']['sample'].dtype == bool) + + +class TestTimeStep2dInitMicroenvFalse(object): ''' tests for loading a pcdl.TimeStep data set with microenv false. ''' mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=False, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) df_cell = mcds.get_cell_df() @@ -98,7 +149,7 @@ def test_mcds_init_settingxml(self, mcds=mcds, df_cell=df_cell): (df_cell.shape[1] == 116) -class TestTimeStepInitGraphFalse(object): +class TestTimeStep2dInitGraphFalse(object): ''' tests for loading a pcdl.TimeStep data set with graph false. ''' mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=False, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) df_cell = mcds.get_cell_df() @@ -127,7 +178,7 @@ def test_mcds_init_settingxml(self, mcds=mcds, df_cell=df_cell): (df_cell.shape[1] == 122) -class TestTimeStepInitPhysibossFalse(object): +class TestTimeStep2dInitPhysibossFalse(object): ''' tests for loading a pcdl.TimeStep data set with physiboss false. ''' mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=False, settingxml='PhysiCell_settings.xml', verbose=True) df_cell = mcds.get_cell_df() @@ -156,7 +207,7 @@ def test_mcds_init_settingxml(self, mcds=mcds, df_cell=df_cell): (df_cell.shape[1] == 122) -class TestTimeStepInitSettingxmlFalse(object): +class TestTimeStep2dInitSettingxmlFalse(object): ''' tests for loading a pcdl.TimeStep data set with settingxml false. ''' mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml=False, verbose=True) df_cell = mcds.get_cell_df() @@ -185,7 +236,7 @@ def test_mcds_init_settingxml(self, mcds=mcds, df_cell=df_cell): (df_cell.shape[1] == 122) -class TestTimeStepInitSettingxmlNone(object): +class TestTimeStep2dInitSettingxmlNone(object): ''' tests for loading a pcdl.TimeStep data set with settingxml none. ''' mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml=None, verbose=True) df_cell = mcds.get_cell_df() @@ -214,17 +265,7 @@ def test_mcds_init_settingxml(self, mcds=mcds, df_cell=df_cell): (df_cell.shape[1] == 122) -class TestTimeStepInitCustomdataastype(object): - ''' tests for loading a pcdl.TimeStep data and run custom_data_astype function. ''' - mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) - - def test_mcds_custom_data_astype(self, mcds=mcds): - mcds.custom_data_astype({'sample': bool}) - assert(str(type(mcds)) == "") and \ - (mcds.data['cell']['df_cell']['sample'].dtype == bool) - - -class TestTimeStepInitVerboseTrue(object): +class TestTimeStep2dInitVerboseTrue(object): ''' tests for loading a pcdl.TimeStep data set and set_verbose_false function. ''' mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) @@ -238,7 +279,7 @@ def test_mcds_set_verbose_false(self, mcds=mcds): (not mcds.verbose) -class TestTimeStepInitVerboseFalse(object): +class TestTimeStep2dInitVerboseFalse(object): ''' tests for loading a pcdl.TimeStep data set and set_verbose_true function. ''' mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=False) @@ -254,7 +295,7 @@ def test_mcds_set_verbose_true(self, mcds=mcds): ## metadata related functions ## -class TestTimeStepMetadata(object): +class TestTimeStep2dMetadata(object): ''' tests for pcdl.TimeStep metadata related functions. ''' mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) @@ -295,9 +336,9 @@ def test_mcds_get_runtime(self, mcds=mcds): (r_runtime == 1.952156) -## setting related functions ## +## parameter setting related functions ## -class TestTimeStepSetting(object): +class TestTimeStep2dUnitdict(object): ''' tests for pcdl.TimeStep setting related functions. ''' mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) @@ -311,7 +352,7 @@ def test_mcds_get_unit_dict(self, mcds=mcds): ## mesh related functions ## -class TestTimeStepMesh(object): +class TestTimeStep2dMesh(object): ''' tests for pcdl.TimeStep mesh related functions. ''' mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) @@ -399,12 +440,6 @@ def test_mcds_get_mesh_coordinate(self, mcds=mcds): (set(aar_voxel[1]) == er_n_cube) and \ (set(aar_voxel[2]) == er_p_cube) - def test_mcds_get_voxel_volume(self, mcds=mcds): - r_volume = mcds.get_voxel_volume() - assert(str(type(mcds)) == "") and \ - (str(type(r_volume)) == "") and \ - (r_volume == 6000.0) - # bue: check else in 3D def test_mcds_get_mesh_spacing(self, mcds=mcds): lr_spacing = mcds.get_mesh_spacing() @@ -415,14 +450,6 @@ def test_mcds_get_mesh_spacing(self, mcds=mcds): (str(type(lr_spacing[-1])) == "") and \ (lr_spacing == [30.0, 20.0, 10.0]) - def test_mcds_get_voxel_spacing(self, mcds=mcds): - lr_spacing = mcds.get_voxel_spacing() - assert(str(type(mcds)) == "") and \ - (str(type(lr_spacing)) == "") and \ - (str(type(lr_spacing[0])) == "") and \ - (str(type(lr_spacing[-1])) == "") and \ - (lr_spacing == [30.0, 20.0, 10.0]) - def test_mcds_is_in_mesh(self, mcds=mcds): assert(str(type(mcds)) == "") and \ (mcds.is_in_mesh(x=0, y=0, z=0, halt=False)) and \ @@ -443,6 +470,20 @@ def test_mcds_get_mesh_mnp(self, mcds=mcds): (li_mesh_2 == [15.0, 10.0, 0.0]) and \ (li_mesh_none is None) + def test_mcds_get_voxel_spacing(self, mcds=mcds): + lr_spacing = mcds.get_voxel_spacing() + assert(str(type(mcds)) == "") and \ + (str(type(lr_spacing)) == "") and \ + (str(type(lr_spacing[0])) == "") and \ + (str(type(lr_spacing[-1])) == "") and \ + (lr_spacing == [30.0, 20.0, 10.0]) + + def test_mcds_get_voxel_volume(self, mcds=mcds): + r_volume = mcds.get_voxel_volume() + assert(str(type(mcds)) == "") and \ + (str(type(r_volume)) == "") and \ + (r_volume == 6000.0) + def test_mcds_get_voxel_ijk(self, mcds=mcds): li_voxel_0 = mcds.get_voxel_ijk(x=0, y=0, z=0, is_in_mesh=True) # if b_calc li_voxel_1 = mcds.get_voxel_ijk(x=15, y=10, z=0, is_in_mesh=True) # if b_calc @@ -459,11 +500,11 @@ def test_mcds_get_voxel_ijk(self, mcds=mcds): ## micro environment related functions ## -class TestTimeStepMicroenv(object): +class TestTimeStep2dMicroenv(object): ''' tests for pcdl.TimeStep micro environment related functions. ''' mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) - def test_mcds_get_substrate_name(self, mcds=mcds): + def test_mcds_get_substrate_list(self, mcds=mcds): ls_substrate = mcds.get_substrate_list() assert(str(type(mcds)) == "") and \ (str(type(ls_substrate)) == "") and \ @@ -580,19 +621,13 @@ def test_mcds_make_conc_vtk(self, mcds=mcds): (os.path.getsize(s_pathfile) > 2**10) os.remove(s_pathfile) + ## cell related functions ## -class TestTimeStepCell(object): +class TestTimeStep2dCell(object): ''' tests for pcdl.TimeStep cell related functions. ''' mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) - def test_mcds_get_cell_attribute_list(self, mcds=mcds): - ls_cellattr = mcds.get_cell_attribute_list() - assert(str(type(mcds)) == "") and \ - (str(type(ls_cellattr)) == "") and \ - (str(type(ls_cellattr[0])) == "") and \ - (len(ls_cellattr) == 110) - def test_mcds_get_celltype_list(self, mcds=mcds): ls_celltype = mcds.get_celltype_list() assert(str(type(mcds)) == "") and \ @@ -635,6 +670,13 @@ def test_mcds_get_cell_df_keep(self, mcds=mcds): (df_cell.shape[0] > 9) and \ (df_cell.shape[1] == 13) + def test_mcds_get_cell_attribute_list(self, mcds=mcds): + ls_cellattr = mcds.get_cell_attribute_list() + assert(str(type(mcds)) == "") and \ + (str(type(ls_cellattr)) == "") and \ + (str(type(ls_cellattr[0])) == "") and \ + (len(ls_cellattr) == 110) + # scatter categorical def test_mcds_plot_scatter_cat_if(self, mcds=mcds): fig = mcds.plot_scatter( @@ -806,9 +848,63 @@ def test_mcds_make_cell_vtk_attribute_many(self, mcds=mcds): os.remove(s_pathfile) +## anndata time step related functions ## + +class TestTimeStep2dAnnData(object): + ''' test for pcdl.TimeStep class. ''' + + ## get_anndata command ## + def test_mcds_get_anndata(self): + mcds = pcdl.TimeStep(s_pathfile_2d, verbose=False) + ann = mcds.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs') + assert(str(type(mcds)) == "") and \ + (str(type(ann)) == "") and \ + (ann.X.shape[0] > 9) and \ + (ann.X.shape[1] == 105) and \ + (ann.obs.shape[0] > 9) and \ + (ann.obs.shape[1] == 8) and \ + (ann.obsm['spatial'].shape[0] > 9) and \ + (ann.obsm['spatial'].shape[1] == 2) and \ + (len(ann.obsp) == 4) and \ + (ann.var.shape == (105, 0)) and \ + (len(ann.uns) == 2) + + +class TestTimeStep2dScaler(object): + ''' test for pcdl.scaler function ''' + a_x = np.array([[ 1.,-1., 2., 0.],[ 2., 0., 0.,0.],[ 0., 1.,-1.,0.]]) + df_x = pd.DataFrame(a_x, columns=['a','b','c','d']) + + def test_scaler_none(self, df_x=df_x): + df_scaled = pcdl.timestep.scaler(df_x=df_x, scale=None) + assert(str(type(df_scaled)) == "") and \ + (all(df_scaled == df_x)) + + def test_scaler_minabs(self, df_x=df_x): + df_scaled = pcdl.timestep.scaler(df_x=df_x, scale='maxabs') + assert(str(type(df_scaled)) == "") and \ + (df_scaled.values.sum().round(3) == 2.0) and \ + (df_scaled.values.min().round(3) == -1.0) and \ + (df_scaled.values.max().round(3) == 1.0) + + def test_scaler_minmax(self, df_x=df_x): + df_scaled = pcdl.timestep.scaler(df_x=df_x, scale='minmax') + assert(str(type(df_scaled)) == "") and \ + (df_scaled.values.sum().round(3) == 4.333) and \ + (df_scaled.values.min().round(3) == 0.0) and \ + (df_scaled.values.max().round(3) == 1.0) + + def test_scaler_std(self, df_x=df_x): + df_scaled = pcdl.timestep.scaler(df_x=df_x, scale='std') + assert(str(type(df_scaled)) == "") and \ + (df_scaled.values.sum().round(3) == 0.0) and \ + (df_scaled.values.min().round(3) == -1.0) and \ + (df_scaled.values.max().round(3) == 1.091) + + ## graph related functions ## -class TestTimeStepGraph(object): +class TestTimeStep2dGraph(object): ''' tests for pcdl.TimeStep graph related functions. ''' mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) @@ -933,178 +1029,9 @@ def test_mcds_make_graph_gml_neighbor_nodeattrtrue(self, mcds=mcds): os.remove(s_pathfile) -## ome tiff related functions ## - -class TestTimeStepOmeTiff(object): - ''' tests for pcdl.TimeStep graph related functions. ''' - mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) - - ## ome tiff related functions ## - def test_mcds_make_ome_tiff_default(self, mcds=mcds): - s_pathfile = mcds.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=True) - assert(str(type(mcds)) == "") and \ - (s_pathfile.replace('\\','/').endswith('pcdl/output_2d/output00000024_oxygen_water_default_blood_cells_ID.ome.tiff')) and \ - (os.path.exists(s_pathfile)) and \ - (os.path.getsize(s_pathfile) > 2**10) - os.remove(s_pathfile) - - def test_mcds_make_ome_tiff_bool(self, mcds=mcds): - a_ometiff = mcds.make_ome_tiff(cell_attribute='dead', conc_cutoff={}, focus=None, file=False) - assert(str(type(mcds)) == "") and \ - (str(type(a_ometiff)) == "") and \ - (a_ometiff.dtype == np.float32) and \ - (a_ometiff.shape == (4, 1, 200, 300)) and \ - (a_ometiff[2].min() == 0.0) and \ - (a_ometiff[3].min() == 0.0) and \ - (a_ometiff[0].max() >= 1.0) and \ - (a_ometiff[1].max() >= 1.0) and \ - (a_ometiff[2].max() >= 1.0) and \ - (a_ometiff[3].max() >= 1.0) - - def test_mcds_make_ome_tiff_int(self, mcds=mcds): - a_ometiff = mcds.make_ome_tiff(cell_attribute='cell_count_voxel', conc_cutoff={}, focus=None, file=False) - assert(str(type(mcds)) == "") and \ - (str(type(a_ometiff)) == "") and \ - (a_ometiff.dtype == np.float32) and \ - (a_ometiff.shape == (4, 1, 200, 300)) and \ - (a_ometiff[2].min() == 0.0) and \ - (a_ometiff[3].min() == 0.0) and \ - (a_ometiff[0].max() >= 1.0) and \ - (a_ometiff[1].max() >= 1.0) and \ - (a_ometiff[2].max() >= 1.0) and \ - (a_ometiff[3].max() >= 1.0) - - def test_mcds_make_ome_tiff_float(self, mcds=mcds): - a_ometiff = mcds.make_ome_tiff(cell_attribute='pressure', conc_cutoff={}, focus=None, file=False) - assert(str(type(mcds)) == "") and \ - (str(type(a_ometiff)) == "") and \ - (a_ometiff.dtype == np.float32) and \ - (a_ometiff.shape == (4, 1, 200, 300)) and\ - (a_ometiff[2].min() == 0.0) and \ - (a_ometiff[3].min() == 0.0) and \ - (a_ometiff[0].max() >= 1.0) and \ - (a_ometiff[1].max() >= 1.0) and \ - (a_ometiff[2].max() >= 1.0) and \ - (a_ometiff[3].max() >= 1.0) - - def test_mcds_make_ome_tiff_conccutoff(self, mcds=mcds): - a_ometiff = mcds.make_ome_tiff(cell_attribute='ID', conc_cutoff={'oxygen': -1}, focus=None, file=False) - assert(str(type(mcds)) == "") and \ - (str(type(a_ometiff)) == "") and \ - (a_ometiff.dtype == np.float32) and \ - (a_ometiff.shape == (4, 1, 200, 300)) and \ - (a_ometiff[2].min() == 0.0) and \ - (a_ometiff[3].min() == 0.0) and \ - (a_ometiff[0].max() >= 1.0) and \ - (a_ometiff[1].max() >= 1.0) and \ - (a_ometiff[2].max() >= 1.0) and \ - (a_ometiff[3].max() >= 1.0) - - def test_mcds_make_ome_tiff_focus(self, mcds=mcds): - a_ometiff = mcds.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus={'default'}, file=False) - assert(str(type(mcds)) == "") and \ - (str(type(a_ometiff)) == "") and \ - (a_ometiff.dtype == np.float32) and \ - (a_ometiff.shape == (1, 1, 200, 300)) and \ - (a_ometiff[0].min() == 0.0) and \ - (a_ometiff[0].max() >= 1.0) - - -class TestTimeStepNeuroglancer(object): - ''' tests for loading a pcdl.TimeStep data set. ''' - mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) - - ## make_gif and magick ommand ## - def test_mcds_render_neuroglancer_default(self, mcds=mcds): - s_tiffpathfile = mcds.make_ome_tiff() - o_viewer = mcds.render_neuroglancer( - tiffpathfile = s_tiffpathfile, - #timestep = 0, - #intensity_cmap='gray', - ) - assert(str(type(o_viewer)) == "") and \ - (str(o_viewer).startswith('http://127.0.0.1:')) - os.remove(s_tiffpathfile) - - def test_mcds_render_neuroglancer_timestep(self, mcds=mcds): - s_tiffpathfile = mcds.make_ome_tiff() - o_viewer = mcds.render_neuroglancer( - tiffpathfile = s_tiffpathfile, - timestep = -1, - intensity_cmap='gray', - ) - assert(str(type(o_viewer)) == "") and \ - (str(o_viewer).startswith('http://127.0.0.1:')) - os.remove(s_tiffpathfile) - - def test_mcds_render_neuroglancer_cmap(self, mcds=mcds): - s_tiffpathfile = mcds.make_ome_tiff() - o_viewer = mcds.render_neuroglancer( - tiffpathfile = s_tiffpathfile, - timestep = 0, - intensity_cmap='magma', - ) - assert(str(type(o_viewer)) == "") and \ - (str(o_viewer).startswith('http://127.0.0.1:')) - os.remove(s_tiffpathfile) - - -## anndata helper function ## -class TestTimeStepScaler(object): - ''' test for pcdl.scaler function ''' - a_x = np.array([[ 1.,-1., 2., 0.],[ 2., 0., 0.,0.],[ 0., 1.,-1.,0.]]) - df_x = pd.DataFrame(a_x, columns=['a','b','c','d']) - - def test_scaler_none(self, df_x=df_x): - df_scaled = pcdl.timestep.scaler(df_x=df_x, scale=None) - assert(str(type(df_scaled)) == "") and \ - (all(df_scaled == df_x)) - - def test_scaler_minabs(self, df_x=df_x): - df_scaled = pcdl.timestep.scaler(df_x=df_x, scale='maxabs') - assert(str(type(df_scaled)) == "") and \ - (df_scaled.values.sum().round(3) == 2.0) and \ - (df_scaled.values.min().round(3) == -1.0) and \ - (df_scaled.values.max().round(3) == 1.0) - - def test_scaler_minmax(self, df_x=df_x): - df_scaled = pcdl.timestep.scaler(df_x=df_x, scale='minmax') - assert(str(type(df_scaled)) == "") and \ - (df_scaled.values.sum().round(3) == 4.333) and \ - (df_scaled.values.min().round(3) == 0.0) and \ - (df_scaled.values.max().round(3) == 1.0) - - def test_scaler_std(self, df_x=df_x): - df_scaled = pcdl.timestep.scaler(df_x=df_x, scale='std') - assert(str(type(df_scaled)) == "") and \ - (df_scaled.values.sum().round(3) == 0.0) and \ - (df_scaled.values.min().round(3) == -1.0) and \ - (df_scaled.values.max().round(3) == 1.091) - - -## anndata time step related functions ## -class TestTimeStepAnnData(object): - ''' test for pcdl.TimeStep class. ''' - - ## get_anndata command ## - def test_mcds_get_anndata(self): - mcds = pcdl.TimeStep(s_pathfile_2d, verbose=False) - ann = mcds.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs') - assert(str(type(mcds)) == "") and \ - (str(type(ann)) == "") and \ - (ann.X.shape[0] > 9) and \ - (ann.X.shape[1] == 105) and \ - (ann.obs.shape[0] > 9) and \ - (ann.obs.shape[1] == 8) and \ - (ann.obsm['spatial'].shape[0] > 9) and \ - (ann.obsm['spatial'].shape[1] == 2) and \ - (len(ann.obsp) == 4) and \ - (ann.var.shape == (105, 0)) and \ - (len(ann.uns) == 2) - - ## muspan time step related functions ## -class TestTimeStepMuspan(object): + +class TestTimeStep2dMuspan(object): ''' test for pcdl.TimeStep class. ''' ## get_muspan command ## @@ -1122,10 +1049,10 @@ def test_mcds_get_muspan_default(self): (len(do_domain[s_key].networks) == 3) and \ (len(do_domain[s_key].objects) > 9) except ModuleNotFoundError: - print('Warning @ pytest TestTimeStepMuspan : muspan module not installed.') + print('Warning @ pytest TestTimeStep2dMuspan : muspan module not installed.') assert True except SystemExit: - print('Warning @ pytest TestTimeStepMuspan : muspan module not installed.') + print('Warning @ pytest TestTimeStep2dMuspan : muspan module not installed.') assert True def test_mcds_get_muspan_zslice(self): @@ -1142,15 +1069,16 @@ def test_mcds_get_muspan_zslice(self): (len(do_domain[s_key].networks) == 3) and \ (len(do_domain[s_key].objects) > 9) except ModuleNotFoundError: - print('Warning @ pytest TestTimeStepMuspan : muspan module not installed.') + print('Warning @ pytest TestTimeStep2dMuspan : muspan module not installed.') assert True except SystemExit: - print('Warning @ pytest TestTimeStepMuspan : muspan module not installed.') + print('Warning @ pytest TestTimeStep2dMuspan : muspan module not installed.') assert True ## spatialdata time step related functions ## -class TestTimeStepSpatialData(object): + +class TestTimeStep2dSpatialData(object): ''' test for pcdl.TimeStep class. ''' ## get_spatialdata command ## @@ -1213,3 +1141,80 @@ def test_mcds_get_spatialdata_none(self): (sdata['subs_table'].shape[1] ==2) and \ (sdata['subs_table'].obs.shape[0] > 9) and \ (sdata['subs_table'].obs.shape[1] == 11) + + +## ome tiff related functions ## + +class TestTimeStep2dOmeTiff(object): + ''' tests for pcdl.TimeStep graph related functions. ''' + mcds = pcdl.TimeStep(xmlfile=s_file_2d, output_path=s_path_2d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) + + ## ome tiff related functions ## + def test_mcds_make_ome_tiff_default(self, mcds=mcds): + s_pathfile = mcds.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=True) + assert(str(type(mcds)) == "") and \ + (s_pathfile.replace('\\','/').endswith('pcdl/output_2d/output00000024_oxygen_water_default_blood_cells_ID.ome.tiff')) and \ + (os.path.exists(s_pathfile)) and \ + (os.path.getsize(s_pathfile) > 2**10) + os.remove(s_pathfile) + + def test_mcds_make_ome_tiff_bool(self, mcds=mcds): + a_ometiff = mcds.make_ome_tiff(cell_attribute='dead', conc_cutoff={}, focus=None, file=False) + assert(str(type(mcds)) == "") and \ + (str(type(a_ometiff)) == "") and \ + (a_ometiff.dtype == np.float32) and \ + (a_ometiff.shape == (4, 1, 200, 300)) and \ + (a_ometiff[2].min() == 0.0) and \ + (a_ometiff[3].min() == 0.0) and \ + (a_ometiff[0].max() >= 1.0) and \ + (a_ometiff[1].max() >= 1.0) and \ + (a_ometiff[2].max() >= 1.0) and \ + (a_ometiff[3].max() >= 1.0) + + def test_mcds_make_ome_tiff_int(self, mcds=mcds): + a_ometiff = mcds.make_ome_tiff(cell_attribute='cell_count_voxel', conc_cutoff={}, focus=None, file=False) + assert(str(type(mcds)) == "") and \ + (str(type(a_ometiff)) == "") and \ + (a_ometiff.dtype == np.float32) and \ + (a_ometiff.shape == (4, 1, 200, 300)) and \ + (a_ometiff[2].min() == 0.0) and \ + (a_ometiff[3].min() == 0.0) and \ + (a_ometiff[0].max() >= 1.0) and \ + (a_ometiff[1].max() >= 1.0) and \ + (a_ometiff[2].max() >= 1.0) and \ + (a_ometiff[3].max() >= 1.0) + + def test_mcds_make_ome_tiff_float(self, mcds=mcds): + a_ometiff = mcds.make_ome_tiff(cell_attribute='pressure', conc_cutoff={}, focus=None, file=False) + assert(str(type(mcds)) == "") and \ + (str(type(a_ometiff)) == "") and \ + (a_ometiff.dtype == np.float32) and \ + (a_ometiff.shape == (4, 1, 200, 300)) and\ + (a_ometiff[2].min() == 0.0) and \ + (a_ometiff[3].min() == 0.0) and \ + (a_ometiff[0].max() >= 1.0) and \ + (a_ometiff[1].max() >= 1.0) and \ + (a_ometiff[2].max() >= 1.0) and \ + (a_ometiff[3].max() >= 1.0) + + def test_mcds_make_ome_tiff_conccutoff(self, mcds=mcds): + a_ometiff = mcds.make_ome_tiff(cell_attribute='ID', conc_cutoff={'oxygen': -1}, focus=None, file=False) + assert(str(type(mcds)) == "") and \ + (str(type(a_ometiff)) == "") and \ + (a_ometiff.dtype == np.float32) and \ + (a_ometiff.shape == (4, 1, 200, 300)) and \ + (a_ometiff[2].min() == 0.0) and \ + (a_ometiff[3].min() == 0.0) and \ + (a_ometiff[0].max() >= 1.0) and \ + (a_ometiff[1].max() >= 1.0) and \ + (a_ometiff[2].max() >= 1.0) and \ + (a_ometiff[3].max() >= 1.0) + + def test_mcds_make_ome_tiff_focus(self, mcds=mcds): + a_ometiff = mcds.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus={'default'}, file=False) + assert(str(type(mcds)) == "") and \ + (str(type(a_ometiff)) == "") and \ + (a_ometiff.dtype == np.float32) and \ + (a_ometiff.shape == (1, 1, 200, 300)) and \ + (a_ometiff[0].min() == 0.0) and \ + (a_ometiff[0].max() >= 1.0) diff --git a/test/test_timestep_3d.py b/test/test_timestep_3d.py index c9288fe..1c4f146 100644 --- a/test/test_timestep_3d.py +++ b/test/test_timestep_3d.py @@ -63,7 +63,9 @@ def test_mcds_get_mesh_spacing(self, mcds=mcds): # test workhorse for speed # ############################ -class TestTimeStepInit(object): +## data loading related functions ## + +class TestTimeStep3dInit(object): ''' tests for loading a pcdl.TimeStep data set. ''' mcds = pcdl.TimeStep(xmlfile=s_file_3d, output_path=s_path_3d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) df_cell = mcds.get_cell_df() @@ -92,7 +94,7 @@ def test_mcds_init_settingxml(self, mcds=mcds, df_cell=df_cell): (df_cell.shape[1] == 122) -class TestTimeStepInitMicroenvFalse(object): +class TestTimeStep3dInitMicroenvFalse(object): ''' tests for loading a pcdl.TimeStep data set with microenv false. ''' mcds = pcdl.TimeStep(xmlfile=s_file_3d, output_path=s_path_3d, custom_data_type={}, microenv=False, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) df_cell = mcds.get_cell_df() @@ -121,7 +123,7 @@ def test_mcds_init_settingxml(self, mcds=mcds, df_cell=df_cell): (df_cell.shape[1] == 116) -class TestTimeStepInitGraphFalse(object): +class TestTimeStep3dInitGraphFalse(object): ''' tests for loading a pcdl.TimeStep data set with graph false. ''' mcds = pcdl.TimeStep(xmlfile=s_file_3d, output_path=s_path_3d, custom_data_type={}, microenv=True, graph=False, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) df_cell = mcds.get_cell_df() @@ -150,7 +152,7 @@ def test_mcds_init_settingxml(self, mcds=mcds, df_cell=df_cell): (df_cell.shape[1] == 122) -class TestTimeStepInitPhysibossFalse(object): +class TestTimeStep3dInitPhysibossFalse(object): ''' tests for loading a pcdl.TimeStep data set with physiboss false. ''' mcds = pcdl.TimeStep(xmlfile=s_file_3d, output_path=s_path_3d, custom_data_type={}, microenv=True, graph=True, physiboss=False, settingxml='PhysiCell_settings.xml', verbose=True) df_cell = mcds.get_cell_df() @@ -179,17 +181,17 @@ def test_mcds_init_settingxml(self, mcds=mcds, df_cell=df_cell): (df_cell.shape[1] == 122) -#class TestTimeStepInitSettingxmlFalse(object): +#class TestTimeStep3dInitSettingxmlFalse(object): # ''' tests for loading a pcdl.TimeStep data set with settingxml false. ''' # NOP PhysiCell >= v1.14.0 -#class TestTimeStepInitSettingxmlNone(object): +#class TestTimeStep3dInitSettingxmlNone(object): # ''' tests for loading a pcdl.TimeStep data set with settingxml none. ''' # NOP PhysiCell >= v1.14.0 -class TestTimeStepInitVerboseTrue(object): +class TestTimeStep3dInitVerboseTrue(object): ''' tests for loading a pcdl.TimeStep data set and set_verbose_false function. ''' mcds = pcdl.TimeStep(xmlfile=s_file_3d, output_path=s_path_3d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True) @@ -203,7 +205,7 @@ def test_mcds_set_verbose_false(self, mcds=mcds): (not mcds.verbose) -class TestTimeStepInitVerboseFalse(object): +class TestTimeStep3dInitVerboseFalse(object): ''' tests for loading a pcdl.TimeStep data set and set_verbose_true function. ''' mcds = pcdl.TimeStep(xmlfile=s_file_3d, output_path=s_path_3d, custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=False) @@ -217,7 +219,9 @@ def test_mcds_set_verbose_true(self, mcds=mcds): (mcds.verbose) -class TestTimeStep3dSettingWorkhorse(object): +## parameter setting related functions ## + +class TestTimeStep3dUnitdictWorkhorse(object): ''' tests on 3D data set, for speed, for pcdl.TimeStep unit related workhorse functions. ''' mcds = pcdl.TimeStep(xmlfile=s_pathfile_3d) # custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True @@ -229,6 +233,8 @@ def test_mcds_get_unit_dict(self, mcds=mcds): (ds_unit['oxygen'] == 'dimensionless') +## metadata related functions ## + class TestTimeStep3dMicroenvWorkhorse(object): ''' tests on 3D data set, for speed, for pcdl.TimeStep microenvironment related workhorse functions. ''' mcds = pcdl.TimeStep(xmlfile=s_pathfile_3d) # custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True @@ -334,6 +340,8 @@ def test_mcds_make_conc_vtk(self, mcds=mcds): os.remove(s_pathfile) +## cell agent related functions ## + class TestTimeStep3dCellWorkhorse(object): ''' tests on 3D data set, for speed, for pcdl.TimeStep cell related workhorse functions. ''' mcds = pcdl.TimeStep(xmlfile=s_pathfile_3d) # custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True @@ -539,6 +547,30 @@ def test_mcds_make_cell_vtk_attribute_many(self, mcds=mcds): os.remove(s_pathfile) +## anndata time step related functions ## + +class TestTimeStep3dAnnData(object): + ''' test for pcdl.TimeStep class. ''' + + ## get_anndata command ## + def test_mcds_get_anndata(self): + mcds = pcdl.TimeStep(s_pathfile_3d, verbose=False) + ann = mcds.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs') + assert(str(type(mcds)) == "") and \ + (str(type(ann)) == "") and \ + (ann.X.shape[0] > 9) and \ + (ann.X.shape[1] == 105) and \ + (ann.obs.shape[0] > 9) and \ + (ann.obs.shape[1] == 8) and \ + (ann.obsm['spatial'].shape[0] > 9) and \ + (ann.obsm['spatial'].shape[1] == 3) and \ + (len(ann.obsp) == 4) and \ + (ann.var.shape == (105, 0)) and \ + (len(ann.uns) == 2) + + +## graph related functions ## + class TestTimeStep3dGraphWorkhorse(object): ''' tests on 3D data set, for speed, for pcdl.TimeStep graph related workhorse functions. ''' mcds = pcdl.TimeStep(xmlfile=s_pathfile_3d) # custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True @@ -645,50 +677,9 @@ def test_mcds_make_graph_gml_neighbor_nodeattrtrue(self, mcds=mcds): os.remove(s_pathfile) -class TestTimeStep3dOmeTiffWorkhorse(object): - ''' tests on 3D data set, for speed, for pcdl.TimeStep ome tiff related workhorse functions. ''' - mcds = pcdl.TimeStep(xmlfile=s_pathfile_3d) # custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True - - ## ome tiff related functions ## - def test_mcds_make_ome_tiff_default(self, mcds=mcds): - s_pathfile = mcds.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=True) - assert(str(type(mcds)) == "") and \ - (s_pathfile.replace('\\','/').endswith('pcdl/output_3d/output00000024_oxygen_water_default_blood_cells_ID.ome.tiff')) and \ - (os.path.exists(s_pathfile)) and \ - (os.path.getsize(s_pathfile) > 2**10) - os.remove(s_pathfile) - - def test_mcds_make_ome_tiff_nofile(self, mcds=mcds): - a_ometiff = mcds.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=False) - assert(str(type(mcds)) == "") and \ - (str(type(a_ometiff)) == "") and \ - (a_ometiff.dtype == np.float32) and \ - (a_ometiff.shape == (4, 11, 200, 300)) - - -## anndata time step related functions ## -class TestTimeStep3dAnnData(object): - ''' test for pcdl.TimeStep class. ''' - - ## get_anndata command ## - def test_mcds_get_anndata(self): - mcds = pcdl.TimeStep(s_pathfile_3d, verbose=False) - ann = mcds.get_anndata(values=1, drop=set(), keep=set(), scale='maxabs') - assert(str(type(mcds)) == "") and \ - (str(type(ann)) == "") and \ - (ann.X.shape[0] > 9) and \ - (ann.X.shape[1] == 105) and \ - (ann.obs.shape[0] > 9) and \ - (ann.obs.shape[1] == 8) and \ - (ann.obsm['spatial'].shape[0] > 9) and \ - (ann.obsm['spatial'].shape[1] == 3) and \ - (len(ann.obsp) == 4) and \ - (ann.var.shape == (105, 0)) and \ - (len(ann.uns) == 2) - - ## muspan time step related functions ## -class TestTimeStepMuspan(object): + +class TestTimeStep3dMuspan(object): ''' test for pcdl.TimeStep class. ''' ## get_muspan command ## @@ -734,7 +725,8 @@ def test_mcds_get_muspan_zslice(self): ## spatialdata time step related functions ## -class TestTimeStepSpatialData(object): + +class TestTimeStep3dSpatialData(object): ''' test for pcdl.TimeStep class. ''' ## get_spatialdata command ## @@ -798,3 +790,25 @@ def test_mcds_get_spatialdata_none(self): (sdata['subs_table'].obs.shape[0] > 9) and \ (sdata['subs_table'].obs.shape[1] == 11) + +## ome tiff related functions ## + +class TestTimeStep3dOmeTiffWorkhorse(object): + ''' tests on 3D data set, for speed, for pcdl.TimeStep ome tiff related workhorse functions. ''' + mcds = pcdl.TimeStep(xmlfile=s_pathfile_3d) # custom_data_type={}, microenv=True, graph=True, physiboss=True, settingxml='PhysiCell_settings.xml', verbose=True + + ## ome tiff related functions ## + def test_mcds_make_ome_tiff_default(self, mcds=mcds): + s_pathfile = mcds.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=True) + assert(str(type(mcds)) == "") and \ + (s_pathfile.replace('\\','/').endswith('pcdl/output_3d/output00000024_oxygen_water_default_blood_cells_ID.ome.tiff')) and \ + (os.path.exists(s_pathfile)) and \ + (os.path.getsize(s_pathfile) > 2**10) + os.remove(s_pathfile) + + def test_mcds_make_ome_tiff_nofile(self, mcds=mcds): + a_ometiff = mcds.make_ome_tiff(cell_attribute='ID', conc_cutoff={}, focus=None, file=False) + assert(str(type(mcds)) == "") and \ + (str(type(a_ometiff)) == "") and \ + (a_ometiff.dtype == np.float32) and \ + (a_ometiff.shape == (4, 11, 200, 300)) From b417dc2efd0cb505a396f387fc982362b1e7e478 Mon Sep 17 00:00:00 2001 From: bue Date: Fri, 4 Sep 2026 19:03:22 -0400 Subject: [PATCH 12/15] @ pcdl : reformat landing page and make workflow files more generic. --- .github/workflows/apple.yml | 4 +- .github/workflows/linux.yml | 4 +- .github/workflows/windows.yml | 4 +- README.md | 356 ++---------------- man/RELEASENOTES.md | 305 +++++++++++++++ man/docstring/mcds.render_neuroglancer.md | 7 + man/docstring/mcdsts.get_sdmcds_list.md | 22 ++ man/docstring/mcdsts.make_gif.md | 8 + man/docstring/mcdsts.make_movie.md | 8 + man/docstring/mcdsts.render_neuroglancer.md | 7 + .../physicelldataloader_concept_v4.0.0.jpg | Bin 235010 -> 260155 bytes .../physicelldataloader_concept_v4.0.0.png | Bin 437609 -> 470181 bytes man/img/physicelldataloafder_github_qr.png | Bin 132163 -> 0 bytes 13 files changed, 389 insertions(+), 336 deletions(-) create mode 100644 man/RELEASENOTES.md create mode 100644 man/docstring/mcds.render_neuroglancer.md create mode 100644 man/docstring/mcdsts.get_sdmcds_list.md create mode 100644 man/docstring/mcdsts.make_gif.md create mode 100644 man/docstring/mcdsts.make_movie.md create mode 100644 man/docstring/mcdsts.render_neuroglancer.md delete mode 100644 man/img/physicelldataloafder_github_qr.png diff --git a/.github/workflows/apple.yml b/.github/workflows/apple.yml index 6e459df..8560036 100644 --- a/.github/workflows/apple.yml +++ b/.github/workflows/apple.yml @@ -22,7 +22,7 @@ jobs: env: MPLBACKEND: Agg # https://github.com/orgs/community/discussions/26434 - PYTHONPATH: /Users/runner/work/physicelldataloader/physicelldataloader + PYTHONPATH: ${{ github.workspace }} steps: - uses: actions/checkout@main @@ -35,7 +35,7 @@ jobs: brew install ffmpeg imagemagick python -m pip install --upgrade pip python -m pip install flake8 pytest anndata bioio bioio-ome-tiff geopandas matplotlib networkx neuroglancer numpy pandas requests scikit-image scipy shapely simulariumio spatialdata vtk - python -m pip install /Users/runner/work/physicelldataloader/physicelldataloader -v + python -m pip install . -v #if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: lint with flake8 run: | diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 1b410b7..f99d4e0 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -22,7 +22,7 @@ jobs: env: MPLBACKEND: Agg # https://github.com/orgs/community/discussions/26434 - PYTHONPATH: /home/runner/work/physicelldataloader/physicelldataloader + PYTHONPATH: ${{ github.workspace }} steps: - uses: actions/checkout@main @@ -36,7 +36,7 @@ jobs: sudo apt install ffmpeg imagemagick python -m pip install --upgrade pip python -m pip install flake8 pytest anndata bioio bioio-ome-tiff geopandas matplotlib networkx neuroglancer numpy pandas requests scikit-image scipy shapely simulariumio spatialdata vtk - python -m pip install /home/runner/work/physicelldataloader/physicelldataloader -v + python -m pip install . -v #if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: lint with flake8 run: | diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 54b70e9..ddb8c83 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -22,7 +22,7 @@ jobs: env: MPLBACKEND: Agg # https://github.com/orgs/community/discussions/26434 - PYTHONPATH: D:\a\physicelldataloader\physicelldataloader + PYTHONPATH: ${{ github.workspace }} steps: - uses: actions/checkout@main @@ -35,7 +35,7 @@ jobs: choco install ffmpeg imagemagick python -m pip install --upgrade pip python -m pip install flake8 pytest anndata bioio bioio-ome-tiff geopandas matplotlib networkx neuroglancer numpy pandas requests scikit-image scipy shapely simulariumio spatialdata vtk - python -m pip install D:\a\physicelldataloader\physicelldataloader -v + python -m pip install . -v #echo 'set PYTHONPATH=D:\a\physicelldataloader\physicelldataloader' >> $GITHUB_ENV #if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: lint with flake8 diff --git a/README.md b/README.md index e454953..a0092e2 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,14 @@ ## Abstract: -physicell data loader (pcdl) provides a platform-independent (Windows, MacOSX, Linux), python3 based, [pip](https://en.wikipedia.org/wiki/Pip_(package_manager))-installable set of commands +physicell data loader (pcdl) provides a platform-independent (Windows, MacOSX, Linux), python3 based, [pip](https://en.wikipedia.org/wiki/Pip_(package_manager))-installable set of commands, to load output, generated with the [PhysiCell](https://github.com/MathCancer/PhysiCell) agent-based modeling and diffusion solver framework, -into [python3](https://en.wikipedia.org/wiki/Python_(programming_language)) or transform PhysiCell output into more widely used data formats. -pcdl can be loaded as a python3 module or run straight from the command line. +into [python3](https://en.wikipedia.org/wiki/Python_(programming_language)) or transform PhysiCell output into more widely used data formats, +straight on the command line. ![pcdl concept](man/img/physicelldataloader_concept_v4.0.0.png) + - -## Header: +## Software Specification: + Language: python [>= 3.11](https://devguide.python.org/versions/) + Library core dependencies: matplotlib, numpy, pandas, scipy @@ -76,10 +77,6 @@ Extras tutorials for GUI software: + [pcdl and neuroglancer](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_neuroglancer.md) + [pcdl and simularium](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_simularium.md) -Slides: - -+ [presentations given](https://github.com/elmbeech/physicelldataloader/tree/master/man/lecture) - ## ✨ Reference Manual: @@ -88,31 +85,13 @@ Slides: ## Discussion: -To be developed. - ++ [presentations given](https://github.com/elmbeech/physicelldataloader/tree/master/man/lecture) + ## Cite: @@ -129,308 +108,25 @@ Developers, please make pull requests to the https://github.com/elmbeech/physice ``` -## Road Map: - -+ downcast, to\file, and read function. -+ evt generate lineage tree graph output files. - - -## Release Notes: - -+ branch v4: active feature development, bug fixes, and security maintenance. - -+ version 4.1.9 (2026-08-28): elmbeech/physicelldataloader - + new TimeSeries **make_simularium** and **pcdl_make_simularium** function. special thanks to Randy Heiland! - -+ version 4.1.8 (2026-08-26): elmbeech/physicelldataloader - + branch v4 compatible with bioconda and galaxy. special thanks to Björn Grüning! +## Contributions: -+ version 4.1.7 (2026-08-21): elmbeech/physicelldataloader - + compatible with pandas v3.0.0. ++ original PhysiCell-Tools python-loader implementation: Patrick Wall, Randy Heiland, Paul Macklin ++ fork pcdl implementation: Elmar Bucher ++ fork pcdl co-programmer: Furkan Kurtoglu, Heber Rocha, Jennifer Eng ++ fork pcdl continuous testing and feedbacks: Aneequa Sundus (python), John Metzcar (python), Raquel Arroya (matlab) ++ student prj on pcdl: + Benjamin Jacobs (make\_graph\_gml), + Jason Lu (render\_neuroglancer), + Katie Pletz (beta testing), + Leena Sohail (beta testing), + Marshal Gress (plot\_scatter), + Nick Oldfather (unit test model), + Thierry-Pascal Fleurant (plot\_timeseries), + Viviana Kwong (render\_neuroglancer) -+ version 4.1.6 (2026-08-20): elmbeech/physicelldataloader - + pcdl installs now by default lightweight with only the core dependencies; users are prompted to install optional libraries if necessary. special thanks to Heber Rocha! -+ version 4.1.5 (2026-04-05): elmbeech/physicelldataloader - + bugfix library dependencies and library versions. +## Develoment: -+ version 4.1.4 (2026-04-04): elmbeech/physicelldataloader - + TimeSeries **mcdsts.make_contour** extrema parameter is replaced by vmin and vmax to be compatible with mcds.make\_contour and plt.contour. - + **make_cell_vtk** and **make_conc_vtk** now offer an ext parameter, allowing to manually specify the exact file extension. special thanks to Danyon Gedris! - + new **make_muspan** TimeStep class and TimeSeries class function and **pcdl_get_muspan** command line command. special thanks to Joshua Moore and Joshua Bull! ++ [Road Map and Release Notes](https://github.com/elmbeech/physicelldataloader/tree/master/man/RELEASENOTES.md) -+ version 4.1.3 (2026-03-21): elmbeech/physicelldataloader - + new **pcdl.pccmap** color map. this is an adaptation of the physicell pathology paint by number color map. - + new TimeStep **get_pcdl_version** function. - -+ version 4.1.2 (2026-03-06): elmbeech/physicelldataloader - + new **custom_data_astype** TimeStep class and TimeSeries class function to set the dtype of custom\_data variables even after the timestep or timeseries is loaded. - + TimeSeries \_\_init\_\_ function can now handle a list of TimeStep objects as input instead of a path. - -+ version 4.1.1 (2026-02-28): elmbeech/physicelldataloader - + reduced memory footprint. - -+ version 4.1.0 (2025-12-31): elmbeech/physicelldataloader - + new **get_spatialdata** TimeStep class and TimeSeries class function and **pcdl_get_spatialdata** command line command. special thanks to Luca Marconato! - + with this release, pcdl officially became an [scverse ecosystem](https://scverse.org/packages/#ecosystem) package. - -+ version 4.0.5 (2025-10-22): elmbeech/physicelldataloader - + **settingxml** default is now set to False, because the cell\_type id label mapping can, in recent PhysiCell output, be retrieved from output\*.xml too. - + **plot_scatter** and **plot_timeseries** now additionally have a cat\_drop and cat\_keep argument to filter categorical data. - + **plot_timeseries(frame=conc)** now plots by default all substrate concentrations over time. - + **plot_timeseries(ext=)** parameter offers to return a dataframe object, dafaframe csv file, image file, or a matplotlib fig object. special thanks to John Nardini and Edward Young! - -+ version 4.0.4 (2025-07-23): elmbeech/physicelldataloader - + command line commands now return **error code 0** if the command runs successfully. - -+ version 4.0.3 (2025-07-20): elmbeech/physicelldataloader - + TimeStep and TimeSeries **plot_contour**, **plot_scatter**, and **plot_timeseries** handle now **kwargs** arguments. - + minor bugfixes. - -+ version 4.0.2 (2025-06-29): elmbeech/physicelldataloader - + minor bugfixes. - -+ version 4.0.1 (2025-06-24): elmbeech/physicelldataloader - + man updated. - + minor bugfixes. - -+ version 4.0.0 (2025-05-13): elmbeech/physicelldataloader - + v4 was forked from v3.3.4! - + **mcds.data** struct was rewritten in more python less c++ way. - + pyMCDS.py and part of pyAnnData.py was fused to **timestep.py**. - + pyMCDSts.py and part of pyAnnData.py was fused to **timeseries.py**. - + pyCLI.py was renames to **commandline.py**. - + data\_timeseries.py was renamed to **output_data.py**. - + TimeStep function **get_concentration** was deprecated because pandas already has this functionlity. - + TimeStep function **get_concentration_at** was deprecated because pandas already has this functionlity. - + TimeStep function **get_cell_df_at** was deprecated because pandas already has this functionlity. - + **make_conc_vtk** and **make_cell_vtk** on the fly visualization was removed because paraview is good enough. - + new TimeStep **get_cell_attribute_list** function, to retrieve a list of all tracked cell attribute labels. - + new **pcdl_get_cell_attribute_list** function, to retrieve a list of all tracked cell attribute labels. - + new **render_neuroglancer** function, to render ome tiff image into neuroglancer. - + new **pcdl_render_neuroglancer** function, to render ome tiff images into neuroglancer. - - -+ branch v3: end-of-life reached. - -+ version 3.3.8 (2025-07-23): elmbeech/physicelldataloader - + command line commands now return **error code 0** if the command runs successfully. - -+ version 3.3.7 (2025-06-01): elmbeech/physicelldataloader - + compatible with current (non end-of-life cycle) python versions. - + minor bugfixes. - -+ version 3.3.6 (2025-05-13): elmbeech/physicelldataloader - + compatible with numpy >= 2.0.0 and current (non end-of-life cycle) python versions. - -+ version 3.3.5 (2025-05-13): elmbeech/physicelldataloader - + compatible with numpy < 2.0.0 and current (non end-of-life cycle) python versions. - + remove pyMCDS and pyMCDSts **make_ome_tiff** and pyCLI **pcdl_make_ome_tiff** to make pyMCS.py stand alone again. - + new TimeStep **get_cell_attribute_list** function, to retrieve a list of all tracked cell attribute labels. - + new **pcdl_get_cell_attribute_list** function, to retrieve a list of all tracked cell attribute labels. - -+ version 3.3.4 (2025-03-07): elmbeech/physicelldataloader - + replace the **aicsimageio** library dependency with its successor **bioio**. special thanks to Joel Eliason! - + **make_ome_tiff** can now handel generated file names with > 255 characters. special thank to Genevieve Stein-O'Brien and DanielBergman! - + **get_mesh_spacing** handels now an edge case correctly that would have resulted in a division by zero. special thanks to Randy Heiland! - -+ version 3.3.3 (2025-01-10): elmbeech/physicelldataloader - + bug fix **plot_contour** plot orientation. special thanks to Marco Ruscone! - + add test data for new improved **unittest physicell model**. special thanks to Nick Oldfather! - + add pyMCDS **make_conc_vtk** on the fly visualization. special thanks to Randy Heiland and Nick Oldfather! - + pyMCDS and pyMCDSts **make_graph_gml** and pyAnnData **get_anndata** handles now spring\_attached\_cells graph too. - -+ version 3.3.2 (2024-11-24): elmbeech/physicelldataloader - + **Warnings** will no longer be piped to standard output if verbose is set to False. - + pyMCDS **make_ome_tiff** function rewriten to be less RAM hungry and more versatile. - -+ version 3.3.1 (2024-09-22): elmbeech/physicelldataloader - + bugfix pyMCDS custom vectors loading. - -+ version 3.3.0 (2024-08-22): elmbeech/physicelldataloader - + **pip install pcdl**: will again install all library dependencies. The fine-tuned version was too confssing. - + pyMCDS handels now intracellular **physinboss** data too; data is stored in cell\_df. - + rename pyMCDS get\_cell\_variables to **get_celltype_list** for conciseness and order list by ID. - + rename pyMCDS get\_substrate\_names to **get_substrate_list** for conciseness and order list by ID. - + rename pyMCDS get\_scatter to **plot_scatter** for conciseness. - + rename pyMCDS get\_contour to **plot_contour** for conciseness. - + rename pyMCDSts make\_imgcell to **plot_scatter** for conciseness. - + rename pyMCDSts make\_imgconc to **plot_contour** for conciseness. - + rename pyMCDSts get\_cell\_df\_states to **get_cell_attribute** for conciseness. - + rename pyMCDSts get\_conc\_df\_states to **get_conc_attribute** for conciseness. - + rewrite pyMCDS mcds.get\_unit\_se into **mcds.get_unit_dict**. - + new pyCLI **pcdl_get_anndata** command line interface function. - + new pyCLI **pcdl_get_celltype_list** command line interface function. - + new pyCLI **pcdl_get_cell_attribute** command line interface function. - + new pyCLI **pcdl_get_cell_df** command line interface function. - + new pyCLI **pcdl_get_substrate_list** command line interface function. - + new pyCLI **pcdl_get_conc_attribute** command line interface function. - + new pyCLI **pcdl_get_conc_df** command line interface function. - + new pyCLI **pcdl_get_graph_gml** command line interface function. - + new pyCLI **pcdl_get_unit_dict** command line interface function. - + new pyCLI **pcdl_get_version** command line interface function. - + new pyCLI **pcdl_make_cell_vtk** command line interface function. - + new pyCLI **pcdl_make_conc_vtk** command line interface function. - + new pyCLI **pcdl_make_gif** command line interface function. - + new pyCLI **pcdl_make_movie** command line interface function. - + new pyCLI **pcdl_make_ome_tiff** command line interface function. - + new pyCLI **pcdl_plot_contour** command line interface function. - + new pyCLI **pcdl_plot_scatter** command line interface function. - + new pyCLI **pcdl_plot_timeseries** command line interface function. - + new pyMCDS **mcds.get_mesh_mnp** function, the mesh version from mcds.get\_voxel\_ijk. - + new pyMCDS **make_conc_vtk** function, to save substrate data as rectilinear grid vtk file. - + new pyMCDS **make_cell_vtk** function, to save cell data as glyph vtk file. - + new pyMCDS **make_graph_gml** function, to save graphs in a networkx and igraph compatible file format. - + new pyMCDS **make_ome_tiff** function, to save the output data in ome tiff file format. - + new pyMCDS **set_verbosity_true** function, to complete pcdl.TimeStep(verbosity=True/False) experience. - + new pyMCDS **set_verbosity_false** function, to complete pcdl.TimeStep(verbosity=True/False) experience. - + new pyMCDSts **get_cell_df** function, to extract one big dataframe or a list of dataframes from the whole time series. - + new pyMCDSts **get_conc_df** function, to extract one big dataframe or a list of dataframes from the whole time series. - + new pyMCDSts **make_cell_vtk** function, to save substrate data as rectilinear grid vtk files. special thanks to Furkan Kurtoglu! - + new pyMCDSts **make_conc_vtk** function, to save cell data as glyph vtk files. special thanks to Furkan Kurtoglu! - + new pyMCDSts **make_graph_gml** function, to save graphs in a networkx and igraph compatible files format. special thanks to Benjamin Jacobs! - + new pyMCDSts **make_ome_tiff** function, to save the output data in ome tiff file format. - + new pyMCDSts **plot_timeseries** function, to plot time series. special thanks to Thierry-Pascal Fleurant! - + new pyMCDSts **set_verbosity_true** function, to complete the pcdl.TimeSeries(verbosity=True/False) experience. - + new pyMCDSts **set_verbosity_false** function to complete the pcdl.TimeSeries(verbosity=True/False) experience. - -+ version 3.2.13 (2023-09-18): elmbeech/physicelldataloader - + rename pyMCDSts make\_imgsubs to **make_imgconc** for consistency. - + add **man/lecture/20230917_pcdl_repl_programming_analysis_plots.pdf** slide deck. - -+ version 3.2.12 (2023-08-12): elmbeech/physicelldataloader - + add **man/jupyter/pcdl_repl_programming.ipynb** : Jupyter notebook to give an idea about how to work with pcdl in a python3 REPL. - + add **man/lecture/20230808_pcws2023_session07_pcdl.pdf** slide deck. - + add github **continuous integration** for all supported python3 versions, all supported operating systems. - -+ version 3.2.11 (2023-08-08): elmbeech/physicelldataloader - + **pip install pcdl**: will only install the bare minimum library dependencies. - + **pip install pcdl[data]**: will install the minimum dependencies plus the dependencies to download the test dataset. - + **pip install pcdl[scverse]**: will install the minimum dependencies plus the dependencies needed to generate an anndata object. - + **pip install pcdl[all]**: will always install all dependencies. - + new TimeSeries **get_annmcds_list** function, which points to the self.l\_annmcds object. - + new pyMCDS **get_scatter** function is split off from pyMCDSts make\_imgcell. - + pyMCDSts **make_imgcell** and **make_imgsubs** bug fixes. - + TimeStep and TimeSeries **get_anndata** evolution. - -+ version 3.2.10 (2023-07-24): elmbeech/physicelldataloader - + rename pyMCDSts get\_cell\_df\_columns\_states to **get_cell_df_states** for conciseness. - + rename pyMCDSts get\_conc\_df\_columns\_states to **get_conc_df_states** for conciseness. - -+ version 3.2.9 (2023-07-23): elmbeech/physicelldataloader - + new class **TimeStep** can do everything pyMCDS can do and more. - + new class **TimeSeries** can do everything pyMCDSts can do and more. - + new TimeStep **get_anndata** function to transform physicell output into [AnnData](https://anndata.readthedocs.io/en/latest/) objects. - + new TimeSeries **get_anndata** function to transform physicell output into [AnnData](https://anndata.readthedocs.io/en/latest/) objects. - + internal pyAnnData **scaler** function. - + internal pyAnnData **\_anndextract** function. - + pyMCDS **_\_init__** seetingxml parameter changed from boolean to string to accept other PhysiCell\_settings.xml filenames than the default. - + pyMCDS **get_cell_df** drop and keep parameters to declare a set of columns to be dropped or kept. - + pyMCDS **get_conc_df** drop and keep parameters to declare a set of columns to be dropped or kept. - + new pyMCDS **get_conc_df** shorthand for get\_concentration\_df. - + pyMCDSts get\_cell\_minstate\_col reimplementation as **get_cell_df_columns_states** function. - + pyMCDSts get\_concentartion\_minstate\_col reimplementation as **get_conc_df_columns_states** function. - + new pyMCDSts **get_mcds_list** function which points to the self.l\_mcds object. - -+ version 3.2.8 (2023-06-21): elmbeech/physicelldataloader - + pyMCDS **get_concentration_df** states parameter to filter out non-informative variables. - + pyMCDS **get_cell_df** states parameter to filter out non-informative variables. - + pyMCDSts **_\_init__** load parameter to specify if the whole time series data straight at object initialization should be loaded. - + new pyMCDSts **get_cell_minstate_col** function to scan the whole time series for informative attributes. - + new pyMCDSts **get_concentartion_minstate_col** function to scan the whole time series for informative attributes. - -+ version 3.2.7 (2023-06-20): elmbeech/physicelldataloader - + pyMCDS and pyMCDSts **_\_init__** custom\_type parameter to specify other custom\_data variable types (int, bool, str) then the generic float. - -+ version 3.2.5 (2023-06-19): elmbeech/physicelldataloader - + pyMCDS resolves incompatibility with earlier PhysiCell and MultiCellDS versions. - -+ version 3.2.4 (2023-06-17): elmbeech/physicelldataloader - + pyMCDS **_\_init__** seetingxml parameter for cases where in the output folder no PhysiCell\_settings.xml can be found. - + pyMCDSts **mcdsts.make_imgcell** extrema parameter is replaced by the z\_axis parameter to account for numerical and categorical variable types. - -+ version 3.2.2 (2023-06-16): elmbeech/physicelldataloader - + pyMCDS **mcds.get_cell_df** sets distinct boolean, categorical, integer number, and real number variable types. categorical number codes are translated. for all spatial variables, the vector length value is calculated and added automatically. - + new pyMCDS **mcds.get_celltype_dict** function. - + new pyMCDS **mcds.get_substrate_dict** function. - + pyMCDSts **mcdsts.make_imgcell** and **mcdsts.make_imgsubs** functions improved. - -+ version 3.2.1 (2023-06-12): elmbeech/physicelldataloader - + pypa odyssey is coming to an end. - + change build system from setuptools to hatching. - + change the library name from pcDataLoader to pcdl. - + to make the library installation more lightweight, test data was excluded from the basic installation. - given the computer is connected to the internet, test data can easily be installed and removed with the **pcdl.install_data()** and **pcdl.uninstall_data()** functions now. - -+ version 3.0.7 (2023-06-08): elmbeech/physicelldataloader - + pyMCDSts: replaces the svg dependent **mcdsts.make_jpeg**, **mcdsts.make_png**, and **mcdsts.make_tiff** with **mcdsts.make_imgcell** and **mcdsts.make_imgsubs** which generate images straight out of the loaded data. the **mcdsts.make_gif** and **mcdsts.make_movie** functions were adjusted accordingly. special thanks to Marshal Gress! - + pyMCDSts: **mcdsts.read_mcds** loads now automatically all mcds snapshots if no xmlfile\_list is provided (default). - -+ version 3.0.6 (2023-04-29): elmbeech/physicelldataloader - + pyMCDS **\_read_xml** is now able to load time steps with zero cells. - + pyMCDS **mcds.get_contour** can handle more input parameters. - -+ version 3.0.5 (2023-02-26): elmbeech/physicelldataloader pyMCDS **mcds.get_contour** plots span now the whole domain and not only to the border voxel centers. -+ version 3.0.4 (2023-02-21): elmbeech/physicelldataloader pyMCDS **mcds.get_contour** function, to easily generate for substrates matplotlib contourf and contour plots because they do not exist as pandas plots. -+ version 3.0.3 (2023-02-19): elmbeech/physicelldataloader branch 3 has no longer anndata and, as such, hdf5 dependency. -+ version 3.0.2 (2023-01-06): elmbeech/physicelldataloader bugfix installing package data. -+ version 3.0.0 (2023-01-06): elmbeech/physicelldataloader - + **pyMCDS** parameter **xml_file** can now handle path/file.xml (unix) or path\file.xml (dos) input, as long output_path is the default. - + **pyMCDS** has a new additional boolean **microenv** parameter, to specify if the microenvironment (substrates) should be read (for completeness) or not (for speed increase and less memory usage). - + **pyMCDS** has a new additional boolean **graph** parameter, to specify if the attached and neighbor graph should be read. - + **pyMCDS** has a new additional boolean **verbose** parameter, to specify if there should be text output while processing. - + pyMCDS **mcds.get_2D_mesh** was renamed to **mcds.get_mesh_2D** for consistency. - + pyMCDS **mcds.get_linear_voxels** was renamed to **mcds.get_mesh_coordinate** for consistency. - + pyMCDS **mcds.get_containing_voxel_ijk** was renamed to **mcds.get_voxel_ijk** for briefness. - + pyMCDS **mcds.get_voxel_spacing** returns now 3 specific values, one for x, y, and z, instead of 1 general value. - + pyMCDS **mcds.get_concentrations** was renamed to **mcds.get_concentration** for consistency - + pyMCDS **mcds.get_concentrations_at** was renamed to **mcds.get_concentration_at** for consistency - + pyMCDS **mcds.get_concentration_at** if z_slice is not a mesh center value, the function will by default adjust to the nearest and no longer break. - + pyMCDS **mcds.get_cell_variables** and **mcds.get_substrate_names** return now a strictly alphabetically ordered list. - + pyMCDS **mcds.get_cell_df** returns now a pandas dataframe with the cell IDs as the index and not as a column. - additionally, this dataframe contains now voxel, mesh_center, substrate parameter, substrate concentration, and cell density information too. - + new pyMCDS **mcds.get_concentration_df** function. - + new pyMCDS **mcds.get_substrate_df** function. - + new pyMCDS **mcds.get_unit_se** function. - + new pyMCDS **mcds.get_multicellds_version** function. - + new pyMCDS **mcds.get_physicell_version** function. - + new pyMCDS **mcds.get_runtime** function. - + new pyMCDS **mcds.get_timestamp** function. - + new pyMCDS **mcds.get_voxel_ijk_range** function. - + new pyMCDS **mcds.get_voxel_ijk_axis** function. - + new pyMCDS **mcds.get_voxel_spacing** function. - + new pyMCDS **mcds.get_voxel_volume** function. - + new pyMCDS **mcds.get_mesh_mnp_range** function. - + new pyMCDS **mcds.get_mesh_mnp_axis** function. - + new pyMCDS **mcds.get_xyz_range** function. - + new pyMCDS **mcds.is_in_mesh** function. - + new pyMCDS **mcds.get_attached_graph_dict** function. - + new pyMCDS **mcds.get_neigbor_graph_dict** function. - + class **pyMCDS_timeseries** was renamed to **pyMCDSts** and completely rewritten. - + new pyMCDSts **get_xmlfile_list** function. - + new pyMCDSts **read_mcds** function. - + new pyMCDSts **make_jpeg** function. - + new pyMCDSts **make_png** function. - + new pyMCDSts **make_tiff** function. - + new pyMCDSts **make_gif** function. - + new pyMCDSts **make_movie** function. - + all **plotting** functions were removed because pcdl only focuses on making the raw data in python3 easy to access for in-depth analysis. - + cell position coordinates are now constantly labeled as **x,y,z**, mesh center coordinates as **m,n,p**, and voxel coordinates as **i,j,k**. - + the underling [mcds object data dictionary structure](https://github.com/elmbeech/physicelldataloader/tree/master/man/img/physicelldataloader_data_dictionary_v3.0.0.png) has changed. - + [pytest](https://en.wikipedia.org/wiki/Pytest) unit tests exist now for all pyMCDS and pyMCDSts functions. - - -+ branch v2: end-of-life reached. - -+ version 2.0.3 (2023-06-16): elmbeech/physicelldataloader pypa odyssey is coming to an end. -+ version 2.0.2 (2023-01-06): elmbeech/physicelldataloader reset patch voxel spacing bugfix, so that branch2 is full compatible with branch1 again. use branch3 for a bugfixed version! -+ version 2.0.1 (2022-11-08): elmbeech/physicelldataloader beta release patch voxel spacing bugfix. -+ version 2.0.0 (2022-08-30): elmbeech/physicelldataloader pip installable release, derived from and compatible with PhysiCell-Tools/python-loader release 1.1.0 (2022-07-20). - - -+ branch v1: end-of-life reached. - -+ version 1.1.1 (2022-07-01): elmbeech/physicelldataloader deprecated np.float replaced with np.float64. -+ version 1.1.0 (2022-05-09): Physicell-Tools/python-loader release compatible with pre-v1.10.x of PhysiCell. -+ version 1.0.1 (2020-01-25): Physicell-Tools/python-loader time-series related bug fix. -+ version 1.0.0 (2019-09-28): Physicell-Tools/python-loader first public release! +Developers, please make pull requests to the https://github.com/elmbeech/physicelldataloader/tree/development branch. Thanks! diff --git a/man/RELEASENOTES.md b/man/RELEASENOTES.md new file mode 100644 index 0000000..2b01d61 --- /dev/null +++ b/man/RELEASENOTES.md @@ -0,0 +1,305 @@ +## Road Map: + ++ [issue #12](https://github.com/elmbeech/physicelldataloader/issues/12): downcast, read, and save to\_pcd functions. ++ [issue #16](https://github.com/elmbeech/physicelldataloader/issues/16): lineage tree graph output files. + + +## Release Notes: + ++ branch v4: active feature development, bug fixes, and security maintenance. + ++ version 4.1.9 (2026-08-28): elmbeech/physicelldataloader + + new TimeSeries **make_simularium** and **pcdl_make_simularium** function. special thanks to Randy Heiland! + ++ version 4.1.8 (2026-08-26): elmbeech/physicelldataloader + + branch v4 compatible with bioconda and galaxy. special thanks to Björn Grüning! + ++ version 4.1.7 (2026-08-21): elmbeech/physicelldataloader + + compatible with pandas v3.0.0. + ++ version 4.1.6 (2026-08-20): elmbeech/physicelldataloader + + pcdl installs now by default lightweight with only the core dependencies; users are prompted to install optional libraries if necessary. special thanks to Heber Rocha! + ++ version 4.1.5 (2026-04-05): elmbeech/physicelldataloader + + bugfix library dependencies and library versions. + ++ version 4.1.4 (2026-04-04): elmbeech/physicelldataloader + + TimeSeries **mcdsts.make_contour** extrema parameter is replaced by vmin and vmax to be compatible with mcds.make\_contour and plt.contour. + + **make_cell_vtk** and **make_conc_vtk** now offer an ext parameter, allowing to manually specify the exact file extension. special thanks to Danyon Gedris! + + new **make_muspan** TimeStep class and TimeSeries class function and **pcdl_get_muspan** command line command. special thanks to Joshua Moore and Joshua Bull! + ++ version 4.1.3 (2026-03-21): elmbeech/physicelldataloader + + new **pcdl.pccmap** color map. this is an adaptation of the physicell pathology paint by number color map. + + new TimeStep **get_pcdl_version** function. + ++ version 4.1.2 (2026-03-06): elmbeech/physicelldataloader + + new **custom_data_astype** TimeStep class and TimeSeries class function to set the dtype of custom\_data variables even after the timestep or timeseries is loaded. + + TimeSeries \_\_init\_\_ function can now handle a list of TimeStep objects as input instead of a path. + ++ version 4.1.1 (2026-02-28): elmbeech/physicelldataloader + + reduced memory footprint. + ++ version 4.1.0 (2025-12-31): elmbeech/physicelldataloader + + new **get_spatialdata** TimeStep class and TimeSeries class function and **pcdl_get_spatialdata** command line command. special thanks to Luca Marconato! + + with this release, pcdl officially became an [scverse ecosystem](https://scverse.org/packages/#ecosystem) package. + ++ version 4.0.5 (2025-10-22): elmbeech/physicelldataloader + + **settingxml** default is now set to False, because the cell\_type id label mapping can, in recent PhysiCell output, be retrieved from output\*.xml too. + + **plot_scatter** and **plot_timeseries** now additionally have a cat\_drop and cat\_keep argument to filter categorical data. + + **plot_timeseries(frame=conc)** now plots by default all substrate concentrations over time. + + **plot_timeseries(ext=)** parameter offers to return a dataframe object, dafaframe csv file, image file, or a matplotlib fig object. special thanks to John Nardini and Edward Young! + ++ version 4.0.4 (2025-07-23): elmbeech/physicelldataloader + + command line commands now return **error code 0** if the command runs successfully. + ++ version 4.0.3 (2025-07-20): elmbeech/physicelldataloader + + TimeStep and TimeSeries **plot_contour**, **plot_scatter**, and **plot_timeseries** handle now **kwargs** arguments. + + minor bugfixes. + ++ version 4.0.2 (2025-06-29): elmbeech/physicelldataloader + + minor bugfixes. + ++ version 4.0.1 (2025-06-24): elmbeech/physicelldataloader + + man updated. + + minor bugfixes. + ++ version 4.0.0 (2025-05-13): elmbeech/physicelldataloader + + v4 was forked from v3.3.4! + + **mcds.data** struct was rewritten in more python less c++ way. + + pyMCDS.py and part of pyAnnData.py was fused to **timestep.py**. + + pyMCDSts.py and part of pyAnnData.py was fused to **timeseries.py**. + + pyCLI.py was renames to **commandline.py**. + + data\_timeseries.py was renamed to **output_data.py**. + + TimeStep function **get_concentration** was deprecated because pandas already has this functionlity. + + TimeStep function **get_concentration_at** was deprecated because pandas already has this functionlity. + + TimeStep function **get_cell_df_at** was deprecated because pandas already has this functionlity. + + **make_conc_vtk** and **make_cell_vtk** on the fly visualization was removed because paraview is good enough. + + new TimeStep **get_cell_attribute_list** function, to retrieve a list of all tracked cell attribute labels. + + new **pcdl_get_cell_attribute_list** function, to retrieve a list of all tracked cell attribute labels. + + new **render_neuroglancer** function, to render ome tiff image into neuroglancer. + + new **pcdl_render_neuroglancer** function, to render ome tiff images into neuroglancer. + + ++ branch v3: end-of-life reached. + ++ version 3.3.8 (2025-07-23): elmbeech/physicelldataloader + + command line commands now return **error code 0** if the command runs successfully. + ++ version 3.3.7 (2025-06-01): elmbeech/physicelldataloader + + compatible with current (non end-of-life cycle) python versions. + + minor bugfixes. + ++ version 3.3.6 (2025-05-13): elmbeech/physicelldataloader + + compatible with numpy >= 2.0.0 and current (non end-of-life cycle) python versions. + ++ version 3.3.5 (2025-05-13): elmbeech/physicelldataloader + + compatible with numpy < 2.0.0 and current (non end-of-life cycle) python versions. + + remove pyMCDS and pyMCDSts **make_ome_tiff** and pyCLI **pcdl_make_ome_tiff** to make pyMCS.py stand alone again. + + new TimeStep **get_cell_attribute_list** function, to retrieve a list of all tracked cell attribute labels. + + new **pcdl_get_cell_attribute_list** function, to retrieve a list of all tracked cell attribute labels. + ++ version 3.3.4 (2025-03-07): elmbeech/physicelldataloader + + replace the **aicsimageio** library dependency with its successor **bioio**. special thanks to Joel Eliason! + + **make_ome_tiff** can now handel generated file names with > 255 characters. special thank to Genevieve Stein-O'Brien and DanielBergman! + + **get_mesh_spacing** handels now an edge case correctly that would have resulted in a division by zero. special thanks to Randy Heiland! + ++ version 3.3.3 (2025-01-10): elmbeech/physicelldataloader + + bug fix **plot_contour** plot orientation. special thanks to Marco Ruscone! + + add test data for new improved **unittest physicell model**. special thanks to Nick Oldfather! + + add pyMCDS **make_conc_vtk** on the fly visualization. special thanks to Randy Heiland and Nick Oldfather! + + pyMCDS and pyMCDSts **make_graph_gml** and pyAnnData **get_anndata** handles now spring\_attached\_cells graph too. + ++ version 3.3.2 (2024-11-24): elmbeech/physicelldataloader + + **Warnings** will no longer be piped to standard output if verbose is set to False. + + pyMCDS **make_ome_tiff** function rewriten to be less RAM hungry and more versatile. + ++ version 3.3.1 (2024-09-22): elmbeech/physicelldataloader + + bugfix pyMCDS custom vectors loading. + ++ version 3.3.0 (2024-08-22): elmbeech/physicelldataloader + + **pip install pcdl**: will again install all library dependencies. The fine-tuned version was too confssing. + + pyMCDS handels now intracellular **physinboss** data too; data is stored in cell\_df. + + rename pyMCDS get\_cell\_variables to **get_celltype_list** for conciseness and order list by ID. + + rename pyMCDS get\_substrate\_names to **get_substrate_list** for conciseness and order list by ID. + + rename pyMCDS get\_scatter to **plot_scatter** for conciseness. + + rename pyMCDS get\_contour to **plot_contour** for conciseness. + + rename pyMCDSts make\_imgcell to **plot_scatter** for conciseness. + + rename pyMCDSts make\_imgconc to **plot_contour** for conciseness. + + rename pyMCDSts get\_cell\_df\_states to **get_cell_attribute** for conciseness. + + rename pyMCDSts get\_conc\_df\_states to **get_conc_attribute** for conciseness. + + rewrite pyMCDS mcds.get\_unit\_se into **mcds.get_unit_dict**. + + new pyCLI **pcdl_get_anndata** command line interface function. + + new pyCLI **pcdl_get_celltype_list** command line interface function. + + new pyCLI **pcdl_get_cell_attribute** command line interface function. + + new pyCLI **pcdl_get_cell_df** command line interface function. + + new pyCLI **pcdl_get_substrate_list** command line interface function. + + new pyCLI **pcdl_get_conc_attribute** command line interface function. + + new pyCLI **pcdl_get_conc_df** command line interface function. + + new pyCLI **pcdl_get_graph_gml** command line interface function. + + new pyCLI **pcdl_get_unit_dict** command line interface function. + + new pyCLI **pcdl_get_version** command line interface function. + + new pyCLI **pcdl_make_cell_vtk** command line interface function. + + new pyCLI **pcdl_make_conc_vtk** command line interface function. + + new pyCLI **pcdl_make_gif** command line interface function. + + new pyCLI **pcdl_make_movie** command line interface function. + + new pyCLI **pcdl_make_ome_tiff** command line interface function. + + new pyCLI **pcdl_plot_contour** command line interface function. + + new pyCLI **pcdl_plot_scatter** command line interface function. + + new pyCLI **pcdl_plot_timeseries** command line interface function. + + new pyMCDS **mcds.get_mesh_mnp** function, the mesh version from mcds.get\_voxel\_ijk. + + new pyMCDS **make_conc_vtk** function, to save substrate data as rectilinear grid vtk file. + + new pyMCDS **make_cell_vtk** function, to save cell data as glyph vtk file. + + new pyMCDS **make_graph_gml** function, to save graphs in a networkx and igraph compatible file format. + + new pyMCDS **make_ome_tiff** function, to save the output data in ome tiff file format. + + new pyMCDS **set_verbosity_true** function, to complete pcdl.TimeStep(verbosity=True/False) experience. + + new pyMCDS **set_verbosity_false** function, to complete pcdl.TimeStep(verbosity=True/False) experience. + + new pyMCDSts **get_cell_df** function, to extract one big dataframe or a list of dataframes from the whole time series. + + new pyMCDSts **get_conc_df** function, to extract one big dataframe or a list of dataframes from the whole time series. + + new pyMCDSts **make_cell_vtk** function, to save substrate data as rectilinear grid vtk files. special thanks to Furkan Kurtoglu! + + new pyMCDSts **make_conc_vtk** function, to save cell data as glyph vtk files. special thanks to Furkan Kurtoglu! + + new pyMCDSts **make_graph_gml** function, to save graphs in a networkx and igraph compatible files format. special thanks to Benjamin Jacobs! + + new pyMCDSts **make_ome_tiff** function, to save the output data in ome tiff file format. + + new pyMCDSts **plot_timeseries** function, to plot time series. special thanks to Thierry-Pascal Fleurant! + + new pyMCDSts **set_verbosity_true** function, to complete the pcdl.TimeSeries(verbosity=True/False) experience. + + new pyMCDSts **set_verbosity_false** function to complete the pcdl.TimeSeries(verbosity=True/False) experience. + ++ version 3.2.13 (2023-09-18): elmbeech/physicelldataloader + + rename pyMCDSts make\_imgsubs to **make_imgconc** for consistency. + + add **man/lecture/20230917_pcdl_repl_programming_analysis_plots.pdf** slide deck. + ++ version 3.2.12 (2023-08-12): elmbeech/physicelldataloader + + add **man/jupyter/pcdl_repl_programming.ipynb** : Jupyter notebook to give an idea about how to work with pcdl in a python3 REPL. + + add **man/lecture/20230808_pcws2023_session07_pcdl.pdf** slide deck. + + add github **continuous integration** for all supported python3 versions, all supported operating systems. + ++ version 3.2.11 (2023-08-08): elmbeech/physicelldataloader + + **pip install pcdl**: will only install the bare minimum library dependencies. + + **pip install pcdl[data]**: will install the minimum dependencies plus the dependencies to download the test dataset. + + **pip install pcdl[scverse]**: will install the minimum dependencies plus the dependencies needed to generate an anndata object. + + **pip install pcdl[all]**: will always install all dependencies. + + new TimeSeries **get_annmcds_list** function, which points to the self.l\_annmcds object. + + new pyMCDS **get_scatter** function is split off from pyMCDSts make\_imgcell. + + pyMCDSts **make_imgcell** and **make_imgsubs** bug fixes. + + TimeStep and TimeSeries **get_anndata** evolution. + ++ version 3.2.10 (2023-07-24): elmbeech/physicelldataloader + + rename pyMCDSts get\_cell\_df\_columns\_states to **get_cell_df_states** for conciseness. + + rename pyMCDSts get\_conc\_df\_columns\_states to **get_conc_df_states** for conciseness. + ++ version 3.2.9 (2023-07-23): elmbeech/physicelldataloader + + new class **TimeStep** can do everything pyMCDS can do and more. + + new class **TimeSeries** can do everything pyMCDSts can do and more. + + new TimeStep **get_anndata** function to transform physicell output into [AnnData](https://anndata.readthedocs.io/en/latest/) objects. + + new TimeSeries **get_anndata** function to transform physicell output into [AnnData](https://anndata.readthedocs.io/en/latest/) objects. + + internal pyAnnData **scaler** function. + + internal pyAnnData **\_anndextract** function. + + pyMCDS **_\_init__** seetingxml parameter changed from boolean to string to accept other PhysiCell\_settings.xml filenames than the default. + + pyMCDS **get_cell_df** drop and keep parameters to declare a set of columns to be dropped or kept. + + pyMCDS **get_conc_df** drop and keep parameters to declare a set of columns to be dropped or kept. + + new pyMCDS **get_conc_df** shorthand for get\_concentration\_df. + + pyMCDSts get\_cell\_minstate\_col reimplementation as **get_cell_df_columns_states** function. + + pyMCDSts get\_concentartion\_minstate\_col reimplementation as **get_conc_df_columns_states** function. + + new pyMCDSts **get_mcds_list** function which points to the self.l\_mcds object. + ++ version 3.2.8 (2023-06-21): elmbeech/physicelldataloader + + pyMCDS **get_concentration_df** states parameter to filter out non-informative variables. + + pyMCDS **get_cell_df** states parameter to filter out non-informative variables. + + pyMCDSts **_\_init__** load parameter to specify if the whole time series data straight at object initialization should be loaded. + + new pyMCDSts **get_cell_minstate_col** function to scan the whole time series for informative attributes. + + new pyMCDSts **get_concentartion_minstate_col** function to scan the whole time series for informative attributes. + ++ version 3.2.7 (2023-06-20): elmbeech/physicelldataloader + + pyMCDS and pyMCDSts **_\_init__** custom\_type parameter to specify other custom\_data variable types (int, bool, str) then the generic float. + ++ version 3.2.5 (2023-06-19): elmbeech/physicelldataloader + + pyMCDS resolves incompatibility with earlier PhysiCell and MultiCellDS versions. + ++ version 3.2.4 (2023-06-17): elmbeech/physicelldataloader + + pyMCDS **_\_init__** seetingxml parameter for cases where in the output folder no PhysiCell\_settings.xml can be found. + + pyMCDSts **mcdsts.make_imgcell** extrema parameter is replaced by the z\_axis parameter to account for numerical and categorical variable types. + ++ version 3.2.2 (2023-06-16): elmbeech/physicelldataloader + + pyMCDS **mcds.get_cell_df** sets distinct boolean, categorical, integer number, and real number variable types. categorical number codes are translated. for all spatial variables, the vector length value is calculated and added automatically. + + new pyMCDS **mcds.get_celltype_dict** function. + + new pyMCDS **mcds.get_substrate_dict** function. + + pyMCDSts **mcdsts.make_imgcell** and **mcdsts.make_imgsubs** functions improved. + ++ version 3.2.1 (2023-06-12): elmbeech/physicelldataloader + + pypa odyssey is coming to an end. + + change build system from setuptools to hatching. + + change the library name from pcDataLoader to pcdl. + + to make the library installation more lightweight, test data was excluded from the basic installation. + given the computer is connected to the internet, test data can easily be installed and removed with the **pcdl.install_data()** and **pcdl.uninstall_data()** functions now. + ++ version 3.0.7 (2023-06-08): elmbeech/physicelldataloader + + pyMCDSts: replaces the svg dependent **mcdsts.make_jpeg**, **mcdsts.make_png**, and **mcdsts.make_tiff** with **mcdsts.make_imgcell** and **mcdsts.make_imgsubs** which generate images straight out of the loaded data. the **mcdsts.make_gif** and **mcdsts.make_movie** functions were adjusted accordingly. special thanks to Marshal Gress! + + pyMCDSts: **mcdsts.read_mcds** loads now automatically all mcds snapshots if no xmlfile\_list is provided (default). + ++ version 3.0.6 (2023-04-29): elmbeech/physicelldataloader + + pyMCDS **\_read_xml** is now able to load time steps with zero cells. + + pyMCDS **mcds.get_contour** can handle more input parameters. + ++ version 3.0.5 (2023-02-26): elmbeech/physicelldataloader pyMCDS **mcds.get_contour** plots span now the whole domain and not only to the border voxel centers. ++ version 3.0.4 (2023-02-21): elmbeech/physicelldataloader pyMCDS **mcds.get_contour** function, to easily generate for substrates matplotlib contourf and contour plots because they do not exist as pandas plots. ++ version 3.0.3 (2023-02-19): elmbeech/physicelldataloader branch 3 has no longer anndata and, as such, hdf5 dependency. ++ version 3.0.2 (2023-01-06): elmbeech/physicelldataloader bugfix installing package data. ++ version 3.0.0 (2023-01-06): elmbeech/physicelldataloader + + **pyMCDS** parameter **xml_file** can now handle path/file.xml (unix) or path\file.xml (dos) input, as long output_path is the default. + + **pyMCDS** has a new additional boolean **microenv** parameter, to specify if the microenvironment (substrates) should be read (for completeness) or not (for speed increase and less memory usage). + + **pyMCDS** has a new additional boolean **graph** parameter, to specify if the attached and neighbor graph should be read. + + **pyMCDS** has a new additional boolean **verbose** parameter, to specify if there should be text output while processing. + + pyMCDS **mcds.get_2D_mesh** was renamed to **mcds.get_mesh_2D** for consistency. + + pyMCDS **mcds.get_linear_voxels** was renamed to **mcds.get_mesh_coordinate** for consistency. + + pyMCDS **mcds.get_containing_voxel_ijk** was renamed to **mcds.get_voxel_ijk** for briefness. + + pyMCDS **mcds.get_voxel_spacing** returns now 3 specific values, one for x, y, and z, instead of 1 general value. + + pyMCDS **mcds.get_concentrations** was renamed to **mcds.get_concentration** for consistency + + pyMCDS **mcds.get_concentrations_at** was renamed to **mcds.get_concentration_at** for consistency + + pyMCDS **mcds.get_concentration_at** if z_slice is not a mesh center value, the function will by default adjust to the nearest and no longer break. + + pyMCDS **mcds.get_cell_variables** and **mcds.get_substrate_names** return now a strictly alphabetically ordered list. + + pyMCDS **mcds.get_cell_df** returns now a pandas dataframe with the cell IDs as the index and not as a column. + additionally, this dataframe contains now voxel, mesh_center, substrate parameter, substrate concentration, and cell density information too. + + new pyMCDS **mcds.get_concentration_df** function. + + new pyMCDS **mcds.get_substrate_df** function. + + new pyMCDS **mcds.get_unit_se** function. + + new pyMCDS **mcds.get_multicellds_version** function. + + new pyMCDS **mcds.get_physicell_version** function. + + new pyMCDS **mcds.get_runtime** function. + + new pyMCDS **mcds.get_timestamp** function. + + new pyMCDS **mcds.get_voxel_ijk_range** function. + + new pyMCDS **mcds.get_voxel_ijk_axis** function. + + new pyMCDS **mcds.get_voxel_spacing** function. + + new pyMCDS **mcds.get_voxel_volume** function. + + new pyMCDS **mcds.get_mesh_mnp_range** function. + + new pyMCDS **mcds.get_mesh_mnp_axis** function. + + new pyMCDS **mcds.get_xyz_range** function. + + new pyMCDS **mcds.is_in_mesh** function. + + new pyMCDS **mcds.get_attached_graph_dict** function. + + new pyMCDS **mcds.get_neigbor_graph_dict** function. + + class **pyMCDS_timeseries** was renamed to **pyMCDSts** and completely rewritten. + + new pyMCDSts **get_xmlfile_list** function. + + new pyMCDSts **read_mcds** function. + + new pyMCDSts **make_jpeg** function. + + new pyMCDSts **make_png** function. + + new pyMCDSts **make_tiff** function. + + new pyMCDSts **make_gif** function. + + new pyMCDSts **make_movie** function. + + all **plotting** functions were removed because pcdl only focuses on making the raw data in python3 easy to access for in-depth analysis. + + cell position coordinates are now constantly labeled as **x,y,z**, mesh center coordinates as **m,n,p**, and voxel coordinates as **i,j,k**. + + the underling [mcds object data dictionary structure](https://github.com/elmbeech/physicelldataloader/tree/master/man/img/physicelldataloader_data_dictionary_v3.0.0.png) has changed. + + [pytest](https://en.wikipedia.org/wiki/Pytest) unit tests exist now for all pyMCDS and pyMCDSts functions. + + ++ branch v2: end-of-life reached. + ++ version 2.0.3 (2023-06-16): elmbeech/physicelldataloader pypa odyssey is coming to an end. ++ version 2.0.2 (2023-01-06): elmbeech/physicelldataloader reset patch voxel spacing bugfix, so that branch2 is full compatible with branch1 again. use branch3 for a bugfixed version! ++ version 2.0.1 (2022-11-08): elmbeech/physicelldataloader beta release patch voxel spacing bugfix. ++ version 2.0.0 (2022-08-30): elmbeech/physicelldataloader pip installable release, derived from and compatible with PhysiCell-Tools/python-loader release 1.1.0 (2022-07-20). + + ++ branch v1: end-of-life reached. + ++ version 1.1.1 (2022-07-01): elmbeech/physicelldataloader deprecated np.float replaced with np.float64. ++ version 1.1.0 (2022-05-09): Physicell-Tools/python-loader release compatible with pre-v1.10.x of PhysiCell. ++ version 1.0.1 (2020-01-25): Physicell-Tools/python-loader time-series related bug fix. ++ version 1.0.0 (2019-09-28): Physicell-Tools/python-loader first public release! diff --git a/man/docstring/mcds.render_neuroglancer.md b/man/docstring/mcds.render_neuroglancer.md new file mode 100644 index 0000000..d393610 --- /dev/null +++ b/man/docstring/mcds.render_neuroglancer.md @@ -0,0 +1,7 @@ +# mcds.render_neuroglancer() + + +help(pcdl.render_neuroglancer) +try: mcds.render_neuroglancer(mcds.make_ome_tiff()) + +``` \ No newline at end of file diff --git a/man/docstring/mcdsts.get_sdmcds_list.md b/man/docstring/mcdsts.get_sdmcds_list.md new file mode 100644 index 0000000..9b6dc9f --- /dev/null +++ b/man/docstring/mcdsts.get_sdmcds_list.md @@ -0,0 +1,22 @@ +# mcdsts.get_sdmcds_list() + + +## input: +``` + self: TimeSeries class instance. + +``` + +## output: +``` + self.l_sdmcds: list of chronologically ordered spatialdata mcds objects. + watch out, this is a pointer to the + self.l_sdmcds list of spdata mcds objects, not a copy of self.l_sdmcds! + +``` + +## description: +``` + function returns a binding to the self.l_sdmcds list of spdata mcds objects. + +``` \ No newline at end of file diff --git a/man/docstring/mcdsts.make_gif.md b/man/docstring/mcdsts.make_gif.md new file mode 100644 index 0000000..74eb90d --- /dev/null +++ b/man/docstring/mcdsts.make_gif.md @@ -0,0 +1,8 @@ +# mcdsts.make_gif() + + +help(pcdl.make_gif) +try: mcdsts.make_gif(mcdsts.plot_scatter()) +try: mcdsts.make_gif(mcdsts.plot_contour('substrate')) + +``` \ No newline at end of file diff --git a/man/docstring/mcdsts.make_movie.md b/man/docstring/mcdsts.make_movie.md new file mode 100644 index 0000000..b416acd --- /dev/null +++ b/man/docstring/mcdsts.make_movie.md @@ -0,0 +1,8 @@ +# mcdsts.make_movie() + + +help(pcdl.make_movie) +try: mcdsts.make_movie(mcdsts.plot_scatter()) +try: mcdsts.make_movie(mcdsts.plot_contour('substrate')) + +``` \ No newline at end of file diff --git a/man/docstring/mcdsts.render_neuroglancer.md b/man/docstring/mcdsts.render_neuroglancer.md new file mode 100644 index 0000000..f4d8730 --- /dev/null +++ b/man/docstring/mcdsts.render_neuroglancer.md @@ -0,0 +1,7 @@ +# mcdsts.render_neuroglancer() + + +help(pcdl.render_neuroglancer) +try: mcdsts.render_neuroglancer(mcdsts.make_ome_tiff(), 0) + +``` \ No newline at end of file diff --git a/man/img/physicelldataloader_concept_v4.0.0.jpg b/man/img/physicelldataloader_concept_v4.0.0.jpg index 888ec28fcc2842bdc071112a12b8a4106680127b..1a24bf3287c1538f60ae31b3240015590c77c8ce 100644 GIT binary patch literal 260155 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