1import os 2import wx 3import time 4import fnmatch 5 6import abipy.gui.awx as awx 7import wx.lib.agw.flatnotebook as fnb 8 9from collections import OrderedDict 10from monty.dev import get_ncpus 11from abipy.gui.events import AbinitEventsFrame, AbinitEventsNotebookFrame 12from abipy.gui.timer import MultiTimerFrame, AbinitTimerFrame 13from abipy.gui.browser import FileListFrame, DirBrowserFrame, frame_from_filepath, frameclass_from_filepath 14from abipy.gui.editor import TextNotebookFrame, SimpleTextViewer 15import abipy.flowtk as flowtk 16 17 18def yaml_manager_dialog(parent): 19 """ 20 Open a dialog that allows the user to select a YAML file with the taskmanager. 21 Returns the new manager or None if the user clicked CANCEL or the specifed file is not valid. 22 """ 23 dialog = wx.FileDialog(parent, message="Choose a taskmanager.yml file", defaultDir=os.getcwd(), 24 wildcard="YAML files (*.yml)|*.yml", 25 style=wx.OPEN | wx.MULTIPLE | wx.CHANGE_DIR) 26 27 # Show the dialog and retrieve the user response. 28 # If it is the OK response, process the data. 29 if dialog.ShowModal() == wx.ID_CANCEL: return None 30 filepath = dialog.GetPath() 31 dialog.Destroy() 32 33 try: 34 return flowtk.TaskManager.from_file(filepath) 35 except: 36 awx.showErrorMessage(parent) 37 return None 38 39 40class FlowViewerFrame(awx.Frame): 41 VERSION = "0.1" 42 43 # Time in second after which we check the status of the tasks. 44 REFRESH_INTERVAL = 120 45 46 HELP_MSG = """\ 47Quick help: 48 49 Task list: 50 51 Left-Click: Open directory with output files. 52 Right-Click: display popup menu with choices. 53 54Also, these key bindings can be used 55(For Mac OSX, replace 'Ctrl' with 'Apple'): 56 57 Ctrl-Q: quit""" 58 59 def __init__(self, parent, flow, **kwargs): 60 """ 61 Args: 62 parent: 63 Parent window. 64 flow: 65 `AbinitFlow` with the list of `Workflow` objects. 66 """ 67 if "title" not in kwargs: 68 kwargs["title"] = self.codename 69 70 super(FlowViewerFrame, self).__init__(parent, -1, **kwargs) 71 72 # This combination of options for config seems to work on my Mac. 73 self.config = wx.FileConfig(appName=self.codename, localFilename=self.codename + ".ini", 74 style=wx.CONFIG_USE_LOCAL_FILE) 75 76 # Build menu, toolbar and status bar. 77 self.SetMenuBar(self.makeMenu()) 78 self.makeToolBar() 79 self.statusbar = self.CreateStatusBar() 80 self.Centre() 81 82 self.flow = flow 83 84 # Disable launch mode if we already executing the flow with the scheduler. 85 self.check_launcher_file() 86 87 # Build UI 88 self.panel = panel = wx.Panel(self, -1) 89 self.main_sizer = main_sizer = wx.BoxSizer(wx.VERTICAL) 90 91 # Here we create a panel and a notebook on the panel 92 self.notebook = FlowNotebook(panel, self.flow) 93 main_sizer.Add(self.notebook, 1, wx.EXPAND, 5) 94 95 submit_button = wx.Button(panel, -1, label='Submit') 96 submit_button.Bind(wx.EVT_BUTTON, self.OnSubmitButton) 97 98 text = wx.StaticText(panel, -1, "Max nlaunch:") 99 text.Wrap(-1) 100 text.SetToolTipString("Maximum number of tasks that can be submitted. Use -1 for unlimited launches.") 101 self.max_nlaunch = wx.SpinCtrl(panel, -1, value=str(get_ncpus()), min=-1) 102 103 hsizer = wx.BoxSizer(wx.HORIZONTAL) 104 hsizer.Add(submit_button, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 5) 105 hsizer.Add(text, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 5) 106 hsizer.Add(self.max_nlaunch, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 5) 107 main_sizer.Add(hsizer, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 5) 108 109 panel.SetSizerAndFit(main_sizer) 110 111 # Register this event when the GUI is IDLE 112 # Not used anymore since the Yaml parser 113 # is very slow when we use the pure python version. 114 self.last_refresh = time.time() 115 #self.Bind(wx.EVT_IDLE, self.OnIdle) 116 117 @property 118 def codename(self): 119 """String with the code name.""" 120 return "Flow Viewer" 121 122 def makeToolBar(self): 123 """Create toolbar.""" 124 self.toolbar = toolbar = self.CreateToolBar() 125 toolbar.SetToolBitmapSize(wx.Size(48, 48)) 126 127 def bitmap(path): 128 return wx.Bitmap(awx.path_img(path)) 129 130 self.ID_SHOW_INPUTS = wx.NewId() 131 self.ID_SHOW_OUTPUTS = wx.NewId() 132 self.ID_SHOW_LOGS = wx.NewId() 133 self.ID_SHOW_JOB_SCRIPTS = wx.NewId() 134 self.ID_SHOW_JOB_OUTERRS = wx.NewId() 135 self.ID_BROWSE = wx.NewId() 136 self.ID_SHOW_MAIN_EVENTS = wx.NewId() 137 self.ID_SHOW_LOG_EVENTS = wx.NewId() 138 self.ID_SHOW_TIMERS = wx.NewId() 139 self.ID_CHECK_STATUS = wx.NewId() 140 141 toolbar.AddSimpleTool(self.ID_SHOW_INPUTS, bitmap("in.png"), "Visualize the input file(s) of the workflow.") 142 toolbar.AddSimpleTool(self.ID_SHOW_OUTPUTS, bitmap("out.png"), "Visualize the output file(s) of the workflow..") 143 toolbar.AddSimpleTool(self.ID_SHOW_LOGS, bitmap("log.png"), "Visualize the log file(s) of the workflow.") 144 toolbar.AddSimpleTool(self.ID_SHOW_JOB_SCRIPTS, bitmap("script.png"), "Visualize the script(s).") 145 toolbar.AddSimpleTool(self.ID_SHOW_JOB_OUTERRS, bitmap("script.png"), "Visualize the script(s).") 146 toolbar.AddSimpleTool(self.ID_BROWSE, bitmap("browse.png"), "Browse all the files of the workflow.") 147 toolbar.AddSimpleTool(self.ID_SHOW_MAIN_EVENTS, bitmap("out_evt.png"), "Show the ABINIT events reported in the main output files.") 148 toolbar.AddSimpleTool(self.ID_SHOW_LOG_EVENTS, bitmap("log_evt.png"), "Show the ABINIT events reported in the log files.") 149 toolbar.AddSimpleTool(self.ID_SHOW_TIMERS, bitmap("timer.png"), "Show the ABINIT timers in the abo files.") 150 151 toolbar.AddSeparator() 152 153 toolbar.AddSimpleTool(self.ID_CHECK_STATUS, bitmap("refresh.png"), "Check the status of the workflow(s).") 154 155 #toolbar.AddSeparator() 156 #self.file_selector = FileSelector(toolbar, self) 157 #toolbar.AddControl(control=self.file_selector) 158 159 toolbar.Realize() 160 161 # Associate menu/toolbar items with their handlers. 162 menu_handlers = [ 163 (self.ID_SHOW_INPUTS, self.OnShowInputs), 164 (self.ID_SHOW_OUTPUTS, self.OnShowOutputs), 165 (self.ID_SHOW_LOGS, self.OnShowLogs), 166 (self.ID_SHOW_JOB_SCRIPTS, self.OnShowJobScripts), 167 (self.ID_SHOW_JOB_OUTERRS, self.OnShowJobOutErrs), 168 (self.ID_BROWSE, self.OnBrowse), 169 (self.ID_SHOW_MAIN_EVENTS, self.OnShowMainEvents), 170 (self.ID_SHOW_LOG_EVENTS, self.OnShowLogEvents), 171 (self.ID_SHOW_TIMERS, self.OnShowTimers), 172 (self.ID_CHECK_STATUS, self.OnCheckStatusButton), 173 ] 174 175 for combo in menu_handlers: 176 mid, handler = combo[:2] 177 self.Bind(wx.EVT_MENU, handler, id=mid) 178 179 def makeMenu(self): 180 """Make the menu bar.""" 181 menu_bar = wx.MenuBar() 182 183 file_menu = wx.Menu() 184 file_menu.Append(wx.ID_OPEN, "&Open", help="Open an existing flow in a new frame") 185 file_menu.Append(wx.ID_CLOSE, "&Close", help="Close the file associated to the active tab") 186 file_menu.Append(wx.ID_EXIT, "&Quit", help="Exit the application") 187 188 file_history = self.file_history = wx.FileHistory(8) 189 file_history.Load(self.config) 190 recent = wx.Menu() 191 file_history.UseMenu(recent) 192 file_history.AddFilesToMenu() 193 file_menu.AppendMenu(-1, "&Recent Files", recent) 194 self.Bind(wx.EVT_MENU_RANGE, self.OnFileHistory, id=wx.ID_FILE1, id2=wx.ID_FILE9) 195 menu_bar.Append(file_menu, "File") 196 197 flow_menu = wx.Menu() 198 199 #self.ID_FLOW_CHANGE_MANAGER = wx.NewId() 200 #flow_menu.Append(self.ID_FLOW_CHANGE_MANAGER, "Change TaskManager", help="") 201 202 self.ID_FLOW_OPEN_OUTFILES = wx.NewId() 203 flow_menu.Append(self.ID_FLOW_OPEN_OUTFILES, "Open output files", help="") 204 205 self.ID_FLOW_TREE_VIEW = wx.NewId() 206 flow_menu.Append(self.ID_FLOW_TREE_VIEW, "Tree view", help="Tree view of the tasks") 207 208 menu_bar.Append(flow_menu, "Flow") 209 210 help_menu = wx.Menu() 211 help_menu.Append(wx.ID_ABOUT, "About " + self.codename, help="Info on the application") 212 menu_bar.Append(help_menu, "Help") 213 214 self.ID_HELP_QUICKREF = wx.NewId() 215 help_menu.Append(self.ID_HELP_QUICKREF, "Quick Reference ", help="Quick reference for " + self.codename) 216 help_menu.Append(wx.ID_ABOUT, "About " + self.codename, help="Info on the application") 217 218 # Associate menu/toolbar items with their handlers. 219 menu_handlers = [ 220 (wx.ID_OPEN, self.OnOpen), 221 (wx.ID_CLOSE, self.OnClose), 222 (wx.ID_EXIT, self.OnExit), 223 (wx.ID_ABOUT, self.OnAboutBox), 224 # 225 #(self.ID_FLOW_CHANGE_MANAGER, self.onChangeManager), 226 (self.ID_FLOW_OPEN_OUTFILES, self.onOpenOutFiles), 227 (self.ID_FLOW_TREE_VIEW, self.onTaskTreeView), 228 # 229 (self.ID_HELP_QUICKREF, self.onQuickRef), 230 ] 231 232 for combo in menu_handlers: 233 mid, handler = combo[:2] 234 self.Bind(wx.EVT_MENU, handler, id=mid) 235 236 return menu_bar 237 238 def check_launcher_file(self, with_dialog=True): 239 """ 240 Disable the launch button if we have a sheduler running, 241 since we don't want to have processes modifying the flow. 242 """ 243 self.disabled_launcher = False 244 pid_files = fnmatch.filter(os.listdir(self.flow.workdir), "*.pid") 245 246 if pid_files: 247 pid_file = os.path.join(self.flow.workdir, pid_files[0]) 248 if not os.path.exists(pid_file): return 249 self.disabled_launcher = True 250 251 with open(pid_file, "r") as fh: 252 pid = int(fh.readline()) 253 254 message = ("Found pid file %s associated to an already running scheduler with pid %d. " 255 "Job submission has been disabled." % (pid_file, pid)) 256 257 if with_dialog: 258 dialog = wx.MessageDialog(None, message=message, caption='Flow is being executed by a scheduler', 259 style=wx.OK | wx.ICON_EXCLAMATION) 260 dialog.ShowModal() 261 dialog.Destroy() 262 263 else: 264 self.statusbar.PushStatusText(message) 265 266 def OnSubmitButton(self, event): 267 """Submit up to max_nlauch tasks.""" 268 if self.disabled_launcher: return 269 max_nlaunch = int(self.max_nlaunch.GetValue()) 270 nlaunch = flowtk.PyLauncher(self.flow).rapidfire(max_nlaunch=max_nlaunch) 271 272 self.statusbar.PushStatusText("Submitted %d tasks" % nlaunch) 273 274 def GetSelectedWork(self): 275 """ 276 Return the selected workflow namely that the 277 workflow associated to the active tab. None if list is empty. 278 """ 279 return self.notebook.GetSelectedWork() 280 281 def OnIdle(self, event): 282 """Function executed when the GUI is idle.""" 283 now = time.time() 284 if (now - self.last_refresh) > self.REFRESH_INTERVAL: 285 self.check_launcher_file(with_dialog=False) 286 self.CheckStatusAndRedraw() 287 self.last_refresh = time.time() 288 289 def OnCheckStatusButton(self, event): 290 """Callback triggereed by the checkstatus button.""" 291 self.CheckStatusAndRedraw() 292 293 def CheckStatusAndRedraw(self): 294 """Check the status of all the workflows and redraw the panel.""" 295 self.statusbar.PushStatusText("Checking status...") 296 297 self.flow.check_status() 298 299 # Count the number of tasks with given status. 300 counter = self.flow.status_counter 301 302 # Save the active tab so that we can set it afterwards. 303 old_selection = self.notebook.GetSelection() 304 305 # Build new notebook and redraw the panel 306 main_sizer = self.main_sizer 307 main_sizer.Hide(0) 308 main_sizer.Remove(0) 309 new_notebook = FlowNotebook(self, self.flow) 310 main_sizer.Insert(0, new_notebook, 1, wx.EXPAND, 5) 311 self.notebook = new_notebook 312 313 self.panel.Layout() 314 315 # Reinstate the old selection 316 self.notebook.SetSelection(old_selection) 317 318 # Write number of jobs with given status. 319 message = ", ".join("%s: %s" % (k, v) for (k, v) in counter.items()) 320 self.statusbar.PushStatusText(message) 321 322 def read_file(self, filepath): 323 self.statusbar.PushStatusText("Reading %s" % filepath) 324 325 try: 326 flow = flowtk.AbinitFlow.pickle_load(filepath) 327 self.AddFileToHistory(filepath) 328 return flow 329 except: 330 awx.showErrorMessage(self) 331 return None 332 333 def OnOpen(self, event): 334 dialog = wx.FileDialog( 335 self, message="Choose a __workflow__.pickle file", defaultDir=os.getcwd(), 336 wildcard="pickle files (*.pickle)|*.pickle", 337 style=wx.OPEN | wx.MULTIPLE | wx.CHANGE_DIR) 338 339 # Show the dialog and retrieve the user response. 340 # If it is the OK response, process the data. 341 if dialog.ShowModal() == wx.ID_CANCEL: return 342 343 filepath = dialog.GetPath() 344 dialog.Destroy() 345 346 # Add to the history. 347 self.file_history.AddFileToHistory(filepath) 348 self.file_history.Save(self.config) 349 self.config.Flush() 350 flow = self.read_file(filepath) 351 352 if flow is not None: 353 # Open new frame. 354 FlowViewerFrame(self, flow).Show() 355 356 def AddFileToHistory(self, filepath): 357 """Add the absolute filepath to the file history.""" 358 self.file_history.AddFileToHistory(filepath) 359 self.file_history.Save(self.config) 360 self.config.Flush() 361 362 def OnFileHistory(self, event): 363 fileNum = event.GetId() - wx.ID_FILE1 364 filepath = self.file_history.GetHistoryFile(fileNum) 365 self.file_history.AddFileToHistory(filepath) 366 self.read_file(filepath) 367 368 def OnClose(self, event): 369 print("onclose") 370 #self.flow.pickle_dump() 371 #self.Close() 372 373 def OnExit(self, event): 374 """Save the status of the flow in the database and exit the GUI.""" 375 print("onexit") 376 #self.flow.pickle_dump() 377 self.Close() 378 379 def OnAboutBox(self, event): 380 """"Info on the application.""" 381 awx.makeAboutBox(codename=self.codename, version=self.VERSION, 382 description="", developers="M. Giantomassi") 383 384 def onQuickRef(self, event=None): 385 dialog = wx.MessageDialog(self, self.HELP_MSG, self.codename + " Quick Reference", 386 wx.OK | wx.ICON_INFORMATION) 387 dialog.ShowModal() 388 dialog.Destroy() 389 390 def OnShowInputs(self, event): 391 """Show all the input files of the selected `Workflow`.""" 392 work = self.GetSelectedWork() 393 if work is None: return 394 TextNotebookFrame.from_files_and_dir(self, dirpath=work.workdir, walk=True, wildcard="*.abi").Show() 395 396 def OnShowOutputs(self, event): 397 """Show all the output files of the selected `Workflow`.""" 398 work = self.GetSelectedWork() 399 if work is None: return 400 TextNotebookFrame.from_files_and_dir(self, dirpath=work.workdir, walk=True, wildcard="*.abo").Show() 401 402 def OnShowJobScripts(self, event): 403 """Show all the job script files of the selected `Workflow`.""" 404 work = self.GetSelectedWork() 405 if work is None: return 406 TextNotebookFrame.from_files_and_dir(self, dirpath=work.workdir, walk=True, wildcard="*.sh").Show() 407 408 def OnShowJobOutErrs(self, event): 409 """Show all the queue output/error files files of the selected `Workflow`.""" 410 work = self.GetSelectedWork() 411 if work is None: return 412 TextNotebookFrame.from_files_and_dir(self, dirpath=work.workdir, walk=True, wildcard="*.qout|*.qerr").Show() 413 414 def OnShowLogs(self, event): 415 """Show all the log files of the selected `Workflow`.""" 416 work = self.GetSelectedWork() 417 if work is None: return 418 TextNotebookFrame.from_files_and_dir(self, dirpath=work.workdir, walk=True, wildcard="*.log").Show() 419 420 def OnBrowse(self, event): 421 """Browse all the output files produced by the selected `Workflow`.""" 422 work = self.GetSelectedWork() 423 if work is None: return 424 FileListFrame(self, dirpaths=work.workdir).Show() 425 #DirBrowserFrame(self, dir=work.workdir).Show() 426 427 def OnShowMainEvents(self, event): 428 """Browse all the main events of the tasks in the selected `Workflow`.""" 429 work = self.GetSelectedWork() 430 if work is None: return 431 AbinitEventsNotebookFrame(self, filenames=[task.output_file.path for task in work]).Show() 432 433 def OnShowLogEvents(self, event): 434 """Browse all the log events of the tasks in the selected `Workflow`.""" 435 work = self.GetSelectedWork() 436 if work is None: return 437 AbinitEventsNotebookFrame(self, [task.log_file.path for task in work]).Show() 438 439 def OnShowTimers(self, event): 440 """Analyze the timing data of all the output files of the selected `Workflow`.""" 441 work = self.GetSelectedWork() 442 if work is None: return 443 timers = work.parse_timers() 444 # Build the frame for analyzing multiple timers. 445 MultiTimerFrame(self, timers).Show() 446 447 def onTaskTreeView(self, event): 448 TaskTreeView(self, self.flow).Show() 449 450 #def onChangeManager(self, event): 451 # #ChangeTaskManager(self, self.flow).Show() 452 # new_manager = yaml_manager_dialog(self) 453 # if new_manager is None: return 454 # print(new_manager) 455 # #status_selected = upper() 456 # #status = None if status_selected == "ALL" else status_selected 457 # # Change the manager of the errored tasks. 458 # #for task in flow.iflat_tasks(status="S_ERROR"): 459 # # task.reset() 460 # # task.set_manager(new_manager) 461 462 def onOpenOutFiles(self, event): 463 FileSelectorFrame(parent=self, viewer=self).Show() 464 465 466class FlowNotebook(fnb.FlatNotebook): 467 """ 468 Notebook class 469 """ 470 def __init__(self, parent, flow): 471 try: 472 style = fnb.FNB_NO_X_BUTTON | fnb.FNB_NAV_BUTTONS_WHEN_NEEDED 473 except AttributeError: 474 style = fnb.FNB_NO_X_BUTTON 475 476 super(FlowNotebook, self).__init__(parent, id=-1, style=style) 477 478 self.flow = flow 479 for work in flow: 480 tab = TabPanel(self, work) 481 self.AddPage(tab, text=os.path.basename(work.workdir)) 482 483 def GetSelectedWork(self): 484 """ 485 Return the selected workflow namely that the workflow associated to the 486 active tab. None is list is empy. 487 """ 488 # FIXME: If we want to allow the user to remove some tab we have 489 # to remove the corresponding work from workflows. 490 # Easy if workflow_viewer can only read data but it might be source 491 # of bugs if we want to modify the object e.g. by submitting the calculation. 492 # For the time-being, use this assertion to prevent users from removing pages. 493 if self.GetPageCount() != len(self.flow): 494 return awx.showErrorMessage(self, message="Bad user has removed pages from the notebook!") 495 496 idx = self.GetSelection() 497 if idx == -1: return None 498 try: 499 return self.flow[idx] 500 except IndexError: 501 return None 502 503 504class TabPanel(wx.Panel): 505 """ 506 Notebook tab. 507 """ 508 def __init__(self, parent, work, **kwargs): 509 wx.Panel.__init__(self, parent=parent, id=-1, **kwargs) 510 511 # List Control with the individual tasks of the workflow. 512 task_listctrl = TaskListCtrl(self, work) 513 514 label = wx.StaticText(self, -1, "Workflow class %s, status: %s, finalized: %s" % ( 515 work.__class__.__name__, work.status, work.finalized)) 516 label.Wrap(-1) 517 518 main_sizer = wx.BoxSizer(wx.VERTICAL) 519 main_sizer.Add(label, 0, wx.ALIGN_LEFT, 5) 520 main_sizer.Add(task_listctrl, 1, wx.EXPAND, 5) 521 522 self.SetSizerAndFit(main_sizer) 523 524 525class TaskListCtrl(wx.ListCtrl): 526 """ 527 ListCtrl with the list of task in the `Workflow`. 528 """ 529 def __init__(self, parent, work, **kwargs): 530 """ 531 Args: 532 parent: 533 Parent window. 534 work: 535 `Workflow` containig the List of `Task` instances. 536 """ 537 super(TaskListCtrl, self).__init__(parent, id=-1, style=wx.LC_REPORT | wx.BORDER_SUNKEN, **kwargs) 538 539 self.work = work 540 541 columns = ["Task", "Status", "Queue_id", 542 "Errors", "Warnings", "Comments", 543 "MPI", "OMP", 544 "num_restarts", "Task Class", 545 ] 546 547 for index, col in enumerate(columns): 548 self.InsertColumn(index, col) 549 550 # Used to store the Max width in pixels for the data in the column. 551 column_widths = [awx.get_width_height(self, s)[0] for s in columns] 552 553 for task in work: 554 events = map(str, 3*["N/A"]) 555 556 try: 557 report = task.get_event_report() 558 if report is not None: 559 events = map(str, [report.num_errors, report.num_warnings, report.num_comments]) 560 except: 561 pass 562 563 cpu_info = [task.mpi_procs, task.omp_threads] 564 entry = map(str, [task.name, str(task.status), task.queue_id] + 565 events + 566 cpu_info + 567 [task.num_restarts, task.__class__.__name__] 568 ) 569 w = [awx.get_width_height(self, s)[0] for s in entry] 570 column_widths = map(max, zip(w, column_widths)) 571 572 self.Append(entry) 573 574 # Set the width in pixel for each column. 575 for index, col in enumerate(columns): 576 self.SetColumnWidth(index, column_widths[index]) 577 578 self.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnRightClick) 579 self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemActivated) 580 581 def OnRightClick(self, event): 582 currentItem = event.m_itemIndex 583 if currentItem == -1: return 584 585 # Open the popup menu then destroy it to avoid mem leak. 586 task = self.work[currentItem] 587 menu = TaskPopupMenu(parent=self, task=task) 588 self.PopupMenu(menu, event.GetPoint()) 589 menu.Destroy() 590 591 def OnItemActivated(self, event): 592 """ 593 Browse the outdir of the task 594 """ 595 currentItem = event.m_itemIndex 596 task = self.work[currentItem] 597 FileListFrame(self, dirpaths=task.outdir.path, title=task.outdir.relpath).Show() 598 599 600# Callbacks 601 602def show_task_main_output(parent, task): 603 """Show the main output file of the task.""" 604 file = task.output_file 605 606 if file.exists: 607 frame_from_filepath(parent, file.path).Show() 608 else: 609 awx.showErrorMessage(parent=parent, message="Output file %s does not exist" % file.path) 610 611 612def show_task_log(parent, task): 613 """Show the log file of the task.""" 614 file = task.log_file 615 616 if file.exists: 617 frame_from_filepath(parent, file.path).Show() 618 else: 619 awx.showErrorMessage(parent=parent, message="Output file %s does not exist" % file.path) 620 621 622def show_task_main_events(parent, task): 623 """Show the events reported in the main output file.""" 624 file = task.output_file 625 626 if file.exists: 627 AbinitEventsFrame(parent, file.path).Show() 628 else: 629 awx.showErrorMessage(parent=parent, message="Output file %s does not exist" % file.path) 630 631 632def show_task_log_events(parent, task): 633 """Show the events reported in the log file.""" 634 file = task.log_file 635 636 if file.exists: 637 AbinitEventsFrame(parent, file.path).Show() 638 else: 639 awx.showErrorMessage(parent=parent, message="Log file %s does not exist" % file.path) 640 641 642def browse_indir(parent, task): 643 """Open a window that allows the user to browse the input files in indir.""" 644 FileListFrame(parent, dirpaths=task.indir.path).Show() 645 646 647def browse_outdir(parent, task): 648 """Open a window that allows the user to browse the output files in outdir.""" 649 FileListFrame(parent, dirpaths=task.outdir.path).Show() 650 651 652def browse_tmpdir(parent, task): 653 """Open a window that allows the user to browse the output files in outdir.""" 654 FileListFrame(parent, dirpaths=task.tmpdir.path).Show() 655 656 657def show_history(parent, task): 658 """Show the history of the task.""" 659 text = "\n".join(task.history) 660 SimpleTextViewer(parent, text).Show() 661 662 663def show_timer(parent, task): 664 """Show timing data of the k.""" 665 try: 666 frame = AbinitTimerFrame(parent, task.output_file.path) 667 frame.Show() 668 except awx.Error as exc: 669 awx.showErrorMessage(parent, str(exc)) 670 671 672def check_status_and_pickle(task): 673 """Check the status of the task and update the pickle database.""" 674 task.flow.check_status() 675 task.flow.pickle_dump() 676 677 678def task_restart(parent, task): 679 """Restart the task.""" 680 task.restart() 681 check_status_and_pickle(task) 682 683 684def task_reset(parent, task): 685 """Reset the status of the task.""" 686 task.reset() 687 check_status_and_pickle(task) 688 689 690def task_set_status(parent, task): 691 """Reset the status of the task.""" 692 choices = [str(s) for s in flowtk.Node.ALL_STATUS] 693 dialog = wx.SingleChoiceDialog(parent, message="Select new status", caption="", choices=choices) 694 if dialog.ShowModal() == wx.ID_CANCEL: return None 695 status = choices[dialog.GetSelection()] 696 dialog.Destroy() 697 698 task.set_status(status, info_msg="Status changed by user on %s" % time.asctime()) 699 #task.reset() 700 check_status_and_pickle(task) 701 702 703def task_show_deps(parent, task): 704 """Show the dependencies of the task.""" 705 text = task.str_deps() 706 SimpleTextViewer(parent, text).Show() 707 708 709def task_inspect(parent, task): 710 """Inspect the results at runtime.""" 711 if hasattr(task, "inspect"): 712 task.inspect() 713 714 715class TaskPopupMenu(wx.Menu): 716 """ 717 A `TaskPopupMenu` has a list of callback functions indexed by the menu title. 718 The signature of the callback function is func(parent, task) where parent is 719 the wx Window that will become the parent of the new frame created by the callback. 720 and task is a `Task` instance. 721 """ 722 MENU_TITLES = OrderedDict([ 723 ("output", show_task_main_output), 724 ("log", show_task_log), 725 ("main events", show_task_main_events), 726 ("log events", show_task_log_events), 727 ("browse indir", browse_indir), 728 ("browse outdir", browse_outdir), 729 ("browse tmpdir", browse_tmpdir), 730 ("history", show_history), 731 ("restart", task_restart), 732 ("reset", task_reset), 733 ("dependencies", task_show_deps), 734 ("inspect", task_inspect), 735 ("set status", task_set_status), 736 ("timer", show_timer), 737 ]) 738 739 def __init__(self, parent, task): 740 super(TaskPopupMenu, self).__init__() 741 self.parent, self.task = parent, task 742 743 self._make_menu() 744 745 def _make_menu(self): 746 """Build the menu""" 747 self.menu_title_by_id = OrderedDict() 748 749 for title in self.MENU_TITLES: 750 self.menu_title_by_id[wx.NewId()] = title 751 752 for (id, title) in self.menu_title_by_id.items(): 753 # Register menu handlers with EVT_MENU, on the menu. 754 self.Append(id, title) 755 wx.EVT_MENU(self, id, self.OnMenuSelection) 756 757 def _get_callback(self, title): 758 return self.MENU_TITLES[title] 759 760 def OnMenuSelection(self, event): 761 title = self.menu_title_by_id[event.GetId()] 762 callback = self._get_callback(title) 763 #print("Calling callback %s on task %s" % (callback, self.task)) 764 try: 765 callback(self.parent, self.task) 766 except: 767 awx.showErrorMessage(parent=self.parent) 768 769#class ChangeTaskManager(awx.Frame): 770# def __init__(self, parent, flow, **kwargs) 771# super(ChangeTaskManager, self).__init__(self, parent, **kwargs) 772# 773# 774# def onOkButton(self, event): 775# new_manager = yaml_manager_dialog(self) 776# if new_manager is None: return 777# print(new_manager) 778# #status_selected = upper() 779# #status = None if status_selected == "ALL" else status_selected 780# # Change the manager of the errored tasks. 781# #for task in flow.iflat_tasks(status="S_ERROR"): 782# # task.reset() 783# # task.set_manager(new_manager) 784 785 786class TaskStatusTreePanel(awx.Panel): 787 """ 788 Panel with a TreeCtrl that allows the user to navigate the tasks grouped by status. 789 """ 790 def __init__(self, parent, flow, **kwargs): 791 super(TaskStatusTreePanel, self).__init__(parent, -1, **kwargs) 792 793 main_sizer = wx.BoxSizer(wx.VERTICAL) 794 vbox = wx.BoxSizer(wx.VERTICAL) 795 panel1 = awx.Panel(self, -1) 796 panel2 = awx.Panel(self, -1) 797 798 self.tree = tree = wx.TreeCtrl(panel1, 1, wx.DefaultPosition, (-1, -1), wx.TR_HIDE_ROOT | wx.TR_HAS_BUTTONS) 799 800 root = self.tree.AddRoot('Task Status') 801 802 # entry = collections.namedtuple("Entry", "task wi ti") 803 #print(status2entries) 804 self.status2entries = flow.groupby_status() 805 806 self.status_nodes = [] 807 for status, entries in self.status2entries.items(): 808 node = tree.AppendItem(root, "%d %s tasks" % (len(entries), str(status)), data=wx.TreeItemData(status)) 809 self.status_nodes.append(node) 810 for entry in entries: 811 tree.AppendItem(node, "Task: " + str(entry.task), data=wx.TreeItemData(entry)) 812 813 tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.onSelChanged) 814 tree.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.onItemRightClick) 815 816 self.display = wx.StaticText(panel2, -1, '', (10, 10), style=wx.ALIGN_LEFT) 817 818 vbox.Add(self.tree, 1, wx.EXPAND) 819 main_sizer.Add(panel1, 1, wx.EXPAND) 820 main_sizer.Add(panel2, 1, wx.EXPAND) 821 panel1.SetSizerAndFit(vbox) 822 823 self.SetSizerAndFit(main_sizer) 824 self.Centre() 825 826 def onSelChanged(self, event): 827 node = event.GetItem() 828 if node in self.status_nodes: return 829 830 proxy = self.tree.GetItemData(node) 831 if proxy is None: return 832 entry = proxy.GetData() 833 834 task = entry.task 835 s = str(task) 836 s += "\nHistory:\n" + task.str_history 837 838 self.display.SetLabel(s) 839 840 def onItemRightClick(self, event): 841 node = event.GetItem() 842 proxy = self.tree.GetItemData(node) 843 if proxy is None: return 844 845 if node in self.status_nodes: 846 status = proxy.GetData() 847 print("received set of tasks with status %s" % status) 848 popup_menu = self.makePopupMenuStatus() 849 self.PopupMenu(popup_menu, event.GetPoint()) 850 popup_menu.Destroy() 851 852 #print("click") 853 #print("event", dir(event)) 854 855 def makePopupMenuStatus(self): 856 self.ID_POPUP_CHANGE_MANAGER = wx.NewId() 857 menu = wx.Menu() 858 menu.Append(self.ID_POPUP_CHANGE_MANAGER, "Change manager") 859 860 # Associate menu/toolbar items with their handlers. 861 menu_handlers = [ 862 (self.ID_POPUP_CHANGE_MANAGER, self.onChangeManager), 863 ] 864 865 for combo in menu_handlers: 866 mid, handler = combo[:2] 867 self.Bind(wx.EVT_MENU, handler, id=mid) 868 869 return menu 870 871 def onChangeManager(self, event): 872 print("changer manager") 873 node = self.tree.GetSelection() 874 status = self.tree.GetItemData(node).GetData() 875 print("status", status) 876 877 entries = self.status2entries[status] 878 879 new_manager = yaml_manager_dialog(self) 880 for e in entries: 881 e.task.reset() 882 e.task.set_manager(new_manager) 883 884 #self.flow.build_and_pickle_dump() 885 886 887class TaskTreeView(awx.Frame): 888 """ 889 A frame with a TaskStatusTreePanel. 890 """ 891 def __init__(self, parent, flow, **kwargs): 892 if "title" not in kwargs: 893 kwargs["title"] = "Task tree view: %s" % flow.workdir 894 895 super(TaskTreeView, self).__init__(parent, **kwargs) 896 897 panel = TaskStatusTreePanel(self, flow) 898 899 900class FileSelector(awx.Panel): 901#class FileSelector(wx.Control): 902 """ 903 Control that allows the user to select multiple output files of the same type (either inside 904 a `Workflow` on in the entire `Flow`. The open button will open a viewer to analyze 905 the multiple files selected. 906 """ 907 def __init__(self, parent, viewer, **kwargs): 908 super(FileSelector, self).__init__(parent, -1, **kwargs) 909 self.viewer = viewer 910 panel = self #wx.Panel(self, -1) 911 912 wcards = ["*GSR.nc", "*WFK-etsf.nc", "*SIGRES.nc", "*MDF.nc"] 913 914 self.wcards_cbox = wx.ComboBox(panel, id=-1, name='File type', choices=wcards, value=wcards[0], style=wx.CB_READONLY) 915 916 smodes = ["Selected Workflow", "Entire Flow"] 917 self.select_rbox = wx.RadioBox(panel, id=1, name="Selection Mode", choices=smodes, style=wx.RA_SPECIFY_ROWS) 918 919 open_button = wx.Button(panel, -1, label='Open files') 920 open_button.Bind(wx.EVT_BUTTON, self.onOpenButton) 921 922 #main_sizer = wx.BoxSizer(wx.HORIZONTAL) 923 main_sizer = wx.GridBagSizer(10, 10) 924 925 #vsizer = wx.BoxSizer(wx.VERTICAL) 926 #vsizer.Add(self.wcards_cbox, wx.ALIGN_CENTER_VERTICAL | wx.TOP | wx.BOTTOM | wx.LEFT, 5) 927 #vsizer.Add(open_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.TOP | wx.BOTTOM | wx.LEFT, 5) 928 #main_sizer.Add(vsizer) 929 #main_sizer.Add(self.select_rbox, wx.ALIGN_CENTER_VERTICAL | wx.TOP | wx.BOTTOM | wx.LEFT, 5) 930 931 main_sizer.Add(self.wcards_cbox, (0, 0), (1,1), wx.ALIGN_CENTER) 932 main_sizer.Add(open_button, (1, 0), (1,1), wx.ALIGN_CENTER) 933 main_sizer.Add(self.select_rbox, (0, 1), (3,3), wx.EXPAND) 934 935 panel.SetSizerAndFit(main_sizer) 936 937 def getWildCard(self): 938 """Returns a string with the Abinit extension selected by the user.""" 939 return self.wcards_cbox.GetValue() 940 941 def getSelectionMode(self): 942 """Returns a string with the selection mode selected by the user.""" 943 index = self.select_rbox.GetSelection() 944 return self.select_rbox.GetString(index) 945 946 def onOpenButton(self, event): 947 wcard = self.getWildCard() 948 smode = self.getSelectionMode() 949 print("wcard", wcard, "smode", smode) 950 951 # Find the filepaths according to smode. 952 filepaths = [] 953 954 if smode == "Selected Workflow": 955 work = self.viewer.GetSelectedWork() 956 for task in work: 957 filepaths.extend(task.outdir.list_filepaths(wildcard=wcard)) 958 959 elif smode == "Entire Flow": 960 for work in self.viewer.flow: 961 for task in work: 962 filepaths.extend(task.outdir.list_filepaths(wildcard=wcard)) 963 964 else: 965 return awx.showErrorMessage(self, "Wrong value of smode: %s" % smode) 966 967 if not filepaths: 968 return awx.showErrorMessage(self, "Cannot find any file matching the specified shell pattern") 969 970 print("filepaths", filepaths) 971 972 # Get the viewer class associated to these files, build the frame and show it. 973 frame_class = frameclass_from_filepath(filepaths[0]) 974 if frame_class is None: return 975 frame_class(self, filepaths).Show() 976 977 978class FileSelectorFrame(wx.Frame): 979 def __init__(self, parent, viewer, **kwargs): 980 super(FileSelectorFrame, self).__init__(parent, -1, **kwargs) 981 982 panel = wx.Panel(self, -1) 983 file_selector = FileSelector(panel, viewer) 984 985 main_sizer = wx.BoxSizer(wx.VERTICAL) 986 main_sizer.Add(file_selector, 1, wx.EXPAND , 5) 987 panel.SetSizerAndFit(main_sizer) 988 989 990#class StatusSelectorFrame(wx.Frame): 991# def __init__(self, parent, viewer, **kwargs): 992# super(StatusSelectorFrame, self).__init__(parent, -1, **kwargs) 993# 994# panel = wx.Panel(self, -1) 995# 996# #choices = ["S_OK", "*WFK-etsf.nc", "*SIGRES.nc", "*MDF.nc"] 997# 998# #self.status_cbox = wx.ComboBox(panel, id=-1, name='File type', choices=choices, value=choices[0], style=wx.CB_READONLY) 999# 1000# #smodes = ["Selected Workflow", "Entire Flow"] 1001# #self.select_rbox = wx.RadioBox(panel, id=1, name="Selection Mode", choices=smodes, style=wx.RA_SPECIFY_ROWS) 1002# 1003# #open_button = wx.Button(panel, -1, label='Open files') 1004# #open_button.Bind(wx.EVT_BUTTON, self.onOpenButton) 1005# 1006# main_sizer = wx.BoxSizer(wx.VERTICAL) 1007# main_sizer.Add(file_selector, 1, wx.EXPAND , 5) 1008# panel.SetSizerAndFit(main_sizer) 1009# 1010# def getSelectedStatus(self): 1011# return self.choices_cbox.GetValue() 1012 1013 1014def wxapp_flow_viewer(works): 1015 """Standalone application for `FlowViewerFrame`.""" 1016 app = awx.App() 1017 FlowViewerFrame(None, works).Show() 1018 return app 1019