pb4covid19.py 16.5 KB
Newer Older
Randy Heiland's avatar
Randy Heiland committed
1
2
3
4
5
6
7
8
9
10
11
12
import ipywidgets as widgets
import xml.etree.ElementTree as ET  # https://docs.python.org/2/library/xml.etree.elementtree.html
import os
import glob
import shutil
import math
import datetime
import tempfile
from about import AboutTab
from config import ConfigTab
from microenv_params import MicroenvTab
from user_params import UserTab
Randy Heiland's avatar
v3    
Randy Heiland committed
13
14
15
16
17
try:
    from cell_types import CellTypesTab
except:
    # print("cell_types.py does not exist due to no <cell_definitions>")
    pass
Randy Heiland's avatar
Randy Heiland committed
18
19
# from svg import SVGTab
from substrates import SubstrateTab
Randy Heiland's avatar
Randy Heiland committed
20
from animate_tab import AnimateTab
Vincent Noël's avatar
Vincent Noël committed
21
22
from physiboss import PhysiBoSSTab
from populations import PopulationsTab
Randy Heiland's avatar
Randy Heiland committed
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from pathlib import Path
import platform
import subprocess
from debug import debug_view

hublib_flag = True
if platform.system() != 'Windows':
    try:
#        print("Trying to import hublib.ui")
        from hublib.ui import RunCommand, Submit
        # from hublib2.ui import RunCommand, Submit
    except:
        hublib_flag = False
else:
    hublib_flag = False

Vincent Noël's avatar
Vincent Noël committed
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
style = """
    <style>
       .jupyter-widgets-output-area .output_scroll {
            height: unset !important;
            border-radius: unset !important;
            -webkit-box-shadow: unset !important;
            box-shadow: unset !important;
        }
        .jupyter-widgets-output-area  {
            height: auto !important;
            width: 100%; !important;
        }
        .container { width:100% !important; }
    </style>
    """
    
Randy Heiland's avatar
Randy Heiland committed
55
56
57
58
59
60
61
62
63
64
65
66
# join_our_list = "(Join/ask questions at https://groups.google.com/forum/#!forum/physicell-users)\n"


# create the tabs, but don't display yet
about_tab = AboutTab()
config_tab = ConfigTab()

xml_file = os.path.join('data', 'PhysiCell_settings.xml')
full_xml_filename = os.path.abspath(xml_file)

tree = ET.parse(full_xml_filename)  # this file cannot be overwritten; part of tool distro
xml_root = tree.getroot()
Randy Heiland's avatar
v3    
Randy Heiland committed
67

Randy Heiland's avatar
Randy Heiland committed
68
69
microenv_tab = MicroenvTab()
user_tab = UserTab()
Randy Heiland's avatar
v3    
Randy Heiland committed
70
71
72
73

if xml_root.find('.//cell_definitions'):
    cell_types_tab = CellTypesTab()

Randy Heiland's avatar
Randy Heiland committed
74
# svg = SVGTab()
Vincent Noël's avatar
Vincent Noël committed
75
76
77
sub = SubstrateTab(cell_types_tab)
populations = PopulationsTab()
physiboss = PhysiBoSSTab()
Randy Heiland's avatar
Randy Heiland committed
78
animate_tab = AnimateTab()
Randy Heiland's avatar
Randy Heiland committed
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137

nanoHUB_flag = False
if( 'HOME' in os.environ.keys() ):
    nanoHUB_flag = "home/nanohub" in os.environ['HOME']


# callback when user selects a cached run in the 'Load Config' dropdown widget.
# HOWEVER, beware if/when this is called after a sim finishes and the Load Config dropdown widget reverts to 'DEFAULT'.
# In that case, we don't want to recompute substrate.py self.numx, self.numy because we're still displaying plots from previous sim.
def read_config_cb(_b):
    # with debug_view:
    #     print("read_config_cb", read_config.value)

    sub.first_time = True

    if read_config.value is None:  #occurs when a Run just finishes and we update pulldown with the new cache dir??
        # with debug_view:
        #     print("NOTE: read_config_cb(): No read_config.value. Returning!")
        # print("NOTE: read_config_cb(): No read_config.value. Returning!")
        return

    if os.path.isdir(read_config.value):
        is_dir = True
        config_file = os.path.join(read_config.value, 'config.xml')
        # print("read_config_cb(): is_dir=True; config_file=",config_file)
    else:
        is_dir = False
        config_file = read_config.value
        # print("read_config_cb(): is_dir=False; --- config_file=",config_file)

    if Path(config_file).is_file():
        # with debug_view:
        # print("read_config_cb():  calling fill_gui_params with ",config_file)
        fill_gui_params(config_file)  #should verify file exists!

        # If cells or substrates toggled off in Config tab, toggle off in Plots tab
        if config_tab.toggle_svg.value == False:
            sub.cells_toggle.value = False
            sub.cells_toggle.disabled = True
        else:
            sub.cells_toggle.disabled = False

        if config_tab.toggle_mcds.value == False:
            sub.substrates_toggle.value = False
            sub.substrates_toggle.disabled = True
        else:
            sub.substrates_toggle.disabled = False

    else:
        # with debug_view:
        #     print("read_config_cb: ",config_file, " does not exist.")
        return
    
    # update visualization tabs
    if is_dir:
        # svg.update(read_config.value)
        # print("read_config_cb():  is_dir True, calling update_params")
        sub.update_params(config_tab, user_tab)
        sub.update(read_config.value)
Vincent Noël's avatar
Vincent Noël committed
138
        physiboss.update(read_config.value)
Randy Heiland's avatar
Randy Heiland committed
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
    # else:  # may want to distinguish "DEFAULT" from other saved .xml config files
        # FIXME: really need a call to clear the visualizations
        # svg.update('')
        # sub.update('')
        # print("read_config_cb():  is_dir False, calling update_params")
        # sub.update_params(config_tab)
        # print("read_config_cb():  is_dir False, calling sub.update()")
        # sub.update()  # NOTE: even if we attempt this, it doesn't really refresh the plots
        # pass
        

# Using the param values in the GUI, write a new .xml config file
def write_config_file(name):
    # with debug_view:
    #     print("write_config_file: based on ",full_filename)
    tree = ET.parse(full_xml_filename)  # this file cannot be overwritten; part of tool distro
    xml_root = tree.getroot()
    config_tab.fill_xml(xml_root)
    microenv_tab.fill_xml(xml_root)
    user_tab.fill_xml(xml_root)
Vincent Noël's avatar
Vincent Noël committed
159
160
    if xml_root.find('.//cell_definitions'):
        cell_types_tab.fill_xml(xml_root)
Randy Heiland's avatar
Randy Heiland committed
161
162
163
164
165
166
    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 )
Vincent Noël's avatar
Vincent Noël committed
167
    # print("pb4covid19.py: ------- sub.numx, sub.numy = ", sub.numx, sub.numy)
Randy Heiland's avatar
Randy Heiland committed
168
169
170
171


# callback from write_config_button
# def write_config_file_cb(b):
Vincent Noël's avatar
Vincent Noël committed
172
#     path_to_share = os.path.join('~', '.local','share','pb4covid19')
Randy Heiland's avatar
Randy Heiland committed
173
174
175
176
177
178
179
180
181
182
183
184
185
#     dirname = os.path.expanduser(path_to_share)

#     val = write_config_box.value
#     if val == '':
#         val = write_config_box.placeholder
#     name = os.path.join(dirname, val)
#     write_config_file(name)


# Fill the "Load Config" dropdown widget with valid cached results (and 
# default & previous config options)
def get_config_files():
    cf = {'DEFAULT': full_xml_filename}
Vincent Noël's avatar
Vincent Noël committed
186
    path_to_share = os.path.join('~', '.local','share','pb4covid19')
Randy Heiland's avatar
Randy Heiland committed
187
188
189
190
191
192
193
194
195
196
197
    dirname = os.path.expanduser(path_to_share)
    try:
        os.makedirs(dirname)
    except:
        pass
    files = glob.glob("%s/*.xml" % dirname)
    # dict.update() will append any new (key,value) pairs
    cf.update(dict(zip(list(map(os.path.basename, files)), files)))

    # Find the dir path (full_path) to the cached dirs
    if nanoHUB_flag:
Vincent Noël's avatar
Vincent Noël committed
198
        full_path = os.path.expanduser("~/data/results/.submit_cache/pb4covid19")  # does Windows like this?
Randy Heiland's avatar
Randy Heiland committed
199
200
201
202
    else:
        # local cache
        try:
            cachedir = os.environ['CACHEDIR']
Vincent Noël's avatar
Vincent Noël committed
203
            full_path = os.path.join(cachedir, "pb4covid19")
Randy Heiland's avatar
Randy Heiland committed
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
        except:
            # print("Exception in get_config_files")
            return cf

    # Put all those cached (full) dirs into a list
    dirs_all = [os.path.join(full_path, f) for f in os.listdir(full_path) if f != '.cache_table']

    # Only want those dirs that contain output files (.svg, .mat, etc), i.e., handle the
    # situation where a user cancels a Run before it really begins, which may create a (mostly) empty cached dir.
    dirs = [f for f in dirs_all if len(os.listdir(f)) > 5]   # "5" somewhat arbitrary
    # with debug_view:
    #     print(dirs)

    # Get a list of sorted dirs, according to creation timestamp (newest -> oldest)
    sorted_dirs = sorted(dirs, key=os.path.getctime, reverse=True)
    # with debug_view:
    #     print(sorted_dirs)

    # Get a list of timestamps associated with each dir
    sorted_dirs_dates = [str(datetime.datetime.fromtimestamp(os.path.getctime(x))) for x in sorted_dirs]
    # Create a dict of {timestamp:dir} pairs
    cached_file_dict = dict(zip(sorted_dirs_dates, sorted_dirs))
    cf.update(cached_file_dict)
    # with debug_view:
    #     print(cf)
    return cf


# Using params in a config (.xml) file, fill GUI widget values in each of the "input" tabs
def fill_gui_params(config_file):
    # with debug_view:
    #     print("fill_gui_params: filling with ",config_file)
    # print("fill_gui_params: filling with ",config_file)
    tree = ET.parse(config_file)
    xml_root = tree.getroot()
    config_tab.fill_gui(xml_root)
    microenv_tab.fill_gui(xml_root)
    user_tab.fill_gui(xml_root)
Vincent Noël's avatar
Vincent Noël committed
242
243
    if xml_root.find('.//cell_definitions'):
        cell_types_tab.fill_gui(xml_root)
Randy Heiland's avatar
Randy Heiland committed
244
245
246
247
248
249
250
251


def run_done_func(s, rdir):
    # with debug_view:
    #     print('run_done_func: results in', rdir)
    
    if nanoHUB_flag:
        # Email the user that their job has completed
Vincent Noël's avatar
Vincent Noël committed
252
        os.system("submit  mail2self -s 'nanoHUB pb4covid19' -t 'Your Run completed.'&")
Randy Heiland's avatar
Randy Heiland committed
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271

    # save the config file to the cache directory
    shutil.copy('config.xml', rdir)

    # cd out of tmpdir 
    os.chdir(homedir)

    # new results are available, so update dropdown
    # with debug_view:
    #     print('run_done_func: ---- before updating read_config.options')
    read_config.options = get_config_files()
    # with debug_view:
    #     print('run_done_func: ---- after updating read_config.options')

    # sub.update_dropdown_fields("data")   # WARNING: fill in the substrate field(s)

    # and update visualizations
    # svg.update(rdir)
    sub.update(rdir)
Vincent Noël's avatar
Vincent Noël committed
272
273
    physiboss.update(rdir)
    populations.update(rdir)
Randy Heiland's avatar
Randy Heiland committed
274
275
    animate_tab.gen_button.disabled = False

Randy Heiland's avatar
Randy Heiland committed
276
277
278
279
280
281
282
283
284
    # with debug_view:
    #     print('RDF DONE')


# This is used now for the ("smart") RunCommand
def run_sim_func(s):
    # with debug_view:
    #     print('run_sim_func')

Randy Heiland's avatar
Randy Heiland committed
285
286
    animate_tab.gen_button.disabled = True

Randy Heiland's avatar
Randy Heiland committed
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
    # If cells or substrates toggled off in Config tab, toggle off in Plots tab
    if config_tab.toggle_svg.value == False:
        sub.cells_toggle.value = False
        sub.cells_toggle.disabled = True
    else:
        sub.cells_toggle.disabled = False
    if config_tab.toggle_mcds.value == False:
        sub.substrates_toggle.value = False
        sub.substrates_toggle.disabled = True
    else:
        sub.substrates_toggle.disabled = False

    # make sure we are where we started
    os.chdir(homedir)

    # remove any previous data
    # NOTE: this dir name needs to match the <folder>  in /data/<config_file.xml>
    os.system('rm -rf tmpdir*')
    if os.path.isdir('tmpdir'):
        # something on NFS causing issues...
        tname = tempfile.mkdtemp(suffix='.bak', prefix='tmpdir_', dir='.')
        shutil.move('tmpdir', tname)
    os.makedirs('tmpdir')

    # write the default config file to tmpdir
    new_config_file = "tmpdir/config.xml"  # use Path; work on Windows?
    write_config_file(new_config_file)  

    with open(new_config_file) as f:
        run_name = s.make_rname(f.read())

    tdir = os.path.abspath('tmpdir')
    os.chdir(tdir)  # operate from tmpdir; temporary output goes here.  may be copied to cache later
    # svg.update(tdir)
    # sub.update_params(config_tab)
    sub.update(tdir)
Vincent Noël's avatar
Vincent Noël committed
323
    physiboss.update(tdir)
Randy Heiland's avatar
Randy Heiland committed
324
325
326
327
    # animate_tab.update(tdir)

    if nanoHUB_flag:
        if remote_cb.value:
Vincent Noël's avatar
Vincent Noël committed
328
            s.run(run_name, "-v ncn-hub_M@brown -n 8 -w 1440 pb4covid19-r7 config.xml")   # "-r7" suffix??
Randy Heiland's avatar
Randy Heiland committed
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
        else:
            # read_config.index = 0   # reset Dropdown 'Load Config' to 'DEFAULT' when Run interactively
            s.run(run_name, "--local ../bin/myproj config.xml")
    else:
        # reset Dropdown 'Load Config' to 'DEFAULT' when Run interactively
        # Warning: this will trigger read_config_cb() !!
        # read_config.index = 0   
        s.run("../bin/myproj config.xml", runname=run_name)

    # with debug_view:
    #     print('run_sim_func DONE')


def outcb(s):
    # This is called when new output is received.
    # Only update file list for certain messages: 
    # print("outcb(): s=",s)
    if "simulat" in s:    # "current simulated time: 60 min (max: 14400 min)"
        # New Data. update visualizations
        # svg.update('')
        # sub.update('')
        # sub.update_params(config_tab)
Randy Heiland's avatar
v3    
Randy Heiland committed
351
        sub.update()
Vincent Noël's avatar
Vincent Noël committed
352
353
        physiboss.update()
        populations.update()
Randy Heiland's avatar
Randy Heiland committed
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
    return s


# Callback for the ("dumb") 'Run' button (without hublib.ui)
def run_button_cb(s):
#    with debug_view:
#        print('run_button_cb')

#    new_config_file = full_xml_filename
    # print("new_config_file = ", new_config_file)
#    write_config_file(new_config_file)

    # make sure we are where we started
    os.chdir(homedir)

    # remove any previous data
    # NOTE: this dir name needs to match the <folder>  in /data/<config_file.xml>
    os.system('rm -rf tmpdir*')
    if os.path.isdir('tmpdir'):
        # something on NFS causing issues...
        tname = tempfile.mkdtemp(suffix='.bak', prefix='tmpdir_', dir='.')
        shutil.move('tmpdir', tname)
    os.makedirs('tmpdir')

    # write the default config file to tmpdir
    new_config_file = "tmpdir/config.xml"  # use Path; work on Windows?
    write_config_file(new_config_file)  

    tdir = os.path.abspath('tmpdir')
    os.chdir(tdir)  # operate from tmpdir; temporary output goes here.  may be copied to cache later
    # svg.update(tdir)
    # sub.update_params(config_tab)
    sub.update(tdir)
Vincent Noël's avatar
Vincent Noël committed
387
    physiboss.update(tdir)
Randy Heiland's avatar
Randy Heiland committed
388
389
390
391
392
393
394
395
    subprocess.Popen(["../bin/myproj", "config.xml"])


#-------------------------------------------------
if nanoHUB_flag:
    run_button = Submit(label='Run',
                       start_func=run_sim_func,
                        done_func=run_done_func,
Vincent Noël's avatar
Vincent Noël committed
396
                        cachename='pb4covid19',
Randy Heiland's avatar
Randy Heiland committed
397
398
399
400
401
402
                        showcache=False,
                        outcb=outcb)
else:
    if (hublib_flag):
        run_button = RunCommand(start_func=run_sim_func,
                            done_func=run_done_func,
Vincent Noël's avatar
Vincent Noël committed
403
                            cachename='pb4covid19',
Randy Heiland's avatar
Randy Heiland committed
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
                            showcache=False,
                            outcb=outcb)  
    else:
        run_button = widgets.Button(
            description='Run',
            button_style='success',  # 'success', 'info', 'warning', 'danger' or ''
            tooltip='Run a simulation',
        )
        run_button.on_click(run_button_cb)


if nanoHUB_flag or hublib_flag:
    read_config = widgets.Dropdown(
        description='Load Config',
        options=get_config_files(),
        tooltip='Config File or Previous Run',
    )
    read_config.style = {'description_width': '%sch' % str(len(read_config.description) + 1)}
    read_config.observe(read_config_cb, names='value') 

tab_height = 'auto'
Vincent Noël's avatar
Vincent Noël committed
425
tab_layout = widgets.Layout(width='auto',height=tab_height, overflow_y='scroll',)   # border='2px solid black',
Randy Heiland's avatar
v3    
Randy Heiland committed
426
427

if xml_root.find('.//cell_definitions'):
Vincent Noël's avatar
Vincent Noël committed
428
429
430
431
    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)
Randy Heiland's avatar
v3    
Randy Heiland committed
432
else:
Vincent Noël's avatar
Vincent Noël committed
433
434
    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],
Randy Heiland's avatar
Randy Heiland committed
435
436
437
438
439
                   _titles={i: t for i, t in enumerate(titles)},
                   layout=tab_layout)

homedir = os.getcwd()

Vincent Noël's avatar
Vincent Noël committed
440
tool_title = widgets.Label(r'\(\textbf{pb4covid19}\)')
Randy Heiland's avatar
Randy Heiland committed
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
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')

    top_row = widgets.HBox(children=[read_config, tool_title])
    gui = widgets.VBox(children=[top_row, tabs, run_button.w])
    fill_gui_params(read_config.options['DEFAULT'])
else:
    top_row = widgets.HBox(children=[tool_title])
    gui = widgets.VBox(children=[top_row, tabs, run_button])
    fill_gui_params("data/PhysiCell_settings.xml")


# pass in (relative) directory where output data is located
output_dir = "tmpdir"
# svg.update(output_dir)

sub.update_dropdown_fields("data")   # WARNING: generates multiple "<Figure size...>" stdout!
# animate_tab.update_dropdown_fields("data")   

# print('config_tab.svg_interval.value= ',config_tab.svg_interval.value )
# print('config_tab.mcds_interval.value= ',config_tab.mcds_interval.value )
#sub.update_params(config_tab)