diff --git a/.github/workflows/apple.yml b/.github/workflows/apple.yml index 8fb3e21..b76c849 100644 --- a/.github/workflows/apple.yml +++ b/.github/workflows/apple.yml @@ -25,16 +25,16 @@ jobs: PYTHONPATH: /Users/runner/work/physicelldataloader/physicelldataloader steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@main - name: set up python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@main with: python-version: ${{ matrix.python-version }} - name: install dependencies run: | brew install ffmpeg imagemagick python -m pip install --upgrade pip - python -m pip install flake8 pytest anndata bioio bioio-ome-tiff matplotlib neuroglancer numpy pandas requests scikit-image scipy vtk + python -m pip install flake8 pytest anndata bioio bioio-ome-tiff matplotlib networkx neuroglancer numpy pandas requests scikit-image scipy vtk "ome-zarr<0.14.0" 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 8c0a067..b860afb 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -25,16 +25,17 @@ jobs: PYTHONPATH: /home/runner/work/physicelldataloader/physicelldataloader steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@main - name: set up python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@main with: python-version: ${{ matrix.python-version }} - name: install dependencies run: | + 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 matplotlib neuroglancer numpy pandas requests scikit-image scipy vtk + python -m pip install flake8 pytest anndata bioio bioio-ome-tiff matplotlib networkx neuroglancer numpy pandas requests scikit-image scipy vtk "ome-zarr<0.14.0" 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 6d8260b..9345835 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -25,16 +25,16 @@ jobs: PYTHONPATH: D:\a\physicelldataloader\physicelldataloader steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@main - name: set up python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@main with: python-version: ${{ matrix.python-version }} - name: install dependencies run: | choco install ffmpeg imagemagick python -m pip install --upgrade pip - python -m pip install flake8 pytest anndata bioio bioio-ome-tiff matplotlib neuroglancer numpy pandas requests scikit-image scipy vtk + python -m pip install flake8 pytest anndata bioio bioio-ome-tiff matplotlib networkx neuroglancer numpy pandas requests scikit-image scipy vtk "ome-zarr<0.14.0" 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/README.md b/README.md index 79b0a40..91d0adb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Abstract: -physicelldataloader (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. @@ -23,7 +23,7 @@ The pcdl python3 library maintains four branches: ## Header: + Language: python [>= 3.11](https://devguide.python.org/versions/) -+ Library dependencies: anndata, bioio, geopandas, matplotlib, neuroglancer, numpy, pandas, (requests), scikit-image, scipy, shapely, spatialdata, vtk ++ Library dependencies: anndata, bioio, geopandas, matplotlib, networkx, neuroglancer, numpy, pandas, (requests), scikit-image, scipy, shapely, 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 @@ -49,7 +49,8 @@ Basics Tutorials: Extras tutorials python3 language: + [pcdl and python3 and json](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_json.md) + [pcdl and python3 and pandas](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_pandas.md) -+ [pcdl and python3 and scipy and scanpy](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_python3_scverse.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) @@ -129,6 +130,18 @@ Developers, please make pull requests to the https://github.com/elmbeech/physice ## Release Notes: ++ version 4.1.5 (2026-04-05) + + bugfix library dependencies and library versions. + ++ version 4.1.4 (2026-04-04) + + pyMCDSts **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! + ++ version 4.1.3 (2026-03-21): + + 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. @@ -137,13 +150,14 @@ 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. + + new **get_spatialdata** TimeStep class and TimeSeris 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. + + **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. diff --git a/man/REFERENCE.md b/man/REFERENCE.md index 076deb6..06d04c3 100644 --- a/man/REFERENCE.md +++ b/man/REFERENCE.md @@ -46,6 +46,7 @@ Basically, there are four types of functions: ### 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_physicell_version)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcds.get_physicell_version.md) #! workhorse function *time* @@ -99,6 +100,8 @@ Basically, there are four types of functions: + [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 ++ [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 @@ -146,6 +149,8 @@ Basically, there are four types of functions: + [help(mcdsts.get_graph_gml)](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/mcdsts.make_graph_gml.md) #! workhose function ### 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 @@ -196,6 +201,8 @@ The command line interface functions mimic the name and parameter arguments as c + [pcdl_make_graph_gml --help](https://github.com/elmbeech/physicelldataloader/tree/master/man/docstring/pcdl_make_graph_gml.md) #! workhorse function ### 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 diff --git a/man/TUTORIAL_blender.md b/man/TUTORIAL_blender.md index d5e05ca..cad8ba3 100644 --- a/man/TUTORIAL_blender.md +++ b/man/TUTORIAL_blender.md @@ -10,13 +10,16 @@ And there exists a bioxel nodes plugin, that lets us load ome tiff files. The blender bvtk nodes plugin allows us to load vtk files into blender. +Note, to be able to load whole time series, the blender bvtk nodes plugin needs a simplified output00000000.vtp file name and extension (which is different from the pcdl default output00000000\_cell.vtp). +This is why the ext parameter explicitly has to be set. + ### Generate vtk files from the command line ```bash pcdl_make_conc_vtk output ``` ```bash -pcdl_make_cell_vtk output +pcdl_make_cell_vtk output --ext .vtp # blender bvtk nodes compatible filename and extension. ``` ### Generate vtk files from within python @@ -26,7 +29,7 @@ import pcdl mcdsts = pcdl.TimeSeries('output/') mcdsts.make_conc_vtk() -mcdsts.make_cell_vtk() +mcdsts.make_cell_vtk(ext='.vtp') # blender bvtk nodes compatible filename and extension. ``` ### Blender vtk nodes plugin installation diff --git a/man/TUTORIAL_commandline.md b/man/TUTORIAL_commandline.md index 2ac3aaa..c82d3c9 100644 --- a/man/TUTORIAL_commandline.md +++ b/man/TUTORIAL_commandline.md @@ -249,7 +249,7 @@ Further readings: 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. -Anndata is the standard data format in the python single cell community. +Anndata is the standard data format in the python3 single cell community. Data stored in this format can be analyzed the same way as usually sc RNA seq data is analyzed. ```bash @@ -337,12 +337,33 @@ Further readings: ## Microenvironment and cell agent related commands +### ✨ pcdl\_get\_muspan + +From time series and single time steps, generate [muspan](https://www.muspan.co.uk/) domain files. +One file per time step and z-layer. + +Muspan is a sphisticated python3 libaray for multiscale spatial data analysis, develop by researchers at the University of Oxford. + +```bash +pcdl_get_muspan output/output00000000.xml +``` +```bash +pcdl_get_muspan output +``` +```bash +pcdl_get_muspand -h +``` + +Further readings: ++ [TUTORIAL_python3_muspan.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_muspan.md) + + ### ✨ pcdl\_get\_spatialdata From a single time step, generate [spatialdata](https://spatialdata.scverse.org/en/stable/) [zarr](https://zarr.dev/) files. The spatialdata format should, in the long run, become comaptibel with the [OME-NGFF](https://ngff.openmicroscopy.org/latest/index.html) data format. -Spatialdata is the standard data format in the python spatial single cell community. +Spatialdata is the standard data format in the python3 spatial single cell community. Data stored in this format can be analyzed the same way as spatial sc RNA seq data is analyzed. ```bash diff --git a/man/TUTORIAL_introduction.md b/man/TUTORIAL_introduction.md index ee8ea4a..8aea2fb 100644 --- a/man/TUTORIAL_introduction.md +++ b/man/TUTORIAL_introduction.md @@ -1,6 +1,6 @@ # PhysiCell Data Loader Tutorial: pcdl Introduction -If you have not already done so, please install the latest version of physicelldataloader (pcdl), +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. @@ -23,10 +23,10 @@ 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. 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 physicelldataloader library branch v2, v3. +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. -The result from all of this is the pcdl physicelldataloader library here.\ +The result from all of this 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. diff --git a/man/TUTORIAL_python3_muspan.md b/man/TUTORIAL_python3_muspan.md new file mode 100644 index 0000000..5ab0054 --- /dev/null +++ b/man/TUTORIAL_python3_muspan.md @@ -0,0 +1,30 @@ +# PhysiCell Data Loader Tutorial: pcdl and Python and MuSpAn + +[MuSpAn](https://www.muspan.co.uk/) is a multiscale spatial analysis toolbox for analyzing spatial transcriptomics data, multiplex immunohistochemistry data, imaging mass cytometry data, and more. +It uses cutting-edge mathematical and statistical approaches to data analysis to provide the most comprehensive spatial analysis available and is being continually expanded, with a team of quantitative researchers constantly developing new methodology to tackle multiscale spatial analysis problems. + +Pcdl offers a time step and time series get\_muspan function to translate cell and substrate data into a dictionary of muspan domain objects, one domain per time step z-layer. + +Additionally, pcdl provides a pcdl\_get\_muspan command line command to translate PhysiCell output into muspan domain files. + +For installation and learning how to use muspan, please follow the official documentation. + ++ https://www.muspan.co.uk/ ++ https://docs.muspan.co.uk/latest/Documentation.html ++ https://github.com/joshwillmoore1/MuSpAn-Public + +## Translate mcds time step and time series into muspan domains. + +```python +import pcdl +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 + +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. +``` + +That's it. The rest is analysis! diff --git a/man/TUTORIAL_python3_timeseries.md b/man/TUTORIAL_python3_timeseries.md index a265b39..7184acf 100644 --- a/man/TUTORIAL_python3_timeseries.md +++ b/man/TUTORIAL_python3_timeseries.md @@ -1,6 +1,6 @@ # PhysiCell Data Loader Tutorial: pcdl and Python and MCDS TimeSeries -If not already done so, please install the latest version of physicelldataloader (pcdl), as described in the [HowTo](https://github.com/elmbeech/physicelldataloader/blob/master/man/HOWTO.md) section. +If 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. And maybe read about the pcdl [background](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_introduction.md) information. And perhaps, work thorough the [TUTORIAL_python3_timestep.md](https://github.com/elmbeech/physicelldataloader/blob/master/man/TUTORIAL_python3_timestep.md) diff --git a/man/TUTORIAL_python3_timestep.md b/man/TUTORIAL_python3_timestep.md index 5fbf204..184233c 100644 --- a/man/TUTORIAL_python3_timestep.md +++ b/man/TUTORIAL_python3_timestep.md @@ -2,7 +2,7 @@ In this chapter, we will load the pcdl library and use its TimeStep class to load the data snapshot output/00000012, from the 2D time series test dataset (https://github.com/elmbeech/physicelldataloader/blob/master/output_2d.tar.gz). -First, please install the latest version of physicelldataloader (pcdl), +First, 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) chapter. And, if not already done so, have a quick read through the pcdl [background](https://github.com/elmbeech/physicelldataloader/tree/master/man/TUTORIAL_introduction.md) information. @@ -116,6 +116,9 @@ Fetch the data's MultiCellDS version, and the PhysiCell version the data was gen mcds.get_multicellds_version() # will return a string like MultiCellDS_2 or MultiCellDS_0.5 ``` ```python +mcds.get_pcdl_version() # will return a string like pcdl_4.1.3 +``` +```python mcds.get_physicell_version() # will return a string like PhysiCell_1.10.4 or BioFVM_1.1.7 ``` diff --git a/man/docstring/mcds.get_multicellds_version.md b/man/docstring/mcds.get_multicellds_version.md index 0ba91cb..f03bcc0 100644 --- a/man/docstring/mcds.get_multicellds_version.md +++ b/man/docstring/mcds.get_multicellds_version.md @@ -9,7 +9,7 @@ ## output: ``` s_version : sting - MultiCellDS xml version which stored the data. + MultiCellDS xml version that stored the data. ``` diff --git a/man/docstring/mcds.get_muspan.md b/man/docstring/mcds.get_muspan.md new file mode 100644 index 0000000..4470097 --- /dev/null +++ b/man/docstring/mcds.get_muspan.md @@ -0,0 +1,44 @@ +# mcds.get_muspan() + + +## 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. + + 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. + +``` + +## 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 + +``` \ No newline at end of file diff --git a/man/docstring/mcds.get_pcdl_version.md b/man/docstring/mcds.get_pcdl_version.md new file mode 100644 index 0000000..d0e9977 --- /dev/null +++ b/man/docstring/mcds.get_pcdl_version.md @@ -0,0 +1,22 @@ +# mcds.get_pcdl_version() + + +## input: +``` + +``` + +## output: +``` + s_version : sting + physicell data loader version that was used + to loaded the data. + +``` + +## description: +``` + function returns as a string the physicell data loader version + that was used to load the mcds time step. + +``` \ No newline at end of file diff --git a/man/docstring/mcds.get_physicell_version.md b/man/docstring/mcds.get_physicell_version.md index 1162e31..c8f4f8d 100644 --- a/man/docstring/mcds.get_physicell_version.md +++ b/man/docstring/mcds.get_physicell_version.md @@ -9,7 +9,7 @@ ## output: ``` s_version : sting - PhysiCell version which generated the data. + PhysiCell version that generated the data. ``` diff --git a/man/docstring/mcds.make_cell_vtk.md b/man/docstring/mcds.make_cell_vtk.md index 53e2da0..25e08ef 100644 --- a/man/docstring/mcds.make_cell_vtk.md +++ b/man/docstring/mcds.make_cell_vtk.md @@ -6,6 +6,9 @@ attribute: list of strings; default is ['cell_type'] column name within cell dataframe. + ext: string; default '_cell.vtp'. + file extension. + ``` ## output: diff --git a/man/docstring/mcds.make_conc_vtk.md b/man/docstring/mcds.make_conc_vtk.md index dc40043..5b01303 100644 --- a/man/docstring/mcds.make_conc_vtk.md +++ b/man/docstring/mcds.make_conc_vtk.md @@ -3,6 +3,8 @@ ## input: ``` + ext: string; default '_conc.vtr'. + file extension. ``` diff --git a/man/docstring/mcds.plot_scatter.md b/man/docstring/mcds.plot_scatter.md index 37f9d31..04daa67 100644 --- a/man/docstring/mcds.plot_scatter.md +++ b/man/docstring/mcds.plot_scatter.md @@ -32,9 +32,10 @@ alpha channel transparency value between 1 (not transparent at all) and 0 (totally transparent). - cmap: dictionary of strings or string; default viridis. - dictionary that maps labels to colors strings. - matplotlib colormap string. + cmap: string or dictionary of strings or list of list of floats; default viridis. + matplotlib colormap string. e.g viridis + dictionary that maps labels to color or #hex strings. e.g. {'default': 'maroon'} + dictionary that maps labels to list of rgb floats. e.g. {'default': [0.5,0.0,0.0]} https://matplotlib.org/stable/tutorials/colors/colormaps.html title: string; default None @@ -48,7 +49,7 @@ possible strings are: best, upper right, upper center, upper left, center left, lower left, lower center, lower right, center right, - center. + center, None, and False. xlim: tuple of two floats; default is None x axis min and max value. @@ -63,8 +64,7 @@ s: floating point number; default is 1.0 scatter plot dot size scale factor. - with figsizepx extracted from initial.svg, scale factor 1.0 - should be ok. adjust if necessary. + adjust if necessary. ax: matplotlib axis object; default setting is None the ax object, which will be used as a canvas for plotting. diff --git a/man/docstring/mcdsts.get_muspan.md b/man/docstring/mcdsts.get_muspan.md new file mode 100644 index 0000000..4f0d5a7 --- /dev/null +++ b/man/docstring/mcdsts.get_muspan.md @@ -0,0 +1,44 @@ +# mcdsts.get_muspan() + + +## 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. + + 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. + +``` + +## output: +``` + do_domain: dictionary of muspa domains, one for each time step 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 + +``` \ No newline at end of file diff --git a/man/docstring/mcdsts.make_cell_vtk.md b/man/docstring/mcdsts.make_cell_vtk.md index 1de2295..6142a99 100644 --- a/man/docstring/mcdsts.make_cell_vtk.md +++ b/man/docstring/mcdsts.make_cell_vtk.md @@ -6,6 +6,9 @@ attribute: list of strings; default is ['cell_type'] column name within cell dataframe. + ext: string; default '_cell.vtp'. + file extension. + ``` ## output: diff --git a/man/docstring/mcdsts.make_conc_vtk.md b/man/docstring/mcdsts.make_conc_vtk.md index 23a854c..9251931 100644 --- a/man/docstring/mcdsts.make_conc_vtk.md +++ b/man/docstring/mcdsts.make_conc_vtk.md @@ -3,6 +3,8 @@ ## input: ``` + ext: string; default '_conc.vtr'. + file extension. ``` diff --git a/man/docstring/mcdsts.plot_contour.md b/man/docstring/mcdsts.plot_contour.md index 659e205..aaeb7d0 100644 --- a/man/docstring/mcdsts.plot_contour.md +++ b/man/docstring/mcdsts.plot_contour.md @@ -15,8 +15,15 @@ will be adjusted to the nearest mesh center value, the smaller one, if the coordinate lies on a saddle point. - extrema: tuple of two floats; default is None - default takes min and max from data, from the whole time series. + vmin: floating point number; default is None + color scale min value. + None will take the min value from the whole time series + found in the data. + + vmax: floating point number; default is None + color scale max value. + None will take the min value from the whole time series + found in the data. alpha: floating point number; default is 1 alpha channel transparency value diff --git a/man/docstring/pcdl_make_cell_vtk.md b/man/docstring/pcdl_make_cell_vtk.md index 8c8e335..d835f46 100644 --- a/man/docstring/pcdl_make_cell_vtk.md +++ b/man/docstring/pcdl_make_cell_vtk.md @@ -1,7 +1,7 @@ ``` usage: pcdl_make_cell_vtk [-h] [--custom_data_type [CUSTOM_DATA_TYPE ...]] [--microenv MICROENV] [--physiboss PHYSIBOSS] - [--settingxml SETTINGXML] [-v VERBOSE] + [--settingxml SETTINGXML] [-v VERBOSE] [--ext EXT] [path] [attribute ...] function that generates 3D glyph vtk file for cells. cells can have specified @@ -39,6 +39,10 @@ options: -v VERBOSE, --verbose VERBOSE setting verbose to False for less text output, while processing. default is True. + --ext EXT set file extension for the vtk polydata file. for + example, the blender BVTKNodes plugin needs a + simplified .vtp file extension to be able to load + timeseries directly. homepage: https://github.com/elmbeech/physicelldataloader ``` diff --git a/man/docstring/pcdl_make_conc_vtk.md b/man/docstring/pcdl_make_conc_vtk.md index 3c80aaa..d1d451c 100644 --- a/man/docstring/pcdl_make_conc_vtk.md +++ b/man/docstring/pcdl_make_conc_vtk.md @@ -1,5 +1,5 @@ ``` -usage: pcdl_make_conc_vtk [-h] [-v VERBOSE] [path] +usage: pcdl_make_conc_vtk [-h] [-v VERBOSE] [--ext EXT] [path] function generates rectilinear grid vtk files, one per mcds time step, contains distribution of substrates over microenvironment. you can post- @@ -15,6 +15,7 @@ options: -v VERBOSE, --verbose VERBOSE setting verbose to False for less text output, while processing. default is True. + --ext EXT file extension for the vtk rectilinear grid file. homepage: https://github.com/elmbeech/physicelldataloader ``` diff --git a/man/docstring/pcdl_plot_contour.md b/man/docstring/pcdl_plot_contour.md index c368910..8c0cf3a 100644 --- a/man/docstring/pcdl_plot_contour.md +++ b/man/docstring/pcdl_plot_contour.md @@ -1,9 +1,9 @@ ``` -usage: pcdl_plot_contour [-h] [-v VERBOSE] [--z_slice Z_SLICE] - [--extrema EXTREMA [EXTREMA ...]] [--alpha ALPHA] - [--fill FILL] [--cmap CMAP] [--title TITLE] - [--grid GRID] [--xlim XLIM [XLIM ...]] - [--ylim YLIM [YLIM ...]] [--xyequal XYEQUAL] +usage: pcdl_plot_contour [-h] [-v VERBOSE] [--z_slice Z_SLICE] [--vmin VMIN] + [--vmax VMAX] [--alpha ALPHA] [--fill FILL] + [--cmap CMAP] [--title TITLE] [--grid GRID] + [--xlim XLIM [XLIM ...]] [--ylim YLIM [YLIM ...]] + [--xyequal XYEQUAL] [--figsizepx FIGSIZEPX [FIGSIZEPX ...]] [--ext EXT] [--figbgcolor FIGBGCOLOR] [path] [focus] @@ -26,9 +26,8 @@ options: 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. - --extrema EXTREMA [EXTREMA ...] - listing of two floats. None takes min and max from - data. default is None. + --vmin VMIN float. None takes min from data. default is None. + --vmax VMAX float. None takes max from data. default is None. --alpha ALPHA alpha channel transparency value between 1 (not transparent at all) and 0 (totally transparent). default is 1.0. diff --git a/man/img/physicelldataloader_concept_v4.0.0.jpg b/man/img/physicelldataloader_concept_v4.0.0.jpg index 79ba3c3..888ec28 100644 Binary files a/man/img/physicelldataloader_concept_v4.0.0.jpg and b/man/img/physicelldataloader_concept_v4.0.0.jpg differ diff --git a/man/img/physicelldataloader_concept_v4.0.0.png b/man/img/physicelldataloader_concept_v4.0.0.png index 4b4f72e..b0052f1 100644 Binary files a/man/img/physicelldataloader_concept_v4.0.0.png and b/man/img/physicelldataloader_concept_v4.0.0.png differ diff --git a/man/scarab.py b/man/scarab.py index f49ab20..8252c60 100644 --- a/man/scarab.py +++ b/man/scarab.py @@ -145,6 +145,10 @@ def docstring_md(s_function, ls_doc, s_header=None, s_opath='man/docstring/'): s_function = 'mcds.get_multicellds_version', ls_doc = pcdl.TimeStep.get_multicellds_version.__doc__.split('\n'), ) +docstring_md( + s_function = 'mcds.get_pcdl_version', + ls_doc = pcdl.TimeStep.get_pcdl_version.__doc__.split('\n'), +) docstring_md( s_function = 'mcds.get_physicell_version', ls_doc = pcdl.TimeStep.get_physicell_version.__doc__.split('\n'), @@ -303,6 +307,10 @@ def docstring_md(s_function, ls_doc, s_header=None, s_opath='man/docstring/'): ) # write TimeStep microenvironment and cells function markdown files +docstring_md( + s_function = 'mcds.get_muspan', + ls_doc = pcdl.TimeStep.get_muspan.__doc__.split('\n'), +) docstring_md( s_function = 'mcds.get_spatialdata', ls_doc = pcdl.TimeStep.get_spatialdata.__doc__.split('\n'), @@ -413,6 +421,10 @@ def docstring_md(s_function, ls_doc, s_header=None, s_opath='man/docstring/'): ) # write TimeSeries microenvironment and cells function makdown files +docstring_md( + s_function = 'mcdsts.get_muspan', + ls_doc = pcdl.TimeSeries.get_muspan.__doc__.split('\n'), +) docstring_md( s_function = 'mcdsts.get_spatialdata', ls_doc = pcdl.TimeSeries.get_spatialdata.__doc__.split('\n'), diff --git a/pcdl/VERSION.py b/pcdl/VERSION.py index 96e5585..8fcba80 100644 --- a/pcdl/VERSION.py +++ b/pcdl/VERSION.py @@ -1 +1 @@ -__version__ = '4.1.2' +__version__ = '4.1.5' diff --git a/pcdl/__init__.py b/pcdl/__init__.py index d0d0da2..7628a99 100644 --- a/pcdl/__init__.py +++ b/pcdl/__init__.py @@ -1,4 +1,4 @@ -from pcdl.timestep import TimeStep, graphfile_parser, render_neuroglancer, scaler +from pcdl.timestep import TimeStep, graphfile_parser, pccmap, render_neuroglancer, scaler from pcdl.timeseries import TimeSeries, make_gif, make_movie from pcdl.VERSION import __version__ from pcdl.output_data import install_data, uninstall_data diff --git a/pcdl/commandline.py b/pcdl/commandline.py index 51af143..75bf4ab 100644 --- a/pcdl/commandline.py +++ b/pcdl/commandline.py @@ -20,6 +20,10 @@ # library import argparse import json +try: + import muspan as ms +except ModuleNotFoundError: + ms = None import numpy as np import os import pandas as pd @@ -96,7 +100,7 @@ def get_version(): settingxml = None, verbose = True if args.verbose.lower().startswith('t') else False ) - s_version = f'version:\n{mcds.get_physicell_version()}\n{mcds.get_multicellds_version()}\npcdl_{pcdl.__version__}' + s_version = f'version:\n{mcds.get_physicell_version()}\n{mcds.get_multicellds_version()}\npcdl_{mcds.get_pcdl_version()}' # going home print(s_version) return 0 @@ -520,12 +524,17 @@ def plot_contour(): type = float, help = '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.', ) - # plot_contour extrema + # plot_contour vmin parser.add_argument( - '--extrema', - nargs = '+', - default = ['none'], - help = 'listing of two floats. None takes min and max from data. default is None.', + '--vmin', + default = 'none', + help = 'float. None takes min from data. default is None.', + ) + # plot_contour vmax + parser.add_argument( + '--vmax', + default = 'none', + help = 'float. None takes max from data. default is None.', ) # plot_contour alpha parser.add_argument( @@ -632,16 +641,23 @@ def plot_contour(): settingxml = None, verbose = False if args.verbose.lower().startswith('f') else True ) - # handle extrema - if (args.extrema[0].lower() == 'none'): + # handle z-axis + if (args.vmin.lower() == 'none') or (args.vmax.lower() == 'none'): df_conc = mcds.get_conc_df() + # vmin + if (args.vmin.lower() == 'none'): r_zmin = df_conc.loc[:, args.focus].min() + if mcds.verbose: + print(f'vmin set to {r_zmin}.') + else: + r_zmin = float(args.vmin) + # vmax + if (args.vmax.lower() == 'none'): r_zmax = df_conc.loc[:, args.focus].max() if mcds.verbose: - print(f'min max extrema set to {r_zmin} {r_zmax}.') + print(f'vmax set to {r_zmax}.') else: - r_zmin = args.extrema[0] - r_zmax = args.extrema[1] + r_zmax = float(args.vmax) # plot s_opathfile = mcds.plot_contour( focus = args.focus, @@ -675,11 +691,29 @@ def plot_contour(): settingxml = None, verbose = False if args.verbose.lower().startswith('f') else True, ) + # handle z-axis + if (args.vmin.lower() == 'none') or (args.vmax.lower() == 'none'): + df_conc = mcdsts.get_conc_df() + # vmin + if (args.vmin.lower() == 'none'): + r_zmin = df_conc.loc[:, args.focus].min() + if mcdsts.verbose: + print(f'vmin set to {r_zmin}.') + else: + r_zmin = float(args.vmin) + # vmax + if (args.vmax.lower() == 'none'): + r_zmax = df_conc.loc[:, args.focus].max() + if mcdsts.verbose: + print(f'vmax set to {r_zmax}.') + else: + r_zmax = float(args.vmax) # plot ls_opathfile = mcdsts.plot_contour( focus = args.focus, z_slice = args.z_slice, - extrema = None if (args.extrema[0].lower() == 'none') else args.extrema, + vmin = r_zmin, + vmax = r_zmax, alpha = args.alpha, fill = False if args.fill.lower().startswith('f') else True, cmap = args.cmap, @@ -727,6 +761,12 @@ def make_conc_vtk(): default = 'true', help = 'setting verbose to False for less text output, while processing. default is True.', ) + # make_conc_vtk file extension + parser.add_argument( + '--ext', + default = '_conc.vtr', + help = 'file extension for the vtk rectilinear grid file.', + ) # parse arguments args = parser.parse_args() @@ -758,7 +798,9 @@ def make_conc_vtk(): settingxml = None, verbose = False if args.verbose.lower().startswith('f') else True ) - s_opathfile = mcds.make_conc_vtk() + s_opathfile = mcds.make_conc_vtk( + ext = args.ext, + ) # going home print(s_opathfile) @@ -773,7 +815,9 @@ def make_conc_vtk(): settingxml = None, verbose = False if args.verbose.lower().startswith('f') else True, ) - ls_opathfile = mcdsts.make_conc_vtk() + ls_opathfile = mcdsts.make_conc_vtk( + ext = args.ext, + ) # going home print(ls_opathfile) @@ -1869,6 +1913,12 @@ def make_cell_vtk(): default = ['cell_type'], help = 'listing of mcds.get_cell_df dataframe column names, used for cell attributes. default is a single term: cell_type.', ) + # make_cell_vtk file extension + parser.add_argument( + '--ext', + default = '_cell.vtp', + help = 'set file extension for the vtk polydata file. for example, the blender BVTKNodes plugin needs a simplified .vtp file extension to be able to load timeseries directly.', + ) # parse arguments args = parser.parse_args() @@ -1914,6 +1964,7 @@ def make_cell_vtk(): ) s_opathfile = mcds.make_cell_vtk( attribute = args.attribute, + ext = args.ext, ) # going home print(s_opathfile) @@ -1931,6 +1982,7 @@ def make_cell_vtk(): ) ls_opathfile = mcdsts.make_cell_vtk( attribute = args.attribute, + ext = args.ext, ) # going home print(ls_opathfile) @@ -1943,6 +1995,169 @@ def make_cell_vtk(): # substrate and cell agent command line function # ################################################### +def get_muspan(): + # argv + parser = argparse.ArgumentParser( + prog = 'pcdl_get_muspan', + description = 'function to transform mcds time steps into muspan domain 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 muspan domain 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 muspan domain 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 muspan domain 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_muspan z_slice + parser.add_argument( + 'z_slice', + nargs = '?', + default = 0.0, + type = float, + help = '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.', + ) + # get_muspan values + parser.add_argument( + '--values', + 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_muspan 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_muspan 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." + ) + # 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_muspan : {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_muspan : {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 + ) + do_domain = mcds.get_muspan( + z_slice = args.z_slice, + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + ) + # going home + for s_domain, o_domain in sorted(do_domain.items()): + ms.io.save_domain(o_domain, path_to_save=mcds.path, save_summary=mcds.verbose) + s_opathfile = f'{mcds.path}/{s_domain}.muspan' + 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, + ) + do_domain = mcdsts.get_muspan( + z_slice = args.z_slice, + values = args.values, + drop = set(args.drop), + keep = set(args.keep), + ) + # going home + ls_opathfile = [] + for s_domain, o_domain in sorted(do_domain.items()): + ms.io.save_domain(o_domain, path_to_save=mcdsts.path, save_summary=mcdsts.verbose) + ls_opathfile.append(f'{mcdsts.path}/{s_domain}.muspan') + print(ls_opathfile) + + # going home + return 0 + def get_spatialdata(): # argv parser = argparse.ArgumentParser( diff --git a/pcdl/pdplt.py b/pcdl/pdplt.py index c45ed4f..4d8666b 100644 --- a/pcdl/pdplt.py +++ b/pcdl/pdplt.py @@ -19,7 +19,7 @@ import matplotlib.patches as mpatches import numpy as np import random - +import sys # pandas to matplotlib #fig, ax = plt.subplots() @@ -32,13 +32,15 @@ # plot stuff -def df_label_to_color(df_abc, s_focus, es_label=None, s_nolabel='gray', s_cmap='viridis', b_shuffle=False): +def df_label_to_color(df_abc=None, s_focus=None, ls_label=None, s_nolabel='silver', s_cmap='viridis', b_shuffle=False): ''' input: df_abc: dataframe to which the color column will be added. - s_focus: column name with sample labels for which a color column will be generated. - es_label: set of labels to color. if None, es_label will be extracted for the s_focus column. - s_nolabel: color for labels not defined in es_label. + s_focus: column name with sample labels for which a color column + will be generated. + ls_label: ordered list of labels to color. if None, + ls_label will be extracted for the s_focus column. + s_nolabel: color for labels not defined in ls_label. s_cmap: matplotlib color map label. https://matplotlib.org/stable/tutorials/colors/colormaps.html b_shuffle: should colors be given by alphabetical order, @@ -52,29 +54,39 @@ def df_label_to_color(df_abc, s_focus, es_label=None, s_nolabel='gray', s_cmap=' function adds for the selected label column a color column to the df_abc dataframe. ''' - if (es_label is None): - es_label = set(df_abc.loc[:,s_focus]) + # map labels to color + if (ls_label is None): + ls_label = sorted(set(df_abc.loc[:,s_focus])) if b_shuffle: - ls_label = list(es_label) random.shuffle(ls_label) + as_color = np.apply_along_axis( + colors.to_hex, + axis=1, + arr=plt.get_cmap(s_cmap)(np.linspace(0, 1, len(ls_label))), + ) + ds_color = dict(zip(ls_label, as_color)) + # process no data frame + if (df_abc is None) and (s_focus is None): + pass + # process data frame + elif (not (df_abc is None)) and (not (s_focus is None)): + df_abc[f'{s_focus}_color'] = s_nolabel + for s_category, s_color in ds_color.items(): + df_abc.loc[(df_abc.loc[:,s_focus] == s_category), f'{s_focus}_color'] = s_color + # error handling else: - ls_label = sorted(es_label) - a_color = plt.get_cmap(s_cmap)(np.linspace(0, 1, len(ls_label))) - do_color = dict(zip(ls_label, a_color)) - df_abc[f'{s_focus}_color'] = s_nolabel - ds_color = {} - for s_category, o_color in do_color.items(): - s_color = colors.to_hex(o_color) - ds_color.update({s_category : s_color}) - df_abc.loc[(df_abc.loc[:,s_focus] == s_category), f'{s_focus}_color'] = s_color + sys.exit('Error @ both, df_abc and s_focus, have either to be None or not None!') # output return(ds_color) -def ax_colorlegend(ax, ds_color, s_loc='lower left', s_fontsize='small'): + +def ax_colorlegend(ax, ds_color, ls_label=None, s_loc='lower left', s_fontsize='small'): ''' input: ax: matplotlib axis object to which a color legend will be added. ds_color: lables to color strings mapping dictionary + ls_label: ordered list of labels to color. if None, ls_label + will be extracted from ds_color and sortred alphabetically. s_loc: the location of the legend. possible strings are: best, upper right, upper center, upper left, center left, @@ -89,9 +101,13 @@ def ax_colorlegend(ax, ds_color, s_loc='lower left', s_fontsize='small'): description: function to add color legend to a figure. ''' + # manimupate input + if (ls_label is None): + ls_label = sorted(ds_color.keys()) + # processing lo_patch = [] - for s_label, s_color in sorted(ds_color.items()): - o_patch = mpatches.Patch(color=s_color, label=s_label) + for s_label in ls_label: + o_patch = mpatches.Patch(color=ds_color[s_label], label=s_label) lo_patch.append(o_patch) ax.legend( handles = lo_patch, @@ -99,6 +115,7 @@ def ax_colorlegend(ax, ds_color, s_loc='lower left', s_fontsize='small'): fontsize = s_fontsize ) + def ax_colorbar(ax, r_vmin, r_vmax, s_cmap='viridis', s_text=None, o_fontsize='medium', b_axis_erase=False): ''' input: diff --git a/pcdl/timeseries.py b/pcdl/timeseries.py index 037dc9b..92097bf 100644 --- a/pcdl/timeseries.py +++ b/pcdl/timeseries.py @@ -26,6 +26,7 @@ import pandas as pd from pcdl import render_neuroglancer from pcdl.timestep import TimeStep, es_coor_cell, es_coor_conc, _anndextract +from pcdl.VERSION import __version__ import platform import sys @@ -216,8 +217,21 @@ def __init__(self, output_path='.', custom_data_type={}, load=True, microenv=Tru # load mcds timeseries from list if mcds timesteps if (type(output_path) is list): + # check mcds pcdl version + try: + s_mcdsv = output_path[0].get_pcdl_version() + except AttributeError: + s_mcdsv = 'pcdl_<4.1.3' + except KeyError: + s_mcdsv = 'pcdl_<4.1.3' + except IndexError: + sys.exit(f'Error @ TimeSeries.__init__ : {output_path}. mcds list is empty!') + if (f'pcdl_{__version__}' != s_mcdsv): + print(f'Warning @ TimeSeries.__init__ : pcdl_{__version__} != {s_mcdsv}. the installed pcdl version and the pcdl version the mcds were generated from are not the same.\n the TimeSeries functions might or might not work.') + # set variables self.ls_xmlfile = None self.l_mcds = output_path + self.path = '.' self.custom_data_type = None self.microenv = None self.graph = None @@ -453,7 +467,7 @@ def get_conc_df(self, values=1, drop=set(), keep=set(), collapse=True): df_concts = None # load data - for i, mcds in enumerate(self.get_mcds_list()): + for mcds in self.get_mcds_list(): # pack collapsed if collapse: df_conc = mcds.get_conc_df( @@ -562,7 +576,7 @@ def get_conc_attribute(self, values=1, drop=set(), keep=set(), allvalues=False): return dlr_variable_range - def plot_contour(self, focus, z_slice=0.0, extrema=None, alpha=1, fill=True, cmap='viridis', title='', grid=True, xlim=None, ylim=None, xyequal=True, figsizepx=None, ext='jpeg', figbgcolor=None, **kwargs): + def plot_contour(self, focus, z_slice=0.0, vmin=None, vmax=None, alpha=1, fill=True, cmap='viridis', title='', grid=True, xlim=None, ylim=None, xyequal=True, figsizepx=None, ext='jpeg', figbgcolor=None, **kwargs): """ input: self: TimeSeries class instance @@ -577,8 +591,15 @@ def plot_contour(self, focus, z_slice=0.0, extrema=None, alpha=1, fill=True, cma will be adjusted to the nearest mesh center value, the smaller one, if the coordinate lies on a saddle point. - extrema: tuple of two floats; default is None - default takes min and max from data, from the whole time series. + vmin: floating point number; default is None + color scale min value. + None will take the min value from the whole time series + found in the data. + + vmax: floating point number; default is None + color scale max value. + None will take the min value from the whole time series + found in the data. alpha: floating point number; default is 1 alpha channel transparency value @@ -652,19 +673,18 @@ def plot_contour(self, focus, z_slice=0.0, extrema=None, alpha=1, fill=True, cma if self.verbose: print(f'z_slice set to {z_slice}.') - # handle extrema - if extrema == None: - extrema = [None, None] + # handle z-axis + if (vmin is None) or (vmax is None): for mcds in self.get_mcds_list(): df_conc = mcds.get_conc_df() r_min = df_conc.loc[:,focus].min() r_max = df_conc.loc[:,focus].max() - if (extrema[0] is None) or (extrema[0] > r_min): - extrema[0] = np.floor(r_min) - if (extrema[1] is None) or (extrema[1] < r_max): - extrema[1] = np.ceil(r_max) + if (vmin is None) or (vmin > r_min): + vmin = np.floor(r_min) + if (vmax is None) or (vmax < r_max): + vmax = np.ceil(r_max) if self.verbose: - print(f'min max extrema set to {extrema}.') + print(f'z-axis min max set to {vmin} {vmax}.') # handle xlim and ylim if (xlim is None): @@ -677,20 +697,20 @@ def plot_contour(self, focus, z_slice=0.0, extrema=None, alpha=1, fill=True, cma print(f'ylim set to {ylim}.') # handle output path - s_path = self.path + f'/conc_{focus}_z{round(z_slice,9)}/' + s_path = self.path + f'/conc_{focus}_z{round(z_slice,3)}/' # plotting lo_output = [] - for i, mcds in enumerate(self.get_mcds_list()): + for mcds in self.get_mcds_list(): o_output = mcds.plot_contour( focus = focus, z_slice = z_slice, - vmin = extrema[0], - vmax = extrema[1], + vmin = vmin, + vmax = vmax, alpha = alpha, fill = fill, cmap = cmap, - title = f'{title}{focus} z{round(z_slice,9)}\n{round(mcds.get_time(),9)}[min]', + title = f'{title}{focus} z{round(z_slice,3)}\n{round(mcds.get_time(),3)}[min]', grid = grid, xlim = xlim, ylim = ylim, @@ -707,9 +727,11 @@ def plot_contour(self, focus, z_slice=0.0, extrema=None, alpha=1, fill=True, cma return lo_output - def make_conc_vtk(self): + def make_conc_vtk(self, ext='_conc.vtr'): """ input: + ext: string; default '_conc.vtr'. + file extension. output: ls_vtkpathfile: one vtk file per mcds time step that contains @@ -727,7 +749,7 @@ def make_conc_vtk(self): # processing ls_vtkpathfile = [] for mcds in self.get_mcds_list(): - s_vtkpathfile = mcds.make_conc_vtk() + s_vtkpathfile = mcds.make_conc_vtk(ext=ext) ls_vtkpathfile.append(s_vtkpathfile) # output @@ -781,7 +803,7 @@ def get_cell_df(self, values=1, drop=set(), keep=set(), collapse=True): df_cellts = None # load data - for i, mcds in enumerate(self.get_mcds_list()): + for mcds in self.get_mcds_list(): # pack collapsed if collapse: df_cell = mcds.get_cell_df( @@ -998,7 +1020,7 @@ def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slic """ # plotting lo_output = [] - for i, mcds in enumerate(self.get_mcds_list()): + for mcds in self.get_mcds_list(): df_cell = mcds.get_cell_df() o_output = mcds.plot_scatter( focus = focus, @@ -1008,7 +1030,7 @@ def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slic z_axis = z_axis, alpha = alpha, cmap = cmap, - title = f'{title}{focus} z{round(z_slice,9)}\n{df_cell.shape[0]}[agent] {round(mcds.get_time(),9)}[min]', + title = f'{title}{focus} z{round(z_slice,3)}\n{df_cell.shape[0]}[agent] {round(mcds.get_time(),3)}[min]', grid = grid, legend_loc = legend_loc, xlim = xlim, @@ -1027,12 +1049,15 @@ def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slic return lo_output - def make_cell_vtk(self, attribute=['cell_type']): + def make_cell_vtk(self, attribute=['cell_type'], ext='_cell.vtp'): """ input: attribute: list of strings; default is ['cell_type'] column name within cell dataframe. + ext: string; default '_cell.vtp'. + file extension. + output: ls_vtkpathfile: one 3D glyph vtk file per mcds time step that contains cells. @@ -1050,6 +1075,7 @@ def make_cell_vtk(self, attribute=['cell_type']): for mcds in self.get_mcds_list(): s_vtkpathfile = mcds.make_cell_vtk( attribute = attribute, + ext = ext, ) ls_vtkpathfile.append(s_vtkpathfile) @@ -1105,9 +1131,9 @@ def make_ome_tiff(self, cell_attribute='ID', conc_cutoff={}, focus=None, file=Tr https://napari.org/stable/ https://fiji.sc/ """ - # for each T time step + # for each time step l_tczyx_img = [] - for i, mcds in enumerate(self.get_mcds_list()): + 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 @@ -1648,66 +1674,59 @@ def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs', collapse 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())) - # collapse warning + # package collapse if collapse and self.verbose: + # warning print('Warning @ mcdsts.get_anndata : only df_cell data, but not graph data, can be collapsed.') + df_cell = self.get_cell_df(values=values, drop=drop, keep=keep, collapse=True) - # 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 - r_time = round(mcds.get_time(),9) - if self.verbose: - print(f'processing: {i+1}/{i_mcds} {r_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 = {}, + #graph_neighbor = {}, + #graph_spring = {}, + #graph_method = s_physicellv, + ) - # pack collapsed - if collapse: - # extract - 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, - ) - # count - df_count.reset_index(inplace=True) - df_count.index = df_count.ID + f'id_{r_time}min' - df_count.index.name = 'id_time' - df_count.drop('ID', axis=1, inplace=True) - if df_anncount is None: - df_anncount = df_count - else: - df_anncount = pd.concat([df_anncount, df_count], axis=0) - # obs - df_obs.reset_index(inplace=True) - df_obs.index = df_obs.ID + f'id_{r_time}min' - df_obs.index.name = 'id_time' - if df_annobs is None: - df_annobs = df_obs - else: - df_annobs = pd.concat([df_annobs, df_obs], axis=0) - # obsm (spatial) - if ar_annobsm is None: - ar_annobsm = d_obsm['spatial'] + # 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: - ar_annobsm = np.vstack([ar_annobsm, d_obsm['spatial']]) - # obsp: nop (graph) - # uns: nop (graph) + 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] - # pack not collapsed - else: # extract df_count, df_obs, d_obsm, d_obsp, d_uns = _anndextract( df_cell=df_cell, @@ -1717,6 +1736,7 @@ def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs', collapse graph_spring = mcds.get_spring_graph_dict(), graph_method = s_physicellv, ) + # annmcds ann_mcds = ad.AnnData( X = df_count, @@ -1727,21 +1747,10 @@ def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs', collapse ) lann_mcds.append(ann_mcds) - # output - if collapse: - ann_mcdsts = ad.AnnData( - X = df_anncount, - obs = df_annobs, - obsm = {'spatial': ar_annobsm}, - #obsp = d_obsp, - #uns = d_uns - ) - return ann_mcdsts - else: + # output self.l_annmcds = lann_mcds return self.l_annmcds - def get_annmcds_list(self): """ input: @@ -1828,9 +1837,8 @@ def get_spatialdata(self, images={'subs'}, labels=set(), points={'subs'}, shapes mcds = self.l_mcds.pop(0) # extract time and dataframes - r_time = round(mcds.get_time(),9) if self.verbose: - print(f'\nprocessing: {i+1}/{i_mcds} {r_time}[min] mcds into spatialdata obj.') + print(f'\nprocessing: {i+1}/{i_mcds} {mcds.get_time()}[min] mcds into spatialdata obj.') # get spatialdata object sd_mcds = mcds.get_spatialdata( @@ -1848,7 +1856,6 @@ def get_spatialdata(self, images={'subs'}, labels=set(), points={'subs'}, shapes return self.l_sdmcds - def get_sdmcds_list(self): """ input: @@ -1863,3 +1870,60 @@ def get_sdmcds_list(self): 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. + + 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. + + output: + do_domain: dictionary of muspa domains, one for each time step 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 + """ + # variable triage + es_keep = set(self.get_cell_attribute(values=values, drop=drop, keep=keep, allvalues=False).keys()) + + # 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, + ) + ) + + # output + return do_domain diff --git a/pcdl/timestep.py b/pcdl/timestep.py index 373e282..e960ff9 100644 --- a/pcdl/timestep.py +++ b/pcdl/timestep.py @@ -22,6 +22,11 @@ import matplotlib.pyplot as plt from matplotlib import cm from matplotlib import colors +try: + import muspan as ms +except ModuleNotFoundError: + ms = None +import networkx as nx import neuroglancer import numpy as np import os @@ -59,7 +64,6 @@ '102' : 'autophagy_death_model', '9999' : 'custom_cycle_model', } - ds_cycle_phase = { '0' : 'Ki67_positive_premitotic', '1' : 'Ki67_positive_postmitotic', @@ -90,6 +94,23 @@ '104' : 'debris', } +pccmap = [ + 'Gray', #colors.to_hex([0.5, 0.5, 0.5]), + 'Red', #colors.to_hex([1, 0, 0]), + 'Gold', #colors.to_hex([1, 1, 0]), # x11 yellow + 'Green', #colors.to_hex([0, 1, 0]), + 'Blue', #colors.to_hex([0, 0, 1]), + 'Magenta', #colors.to_hex([1, 0, 1]), + 'Orange', #colors.to_hex([1, 0.65, 0]), + 'Lime', #colors.to_hex([0.2, 0.8, 0.2]), + 'Cyan', #colors.to_hex([0, 1, 1]), + 'Purple', #colors.to_hex([1, 0.41, 0.71]), # x11 hot pink + 'Maroon', #colors.to_hex([1, 0.85, 0.73]), # x11 peach puff + 'Teal', #colors.to_hex([143/255, 188/255, 143/255]), # x11 dark sea green + 'Navy', #colors.to_hex([135/255, 206/255, 250/255]), # x11 light sky blue +] + + # const physicell variable names es_var_subs = { # variable size=1 (check for the s at the end of the label) # cycle NOP @@ -301,6 +322,7 @@ def scaler(df_x, scale='maxabs'): + https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.minmax_scale.html + https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.scale.html """ + # nop if scale is None: pass # -1,1 @@ -371,19 +393,22 @@ def _anndextract(df_cell, scale='maxabs', graph_attached={}, graph_neighbor={}, and one dictionary of string (d_uns), which downstream might be transformed into an anndata object. """ + # make a copy of the input + df_cell = df_cell.copy() + # transform index to string - df_coor = df_cell.loc[:,['position_x','position_y','position_z']].copy() + df_coor = df_cell.loc[:,['position_x','position_y','position_z']] df_cell.index = df_cell.index.astype(str) # build obs anndata object (annotation of observations) - df_obs = df_cell.loc[:,['mesh_center_p','time']].copy() + df_obs = df_cell.loc[:,['mesh_center_p','time']] df_obs.columns = ['z_layer', 'time'] # buil obsm anndata object spatial (multi-dimensional annotation of observations) if (len(set(df_cell.position_z)) == 1): - df_obsm = df_cell.loc[:,['position_x','position_y']].copy() + df_obsm = df_cell.loc[:,['position_x','position_y']] else: - df_obsm = df_cell.loc[:,['position_x','position_y','position_z']].copy() + df_obsm = df_cell.loc[:,['position_x','position_y','position_z']] d_obsm = {"spatial": df_obsm.values} # build obsp and uns anndata object graph (pairwise annotation of obeservation) and (unstructured data) @@ -444,6 +469,7 @@ def _anndextract(df_cell, scale='maxabs', graph_attached={}, graph_neighbor={}, # extract discrete cell data es_drop = set(df_cell.columns).intersection({ + 'ID', 'voxel_i', 'voxel_j', 'voxel_k', 'mesh_center_m', 'mesh_center_n', 'mesh_center_p', 'position_x', 'position_y','position_z', @@ -463,11 +489,12 @@ def _anndextract(df_cell, scale='maxabs', graph_attached={}, graph_neighbor={}, elif str(se_cell.dtype).startswith('object'): des_type['str'].add(se_cell.name) else: - print(f'Error @ TimeSeries._anndextract : column {se_cell.name} detected with unknown dtype {str(se_cell.dtype)}.') + sys.exit(f'Error @ TimeStep._anndextract : column {se_cell.name} detected with unknown dtype {str(se_cell.dtype)}.') # build on obs and X anndata object df_cat = df_cell.loc[:,sorted(des_type['str'])].copy() df_obs = pd.merge(df_obs, df_cat, left_index=True, right_index=True) + df_obs = df_obs.astype('category') es_num = des_type['float'].union(des_type['int'].union(des_type['bool'])) df_count = df_cell.loc[:,sorted(es_num)].copy() for s_col in des_type['bool']: @@ -602,7 +629,7 @@ def get_multicellds_version(self): output: s_version : sting - MultiCellDS xml version which stored the data. + MultiCellDS xml version that stored the data. description: function returns as a string the MultiCellDS xml version @@ -611,13 +638,29 @@ def get_multicellds_version(self): return self.data['metadata']['multicellds_version'] + def get_pcdl_version(self): + """ + input: + + output: + s_version : sting + physicell data loader version that was used + to loaded the data. + + description: + function returns as a string the physicell data loader version + that was used to load the mcds time step. + """ + return self.data['metadata']['pcdl_version'] + + def get_physicell_version(self): """ input: output: s_version : sting - PhysiCell version which generated the data. + PhysiCell version that generated the data. description: function returns as a string the PhysiCell version @@ -1344,9 +1387,11 @@ def plot_contour(self, focus, z_slice=0.0, vmin=None, vmax=None, alpha=1, fill=T return s_pathfile - def make_conc_vtk(self): + def make_conc_vtk(self, ext='_conc.vtr'): """ input: + ext: string; default '_conc.vtr'. + file extension. output: s_vtkpathfile: vtk rectilinear grid file that contains @@ -1360,7 +1405,7 @@ def make_conc_vtk(self): https://www.paraview.org/ """ # off we go. - s_vtkfile = self.xmlfile.replace('.xml','_conc.vtr') + s_vtkfile = self.xmlfile.replace('.xml', ext) if self.verbose: print(f'processing: {s_vtkfile} ...') @@ -1574,9 +1619,10 @@ def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slic alpha channel transparency value between 1 (not transparent at all) and 0 (totally transparent). - cmap: dictionary of strings or string; default viridis. - dictionary that maps labels to colors strings. - matplotlib colormap string. + cmap: string or dictionary of strings or list of list of floats; default viridis. + matplotlib colormap string. e.g viridis + dictionary that maps labels to color or #hex strings. e.g. {'default': 'maroon'} + dictionary that maps labels to list of rgb floats. e.g. {'default': [0.5,0.0,0.0]} https://matplotlib.org/stable/tutorials/colors/colormaps.html title: string; default None @@ -1590,7 +1636,7 @@ def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slic possible strings are: best, upper right, upper center, upper left, center left, lower left, lower center, lower right, center right, - center. + center, None, and False. xlim: tuple of two floats; default is None x axis min and max value. @@ -1605,8 +1651,7 @@ def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slic s: floating point number; default is 1.0 scatter plot dot size scale factor. - with figsizepx extracted from initial.svg, scale factor 1.0 - should be ok. adjust if necessary. + adjust if necessary. ax: matplotlib axis object; default setting is None the ax object, which will be used as a canvas for plotting. @@ -1684,34 +1729,37 @@ def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slic df_cell = df_cell.loc[(df_cell.mesh_center_p == z_slice),:] # calculate marker size - df_cell.loc[:,'s'] = ((6 * df_cell.total_volume) / np.pi)**(2/3) # diamter of a sphere and plt.rcParams['lines.markersize']**2. + # default s is in matplotlib plt.rcParams['lines.markersize']**2. + df_cell.loc[:,'s'] = ((6 * df_cell.total_volume) / np.pi)**(2/3) * s # diamter of a sphere times s # handle z_axis categorical cases if (str(df_cell.loc[:,focus].dtype) in {'bool', 'object'}): - lr_extrema = [None, None] + vmin = None + vmax = None if (z_axis is None): # extract set of labels from data es_category = set(df_cell.loc[:,focus]) if (str(df_cell.loc[:,focus].dtype) in {'bool'}): es_category = es_category.union({True, False}) + ls_category = sorted(es_category) else: - es_category = z_axis + ls_category = z_axis # handle z_axis numerical cases else: # df_cell.loc[:,focus].dtype is numeric - es_category = None + ls_category = None if (z_axis is None): # extract min and max values from data - r_zmin = df_cell.loc[:,focus].min() - r_zmax = df_cell.loc[:,focus].max() - lr_extrema = [r_zmin, r_zmax] + vmin = df_cell.loc[:,focus].min() + vmax = df_cell.loc[:,focus].max() else: - lr_extrema = z_axis + vmin = z_axis[0] + vmax = z_axis[1] # handle z_axis summary if self.verbose: - print(f'categories found: {es_category}.') - print(f'min max extrema set to: {lr_extrema}.') + print(f'categories found: {ls_category}.') + print(f'min max z-axis set to: {vmin} [vmax].') # handle xlim and ylim if (xlim is None): @@ -1737,12 +1785,15 @@ def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slic ax.axis('equal') # handle categorical variable - if not (es_category is None): + if not (ls_category is None): s_focus_color = focus + '_color' # use specified category color dictionary - if type(cmap) is dict: + if type(cmap) == dict: + if type(list(cmap.values())[0]) == list: + for s_category, lr_color in cmap.items(): + cmap[s_category] = colors.to_hex(lr_color) ds_color = cmap - df_cell[s_focus_color] = 'gray' + df_cell[s_focus_color] = 'silver' for s_category, s_color in ds_color.items(): df_cell.loc[(df_cell.loc[:,focus] == s_category), s_focus_color] = s_color # generate category color dictionary @@ -1750,17 +1801,17 @@ def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slic ds_color = pdplt.df_label_to_color( df_abc = df_cell, s_focus = focus, - es_label = es_category, - s_nolabel = 'gray', + ls_label = ls_category, + s_nolabel = 'silver', s_cmap = cmap, b_shuffle = False, ) # filter categories es_cat = set() if (len(cat_keep) > 0): - es_cat = es_category.intersection(cat_keep) + es_cat = set(ls_category).intersection(cat_keep) else: - es_cat = es_category.difference(cat_drop) + es_cat = set(ls_category).difference(cat_drop) df_cell= df_cell.loc[df_cell.loc[:,focus].isin(es_cat),:] # generate color list c = list(df_cell.loc[:, s_focus_color].values) @@ -1777,8 +1828,8 @@ def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slic x = 'position_x', y = 'position_y', c = c, - vmin = lr_extrema[0], - vmax = lr_extrema[1], + vmin = vmin, + vmax = vmax, alpha = alpha, cmap = s_cmap, title = title, @@ -1791,13 +1842,14 @@ def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slic ) # plot categorical data legen - if not (es_category is None): - pdplt.ax_colorlegend( - ax = ax, - ds_color = ds_color, - s_loc = legend_loc, - s_fontsize = 'small', - ) + if not (ls_category is None) and not (legend_loc in {None, False}): + pdplt.ax_colorlegend( + ax = ax, + ds_color = ds_color, + ls_label = ls_category, + s_loc = legend_loc, + s_fontsize = 'small', + ) # finalize if (ext is None): @@ -1824,12 +1876,15 @@ def plot_scatter(self, focus='cell_type', cat_drop=set(), cat_keep=set(), z_slic return fig - def make_cell_vtk(self, attribute=['cell_type']): + def make_cell_vtk(self, attribute=['cell_type'], ext='_cell.vtp'): """ input: attribute: list of strings; default is ['cell_type'] column name within cell dataframe. + ext: string; default '_cell.vtp'. + file extension. + output: s_vtkpathfile: vtk 3D glyph polynomial data file that contains cells. @@ -1842,7 +1897,7 @@ def make_cell_vtk(self, attribute=['cell_type']): https://www.paraview.org/ """ # off we go. - s_vtkfile = self.xmlfile.replace('.xml','_cell.vtp') + s_vtkfile = self.xmlfile.replace('.xml', ext) if self.verbose: print(f'processing: {s_vtkfile} ...') @@ -2026,7 +2081,6 @@ def make_ome_tiff(self, cell_attribute='ID', conc_cutoff={}, focus=None, file=Tr except KeyError: pass - # get cell data df_cell = self.get_cell_df().reset_index() @@ -2359,6 +2413,7 @@ def get_anndata(self, values=1, drop=set(), keep=set(), scale='maxabs'): 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( @@ -2627,6 +2682,183 @@ def get_spatialdata(self, images={'subs'}, labels={}, points={'subs'}, shapes={' return sdata + ## 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. + + 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. + + 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: + 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/') + + # 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'): + 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) + + ## 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]) + ) + ) + + # update output + do_domain.update({s_domain : o_domain}) + + # output + return do_domain + + ## LOAD DATA ## def _read_xml(self, xmlfile, output_path='.'): @@ -2733,6 +2965,9 @@ def _read_xml(self, xmlfile, output_path='.'): ## get multicellds xml version d_mcds['metadata']['multicellds_version'] = f"MultiCellDS_{x_root.get('version')}" + ## get pcdl version + d_mcds['metadata']['pcdl_version'] = f"pcdl_{__version__}" + ## get physicell software version x_software = x_metadata.find('software') x_physicelln = x_software.find('name') diff --git a/pyproject.toml b/pyproject.toml index fe2fa7d..3b12f79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ # 1. git add pcdl/VERSION.py # 2. python3 man/scarab.py # 3. git status -# 4. git commit -m'@ physicelldataloader : next release.' +# 4. git commit -m'@ physicell data loader : next release.' # 5. git tag -a v0.0.0 -m'version 0.0.0' # 6. rm -r dist # 7. python3 -c "import pcdl; pcdl.uninstall_data()" @@ -75,6 +75,7 @@ dependencies = [ "bioio-ome-tiff", "geopandas>=0.14", # spatialdata "matplotlib", + "networkx", "neuroglancer", "numpy", "pandas>=2.2.2", @@ -84,6 +85,7 @@ dependencies = [ "shapely>=2.0.1", # spatialdata "spatialdata>=0.7.2", "vtk", + "ome-zarr<0.14.0", # bue 20260405: bugfix for spatialdata 0.7.2 ] @@ -108,6 +110,7 @@ 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" # substrate and cell agent +pcdl_get_muspan = "pcdl.commandline:get_muspan" pcdl_get_spatialdata = "pcdl.commandline:get_spatialdata" pcdl_plot_timeseries = "pcdl.commandline:plot_timeseries" pcdl_make_ome_tiff = "pcdl.commandline:make_ome_tiff" diff --git a/test/test_commandline_2d.py b/test/test_commandline_2d.py index b3b8ea8..f14c814 100644 --- a/test/test_commandline_2d.py +++ b/test/test_commandline_2d.py @@ -313,7 +313,8 @@ class TestCommandLineInterfacePlotContour(object): # + verbose (true, _false_) nop # + focus (_oxygen_) ok # + z_slize (0.0, _1.1_) ok - # + extrema (none, _0.0 40.0_) ok + # + vmin (none, _0.0_) ok + # + vmax (none, _40.0_) ok # + alpha (1.0, _0.2_) ok # + fill (true, _false_) # + cmp (viridis, _magma_) @@ -341,7 +342,8 @@ def test_pcdl_plot_contour_set(self): o_result = subprocess.run([ 'pcdl_plot_contour', s_pathfile_2d, 'oxygen', '--z_slice', '1.1', - '--extrema', '0.0', '40.0', + '--vmin', '0.0', + '--vmax', '40.0', '--alpha', '0.5', '--fill', 'false', '--cmap', 'magma', @@ -368,6 +370,7 @@ class TestCommandLineInterfaceConcVtk(object): # timestep and timeseries: # + path nop # + verbose (true, _false_) nop + # + ext (_conc.vtr, _.vtr_) nop def test_pcdl_make_conc_vtk_timeseries_default(self): o_result = subprocess.run(['pcdl_make_conc_vtk', s_path_2d], check=False, capture_output=True) @@ -379,6 +382,16 @@ def test_pcdl_make_conc_vtk_timeseries_default(self): os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_conc.vtr') assert o_result.returncode == 0 + def test_pcdl_make_conc_vtk_timeseries_ext(self): + o_result = subprocess.run(['pcdl_make_conc_vtk', s_path_2d, '--ext', '.vtr'], 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)}.vtr') + assert o_result.returncode == 0 + def test_pcdl_make_conc_vtk_timestep_default(self): o_result = subprocess.run(['pcdl_make_conc_vtk', s_pathfile_2d], check=False, capture_output=True) print(f'o_result: {o_result}\n') @@ -388,6 +401,15 @@ def test_pcdl_make_conc_vtk_timestep_default(self): os.remove(f'{s_path_2d}/output00000024_conc.vtr') assert o_result.returncode == 0 + def test_pcdl_make_conc_vtk_timestep_ext(self): + o_result = subprocess.run(['pcdl_make_conc_vtk', s_pathfile_2d, '--ext', '.vtr'], 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.vtr') + assert o_result.returncode == 0 + ################################ # cell agent realted test code # @@ -398,6 +420,7 @@ class TestCommandLineInterfaceCelltypeList(object): # timestep: # + path (pathfile, path) ok + # + settingxml (string, _none_, _false_) ok # + verbose (true, _false_) nop def test_pcdl_get_celltype_list_timeseries(self): @@ -408,6 +431,22 @@ def test_pcdl_get_celltype_list_timeseries(self): print(f'o_result.stderr: {o_result.stderr}\n') assert o_result.returncode == 0 + def test_pcdl_get_celltype_list_timeseries_settingxmlnone(self): + o_result = subprocess.run(['pcdl_get_celltype_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') + assert o_result.returncode == 0 + + def test_pcdl_get_celltype_list_timeseries_settingxmlfalse(self): + o_result = subprocess.run(['pcdl_get_celltype_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') + assert o_result.returncode == 0 + def test_pcdl_get_celltype_list_timestep(self): o_result = subprocess.run(['pcdl_get_celltype_list', s_pathfile_2d], check=False, capture_output=True) print(f'o_result: {o_result}\n') @@ -416,6 +455,22 @@ def test_pcdl_get_celltype_list_timestep(self): print(f'o_result.stderr: {o_result.stderr}\n') assert o_result.returncode == 0 + def test_pcdl_get_celltype_list_timestep_settingxmlnone(self): + o_result = subprocess.run(['pcdl_get_celltype_list', 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') + assert o_result.returncode == 0 + + def test_pcdl_get_celltype_list_timestep_settingxmlfalse(self): + o_result = subprocess.run(['pcdl_get_celltype_list', 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') + assert o_result.returncode == 0 + class TestCommandLineInterfaceCellAttributeList(object): ''' tests for one pcdl command line interface function. ''' @@ -423,7 +478,7 @@ class TestCommandLineInterfaceCellAttributeList(object): # timeseries collapsed: # + path (str) nop # + microenv (true, _false_) ok - # + physiboss (true, _false_) + # + physiboss (true, _false_) ok # + settingxml (string, _none_, _false_) ok # + verbose (true, _false_) nop @@ -984,7 +1039,7 @@ class TestCommandLineInterfaceGraphGml(object): # + path nop # + customtype ([], _sample:bool_) ok # + microenv (true, false) ok - # + physiboss (true, _false_) + # + physiboss (true, _false_) ok # + settingxml (string, _none_, _false_) ok # + verbose (true, _false_) nop # + graph_type (neighbor, _attached_) ok @@ -1188,25 +1243,25 @@ class TestCommandLineInterfacePlotScatter(object): # time series and time steps. # + path nop # + customtype ([], _sample:bool_) ok - # + microenv (true, _false_) - # + physiboss (true, _false_) - # + settingxml (PhysiCell_settings.xml, _false_) + # + 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_) - # + title (, _abc_) - # + grid (true, _false_) - # + legend_loc ('lower left', _'upper right'_) - # + xlim (none, _-40_400_) - # + ylim (none, _-30_300_) - # + xyequal (true, _false_) - # + s ('none', '74') - # + figsizepx (none, _[641, 481]_) - # + ext (jpeg, _tiff_) - # + figbgcolor (none, _yellow_) + # + 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([ @@ -1258,10 +1313,11 @@ class TestCommandLineInterfaceCellVtk(object): # + path nop # + customtype ([], _sample:bool_) ok # + microenv (true, _false) ok - # + physiboss (true, _false_) + # + 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) @@ -1333,6 +1389,16 @@ def test_pcdl_make_cell_vtk_timeseries_attribute_many(self): os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_cell.vtp') 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) + 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') + 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) print(f'o_result: {o_result}\n') @@ -1396,11 +1462,358 @@ def test_pcdl_make_cell_vtk_timestep_attribute_many(self): os.remove(f'{s_path_2d}/output00000024_cell.vtp') 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) + 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') + assert o_result.returncode == 0 + ####################################### # substrate and cell agenat test code # ####################################### +class TestCommandLineInterfaceMuspan(object): + ''' tests for one pcdl command line interface function. ''' + + # timestep and timeseries: + # + path nop + # + customtype ([], _sample:bool_) ok + # + microenv (true, _false_) ok + # + graph (true, _false_) ok + # + physiboss (true, _false_) ok + # + settingxml (string, _none_, _false_) ok + # + verbose (true, _false_) nop + # + z_slize (0.0, _1.1_) ok + # + values (int) ok + # + drop (cell_type oxygen) ok + # + keep (cell_type oxygen) ok + + # timeseries + def test_pcdl_get_muspan_timeseries(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', 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)}_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timeseries_customtype(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', 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)}_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timeseries_microenv(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', 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)}_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timeseries_graph(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--graph', '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)}_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timeseries_physiboss(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', 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)}_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timeseries_settingxmlfalse(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', 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') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timeseries_settingxmlnone(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', 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') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timeseries_zslice(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '1.1'], 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)}_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timeseries_value(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', s_path_2d, '--value', '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') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timeseries_drop(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', 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') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timeseries_keep(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', 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') + for i_step in range(25): + os.remove(f'{s_path_2d}/output000000{str(i_step).zfill(2)}_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + # timestep + def test_pcdl_get_muspan_timestep(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', 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_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timestep_customtype(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_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') + os.remove(f'{s_path_2d}/output00000024_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timestep_microenv(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', 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_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timestep_graph(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--graph', '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_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timestep_physiboss(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', 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_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timestep_settingxmlfalse(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', 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}/output00000024_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timestep_settingxmlnone(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', 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}/output00000024_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timestep_zslice(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '1.1'], 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_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timestep_value(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', s_pathfile_2d, '--value', '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}/output00000024_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timestep_drop(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', 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}/output00000024_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + def test_pcdl_get_muspan_timestep_keep(self): + try: + import muspan as ms + o_result = subprocess.run(['pcdl_get_muspan', 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}/output00000024_z0.muspan') + assert o_result.returncode == 0 + except ModuleNotFoundError: + print('Warning @ pytest TestCommandLineInterfaceMuspan : muspan module not installed.') + assert True + + class TestCommandLineInterfaceSpatialdata(object): ''' tests for one pcdl command line interface function. ''' @@ -1676,34 +2089,34 @@ class TestCommandLineInterfacePlotTimeSeries(object): # time series. # + path nop # + customtype ([], _sample:bool_) ok - # + microenv (true, _false_) - # + physiboss (true, _false_) - # + settingxml (PhysiCell_settings.xml, _false_) - # + verbose (true, _false_) nop - # + focus_cat ('none', _cell_type_) ok - # + focus_num ('none', _oxygen_) ok + # + microenv (true, _false_) nop + # + physiboss (true, _false_) ok + # + settingxml (PhysiCell_settings.xml, _false_) ok + # + verbose (true, _false_) ok + # + focus_cat ('none', _cell_type_) nop + # + focus_num ('none', _oxygen_) nop # + aggregate_num ('mean', 'entropy') ok - # + cat_drop ('', '1 2 3 4') - # + cat_keep ('', '1 2 3 4') - # + frame ('cell', 'conc') + # + cat_drop ('', '1 2 3 4') nop + # + cat_keep ('', '1 2 3 4') nop + # + frame ('cell', 'conc') ok # + z_slice ('none', _1.1_) ok - # + logy (false, _true_) - # + ylim (['none'], ['', '']) - # + secondary_y (false, _true_) - # + subplots (false, _true_) - # + sharex (false, _true_) - # + sharey (false, _true_) - # + linestyle ('-', '-.') - # + linewidth ('none', 9.0) - # + cmap ('none', 'magma') - # + color ('none', 'maroon') - # + grid (true, _false_) - # + legend (true, _false_) - # + yunit ('none', 'myunit') - # + title ('none', 'my title') - # + figsizepx (none, _[641, 481]_) - # + ext (csv, jpeg, png, _tiff_) - # + figbgcolor (none, _yellow_) + # + logy (false, _true_) ok + # + ylim (['none'], ['6.0', '7.0']) + # + secondary_y (false, _true_) ok + # + subplots (false, _true_) ok + # + sharex (false, _true_) ok + # + sharey (false, _true_) ok + # + linestyle ('-', '-.') ok + # + linewidth ('none', 9.0) ok + # + cmap ('none', 'magma') ok + # + color ('none', 'maroon') ok + # + grid (true, _false_) ok + # + legend (true, false, _reverse_) + # + yunit ('none', 'myunit') ok + # + title ('none', 'my title') ok + # + figsizepx (none, _[641, 481]_) ok + # + ext (csv, jpeg, png, _tiff_) ok + # + figbgcolor (none, _yellow_) ok def test_pcdl_plot_timeseries_default(self): o_result = subprocess.run([ diff --git a/test/test_timeseries_2d.py b/test/test_timeseries_2d.py index 74fd3e7..25e6c83 100644 --- a/test/test_timeseries_2d.py +++ b/test/test_timeseries_2d.py @@ -184,9 +184,10 @@ def test_mcdsts_read_mcds_xmlfilelist(self): (mcdsts.l_mcds == l_mcds) def test_mcdsts_form_list_of_mcds(self): - mcdsts = pcdl.TimeSeries([]) + mcdsts = pcdl.TimeSeries(s_path_2d, load=True, verbose=True) + mcdsts = pcdl.TimeSeries(mcdsts.l_mcds) assert(str(type(mcdsts)) == "") and \ - (len(mcdsts.l_mcds) == 0) + (len(mcdsts.l_mcds) == 25) ## micro environment related functions ## @@ -242,7 +243,8 @@ def test_mcdsts_plot_contour_if(self, mcdsts=mcdsts): ls_pathfile = mcdsts.plot_contour( focus = 'oxygen', z_slice = -3.333, # test if - extrema = None, # test if and for loop + vmin = None, # test if and for loop + vmax = None, # test if and for loop #alpha = 1, # pyMCD #fill = True, # pyMCD #cmap = 'viridis', # pyMCD @@ -271,7 +273,8 @@ def test_mcdsts_plot_contour_else(self, mcdsts=mcdsts): l_fig = mcdsts.plot_contour( focus = 'oxygen', z_slice = 0.0, # jump over if - extrema = [0, 38], # jump over if + vmin = 0, # jump over if + vmax = 38, # jump over if #alpha = 1, # TimeStep #fill = True, # TimeStep #cmap = 'viridis', # TimeStep @@ -386,7 +389,7 @@ def test_mcdsts_plot_scatter_num(self, mcdsts=mcdsts): xlim = None, # test if ylim = None, # test if #xyequal = True, # TimeStep - s = None, # test if + s = 0.9, # test calculation figsizepx = None, # case extract from initial.svg ext = 'jpeg', # generate file case figbgcolor = None, # test if @@ -417,7 +420,7 @@ def test_mcdsts_plot_scatter_cat(self, mcdsts=mcdsts): xlim = None, # test if ylim = None, # test if #xyequal = True, # TimeStep - s = None, # test if + #s = 1.0, # test calculation figsizepx = [641, 481], # test case non even pixel number ext = None, # test fig case figbgcolor = None, # not a file @@ -929,7 +932,7 @@ def test_mcdsts_get_anndata(self): (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.obs.shape[1] == 7) and \ (ann.obsm['spatial'].shape[0] > 9) and \ (ann.obsm['spatial'].shape[1] == 2) and \ (len(ann.obsp) == 0) and \ @@ -948,7 +951,7 @@ def test_mcdsts_get_anndata_value(self): (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.obs.shape[1] == 6) and \ (ann.obsm['spatial'].shape[0] > 9) and \ (ann.obsm['spatial'].shape[1] == 2) and \ (len(ann.obsp) == 0) and \ @@ -987,7 +990,7 @@ def test_mcdsts_get_anndata_keepmcdsfalse(self): (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.obs.shape[1] == 7) and \ (ann.obsm['spatial'].shape[0] > 9) and \ (ann.obsm['spatial'].shape[1] == 2) and \ (len(ann.obsp) == 0) and \ @@ -995,6 +998,46 @@ def test_mcdsts_get_anndata_keepmcdsfalse(self): (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 + + 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 + + ## spatialdata time seris related functions ## class TestTimeSeriesSpatialData(object): ''' test for pcdl.TestSeries class. ''' diff --git a/test/test_timeseries_3d.py b/test/test_timeseries_3d.py index d1bdace..2d68e8a 100644 --- a/test/test_timeseries_3d.py +++ b/test/test_timeseries_3d.py @@ -111,9 +111,10 @@ def test_mcdsts_read_mcds_xmlfilelist(self): (mcdsts.l_mcds == l_mcds) def test_mcdsts_form_list_of_mcds(self): - mcdsts = pcdl.TimeSeries([]) + mcdsts = pcdl.TimeSeries(s_path_3d, load=True, verbose=True) + mcdsts = pcdl.TimeSeries(mcdsts.l_mcds) assert(str(type(mcdsts)) == "") and \ - (len(mcdsts.l_mcds) == 0) + (len(mcdsts.l_mcds) == 25) ## micro environment related functions ## @@ -169,7 +170,8 @@ def test_mcdsts_plot_contour_if(self, mcdsts=mcdsts): ls_pathfile = mcdsts.plot_contour( focus = 'oxygen', z_slice = -3.333, # test if - extrema = None, # test if and for loop + vmin = None, # test if and for loop + vmax = None, # test if and for loop #alpha = 1, # TimeStep #fill = True, # TimeStep #cmap = 'viridis', # TimeStep @@ -197,7 +199,8 @@ def test_mcdsts_plot_contour_else(self, mcdsts=mcdsts): l_fig = mcdsts.plot_contour( focus = 'oxygen', z_slice = 0.0, # jump over if - extrema = [0, 38], # jump over if + vmin = 0, # jump over if + vmax = 38, # jump over if #alpha = 1, # TimeStep #fill = True, # TimeStep #cmap = 'viridis', # TimeStep @@ -313,7 +316,7 @@ def test_mcdsts_plot_scatter_num(self, mcdsts=mcdsts): xlim = None, # test if ylim = None, # test if #xyequal = True, # TimeStep - s = None, # test if + s = 0.1, # test calculation figsizepx = None, # case extract from initial.svg ext = 'jpeg', figbgcolor = None, # test if @@ -344,7 +347,7 @@ def test_mcdsts_plot_scatter_cat(self, mcdsts=mcdsts): xlim = None, # test if ylim = None, # test if #xyequal = True, # TimeStep - s = None, # test if + #s = 1.0, # test calculation figsizepx = [641, 481], # test case non even pixel number ext = None, figbgcolor = 'cyan', # jump over if @@ -792,7 +795,7 @@ def test_mcdsts_get_anndata(self): (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.obs.shape[1] == 7) and \ (ann.obsm['spatial'].shape[0] > 9) and \ (ann.obsm['spatial'].shape[1] == 3) and \ (len(ann.obsp) == 0) and \ @@ -811,7 +814,7 @@ def test_mcdsts_get_anndata_value(self): (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.obs.shape[1] == 6) and \ (ann.obsm['spatial'].shape[0] > 9) and \ (ann.obsm['spatial'].shape[1] == 3) and \ (len(ann.obsp) == 0) and \ @@ -850,7 +853,7 @@ def test_mcdsts_get_anndata_keepmcdsfalse(self): (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.obs.shape[1] == 7) and \ (ann.obsm['spatial'].shape[0] > 9) and \ (ann.obsm['spatial'].shape[1] == 3) and \ (len(ann.obsp) == 0) and \ @@ -858,6 +861,46 @@ def test_mcdsts_get_anndata_keepmcdsfalse(self): (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 + + 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 + + ## spatialdata time seris related functions ## class TestTimeSeriesSpatialData(object): ''' test for pcdl.TestSeries class. ''' diff --git a/test/test_timestep_2d.py b/test/test_timestep_2d.py index d1938a8..a0e595b 100644 --- a/test/test_timestep_2d.py +++ b/test/test_timestep_2d.py @@ -275,6 +275,12 @@ def test_mcds_get_multicellds_version(self, mcds=mcds): (str(type(s_mcdsversion)) == "") and \ (s_mcdsversion == 'MultiCellDS_2') + def test_mcds_get_pcdl_version(self, mcds=mcds): + s_pcdlversion = mcds.get_pcdl_version() + assert(str(type(mcds)) == "") and \ + (str(type(s_pcdlversion)) == "") and \ + (s_pcdlversion == f'pcdl_{pcdl.__version__}') + def test_mcds_get_physicell_version(self, mcds=mcds): s_pcversion = mcds.get_physicell_version() assert(str(type(mcds)) == "") and \ @@ -574,9 +580,9 @@ def test_mcds_plot_contourf(self, mcds=mcds): os.remove(s_pathfile) def test_mcds_make_conc_vtk(self, mcds=mcds): - s_pathfile = mcds.make_conc_vtk() + s_pathfile = mcds.make_conc_vtk(ext='.vtr') # test set ext parameter. assert(str(type(mcds)) == "") and \ - (s_pathfile.replace('\\','/').endswith('/pcdl/output_2d/output00000024_conc.vtr')) and \ + (s_pathfile.replace('\\','/').endswith('/pcdl/output_2d/output00000024.vtr')) and \ (os.path.exists(s_pathfile)) and \ (os.path.getsize(s_pathfile) > 2**10) os.remove(s_pathfile) @@ -652,7 +658,7 @@ def test_mcds_plot_scatter_cat_if(self, mcds=mcds): xlim = None, # test if ylim = None, # test if xyequal = True, # test if - #s = None, # matplotlib + s = 1.1, # test calculation ax = None, # generate matplotlib figure figsizepx = None, # test if case ax none ext = None, # test fig case @@ -677,7 +683,7 @@ def test_mcds_plot_scatter_cat_else1(self, mcds=mcds): xlim = [-31, 301], # jump over if ylim = [-21, 201], # jump over if xyequal = False, # jump over if - #s = None, # matplotlib + #s = 1.0, # test calculation ax = None, # use axis from existing matplotlib figure figsizepx = [701, 501], # jump over if case ax none ext = 'tiff', # test file case @@ -698,14 +704,14 @@ def test_mcds_plot_scatter_cat_else2(self, mcds=mcds): z_slice = 0, # jump over if z_axis = {'default'}, # test else case categorical #alpha = 1, # matplotlib - cmap = 'viridis', # test else case es_categorical + cmap = {'default': [0.5, 0.0, 0.0]}, # maroon test else case es_categorical title ='test_mcds_plot_scatter_else2', # matplotlib #grid = True, # matplotlib #legend_loc = 'lower left', # matplotlib xlim = None, # test if ylim = None, # test if xyequal = True, # test if - #s = None, # matplotlib + #s = 1.0, # test calculation ax = ax, # use axis from existing matplotlib figure #figsizepx = None, # test case ax ax #ext = None, # test fig case @@ -731,7 +737,7 @@ def test_mcds_plot_scatter_num_if(self, mcds=mcds): xlim = None, # test if ylim = None, # test if #xyequal = True, # test if - #s = None, # matplotlib + #s = 1.0, # matplotlib #ax = None, # generate matplotlib figure #figsizepx = None, # test if #ext = None, # test fig case @@ -769,6 +775,7 @@ def test_mcds_plot_scatter_num_else(self, mcds=mcds): def test_mcds_make_cell_vtk_attribute_default(self, mcds=mcds): s_pathfile = mcds.make_cell_vtk( #attribute=['cell_type'], + #ext='_cell.vtp', ) assert(str(type(mcds)) == "") and \ (s_pathfile.replace('\\','/').endswith('/pcdl/output_2d/output00000024_cell.vtp')) and \ @@ -777,9 +784,12 @@ def test_mcds_make_cell_vtk_attribute_default(self, mcds=mcds): os.remove(s_pathfile) def test_mcds_make_cell_vtk_attribute_zero(self, mcds=mcds): - s_pathfile = mcds.make_cell_vtk(attribute=[]) + s_pathfile = mcds.make_cell_vtk( + attribute=[], + ext='.vtp', # test set ext parameter + ) assert(str(type(mcds)) == "") and \ - (s_pathfile.replace('\\','/').endswith('/pcdl/output_2d/output00000024_cell.vtp')) and \ + (s_pathfile.replace('\\','/').endswith('/pcdl/output_2d/output00000024.vtp')) and \ (os.path.exists(s_pathfile)) and \ (os.path.getsize(s_pathfile) > 2**10) os.remove(s_pathfile) @@ -787,6 +797,7 @@ def test_mcds_make_cell_vtk_attribute_zero(self, mcds=mcds): def test_mcds_make_cell_vtk_attribute_many(self, mcds=mcds): s_pathfile = mcds.make_cell_vtk( attribute=['dead', 'cell_count_voxel', 'pressure', 'cell_type'], + #ext='_cell.vtp', # test default ext parameter ) assert(str(type(mcds)) == "") and \ (s_pathfile.replace('\\','/').endswith('/pcdl/output_2d/output00000024_cell.vtp')) and \ @@ -1087,9 +1098,49 @@ def test_mcds_get_anndata(self): (ann.obs.shape[1] == 7) and \ (ann.obsm['spatial'].shape[0] > 9) and \ (ann.obsm['spatial'].shape[1] == 2) and \ - (len(ann.obsp) == 2) and \ + (len(ann.obsp) == 4) and \ (ann.var.shape == (105, 0)) and \ - (len(ann.uns) == 1) + (len(ann.uns) == 2) + + +## muspan time step related functions ## +class TestTimeStepMuspan(object): + ''' test for pcdl.TimeStep class. ''' + + ## get_muspan command ## + def test_mcds_get_muspan_default(self): + try: + import muspan as ms + mcds = pcdl.TimeStep(s_pathfile_2d, verbose=False) + do_domain = mcds.get_muspan(z_slice=None, values=1, drop=set(), keep=set()) + s_key = sorted(do_domain.keys())[-1] + assert(str(type(mcds)) == "") and \ + (type(do_domain) == dict) and \ + (len(do_domain) == 1) 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 TestTimeStepMuspan : muspan module not installed.') + assert True + + def test_mcds_get_muspan_zslice(self): + try: + import muspan as ms + mcds = pcdl.TimeStep(s_pathfile_2d, verbose=False) + do_domain = mcds.get_muspan(z_slice=0.0, values=1, drop=set(), keep=set()) + s_key = sorted(do_domain.keys())[-1] + assert(str(type(mcds)) == "") and \ + (type(do_domain) == dict) and \ + (len(do_domain) == 1) 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 TestTimeStepMuspan : muspan module not installed.') + assert True ## spatialdata time step related functions ## diff --git a/test/test_timestep_3d.py b/test/test_timestep_3d.py index 8a2cfd5..a5edb28 100644 --- a/test/test_timestep_3d.py +++ b/test/test_timestep_3d.py @@ -322,7 +322,7 @@ def test_mcds_plot_contourf(self, mcds=mcds): os.remove(s_pathfile) def test_mcds_make_conc_vtk(self, mcds=mcds): - s_pathfile = mcds.make_conc_vtk() + s_pathfile = mcds.make_conc_vtk() # test default ext parameter assert(str(type(mcds)) == "") and \ (s_pathfile.replace('\\','/').endswith('/pcdl/output_3d/output00000024_conc.vtr')) and \ (os.path.exists(s_pathfile)) and \ @@ -426,7 +426,7 @@ def test_mcds_plot_scatter_cat_else2(self, mcds=mcds): z_slice = 0, # jump over if z_axis = {'default'}, # test else case categorical #alpha = 1, # matplotlib - cmap = 'viridis', # test else case es_categorical + cmap = {'default': '#800000'}, # maroon test else case es_categorical title ='test_mcds_plot_scatter_else2', # matplotlib #grid = True, # matplotlib #legend_loc = 'lower left', # matplotlib @@ -497,6 +497,7 @@ def test_mcds_plot_scatter_num_else(self, mcds=mcds): def test_mcds_make_cell_vtk_attribute_default(self, mcds=mcds): s_pathfile = mcds.make_cell_vtk( #attribute=['cell_type'], + #ext='_cell.vtp', ) assert(str(type(mcds)) == "") and \ (s_pathfile.replace('\\','/').endswith('/pcdl/output_3d/output00000024_cell.vtp')) and \ @@ -504,10 +505,13 @@ def test_mcds_make_cell_vtk_attribute_default(self, mcds=mcds): (os.path.getsize(s_pathfile) > 2**10) os.remove(s_pathfile) - def test_mcds_make_cell_vtk_attribute_zero(self, mcds=mcds): - s_pathfile = mcds.make_cell_vtk(attribute=[]) + def test_mcds_make_cell_vtk_attribute_zero_ext(self, mcds=mcds): + s_pathfile = mcds.make_cell_vtk( + attribute=[], + ext='.vtp', # test set ext parameter + ) assert(str(type(mcds)) == "") and \ - (s_pathfile.replace('\\','/').endswith('/pcdl/output_3d/output00000024_cell.vtp')) and \ + (s_pathfile.replace('\\','/').endswith('/pcdl/output_3d/output00000024.vtp')) and \ (os.path.exists(s_pathfile)) and \ (os.path.getsize(s_pathfile) > 2**10) os.remove(s_pathfile) @@ -515,6 +519,7 @@ def test_mcds_make_cell_vtk_attribute_zero(self, mcds=mcds): def test_mcds_make_cell_vtk_attribute_many(self, mcds=mcds): s_pathfile = mcds.make_cell_vtk( attribute=['dead', 'cell_count_voxel', 'pressure', 'cell_type'], + #ext='_cell.vtp', # test default ext parameter ) assert(str(type(mcds)) == "") and \ (s_pathfile.replace('\\','/').endswith('/pcdl/output_3d/output00000024_cell.vtp')) and \ @@ -666,9 +671,49 @@ def test_mcds_get_anndata(self): (ann.obs.shape[1] == 7) and \ (ann.obsm['spatial'].shape[0] > 9) and \ (ann.obsm['spatial'].shape[1] == 3) and \ - (len(ann.obsp) == 2) and \ + (len(ann.obsp) == 4) and \ (ann.var.shape == (105, 0)) and \ - (len(ann.uns) == 1) + (len(ann.uns) == 2) + + +## muspan time step related functions ## +class TestTimeStepMuspan(object): + ''' test for pcdl.TimeStep class. ''' + + ## get_muspan command ## + def test_mcds_get_muspan_default(self): + try: + import muspan as ms + mcds = pcdl.TimeStep(s_pathfile_3d, verbose=False) + do_domain = mcds.get_muspan(z_slice=None, values=1, drop=set(), keep=set()) + s_key = sorted(do_domain.keys())[-1] + assert(str(type(mcds)) == "") and \ + (type(do_domain) == dict) and \ + (len(do_domain) == 11) 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 TestTimeStepMuspan : muspan module not installed.') + assert True + + def test_mcds_get_muspan_zslice(self): + try: + import muspan as ms + mcds = pcdl.TimeStep(s_pathfile_3d, verbose=False) + do_domain = mcds.get_muspan(z_slice=0.0, values=1, drop=set(), keep=set()) + s_key = sorted(do_domain.keys())[-1] + assert(str(type(mcds)) == "") and \ + (type(do_domain) == dict) and \ + (len(do_domain) == 1) 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 TestTimeStepMuspan : muspan module not installed.') + assert True ## spatialdata time step related functions ##