Commit f7dbd852 authored by Vincent Noël's avatar Vincent Noël
Browse files

First draft of pb4covid

parent 0d5511a7
BSD 3-Clause License
Copyright (c) 2020, PhysiCell-Tools
Copyright (c) 2019, PhysiCell-Tools
All rights reserved.
Redistribution and use in source and binary forms, with or without
......
# pc4covid19 - COVID19 (SARS-CoV-2) tissue simulator nanoHUB app
**Version:** 3.2
**Release date:** 21 July 2020
## Overview
This repository contains code and data for the nanoHUB app https://nanohub.org/tools/pc4covid19.
Here, we primarily report on changes to the app's GUI.
It is based on the model at https://github.com/pc4covid19/COVID19. Refer to that repository
for a summary of changes to the model.
### Caveats and disclaimers:
**This model is under active development using rapid prototyping:**
* It has not been peer reviewed.
* It is intended to drive basic scientific research and public education at this stage.
* **It cannot be used for public policy decisions.**
* **It cannot be used for individual medical decisions.**
**This model will be continually refined with input from the community, particularly experts in infectious diseases. The validation state will be updated as this progresses.**
## Release summary:
### 3.2:
Updates to the core model; nothing new in the GUI.
### 3.1:
Minor updates to `About` text, e.g., explaining nature of stochastic results. Edits to `immune_submodels.cpp` (see details in the core model repository).
### 3.0:
The major change to the GUI in this release is the addition of a 'Cell Types' tab.
This allows editing parameters associated with `<cell_definitions>` in the configuration file.
This version also includes a `<style>` block in the Jupyter notebook that fixed an unwanted scrollbar in the lengthy `About` tab.
### 2.0:
The major change to the GUI in this release is the addition of an 'Animate' tab.
This allows animation of cells (not substrates) in the tab and the generation of a .mp4
video that can be downloaded. The animation uses the cells' SVG files.
### 1.0:
Provides a "standard" PhysiCell-based Jupyter notebook GUI consisting of 5 tabs:
* About: a description of the model and the app
* Config Basics: input parameters common to all models (e.g., domain grid, simulation time, choice/frequency of outputs)
* Microenvironment: microenvironment parameters that are model-specific
* User Params: user parameters that are model-specific
* Out: Plots: output display of cells and substrates
from ipywidgets import Output, Tab, Layout
from ipywidgets import Output
from IPython.display import display, HTML
class AboutTab(object):
def __init__(self):
# self.tab = Output(layout={'height': '900px'})
# self.tab = Output(layout={height=’85%’,min_height=’350px’})
# self.background = Output(layout={'height': 'auto'})
# self.legend = Output(layout={'height': 'auto'})
# self.tab = Output(layout={'height': '600px'})
self.tab = Output(layout={'height': 'auto'})
# self.tab.append_display_data(HTML(filename='doc/about-good.html'))
# self.tab.append_display_data(HTML(filename='doc/about-bad.html'))
# self.background.append_display_data(HTML(filename='doc/about_background.html'))
# self.legend.append_display_data(HTML(filename='doc/about_legend.html'))
self.tab.append_display_data(HTML(filename='doc/about.html'))
# tab_layout = Layout(width='auto',height='auto', overflow_y='scroll',) # border='2px solid black',
# titles = ['Background', 'Legend']
# self.tab = Tab(children=[self.background, self.legend],
# _titles={i: t for i, t in enumerate(titles)},
# layout=tab_layout)
......@@ -72,6 +72,11 @@ class AnimateTab(object):
#---------------------------
# callback for the 'Generate video' button
def gen_button_cb(self, b):
if shutil.which('ffmpeg') is None:
self.feedback.value = 'Error: the program "ffmpeg" cannot be found in your PATH.'
return
self.gen_button.description = 'working'
self.gen_button.button_style = 'warning' # 'success','info','warning','danger' or ''
self.gen_button.disabled = True
......
This diff is collapsed.
......@@ -72,12 +72,10 @@ class ConfigTab(object):
disabled = disable_domain,
layout=Layout(width=constWidth),
)
self.ydelta = BoundedFloatText(
min=1.,
description='dy',
disabled = True,
disabled = disable_domain,
layout=Layout(width=constWidth),
)
self.zdelta = BoundedFloatText(
......@@ -86,16 +84,6 @@ class ConfigTab(object):
disabled = disable_domain,
layout=Layout(width=constWidth),
)
def xdelta_cb(b):
self.ydelta.value = self.xdelta.value
self.zdelta.value = 0.5 * (self.xdelta.value + self.ydelta.value)
self.zmin.value = -0.5 * self.zdelta.value
self.zmax.value = 0.5 * self.zdelta.value
self.xdelta.observe(xdelta_cb)
"""
self.tdelta = BoundedFloatText(
min=0.01,
......@@ -161,14 +149,14 @@ class ConfigTab(object):
# description='$T_0$',
# layout=Layout(width=constWidth),
# )
self.svg_interval = BoundedIntText(
min=1,
self.svg_interval = BoundedFloatText(
min=0.001,
max=99999999, # TODO: set max on all Bounded to avoid unwanted default
description='every',
layout=Layout(width='160px'),
)
self.mcds_interval = BoundedIntText(
min=1,
self.mcds_interval = BoundedFloatText(
min=0.001,
max=99999999,
description='every',
# disabled=True,
......@@ -268,14 +256,14 @@ class ConfigTab(object):
self.toggle_mcds.value = True
else:
self.toggle_mcds.value = False
self.mcds_interval.value = int(xml_root.find(".//full_data//interval").text)
self.mcds_interval.value = float(xml_root.find(".//full_data//interval").text)
# NOTE: do this *after* filling the mcds_interval, directly above, due to the callback/constraints on them
if xml_root.find(".//SVG//enable").text.lower() == 'true':
self.toggle_svg.value = True
else:
self.toggle_svg.value = False
self.svg_interval.value = int(xml_root.find(".//SVG//interval").text)
self.svg_interval.value = float(xml_root.find(".//SVG//interval").text)
# Read values from the GUI widgets and generate/write a new XML
......
......@@ -22,6 +22,7 @@ class MicroenvTab(object):
name_button_layout={'width':'25%'}
widget_layout = {'width': '15%'}
widget2_layout = {'width': '10%'}
units_button_layout ={'width':'15%'}
desc_button_layout={'width':'45%'}
......
......@@ -18,6 +18,8 @@ except:
# from svg import SVGTab
from substrates import SubstrateTab
from animate_tab import AnimateTab
from physiboss import PhysiBoSSTab
from populations import PopulationsTab
from pathlib import Path
import platform
import subprocess
......@@ -55,7 +57,9 @@ if xml_root.find('.//cell_definitions'):
cell_types_tab = CellTypesTab()
# svg = SVGTab()
sub = SubstrateTab()
sub = SubstrateTab(cell_types_tab)
populations = PopulationsTab()
physiboss = PhysiBoSSTab()
animate_tab = AnimateTab()
nanoHUB_flag = False
......@@ -116,6 +120,7 @@ def read_config_cb(_b):
# print("read_config_cb(): is_dir True, calling update_params")
sub.update_params(config_tab, user_tab)
sub.update(read_config.value)
physiboss.update(read_config.value)
# else: # may want to distinguish "DEFAULT" from other saved .xml config files
# FIXME: really need a call to clear the visualizations
# svg.update('')
......@@ -136,18 +141,20 @@ def write_config_file(name):
config_tab.fill_xml(xml_root)
microenv_tab.fill_xml(xml_root)
user_tab.fill_xml(xml_root)
if xml_root.find('.//cell_definitions'):
cell_types_tab.fill_xml(xml_root)
tree.write(name)
# update substrate mesh layout (beware of https://docs.python.org/3/library/functions.html#round)
sub.update_params(config_tab, user_tab)
# sub.numx = math.ceil( (config_tab.xmax.value - config_tab.xmin.value) / config_tab.xdelta.value )
# sub.numy = math.ceil( (config_tab.ymax.value - config_tab.ymin.value) / config_tab.ydelta.value )
# print("pc4covid19_v3.py: ------- sub.numx, sub.numy = ", sub.numx, sub.numy)
# print("pc4covid19.py: ------- sub.numx, sub.numy = ", sub.numx, sub.numy)
# callback from write_config_button
# def write_config_file_cb(b):
# path_to_share = os.path.join('~', '.local','share','pc4covid19_v3')
# path_to_share = os.path.join('~', '.local','share','pc4covid19')
# dirname = os.path.expanduser(path_to_share)
# val = write_config_box.value
......@@ -161,7 +168,7 @@ def write_config_file(name):
# default & previous config options)
def get_config_files():
cf = {'DEFAULT': full_xml_filename}
path_to_share = os.path.join('~', '.local','share','pc4covid19_v3')
path_to_share = os.path.join('~', '.local','share','pc4covid19')
dirname = os.path.expanduser(path_to_share)
try:
os.makedirs(dirname)
......@@ -173,12 +180,12 @@ def get_config_files():
# Find the dir path (full_path) to the cached dirs
if nanoHUB_flag:
full_path = os.path.expanduser("~/data/results/.submit_cache/pc4covid19_v3") # does Windows like this?
full_path = os.path.expanduser("~/data/results/.submit_cache/pc4covid19") # does Windows like this?
else:
# local cache
try:
cachedir = os.environ['CACHEDIR']
full_path = os.path.join(cachedir, "pc4covid19_v3")
full_path = os.path.join(cachedir, "pc4covid19")
except:
# print("Exception in get_config_files")
return cf
......@@ -217,6 +224,8 @@ def fill_gui_params(config_file):
config_tab.fill_gui(xml_root)
microenv_tab.fill_gui(xml_root)
user_tab.fill_gui(xml_root)
if xml_root.find('.//cell_definitions'):
cell_types_tab.fill_gui(xml_root)
def run_done_func(s, rdir):
......@@ -225,7 +234,7 @@ def run_done_func(s, rdir):
if nanoHUB_flag:
# Email the user that their job has completed
os.system("submit mail2self -s 'nanoHUB pc4covid19_v3' -t 'Your Run completed.'&")
os.system("submit mail2self -s 'nanoHUB pc4covid19' -t 'Your Run completed.'&")
# save the config file to the cache directory
shutil.copy('config.xml', rdir)
......@@ -245,7 +254,8 @@ def run_done_func(s, rdir):
# and update visualizations
# svg.update(rdir)
sub.update(rdir)
physiboss.update(rdir)
populations.update(rdir)
animate_tab.gen_button.disabled = False
# with debug_view:
......@@ -295,11 +305,12 @@ def run_sim_func(s):
# svg.update(tdir)
# sub.update_params(config_tab)
sub.update(tdir)
physiboss.update(tdir)
# animate_tab.update(tdir)
if nanoHUB_flag:
if remote_cb.value:
s.run(run_name, "-v ncn-hub_M@brown -n 8 -w 1440 pc4covid19_v3-r7 config.xml") # "-r7" suffix??
s.run(run_name, "-v ncn-hub_M@brown -n 8 -w 1440 pc4covid19-r7 config.xml") # "-r7" suffix??
else:
# read_config.index = 0 # reset Dropdown 'Load Config' to 'DEFAULT' when Run interactively
s.run(run_name, "--local ../bin/myproj config.xml")
......@@ -323,6 +334,8 @@ def outcb(s):
# sub.update('')
# sub.update_params(config_tab)
sub.update()
physiboss.update()
populations.update()
return s
......@@ -356,7 +369,7 @@ def run_button_cb(s):
# svg.update(tdir)
# sub.update_params(config_tab)
sub.update(tdir)
physiboss.update(tdir)
subprocess.Popen(["../bin/myproj", "config.xml"])
......@@ -365,14 +378,14 @@ if nanoHUB_flag:
run_button = Submit(label='Run',
start_func=run_sim_func,
done_func=run_done_func,
cachename='pc4covid19_v3',
cachename='pc4covid19',
showcache=False,
outcb=outcb)
else:
if (hublib_flag):
run_button = RunCommand(start_func=run_sim_func,
done_func=run_done_func,
cachename='pc4covid19_v3',
cachename='pc4covid19',
showcache=False,
outcb=outcb)
else:
......@@ -394,24 +407,22 @@ if nanoHUB_flag or hublib_flag:
read_config.observe(read_config_cb, names='value')
tab_height = 'auto'
#tab_layout = widgets.Layout(width='auto',height=tab_height, overflow_y='scroll',) # border='2px solid black',
tab_layout = widgets.Layout(width='auto',height=tab_height) # border='2px solid black',
tab_layout = widgets.Layout(width='auto',height=tab_height, overflow_y='scroll',) # border='2px solid black',
if xml_root.find('.//cell_definitions'):
titles = ['About', 'Config Basics', 'Microenvironment', 'User Params', 'Cell Types', 'Out: Plots', 'Animate']
tabs = widgets.Tab(children=[about_tab.tab, config_tab.tab, microenv_tab.tab, user_tab.tab, cell_types_tab.tab, sub.tab, animate_tab.tab],
_titles={i: t for i, t in enumerate(titles)})
# layout=tab_layout)
titles = ['About', 'Config Basics', 'Microenvironment', 'User Params', 'Cell Types', 'Out: Plots', 'Out: Populations', 'Out: PhysiBoSS', 'Animate']
tabs = widgets.Tab(children=[about_tab.tab, config_tab.tab, microenv_tab.tab, user_tab.tab, cell_types_tab.tab, sub.tab, populations.tab, physiboss.tab, animate_tab.tab],
_titles={i: t for i, t in enumerate(titles)},
layout=tab_layout)
else:
titles = ['About', 'Config Basics', 'Microenvironment', 'User Params', 'Out: Plots', 'Animate']
tabs = widgets.Tab(children=[about_tab.tab, config_tab.tab, microenv_tab.tab, user_tab.tab, sub.tab, animate_tab.tab],
titles = ['About', 'Config Basics', 'Microenvironment', 'User Params', 'Out: Plots', 'Out: Populations', 'Out: PhysiBoSS', 'Animate']
tabs = widgets.Tab(children=[about_tab.tab, config_tab.tab, microenv_tab.tab, user_tab.tab, sub.tab, populations.tab, physiboss.tab, animate_tab.tab],
_titles={i: t for i, t in enumerate(titles)},
layout=tab_layout)
homedir = os.getcwd()
# tool_title = widgets.Label(r'\(\textbf{pc4covid19_v3}\)')
tool_title = widgets.Label(r'\(\textbf{PhysiCell model for SARS-CoV-2}\)')
tool_title = widgets.Label(r'\(\textbf{pc4covid19}\)')
if nanoHUB_flag or hublib_flag:
# define this, but don't use (yet)
remote_cb = widgets.Checkbox(indent=False, value=False, description='Submit as Batch Job to Clusters/Grid')
......
# PhysiBoSS Tab
import os
from ipywidgets import Layout, Label, Text, Checkbox, Button, HBox, VBox, Box, \
FloatText, BoundedIntText, BoundedFloatText, HTMLMath, Dropdown, interactive, Output
from collections import deque, Counter
import xml.etree.ElementTree as ET
import matplotlib.pyplot as plt
from matplotlib.collections import PatchCollection
import matplotlib.colors as mplc
import numpy as np
import glob
import platform
import numpy as np
import csv
import itertools
import copy
import scipy
# from debug import debug_view
class PhysiBoSSTab(object):
def __init__(self):
# tab_height = '520px'
# tab_layout = Layout(width='900px', # border='2px solid black',
# height=tab_height, overflow_y='scroll')
self.output_dir = '.'
constWidth = '180px'
# self.fig = plt.figure(figsize=(6, 6))
# self.fig = plt.figure(figsize=(7, 7))
config_file = "data/PhysiCell_settings.xml"
self.cell_lines = {}
self.cell_lines_by_name = {}
self.cell_lines_array = ["All"]
if os.path.isfile(config_file):
try:
tree = ET.parse(config_file)
except:
print("Cannot parse",config_file, "- check it's XML syntax.")
return
root = tree.getroot()
uep = root.find('.//cell_definitions') # find unique entry point (uep)
for child in uep.findall('cell_definition'):
self.cell_lines[int(child.attrib["ID"])] = child.attrib["name"]
self.cell_lines_by_name[child.attrib["name"]] = int(child.attrib["ID"])
self.cell_lines_array.append(child.attrib["name"])
# print(child.attrib['name'])
else:
print("config.xml does not exist")
max_frames = 0
self.svg_plot = interactive(self.create_area_chart, frame=(0, max_frames), percentage=(0.0, 10.0), total=False, cell_line=self.cell_lines_array, continuous_update=False)
plot_size = '500px' # small: controls the size of the tab height, not the plot (rf. figsize for that)
plot_size = '700px' # medium
plot_size = '750px' # medium
self.svg_plot.layout.width = '1000px'
self.svg_plot.layout.height = '700px'
self.use_defaults = True
self.axes_min = 0.0
self.axes_max = 2000 # hmm, this can change (TODO?)
self.max_frames = BoundedIntText(
min=0, max=99999, value=max_frames,
description='Max',
layout=Layout(width='160px'),
# layout=Layout(flex='1 1 auto', width='auto'), #Layout(width='160px'),
)
self.max_frames.observe(self.update_max_frames)
items_auto = [Label('select slider: drag or left/right arrows'),
self.max_frames,
]
box_layout = Layout(display='flex',
flex_flow='row',
align_items='stretch',
width='70%')
row1 = Box(children=items_auto, layout=box_layout)
self.tab = VBox([row1, self.svg_plot])
self.count_dict = {}
self.file_dict = {}
def update(self, rdir=''):
# with debug_view:
# print("SVG: update rdir=", rdir)
if rdir:
self.output_dir = rdir
all_files = sorted(glob.glob(os.path.join(self.output_dir, 'snapshot*.svg')))
if len(all_files) > 0:
last_file = all_files[-1]
self.max_frames.value = int(last_file[-12:-4]) # assumes naming scheme: "snapshot%08d.svg"
# self.create_dict(self.max_frames.value, self.output_dir)
# self.state_counter(self.max_frames.value)
# with debug_view:
# print("SVG: added %s files" % len(all_files))
def update_max_frames(self,_b):
self.svg_plot.children[0].max = self.max_frames.value
#-------------------------
def plot_states(self, frame):
# global current_idx, axes_max
global current_frame
current_frame = frame
fname = "snapshot%08d.svg" % frame
full_fname = os.path.join(self.output_dir, fname)
if not os.path.isfile(full_fname):
print("Once output files are generated, click the slider.")
return
tree = ET.parse(full_fname)
root = tree.getroot()
numChildren = 0
for child in root:
if self.use_defaults and ('width' in child.attrib.keys()):
self.axes_max = float(child.attrib['width'])
if child.text and "Current time" in child.text:
svals = child.text.split()
title_str = svals[2] + "d, " + svals[4] + "h, " + svals[7] + "m"
title_str += " (" + str(num_cells) + " agents)"
self.fig = plt.figure(figsize=(15, 15))
plt.xlim(self.axes_min, self.axes_max)
plt.ylim(self.axes_min, self.axes_max)
plt.title(title_str)
def create_dict(self, number_of_files, folder, cells_indexes):
"create a dictionary with the states file in the folder 'output', half of the dict is used to calculate the percentage of the node, the other half is for the states"
# if not os.path.isfile("%sstates_00000000.csv" % folder):
# return
self.file_dict = {}
if number_of_files > 0:
for i in range (0, number_of_files):
nodes_dict = {}
states_dict = {}
with open(os.path.join(self.output_dir, 'states_%08u.csv' % i), newline='') as csvfile:
states_reader = csv.reader(csvfile, delimiter=',')
for row in states_reader:
if row[0] != 'ID' and (cells_indexes is None or int(row[0]) in cells_indexes):
states_dict[row[0]] = row[1]
nodes_dict[row[0]] = row[1].replace("--", "").split()
self.file_dict["node_step{0}".format(i)] = nodes_dict
self.file_dict["state_step{0}".format(i)] = states_dict
# print(self.file_dict)
# return file_dict
def state_counter(self, number_of_files, percentage):
"create a dict with the states of the network, it can be used to print states pie chart"
self.count_dict = {}
temp_dict = {}
max_cell = 0
if number_of_files > 0:
for i in range (0, number_of_files):
state_list = []
for key in self.file_dict["state_step{0}".format(i)]:
state_list.append(self.file_dict["state_step{0}".format(i)][key])
state_counts = Counter(state_list)
max_cell = max_cell + sum(state_counts.values())
#fix_count_dict = {}
#for key, group in itertools.groupby(state_counts, lambda k: 'others' if (state_counts[k]<(0.01* (len(self.file_dict["state_step%s" %(i)])))) else k):
#fix_count_dict[key] = sum([state_counts[k] for k in list(group)])
temp_dict["state_count{0}".format(i)] = state_counts
self.count_dict = self.filter_states(max_cell, temp_dict, percentage)
# return self.count_dict
def create_area_chart(self, frame=None, total=False, percentage=(0.0, 100.0), cell_line="All"):
"plot an area chart with the evolution of the network states during the simulation"
cells_indexes = None
if cell_line != "All":
cells_indexes = set()
for i in range(0, frame):
fname = "output%08d_cells_physicell.mat" % i
full_fname = os.path.join(self.output_dir, fname)
if not os.path.isfile(full_fname):
print("Once output files are generated, click the slider.") # No: output00000000_microenvironment0.mat
return
info_dict = {}
scipy.io.loadmat(full_fname, info_dict)
M = info_dict['cells'][[0,5], :].astype(int)
cell_line_index = self.cell_lines_by_name[cell_line]
indexes = np.where(M[1, :] == cell_line_index)
cells_indexes = cells_indexes.union(set(M[0, indexes][0]))
cells_indexes = list(cells_indexes)
if len(cells_indexes) == 0:
print("There are no %s cells." % cell_line)
return
self.create_dict(frame, self.output_dir, cells_indexes)
self.state_counter(frame, percentage)
state_list = []
all_state = []
a = []
for k in self.count_dict:
state_list.append(list(self.count_dict[k].keys()))
for l in state_list:
for state in l:
all_state.append(state)
all_state = list(dict.fromkeys(all_state))
for state_count in self.count_dict:
b = []
for states in all_state:
try:
b.append(self.count_dict[state_count][states])
except:
b.append(0)
a.append(b)
a = np.array(a)
#print(a)
a = np.transpose(a)
if not total:
percent = a / a.sum(axis=0).astype(float) * 100
else:
percent = a
x = np.arange(len(self.count_dict))
self.fig = plt.figure(figsize=(10,5), dpi=200)
ax = self.fig.add_subplot(111)
ax.stackplot(x, percent, labels=all_state)
ax.legend(labels=all_state, loc='upper center', bbox_to_anchor=(0.5, -0.05),shadow=True, ncol=2)