From 9adc9aac3b3d65a972f1d26a6bb333d72ba3365f Mon Sep 17 00:00:00 2001 From: "Aaron S. Brewster" Date: Thu, 23 May 2024 09:32:04 -0700 Subject: [PATCH] XFEL GUI updates (#992) Series of updates from most recent beamtime: * Switch extant usage of mpi4py to libtbx.mpi4py * Refresh cached database connection after 5 minutes * add pydrive2 to psana_environment * XFEL GUI: only move on to next task if the previous one is in the DONE state * XFEL GUI: more tweaks for energy tab: add max events box, fix for parameter loading at GUI start, hide energy tab if not LCLS, fix for ListCtrl if clicked and empty, remove resizing code, fix invisible boxes in ListCtrl * Prevent duplicate gdrive uploads * Improve mpi safety * only disable mpi if the gui actually runs * XFEL GUI: energy tab, label eBeam using average * XFEL GUI: open image viewer when clicking on a dot (LCLS only for now) * XFEL GUI: better checks for closed image viewer Co-authored-by: Daniel Paley --- libtbx/__init__.py | 3 + libtbx/mpi4py.py | 10 ++ prime/command_line/mpi_run.py | 2 +- prime/command_line/mpi_scale.py | 2 +- simtbx/command_line/errors.py | 2 +- simtbx/command_line/estimate_Ncells_Eta.py | 2 +- simtbx/command_line/integrate.py | 2 +- xfel/amo/pnccd_ana/mpi_fxs_bg.py | 4 +- xfel/amo/pnccd_ana/mpi_fxs_c2.py | 4 +- xfel/amo/pnccd_ana/mpi_fxs_calib.py | 4 +- xfel/amo/pnccd_ana/mpi_fxs_index.py | 4 +- xfel/amo/pnccd_ana/mpi_fxs_launch.py | 2 +- xfel/amo/pnccd_ana/mpi_fxs_mask.py | 4 +- xfel/amo/pnccd_ana/pnccd_hit.py | 2 +- xfel/command_line/FEE_average_plot.py | 2 +- xfel/command_line/cxi_xtc_process.py | 2 +- xfel/command_line/mpi_average.py | 2 +- xfel/command_line/upload_mtz.py | 53 +++++-- xfel/command_line/xfel_process.py | 7 + xfel/command_line/xtc_dump.py | 2 +- xfel/command_line/xtc_process.py | 2 +- xfel/conda_envs/psana_environment.yml | 1 + xfel/cxi/cspad_ana/mod_event_code.py | 2 +- xfel/merging/application/phil/phil.py | 2 +- .../command_line/small_cell_index.py | 2 +- xfel/ui/command_line/xfel_gui_launch.py | 15 +- xfel/ui/components/ebeam_plotter.py | 21 ++- xfel/ui/components/run_stats_plotter.py | 13 +- xfel/ui/components/xfel_gui_dialogs.py | 5 + xfel/ui/components/xfel_gui_init.py | 146 +++++++++++++----- xfel/ui/db/job.py | 7 +- xfel/ui/db/xfel_db.py | 9 +- 32 files changed, 237 insertions(+), 103 deletions(-) diff --git a/libtbx/__init__.py b/libtbx/__init__.py index cb029d73e0..68b7ef65b8 100644 --- a/libtbx/__init__.py +++ b/libtbx/__init__.py @@ -68,6 +68,9 @@ def __new__(cls): Auto = AutoType() +class mpi_import_guard: + disable_mpi = False + class slots_getstate_setstate(object): """ Implements getstate and setstate for classes with __slots__ defined. Allows an diff --git a/libtbx/mpi4py.py b/libtbx/mpi4py.py index 1e13c57a84..edb047ff73 100644 --- a/libtbx/mpi4py.py +++ b/libtbx/mpi4py.py @@ -67,13 +67,23 @@ def size(self): mpiEmulator.COMM_WORLD = mpiCommEmulator() +class MpiDisabledError(Exception): + pass + try: + import libtbx + if libtbx.mpi_import_guard.disable_mpi: + raise MpiDisabledError from mpi4py import MPI using_mpi = True except ImportError: print ("\nWarning: could not import mpi4py. Running as a single process.\n") MPI = mpiEmulator() using_mpi = False +except MpiDisabledError: + MPI = mpiEmulator() + using_mpi = False + def mpi_abort_on_exception(func): """ diff --git a/prime/command_line/mpi_run.py b/prime/command_line/mpi_run.py index 82aa650e4e..8f1639dbe8 100644 --- a/prime/command_line/mpi_run.py +++ b/prime/command_line/mpi_run.py @@ -3,7 +3,7 @@ Find initial scaling factors for all integration results """ from __future__ import absolute_import, division, print_function -from mpi4py import MPI +from libtbx.mpi4py import MPI import sys, os from prime.postrefine.mod_input import process_input, read_pickles from prime.postrefine.mod_util import intensities_scaler diff --git a/prime/command_line/mpi_scale.py b/prime/command_line/mpi_scale.py index a2c032df9e..8b8534ee24 100644 --- a/prime/command_line/mpi_scale.py +++ b/prime/command_line/mpi_scale.py @@ -3,7 +3,7 @@ Find initial scaling factors for all integration results """ from __future__ import absolute_import, division, print_function -from mpi4py import MPI +from libtbx.mpi4py import MPI import sys, os from prime.postrefine.mod_input import process_input, read_pickles from prime.postrefine.mod_util import intensities_scaler diff --git a/simtbx/command_line/errors.py b/simtbx/command_line/errors.py index 4d0ff05c17..e9df001588 100644 --- a/simtbx/command_line/errors.py +++ b/simtbx/command_line/errors.py @@ -10,7 +10,7 @@ parser.add_argument("--ndev", type=int, default=1, help="number of gpu devices") args = parser.parse_args() -from mpi4py import MPI +from libtbx.mpi4py import MPI COMM = MPI.COMM_WORLD import os diff --git a/simtbx/command_line/estimate_Ncells_Eta.py b/simtbx/command_line/estimate_Ncells_Eta.py index 1b6b95349d..d41d33ac97 100644 --- a/simtbx/command_line/estimate_Ncells_Eta.py +++ b/simtbx/command_line/estimate_Ncells_Eta.py @@ -17,7 +17,7 @@ #parser.add_argument("--njobs", type=int, default=5, help="number of jobs (only runs on single node, no MPI)") parser.add_argument("--plot", action="store_true", help="show a histogram at the end") args = parser.parse_args() -from mpi4py import MPI +from libtbx.mpi4py import MPI COMM = MPI.COMM_WORLD #from joblib import Parallel, delayed import json diff --git a/simtbx/command_line/integrate.py b/simtbx/command_line/integrate.py index e1d0cdd3c1..a1f39b9259 100644 --- a/simtbx/command_line/integrate.py +++ b/simtbx/command_line/integrate.py @@ -21,7 +21,7 @@ args = parser.parse_args() -from mpi4py import MPI +from libtbx.mpi4py import MPI COMM = MPI.COMM_WORLD import logging diff --git a/xfel/amo/pnccd_ana/mpi_fxs_bg.py b/xfel/amo/pnccd_ana/mpi_fxs_bg.py index ccb919b84c..7ad7e5d759 100644 --- a/xfel/amo/pnccd_ana/mpi_fxs_bg.py +++ b/xfel/amo/pnccd_ana/mpi_fxs_bg.py @@ -17,7 +17,7 @@ #times. Here ignoring those errors. np.seterr(divide='ignore', invalid='ignore') -from mpi4py import MPI +from libtbx.mpi4py import MPI comm = MPI.COMM_WORLD rank = comm.Get_rank() size = comm.Get_size() @@ -141,7 +141,7 @@ def compute_bg(argv=None) : argv = sys.argv[1:] try: - from mpi4py import MPI + from libtbx.mpi4py import MPI except ImportError: raise Sorry("MPI not found") diff --git a/xfel/amo/pnccd_ana/mpi_fxs_c2.py b/xfel/amo/pnccd_ana/mpi_fxs_c2.py index 09a1c65bdc..4da8aebd52 100644 --- a/xfel/amo/pnccd_ana/mpi_fxs_c2.py +++ b/xfel/amo/pnccd_ana/mpi_fxs_c2.py @@ -18,7 +18,7 @@ #times. Here ignoring those errors. np.seterr(divide='ignore', invalid='ignore') -from mpi4py import MPI +from libtbx.mpi4py import MPI comm = MPI.COMM_WORLD rank = comm.Get_rank() size = comm.Get_size() @@ -148,7 +148,7 @@ def compute_c2(argv=None) : argv = sys.argv[1:] try: - from mpi4py import MPI + from libtbx.mpi4py import MPI except ImportError: raise Sorry("MPI not found") diff --git a/xfel/amo/pnccd_ana/mpi_fxs_calib.py b/xfel/amo/pnccd_ana/mpi_fxs_calib.py index 635c622688..2bb2c5200f 100644 --- a/xfel/amo/pnccd_ana/mpi_fxs_calib.py +++ b/xfel/amo/pnccd_ana/mpi_fxs_calib.py @@ -16,7 +16,7 @@ #times. Here ignoring those errors. np.seterr(divide='ignore', invalid='ignore') -from mpi4py import MPI +from libtbx.mpi4py import MPI comm = MPI.COMM_WORLD rank = comm.Get_rank() size = comm.Get_size() @@ -135,7 +135,7 @@ def compute_calib(argv=None) : argv = sys.argv[1:] try: - from mpi4py import MPI + from libtbx.mpi4py import MPI except ImportError: raise Sorry("MPI not found") diff --git a/xfel/amo/pnccd_ana/mpi_fxs_index.py b/xfel/amo/pnccd_ana/mpi_fxs_index.py index b8d409b53f..f8fe8cb1c5 100644 --- a/xfel/amo/pnccd_ana/mpi_fxs_index.py +++ b/xfel/amo/pnccd_ana/mpi_fxs_index.py @@ -17,7 +17,7 @@ #times. Here ignoring those errors. np.seterr(divide='ignore', invalid='ignore') -from mpi4py import MPI +from libtbx.mpi4py import MPI comm = MPI.COMM_WORLD rank = comm.Get_rank() size = comm.Get_size() @@ -136,7 +136,7 @@ def compute_index(argv=None) : argv = sys.argv[1:] try: - from mpi4py import MPI + from libtbx.mpi4py import MPI except ImportError: raise Sorry("MPI not found") diff --git a/xfel/amo/pnccd_ana/mpi_fxs_launch.py b/xfel/amo/pnccd_ana/mpi_fxs_launch.py index d55b68e3b7..fae8328351 100644 --- a/xfel/amo/pnccd_ana/mpi_fxs_launch.py +++ b/xfel/amo/pnccd_ana/mpi_fxs_launch.py @@ -77,7 +77,7 @@ def launch(argv=None) : argv = sys.argv[1:] try: - from mpi4py import MPI + from libtbx.mpi4py import MPI except ImportError: raise Sorry("MPI not found") diff --git a/xfel/amo/pnccd_ana/mpi_fxs_mask.py b/xfel/amo/pnccd_ana/mpi_fxs_mask.py index 791492ece9..0ef5477f18 100644 --- a/xfel/amo/pnccd_ana/mpi_fxs_mask.py +++ b/xfel/amo/pnccd_ana/mpi_fxs_mask.py @@ -18,7 +18,7 @@ #times. Here ignoring those errors. np.seterr(divide='ignore', invalid='ignore') -from mpi4py import MPI +from libtbx.mpi4py import MPI comm = MPI.COMM_WORLD rank = comm.Get_rank() size = comm.Get_size() @@ -140,7 +140,7 @@ def compute_mask(argv=None) : argv = sys.argv[1:] try: - from mpi4py import MPI + from libtbx.mpi4py import MPI except ImportError: raise Sorry("MPI not found") diff --git a/xfel/amo/pnccd_ana/pnccd_hit.py b/xfel/amo/pnccd_ana/pnccd_hit.py index 0972369a7d..f9fb8fff89 100644 --- a/xfel/amo/pnccd_ana/pnccd_hit.py +++ b/xfel/amo/pnccd_ana/pnccd_hit.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, division, print_function import numpy as np -from mpi4py import MPI +from libtbx.mpi4py import MPI comm = MPI.COMM_WORLD rank = comm.Get_rank() size = comm.Get_size() diff --git a/xfel/command_line/FEE_average_plot.py b/xfel/command_line/FEE_average_plot.py index 343c86c12a..c3cbc0d493 100644 --- a/xfel/command_line/FEE_average_plot.py +++ b/xfel/command_line/FEE_average_plot.py @@ -92,7 +92,7 @@ def run(args): ds = DataSource(dataset_name) src = Source('DetInfo(%s)'%params.input.address) # set up multiprocessing with MPI - from mpi4py import MPI + from libtbx.mpi4py import MPI comm = MPI.COMM_WORLD rank = comm.Get_rank() # each process in MPI has a unique id, 0-indexed size = comm.Get_size() # size: number of processes running in this job diff --git a/xfel/command_line/cxi_xtc_process.py b/xfel/command_line/cxi_xtc_process.py index 76259b4bdf..cdaa3a678c 100644 --- a/xfel/command_line/cxi_xtc_process.py +++ b/xfel/command_line/cxi_xtc_process.py @@ -137,7 +137,7 @@ def run(self): print("Processing run %d of experiment %s using config file %s"%(params.input.run_num, params.input.experiment, params.input.cfg)) if params.mp.method == "mpi": - from mpi4py import MPI + from libtbx.mpi4py import MPI comm = MPI.COMM_WORLD rank = comm.Get_rank() # each process in MPI has a unique id, 0-indexed size = comm.Get_size() # size: number of processes running in this job diff --git a/xfel/command_line/mpi_average.py b/xfel/command_line/mpi_average.py index 1724ace7a9..827fbfa6ad 100644 --- a/xfel/command_line/mpi_average.py +++ b/xfel/command_line/mpi_average.py @@ -22,7 +22,7 @@ def average(argv=None): argv = sys.argv[1:] try: - from mpi4py import MPI + from libtbx.mpi4py import MPI except ImportError: raise Sorry("MPI not found") diff --git a/xfel/command_line/upload_mtz.py b/xfel/command_line/upload_mtz.py index 481f19e127..52a54e9cc9 100644 --- a/xfel/command_line/upload_mtz.py +++ b/xfel/command_line/upload_mtz.py @@ -5,6 +5,7 @@ from dials.util import Sorry import os, sys import re +import fcntl help_message = """ @@ -71,6 +72,21 @@ def _get_log_fname(mtz_fname): assert len(hit.groups()) == 1 return hit.groups()[0] + '_main.log' +class Locker: + """ See https://stackoverflow.com/a/60214222 + """ + def __enter__(self): + try: + self.fp = open(os.path.expanduser('~/.upload_mtz.lock'), 'wb') + except FileNotFoundError: + self.fp = None + if self.fp is not None: + fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX) + def __exit__(self, *args, **kwargs): + if self.fp is not None: + fcntl.flock(self.fp.fileno(), fcntl.LOCK_UN) + self.fp.close() + class pydrive2_interface: """ Wrapper for uploading versioned mtzs and logs using Pydrive2. Constructed from @@ -92,26 +108,29 @@ def __init__(self, cred_file, folder_id): self.drive = GoogleDrive(gauth) self.top_folder_id = folder_id + + def _fetch_or_create_folder(self, fname, parent_id): - query = { - "q": "'{}' in parents and title='{}'".format(parent_id, fname), - "supportsTeamDrives": "true", - "includeItemsFromAllDrives": "true", - "corpora": "allDrives" - } - hits = self.drive.ListFile(query).GetList() - if hits: - assert len(hits)==1 - return hits[0]['id'] - else: + with Locker(): query = { - "title": fname, - "mimeType": "application/vnd.google-apps.folder", - "parents": [{"kind": "drive#fileLink", "id": parent_id}] + "q": "'{}' in parents and title='{}'".format(parent_id, fname), + "supportsTeamDrives": "true", + "includeItemsFromAllDrives": "true", + "corpora": "allDrives" } - f = self.drive.CreateFile(query) - f.Upload() - return f['id'] + hits = self.drive.ListFile(query).GetList() + if hits: + assert len(hits)==1 + return hits[0]['id'] + else: + query = { + "title": fname, + "mimeType": "application/vnd.google-apps.folder", + "parents": [{"kind": "drive#fileLink", "id": parent_id}] + } + f = self.drive.CreateFile(query) + f.Upload() + return f['id'] def _upload_detail(self, file_path, parent_id): title = os.path.split(file_path)[1] diff --git a/xfel/command_line/xfel_process.py b/xfel/command_line/xfel_process.py index 5012086d6c..77ca6e7d27 100644 --- a/xfel/command_line/xfel_process.py +++ b/xfel/command_line/xfel_process.py @@ -3,6 +3,7 @@ # LIBTBX_SET_DISPATCHER_NAME cctbx.xfel.process from __future__ import absolute_import, division, print_function +from libtbx.mpi4py import mpi_abort_on_exception help_message = ''' @@ -85,6 +86,12 @@ def __init__(self): epilog=help_message ) + @mpi_abort_on_exception + def run(self): + super().run() + + + if __name__ == '__main__': import dials.command_line.stills_process dials.command_line.stills_process.Processor = DialsProcessorWithLogging diff --git a/xfel/command_line/xtc_dump.py b/xfel/command_line/xtc_dump.py index aeffd60f68..6157e577de 100644 --- a/xfel/command_line/xtc_dump.py +++ b/xfel/command_line/xtc_dump.py @@ -156,7 +156,7 @@ def run(self): self.params = params self.options = options - from mpi4py import MPI + from libtbx.mpi4py import MPI comm = MPI.COMM_WORLD rank = comm.Get_rank() # each process in MPI has a unique id, 0-indexed size = comm.Get_size() # size: number of processes running in this job diff --git a/xfel/command_line/xtc_process.py b/xfel/command_line/xtc_process.py index fe935ba802..82ec787c23 100644 --- a/xfel/command_line/xtc_process.py +++ b/xfel/command_line/xtc_process.py @@ -615,7 +615,7 @@ def run(self): self.options = options if params.mp.method == "mpi": - from mpi4py import MPI + from libtbx.mpi4py import MPI comm = MPI.COMM_WORLD rank = comm.Get_rank() # each process in MPI has a unique id, 0-indexed size = comm.Get_size() # size: number of processes running in this job diff --git a/xfel/conda_envs/psana_environment.yml b/xfel/conda_envs/psana_environment.yml index d942df5bc3..f508c81d98 100644 --- a/xfel/conda_envs/psana_environment.yml +++ b/xfel/conda_envs/psana_environment.yml @@ -76,6 +76,7 @@ dependencies: # xfel gui - mysql - mysqlclient + - pydrive2 # Avoid numpy 1.21.[01234] # See https://github.com/cctbx/cctbx_project/issues/627 diff --git a/xfel/cxi/cspad_ana/mod_event_code.py b/xfel/cxi/cspad_ana/mod_event_code.py index 8450a36690..76ec18dc86 100644 --- a/xfel/cxi/cspad_ana/mod_event_code.py +++ b/xfel/cxi/cspad_ana/mod_event_code.py @@ -50,7 +50,7 @@ def __init__( self.size = int(os.environ['SGE_TASK_LAST']) - int(os.environ['SGE_TASK_FIRST']) + 1 else: try: - from mpi4py import MPI + from libtbx.mpi4py import MPI except ImportError: self.rank = 0 self.size = 1 diff --git a/xfel/merging/application/phil/phil.py b/xfel/merging/application/phil/phil.py index 7076b11431..f8eba14180 100644 --- a/xfel/merging/application/phil/phil.py +++ b/xfel/merging/application/phil/phil.py @@ -687,7 +687,7 @@ diffbragg_phil = """ diffBragg { - include scope simtbx.command_line.hopper.phil_scope + include scope simtbx.diffBragg.phil.phil_scope } """ diff --git a/xfel/small_cell/command_line/small_cell_index.py b/xfel/small_cell/command_line/small_cell_index.py index 41f5a071c4..e5bff9a776 100644 --- a/xfel/small_cell/command_line/small_cell_index.py +++ b/xfel/small_cell/command_line/small_cell_index.py @@ -118,7 +118,7 @@ def run(argv=None): files = os.listdir(path) try: - from mpi4py import MPI + from libtbx.mpi4py import MPI comm = MPI.COMM_WORLD rank = comm.Get_rank() size = comm.Get_size() diff --git a/xfel/ui/command_line/xfel_gui_launch.py b/xfel/ui/command_line/xfel_gui_launch.py index 592cf09863..36135ec498 100644 --- a/xfel/ui/command_line/xfel_gui_launch.py +++ b/xfel/ui/command_line/xfel_gui_launch.py @@ -14,16 +14,17 @@ import matplotlib as mp mp.use('PS') + from xfel.ui.components.xfel_gui_init import MainWindow from xfel.ui.components.xfel_gui_dialogs import SettingsDialog -from xfel.ui import save_cached_settings +from xfel.ui import load_cached_settings, save_cached_settings class MainApp(wx.App): ''' App for the main GUI window ''' def OnInit(self): - - self.frame = MainWindow(None, -1, title='CCTBX.XFEL') + params = load_cached_settings() + self.frame = MainWindow(None, -1, title='CCTBX.XFEL', params=params) # select primary display and center on that self.frame.SetSize((800, -1)) @@ -33,11 +34,11 @@ def OnInit(self): self.frame.Center() # Start with login dialog before opening main window - self.login = SettingsDialog(self.frame, self.frame.params) + self.login = SettingsDialog(self.frame, params=params) self.login.SetTitle('CCTBX.XFEL Login') self.login.Center() if (self.login.ShowModal() == wx.ID_OK): - save_cached_settings(self.frame.params) + save_cached_settings(params) if self.frame.connect_to_db(drop_tables=self.login.drop_tables): self.exp_tag = '| {}'.format(self.login.db_cred.ctr.GetValue()) self.exp = '| {}'.format(self.login.experiment.ctr.GetValue()) @@ -48,6 +49,7 @@ def OnInit(self): #self.frame.start_run_sentinel() #self.frame.start_job_monitor() #self.frame.start_prg_sentinel() + self.frame.run_window.show_hide_tabs() return True else: return False @@ -55,6 +57,9 @@ def OnInit(self): return False def run(args): + import libtbx + libtbx.mpi_import_guard.disable_mpi = True + if '-h' in args or '--help' in args or '-c' in args: from xfel.ui import master_phil_str from libtbx.phil import parse diff --git a/xfel/ui/components/ebeam_plotter.py b/xfel/ui/components/ebeam_plotter.py index d52b942e56..ed865790fd 100644 --- a/xfel/ui/components/ebeam_plotter.py +++ b/xfel/ui/components/ebeam_plotter.py @@ -42,15 +42,26 @@ def compare_ebeams_with_fees(locfiles, runs=None, plot=True, use_figure=None, ma ebeams_eV.append(eeV:=ENERGY_CONV/ewav) print(f'{i}: {int(feV)} eV FEE / {int(eeV)} eV Ebeam') - if plot: - ax.hist(ebeams_eV, alpha=0.5, bins=40, label=f'run {runs[i]} ebeams ({int(eeV)} eV)') - ax.hist(fee_coms_eV, alpha=0.5, bins=40, label=f'run {runs[i]} FEE COMs ({int(feV)} eV)') + if len(ebeams_eV) == 0: + print("No events found with both FEE and eBeam") + return None, None + fee_coms_eV = np.array(fee_coms_eV) + ebeam_eV = np.array(ebeams_eV) + fee_coms_wav = np.array(fee_coms_wav) + ebeams_wav = np.array(ebeams_wav) - diffs_eV = np.array(fee_coms_eV) - np.array(ebeams_eV) + diffs_eV = fee_coms_eV - ebeam_eV ebeam_eV_offsets.append(sum(diffs_eV)/len(diffs_eV)) - diffs_wav = np.array(fee_coms_wav) - np.array(ebeams_wav) + diffs_wav = fee_coms_wav - ebeams_wav ebeam_wav_offsets.append(sum(diffs_wav)/len(diffs_wav)) + mean_fee_eV = np.mean(fee_coms_eV) + mean_ebeam_eV = np.mean(ebeam_eV) + + if plot: + ax.hist(ebeams_eV, alpha=0.5, bins=40, label=f'run {runs[i]} ebeams ({int(mean_ebeam_eV)} eV)') + ax.hist(fee_coms_eV, alpha=0.5, bins=40, label=f'run {runs[i]} FEE COMs ({int(mean_fee_eV)} eV)') + if plot: ax.legend() ax.set_xlabel('Energy (eV)') diff --git a/xfel/ui/components/run_stats_plotter.py b/xfel/ui/components/run_stats_plotter.py index 6b6a0ef51e..492531e3fa 100644 --- a/xfel/ui/components/run_stats_plotter.py +++ b/xfel/ui/components/run_stats_plotter.py @@ -305,17 +305,6 @@ def resolution(x,pos): if title is not None: plt.title(title) if interactive: - def onclick(event): - ts = event.xdata - if ts is None: return - diffs = flex.abs(t - ts) - ts = t[flex.first_index(diffs, flex.min(diffs))] - print(get_paths_from_timestamps([ts], tag="shot", ext=ext)[0]) - - if hasattr(f, '_cid'): - f.canvas.mpl_disconnect(f._cid) - f._cid = f.canvas.mpl_connect('button_press_event', onclick) - if not figure: plt.show() else: @@ -361,7 +350,7 @@ def plot_multirun_stats(runs, if len(r[0]) > 0: if compress_runs: tslice = r[0] - r[0][0] + offset - offset += (r[0][-1] - r[0][0] + 1/120.) + offset += (r[0][-1] - r[0][0] + 1) else: tslice = r[0] last_end = r[0][-1] diff --git a/xfel/ui/components/xfel_gui_dialogs.py b/xfel/ui/components/xfel_gui_dialogs.py index 25842c6a31..8d6621faeb 100644 --- a/xfel/ui/components/xfel_gui_dialogs.py +++ b/xfel/ui/components/xfel_gui_dialogs.py @@ -52,6 +52,11 @@ def __init__(self, parent, ID=wx.ID_ANY, pos=wx.DefaultPosition, TextEditMixin.__init__(self) self.curRow = -1 + def OnLeftDown(self, evt=None): + try: + return super(EdListCtrl, self).OnLeftDown(evt) + except wx._core.wxAssertionError: + pass class BaseDialog(wx.Dialog): def __init__(self, parent, diff --git a/xfel/ui/components/xfel_gui_init.py b/xfel/ui/components/xfel_gui_init.py index bdcf0fa8c1..ee490f6aa3 100644 --- a/xfel/ui/components/xfel_gui_init.py +++ b/xfel/ui/components/xfel_gui_init.py @@ -177,7 +177,6 @@ def run(self): from xfel.command_line.fee_calibration import fee_phil_string from libtbx.phil import parse self.fee_params = parse(notch_phil_string + fee_phil_string).extract() - self.fee_params.max_events=100 self.energy_tab.refresh_runs() while self.active: @@ -189,8 +188,6 @@ def run(self): if self.energy_tab.ebeam_calib_stale: self.run_ebeam_calib() self.post_refresh_energy() - if self.energy_tab.size_stale: - self.resize() self.parent.run_window.calib_light.change_status('on') # green-- actually means idle time.sleep(1) except Exception as e: @@ -198,20 +195,13 @@ def run(self): self.parent.run_window.calib_light.change_status('alert') # red -- means crashed break - def resize(self): - #for fig in (self.energy_tab.trendline_figure, self.energy_tab.spectra_figure, self.energy_tab.ebeam_figure): - # fig.set_figwidth(self.energy_tab.plotx) - # fig.set_figheight(self.energy_tab.ploty) - self.energy_tab.Layout() - #self.energy_tab.Fit() - def run_fee_calib(self): from serialtbx.util.energy_scan_notch_finder import find_notch, plot_notches, calibrate_energy from xfel.command_line.fee_calibration import tally_fee_data runs = self.energy_tab.fee_runs energies = self.energy_tab.fee_energies - rundata = tally_fee_data(self.energy_tab.experiment, runs, plot=False, verbose=True, max_events=self.fee_params.max_events) + rundata = tally_fee_data(self.energy_tab.experiment, runs, plot=False, verbose=True, max_events=self.energy_tab.max_events) notches = [find_notch(range(len(data)), data, self.fee_params.kernel_size, @@ -299,18 +289,19 @@ def run_ebeam_calib(self, source='loc'): self.energy_tab.ebeam_figure.clear() ebeam_eV_offset, ebeam_wavelength_offset = compare_ebeams_with_fees( - locfiles, - runs=reordered_run_strings, - plot=True, - use_figure=self.energy_tab.ebeam_figure, - max_events=100) + locfiles, + runs=reordered_run_strings, + plot=True, + use_figure=self.energy_tab.ebeam_figure, + max_events=self.energy_tab.max_events) self.energy_tab.ebeam_figure.canvas.draw_idle() - self.energy_tab.ebeam_eV_offset = ebeam_eV_offset - self.energy_tab.ebeam_wavelength_offset = ebeam_wavelength_offset - ang = u'\u212b' # Angstrom - self.energy_tab.ebeam_offset_text.SetLabel(f'{ebeam_eV_offset:.2f} eV ({ebeam_wavelength_offset:.6f} {ang})') - self.energy_tab.ebeam_calib_stale = False + if ebeam_eV_offset is not None: + self.energy_tab.ebeam_eV_offset = ebeam_eV_offset + self.energy_tab.ebeam_wavelength_offset = ebeam_wavelength_offset + ang = u'\u212b' # Angstrom + self.energy_tab.ebeam_offset_text.SetLabel(f'{ebeam_eV_offset:.2f} eV ({ebeam_wavelength_offset:.6f} {ang})') + self.energy_tab.ebeam_calib_stale = False #try: # locfile = os.path.join(db.params.output_folder, f'r{run.run:04d}', f'{trial.trial:03d}_rg{rg.rungroup_id:03d', 'data.loc') @@ -1267,7 +1258,7 @@ def plot_stats_static(self): class MainWindow(wx.Frame): - def __init__(self, parent, id, title): + def __init__(self, parent, id, title, params=None): wx.Frame.__init__(self, parent, id, title, size=(200, 200)) self.run_sentinel = None @@ -1279,7 +1270,9 @@ def __init__(self, parent, id, title): self.unitcell_sentinel = None self.mergingstats_sentinel = None - self.params = load_cached_settings() + if not params: + params = load_cached_settings() + self.params = params self.db = None self.high_vis = False @@ -1640,6 +1633,9 @@ def onLeavingTab(self, e): def onQuit(self, e): self.stop_sentinels() save_cached_settings(self.params) + # wx windows resolve to False if closed + if self.run_window.runstats_tab.sf_frame: + self.run_window.runstats_tab.sf_frame.Close() self.Destroy() @@ -1701,17 +1697,30 @@ def __init__(self, parent): main_sizer.Add(self.main_panel, 1, flag=wx.EXPAND | wx.ALL, border=3) self.SetSizer(main_sizer) + self.show_hide_tabs() + + def show_hide_tabs(self): if self.parent.params.monitoring_mode: self.runs_tab.Hide() - self.energy_tab.Hide() self.trials_tab.Hide() self.jobs_tab.Hide() self.datasets_tab.Hide() self.run_light.Hide() self.job_light.Hide() self.jmn_light.Hide() - - + else: + self.runs_tab.Show() + self.trials_tab.Show() + self.jobs_tab.Show() + self.datasets_tab.Show() + self.run_light.Show() + self.job_light.Show() + self.jmn_light.Show() + + if self.parent.params.facility.name == "lcls" and not self.parent.params.monitoring_mode: + self.energy_tab.Show() + else: + self.energy_tab.Hide() # --------------------------------- UI Tabs ---------------------------------- # @@ -1879,7 +1888,7 @@ def __init__(self, parent, main): NavigationToolbar2WxAgg as NavigationToolbar) # FEE scan section - self.scan_runs_panel = ScrolledPanel(self.calib_panel, size=(220, 300)) + self.scan_runs_panel = ScrolledPanel(self.calib_panel, size=(220, 325)) self.scan_runs_sizer = wx.BoxSizer(wx.VERTICAL) self.scan_runs_panel.SetSizer(self.scan_runs_sizer) @@ -1896,6 +1905,16 @@ def __init__(self, parent, main): self.expt_id_panel.SetSizer(self.expt_id_sizer) self.scan_runs_sizer.Add(self.expt_id_panel) + self.max_evts_panel = wx.Panel(self.scan_runs_panel) + self.max_evts_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.max_evts_label = wx.StaticText(self.max_evts_panel, label='Max events:', size=(80, -1)) + self.max_evts = wx.TextCtrl(self.max_evts_panel, size=(118, -1)) + self.max_evts_sizer.Add(self.max_evts_label) + self.max_evts_sizer.Add(self.max_evts) + self.max_evts.SetValue("200") + self.max_evts_panel.SetSizer(self.max_evts_sizer) + self.scan_runs_sizer.Add(self.max_evts_panel) + self.scan_runs_list = dlg.EdListCtrl(self.scan_runs_panel, style=wx.LC_EDIT_LABELS | wx.LC_REPORT, size=(200,165)) @@ -1903,6 +1922,8 @@ def __init__(self, parent, main): self.scan_runs_list.InsertColumn(0, 'Run', width=60) self.scan_runs_list.InsertColumn(1, 'Notch Energy (eV)', width=140) self.scan_runs_list.integer_columns = {0} + self.scan_runs_list.InsertItem(0, 0) + self.scan_runs_list.Select(0) self.scan_runs_sizer.Add(self.scan_runs_list, 1) @@ -2060,25 +2081,20 @@ def __init__(self, parent, main): self.Bind(wx.EVT_BUTTON, self.onRunsClear, self.ebeam_runs_clear_button) self.Bind(wx.EVT_BUTTON, self.onRunEbeamCalib, self.ebeam_runs_launch_button) self.Bind(wx.EVT_BUTTON, self.onSaveCalib, self.export_button) - self.Bind(wx.EVT_SIZE, self.onSize) self.Layout() self.Fit() - self.onSize() - - def onSize(self, e=None): - self.size_stale = True - # Caution: attempting to resize causes frequent core dumps!! - if e is not None: - e.Skip() def onAddScanRuns(self, e): n_rows = self.scan_runs_list.GetItemCount() self.scan_runs_list.InsertItem(n_rows, 0) + self.scan_runs_list.Select(n_rows) def onClearScanRuns(self, e): n_rows = self.scan_runs_list.GetItemCount() self.scan_runs_list.DeleteAllItems() + self.scan_runs_list.InsertItem(0, 0) + self.scan_runs_list.Select(0) e.Skip() def onRunFEECalib(self, e): @@ -2093,6 +2109,7 @@ def onRunFEECalib(self, e): self.experiment = self.main.params.facility.lcls.experiment else: self.experiment = None + self.max_events = int(self.max_evts.GetValue()) #TODO: validation during input instead? self.fee_runs = [] @@ -2124,6 +2141,7 @@ def onRunFEECalib(self, e): def onRunEbeamCalib(self, e): #self.ebeam_offset_text.SetLabel('') self.selected_runs = self.ebeam_runs_selection.ctr.GetCheckedStrings() + self.max_events = int(self.max_evts.GetValue()) self.ebeam_calib_stale = True e.Skip() @@ -2737,6 +2755,8 @@ def __init__(self, parent, main): self.strong_indexed_image_paths = None self.strong_indexed_image_timestamps = None self.auto_update = True + self.sf_frame = None + self.cached_run = None self.runstats_panel = wx.Panel(self, size=(100, 100)) self.runstats_box = wx.StaticBox(self.runstats_panel, label='Run Statistics') @@ -2946,6 +2966,10 @@ def __init__(self, parent, main): self.Bind(EVT_RUNSTATS_REFRESH, self.onRefresh) self.Bind(wx.EVT_SIZE, self.OnSize) + if hasattr(self.figure, '_cid'): + self.figure.canvas.mpl_disconnect(self.figure._cid) + self.figure._cid = self.figure.canvas.mpl_connect('button_press_event', self.onCanvasClick) + self.Layout() self.Fit() self.runstats_panelsize = self.runstats_box.GetSize() @@ -3004,6 +3028,58 @@ def onToggleOptions(self, e): self.Layout() self.Fit() + @staticmethod + def onCanvasClick(event): + if event.canvas.toolbar.mode: return + if event.xdata is None: return + tab = event.canvas.GetParent().GetParent().GetParent() + params = tab.main.params + if params.facility.name != 'lcls': return + all_stats = tab.main.runstats_sentinel.stats + if not all_stats: + return + x = round(event.xdata) + run_numbers = tab.main.runstats_sentinel.run_numbers + found_it = False + for run_number, stats in zip(run_numbers, all_stats): + timestamps, two_theta_low, two_theta_high, n_strong, resolutions, n_lattices = stats + if x < len(timestamps): + found_it = True + break + else: + x -= len(timestamps) + assert found_it, x + + trial = tab.trial + found_it = False + for rg in trial.rungroups: + for run in rg.runs: + if run.run == run_number: + found_it = True + break + if found_it: + break + assert found_it, run_number + + locator_path = os.path.join(params.output_folder, "r%04d"%int(run_number), \ + "%03d_rg%03d"%(trial.trial, rg.id), 'data.loc') + + from dials.command_line.image_viewer import phil_scope + from dials.util.image_viewer.spotfinder_frame import SpotFrame, chooser_wrapper + from dxtbx.model.experiment_list import ExperimentListFactory + if not tab.sf_frame: # if closed, wx windows resolve to False + expts = ExperimentListFactory.from_filenames([locator_path], load_models=False) + tab.sf_frame = SpotFrame(tab.main, params=phil_scope.extract(), experiments=[expts], reflections=[]) + tab.sf_frame.SetSize((1024, 780)) + elif tab.cached_run.run != run.run: + expts = ExperimentListFactory.from_filenames([locator_path], load_models=False) + tab.sf_frame.imagesets = expts.imagesets() + tab.sf_frame.add_file_name_or_data(chooser_wrapper(tab.sf_frame.imagesets[x], 0)) + print("Loading run %s, image %d"%(run.run, x+1)) + tab.sf_frame.load_image(chooser_wrapper(tab.sf_frame.imagesets[x], 0)) + tab.sf_frame.Show() + tab.cached_run = run + def onChkAutoUpdate(self, e): self.auto_update = self.chk_auto_update.GetValue() diff --git a/xfel/ui/db/job.py b/xfel/ui/db/job.py index 1647436e02..9717c6d4b7 100644 --- a/xfel/ui/db/job.py +++ b/xfel/ui/db/job.py @@ -1060,9 +1060,10 @@ def submit_all_jobs(app): print ("Task %s waiting on job %d (%s) for trial %d, rungroup %d, run %s, task %d" % \ (next_task.type, submitted_job.id, submitted_job.status, trial.trial, rungroup.id, run.run, next_task.id)) break - if submitted_job.status not in ["DONE", "EXIT"]: - print ("Task %s cannot start due to unexpected status for job %d (%s) for trial %d, rungroup %d, run %s, task %d" % \ - (next_task.type, submitted_job.id, submitted_job.status, trial.trial, rungroup.id, run.run, next_task.id)) + if submitted_job.status not in ["DONE"]: + if submitted_job.status != "EXIT": + print ("Task %s cannot start due to unexpected status for job %d (%s) for trial %d, rungroup %d, run %s, task %d" % \ + (next_task.type, submitted_job.id, submitted_job.status, trial.trial, rungroup.id, run.run, next_task.id)) break if submitted_job.status in ("SUBMIT_FAIL", "DELETED") and job.task and job.task.type == "ensemble_refinement": break # XXX need a better way to indicate that a job has failed and shouldn't go through the pipeline due to no data diff --git a/xfel/ui/db/xfel_db.py b/xfel/ui/db/xfel_db.py index 2913ec790d..315f984ab0 100644 --- a/xfel/ui/db/xfel_db.py +++ b/xfel/ui/db/xfel_db.py @@ -21,6 +21,8 @@ from xfel.command_line.experiment_manager import initialize as initialize_base +CACHED_CONNECT_TIMEOUT = 300 + class initialize(initialize_base): expected_tables = ["run", "job", "rungroup", "trial", "tag", "run_tag", "event", "trial_rungroup", "imageset", "imageset_event", "beam", "detector", "experiment", @@ -293,6 +295,7 @@ class db_application(object): def __init__(self, params, cache_connection = True, mode = 'execute'): self.params = params self.dbobj = None + self.dbobj_refreshed_time = None self.cache_connection = cache_connection self.query_count = 0 self.mode = mode @@ -326,8 +329,12 @@ def execute_query(self, query, commit=True): # https://stackoverflow.com/questions/1617637/pythons-mysqldb-not-getting-updated-row if not commit: # connection caching is not attempted if commit=False dbobj = get_db_connection(self.params, autocommit=False) - elif self.dbobj is None: + elif ( + self.dbobj is None + or time.time() - self.dbobj_refreshed_time > CACHED_CONNECT_TIMEOUT + ): dbobj = get_db_connection(self.params, autocommit=True) + self.dbobj_refreshed_time = time.time() if self.cache_connection: self.dbobj = dbobj else: