From 29892e8660336f6267ed2497d74be28090fcf6d6 Mon Sep 17 00:00:00 2001 From: "Aaron S. Brewster" Date: Sat, 22 Nov 2025 20:58:59 -0800 Subject: [PATCH 01/12] Rework the trial dialog - Hide the phil blob - Add first two controls, uc and sg - Add sync code between phil str and controls - Add Edit Phil dialog --- xfel/ui/components/xfel_gui_dialogs.py | 213 +++++++++++++++++++++---- 1 file changed, 180 insertions(+), 33 deletions(-) diff --git a/xfel/ui/components/xfel_gui_dialogs.py b/xfel/ui/components/xfel_gui_dialogs.py index f8738aa02b0..6cff937dfc1 100644 --- a/xfel/ui/components/xfel_gui_dialogs.py +++ b/xfel/ui/components/xfel_gui_dialogs.py @@ -14,6 +14,7 @@ import wx from wx.lib.mixins.listctrl import TextEditMixin, getListCtrlSelection from wx.lib.scrolledpanel import ScrolledPanel +from iotbx.phil import parse from xfel.ui.db.task import task_types import numpy as np @@ -2709,7 +2710,7 @@ def __init__(self, parent, db, button1_label='Import PHIL', button1_size=(120, -1), button2=True, - button2_label='Default PHIL', + button2_label='Edit PHIL' if new else 'Show PHIL', button2_size=(120, -1), value="{}".format(trial_number)) self.trial_comment = gctr.TextButtonCtrl(self, @@ -2718,6 +2719,17 @@ def __init__(self, parent, db, label_style='bold', ghost_button=False) + self.unit_cell = gctr.TextButtonCtrl(self, + label='Unit cell:', + label_size=(100, -1), + label_style='bold', + ghost_button=False) + self.space_group = gctr.TextButtonCtrl(self, + label='Space group:', + label_size=(150, -1), + label_style='bold', + ghost_button=False) + self.phil_box = gctr.RichTextCtrl(self, style=wx.VSCROLL, size=(-1, 400)) choices = [('None', None)] + \ @@ -2775,7 +2787,10 @@ def __init__(self, parent, db, self.main_sizer.Add(self.trial_comment, flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, border=10) - self.main_sizer.Add(self.phil_box, 1, + self.main_sizer.Add(self.unit_cell, + flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, + border=10) + self.main_sizer.Add(self.space_group, flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, border=10) self.main_sizer.Add(self.option_sizer, flag=wx.EXPAND | wx.ALL, border=10) @@ -2814,10 +2829,8 @@ def __init__(self, parent, db, # Disable controls for viewing self.trial_info.button1.Disable() - self.trial_info.button2.Disable() self.trial_info.ctr.SetEditable(False) self.copy_runblocks.Hide() - self.phil_box.SetEditable(False) self.throttle.ctr.Disable() self.num_bins.ctr.Disable() self.d_min.ctr.Disable() @@ -2826,14 +2839,48 @@ def __init__(self, parent, db, target_phil_str = "" if process_percent is None: process_percent = 100 - self.phil_box.SetValue(target_phil_str) + + dispatcher = self.db.params.dispatcher + from xfel.ui import load_phil_scope_from_dispatcher + self.phil_scope = load_phil_scope_from_dispatcher(dispatcher) + + self.sync_controls(target_phil_str) + self.throttle.ctr.SetValue(process_percent) # Bindings self.Bind(wx.EVT_BUTTON, self.onBrowse, self.trial_info.button1) - self.Bind(wx.EVT_BUTTON, self.onDefault, self.trial_info.button2) + self.Bind(wx.EVT_BUTTON, self.onEdit, self.trial_info.button2) self.Bind(wx.EVT_BUTTON, self.onOK, id=wx.ID_OK) + def sync_controls(self, phil_str): + params, _ = self.parse_trial_phil(phil_str) + if params: + if params.indexing.known_symmetry.unit_cell: + self.unit_cell.ctr.SetValue(" ".join("%.f"%p for p in params.indexing.known_symmetry.unit_cell.parameters())) + else: + self.unit_cell.ctr.SetValue("") + if params.indexing.known_symmetry.space_group: + self.space_group.ctr.SetValue(str(params.indexing.known_symmetry.space_group)) + else: + self.space_group.ctr.SetValue("") + self.target_phil_str = phil_str + + def sync_phil_str(self): + phil_str = self.target_phil_str + controls_params = parse(f""" + indexing {{ + known_symmetry {{ + unit_cell = {self.unit_cell.ctr.GetValue()} + space_group = {self.space_group.ctr.GetValue()} + }} + }} + """) + combined_scope = self.phil_scope.fetch(parse(phil_str)).fetch(controls_params) + diff_scope = self.phil_scope.fetch_diff(combined_scope) + self.target_phil_str = diff_scope.as_str() + # XXX check results + def onBrowse(self, e): ''' Open dialog for selecting PHIL file ''' @@ -2848,39 +2895,48 @@ def onBrowse(self, e): target_file = load_dlg.GetPaths()[0] with open(target_file, 'r') as phil_file: phil_file_contents = phil_file.read() - self.phil_box.SetValue(phil_file_contents) + self.sync_controls(phil_file_contents) load_dlg.Destroy() - def onDefault(self, e): - # TODO: Generate default PHIL parameters - pass - - def onOK(self, e): + def onEdit(self, e): if self.new: - target_phil_str = self.phil_box.GetValue() - - # Parameter validation - dispatcher = self.db.params.dispatcher - from xfel.ui import load_phil_scope_from_dispatcher - phil_scope = load_phil_scope_from_dispatcher(dispatcher) + self.sync_phil_str() + edit_phil_dlg = EditPhilDialog(self, + db=self.db, + read_only=not self.new, + phil_scope=self.phil_scope, + phil_str=self.target_phil_str) + edit_phil_dlg.Fit() + + if edit_phil_dlg.ShowModal() == wx.ID_OK: + self.sync_controls(edit_phil_dlg.phil_box.GetValue()) + + def parse_trial_phil(self, target_phil_str): + # Parameter validation + msg = None + try: + trial_params, unused = self.phil_scope.fetch(parse(target_phil_str), track_unused_definitions = True) + except Exception as e: + msg = '\nParameters incompatible with %s dispatcher:\n%s\n' % (self.db.params.dispatcher, str(e)) + else: + if len(unused) > 0: + msg = [str(item) for item in unused] + msg = '\n'.join([' %s' % line for line in msg]) + msg = 'The following definitions were not recognized:\n%s\n' % msg - from iotbx.phil import parse - msg = None try: - trial_params, unused = phil_scope.fetch(parse(target_phil_str), track_unused_definitions = True) + params = trial_params.extract() except Exception as e: - msg = '\nParameters incompatible with %s dispatcher:\n%s\n' % (dispatcher, str(e)) - else: - if len(unused) > 0: - msg = [str(item) for item in unused] - msg = '\n'.join([' %s' % line for line in msg]) - msg = 'The following definitions were not recognized:\n%s\n' % msg + if msg is None: msg = "" + msg += '\nOne or more values could not be parsed:\n%s\n' % str(e) + params = None + return params, msg - try: - params = trial_params.extract() - except Exception as e: - if msg is None: msg = "" - msg += '\nOne or more values could not be parsed:\n%s\n' % str(e) + def onOK(self, e): + if self.new: + self.sync_phil_str() + target_phil_str = self.target_phil_str + _, msg = self.parse_trial_phil(target_phil_str) if msg is not None: msg += '\nFix the parameters and press OK again' @@ -2925,6 +2981,98 @@ def onOK(self, e): self.trial.comment = self.trial_comment.ctr.GetValue() e.Skip() +class EditPhilDialog(BaseDialog): + def __init__(self, parent, db, + read_only=False, + phil_scope=None, + phil_str="", + label_style='bold', + content_style='normal', + *args, **kwargs): + + self.db = db + self.read_only = read_only + self.phil_scope = phil_scope + + BaseDialog.__init__(self, parent, + label_style=label_style, + content_style=content_style, + size=(600, 600), + *args, **kwargs) + + self.phil_box = gctr.RichTextCtrl(self, style=wx.VSCROLL, size=(400, 400)) + + self.main_sizer.Add(self.phil_box, 1, + flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, + border=10) + + # Dialog control + dialog_box = self.CreateSeparatedButtonSizer(wx.OK | wx.CANCEL) + self.main_sizer.Add(dialog_box, + flag=wx.EXPAND | wx.ALL, + border=10) + + # Dialog control + if self.read_only: + dialog_box = self.CreateSeparatedButtonSizer(wx.OK) + else: + dialog_box = self.CreateSeparatedButtonSizer(wx.OK | wx.CANCEL) + + self.main_sizer.Add(dialog_box, + flag=wx.EXPAND | wx.ALL, + border=10) + + self.Layout() + + self.SetTitle('Settings') + if self.read_only: + # Disable controls for viewing + self.phil_box.SetEditable(False) + + if phil_str is None: + phil_str = "" + + self.phil_box.SetValue(phil_str) + + # Bindings + self.Bind(wx.EVT_BUTTON, self.onOK, id=wx.ID_OK) + + def parse_trial_phil(self, target_phil_str): + # Parameter validation + msg = None + params = None + try: + trial_params, unused = self.phil_scope.fetch(parse(target_phil_str), track_unused_definitions = True) + except Exception as e: + msg = '\nParameters incompatible with %s dispatcher:\n%s\n' % (self.db.params.dispatcher, str(e)) + else: + if len(unused) > 0: + msg = [str(item) for item in unused] + msg = '\n'.join([' %s' % line for line in msg]) + msg = 'The following definitions were not recognized:\n%s\n' % msg + + try: + params = trial_params.extract() + except Exception as e: + if msg is None: msg = "" + msg += '\nOne or more values could not be parsed:\n%s\n' % str(e) + return params, msg + + def onOK(self, e): + if not self.read_only: + target_phil_str = self.phil_box.GetValue() + _, msg = self.parse_trial_phil(target_phil_str) + + if msg is not None: + msg += '\nFix the parameters and press OK again' + msgdlg = wx.MessageDialog(self, + message=msg, + caption='Warning', + style=wx.OK | wx.ICON_EXCLAMATION) + msgdlg.ShowModal() + return + e.Skip() + class DatasetDialog(BaseDialog): def __init__(self, parent, db, new=True, @@ -3124,7 +3272,6 @@ def onOK(self, e): # Parameter validation from xfel.ui.db.task import Task - from iotbx.phil import parse dispatcher, phil_scope = Task.get_phil_scope(self.db, task_type) if phil_scope is not None: From e8ca9cbcff158b4c68b7bcd95bdbc5ea260c6336 Mon Sep 17 00:00:00 2001 From: "Aaron S. Brewster" Date: Sat, 22 Nov 2025 23:29:33 -0800 Subject: [PATCH 02/12] Add a few more widgets and re-work the phil handling Includes more validation as well, such as the unit cell being compatible with the space group --- xfel/ui/components/xfel_gui_dialogs.py | 163 +++++++++++++++++-------- 1 file changed, 109 insertions(+), 54 deletions(-) diff --git a/xfel/ui/components/xfel_gui_dialogs.py b/xfel/ui/components/xfel_gui_dialogs.py index 6cff937dfc1..a785eef26bd 100644 --- a/xfel/ui/components/xfel_gui_dialogs.py +++ b/xfel/ui/components/xfel_gui_dialogs.py @@ -2719,17 +2719,51 @@ def __init__(self, parent, db, label_style='bold', ghost_button=False) - self.unit_cell = gctr.TextButtonCtrl(self, + self.overall_panel = wx.Panel(self) + overall_box = wx.StaticBox(self.overall_panel, label='Overall parameters') + self.overall_sizer = wx.StaticBoxSizer(overall_box) + self.overall_panel.SetSizer(self.overall_sizer) + + self.chk_find_spots = wx.CheckBox(self.overall_panel, + label='Find spots') + self.chk_index = wx.CheckBox(self.overall_panel, + label='Index') + self.chk_integrate = wx.CheckBox(self.overall_panel, + label='Integrate') + self.overall_chk_sizer = wx.FlexGridSizer(1, 3, 10, 20) + self.overall_chk_sizer.Add(self.chk_find_spots, flag=wx.ALL, border=10) + self.overall_chk_sizer.Add(self.chk_index, flag=wx.ALL, border=10) + self.overall_chk_sizer.Add(self.chk_integrate, flag=wx.ALL, border=10) + self.overall_sizer.Add(self.overall_chk_sizer) + + self.spotfinding_panel = wx.Panel(self) + spotfinding_box = wx.StaticBox(self.spotfinding_panel, label='Spotfinding parameters') + self.spotfinding_sizer = wx.StaticBoxSizer(spotfinding_box) + self.spotfinding_panel.SetSizer(self.spotfinding_sizer) + + self.indexing_panel = wx.Panel(self) + indexing_box = wx.StaticBox(self.indexing_panel, label='Indexing parameters') + self.indexing_sizer = wx.StaticBoxSizer(indexing_box) + self.indexing_panel.SetSizer(self.indexing_sizer) + + self.unit_cell = gctr.TextButtonCtrl(self.indexing_panel, label='Unit cell:', label_size=(100, -1), label_style='bold', ghost_button=False) - self.space_group = gctr.TextButtonCtrl(self, + self.space_group = gctr.TextButtonCtrl(self.indexing_panel, label='Space group:', label_size=(150, -1), label_style='bold', ghost_button=False) + self.indexing_sizer.Add(self.unit_cell, + flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, + border=10) + self.indexing_sizer.Add(self.space_group, + flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, + border=10) + self.phil_box = gctr.RichTextCtrl(self, style=wx.VSCROLL, size=(-1, 400)) choices = [('None', None)] + \ @@ -2787,12 +2821,9 @@ def __init__(self, parent, db, self.main_sizer.Add(self.trial_comment, flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, border=10) - self.main_sizer.Add(self.unit_cell, - flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, - border=10) - self.main_sizer.Add(self.space_group, - flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, - border=10) + self.main_sizer.Add(self.overall_panel, flag=wx.EXPAND | wx.ALL, border=10) + self.main_sizer.Add(self.spotfinding_panel, flag=wx.EXPAND | wx.ALL, border=10) + self.main_sizer.Add(self.indexing_panel, flag=wx.EXPAND | wx.ALL, border=10) self.main_sizer.Add(self.option_sizer, flag=wx.EXPAND | wx.ALL, border=10) @@ -2843,8 +2874,9 @@ def __init__(self, parent, db, dispatcher = self.db.params.dispatcher from xfel.ui import load_phil_scope_from_dispatcher self.phil_scope = load_phil_scope_from_dispatcher(dispatcher) + self.working_phil_scope = self.phil_scope.fetch(parse(target_phil_str)) - self.sync_controls(target_phil_str) + self.sync_controls() self.throttle.ctr.SetValue(process_percent) @@ -2853,33 +2885,52 @@ def __init__(self, parent, db, self.Bind(wx.EVT_BUTTON, self.onEdit, self.trial_info.button2) self.Bind(wx.EVT_BUTTON, self.onOK, id=wx.ID_OK) - def sync_controls(self, phil_str): - params, _ = self.parse_trial_phil(phil_str) - if params: - if params.indexing.known_symmetry.unit_cell: - self.unit_cell.ctr.SetValue(" ".join("%.f"%p for p in params.indexing.known_symmetry.unit_cell.parameters())) - else: - self.unit_cell.ctr.SetValue("") - if params.indexing.known_symmetry.space_group: - self.space_group.ctr.SetValue(str(params.indexing.known_symmetry.space_group)) - else: - self.space_group.ctr.SetValue("") - self.target_phil_str = phil_str + def sync_controls(self): + params = self.working_phil_scope.extract() + self.chk_find_spots.SetValue(params.dispatch.find_spots) + self.chk_index.SetValue(params.dispatch.index) + self.chk_integrate.SetValue(params.dispatch.integrate) - def sync_phil_str(self): - phil_str = self.target_phil_str - controls_params = parse(f""" + if params.indexing.known_symmetry.unit_cell: + self.unit_cell.ctr.SetValue(" ".join("%.f"%p for p in params.indexing.known_symmetry.unit_cell.parameters())) + else: + self.unit_cell.ctr.SetValue("") + if params.indexing.known_symmetry.space_group: + self.space_group.ctr.SetValue(str(params.indexing.known_symmetry.space_group)) + else: + self.space_group.ctr.SetValue("") + + def sync_phil_scope(self): + trial_phil = f""" + dispatch {{ + find_spots = {self.chk_find_spots.GetValue()} + index = {self.chk_index.GetValue()} + integrate = {self.chk_integrate.GetValue()} + }} indexing {{ known_symmetry {{ unit_cell = {self.unit_cell.ctr.GetValue()} space_group = {self.space_group.ctr.GetValue()} }} }} - """) - combined_scope = self.phil_scope.fetch(parse(phil_str)).fetch(controls_params) - diff_scope = self.phil_scope.fetch_diff(combined_scope) - self.target_phil_str = diff_scope.as_str() - # XXX check results + """ + params, msg = self.parse_trial_phil(trial_phil) + + if msg is None: + unit_cell = params.indexing.known_symmetry.unit_cell + space_group = params.indexing.known_symmetry.space_group + if unit_cell and space_group and space_group.group().is_compatible_unit_cell(unit_cell): + return True + else: + msg = "Unit cell is incompatible with space group" + + msg += '\nFix the parameters and try again' + msgdlg = wx.MessageDialog(self, + message=msg, + caption='Warning', + style=wx.OK | wx.ICON_EXCLAMATION) + msgdlg.ShowModal() + return False def onBrowse(self, e): ''' Open dialog for selecting PHIL file ''' @@ -2895,27 +2946,39 @@ def onBrowse(self, e): target_file = load_dlg.GetPaths()[0] with open(target_file, 'r') as phil_file: phil_file_contents = phil_file.read() - self.sync_controls(phil_file_contents) + _, msg = self.parse_trial_phil(phil_file_contents) + + if msg is not None: + self.sync_controls(phil_file_contents) + else: + msg += '\nFix the parameters in the file and reload' + msgdlg = wx.MessageDialog(self, + message=msg, + caption='Warning', + style=wx.OK | wx.ICON_EXCLAMATION) + msgdlg.ShowModal() + load_dlg.Destroy() def onEdit(self, e): - if self.new: - self.sync_phil_str() + if self.new and not self.sync_phil_scope(): + return edit_phil_dlg = EditPhilDialog(self, db=self.db, read_only=not self.new, phil_scope=self.phil_scope, - phil_str=self.target_phil_str) + working_phil_scope=self.working_phil_scope) edit_phil_dlg.Fit() if edit_phil_dlg.ShowModal() == wx.ID_OK: - self.sync_controls(edit_phil_dlg.phil_box.GetValue()) + self.parse_trial_phil(edit_phil_dlg.phil_box.GetValue()) + self.sync_controls() def parse_trial_phil(self, target_phil_str): # Parameter validation msg = None try: - trial_params, unused = self.phil_scope.fetch(parse(target_phil_str), track_unused_definitions = True) + working_phil_scope, unused = self.phil_scope.fetch(parse(target_phil_str), track_unused_definitions = True) except Exception as e: msg = '\nParameters incompatible with %s dispatcher:\n%s\n' % (self.db.params.dispatcher, str(e)) else: @@ -2925,27 +2988,20 @@ def parse_trial_phil(self, target_phil_str): msg = 'The following definitions were not recognized:\n%s\n' % msg try: - params = trial_params.extract() + params = working_phil_scope.extract() except Exception as e: if msg is None: msg = "" msg += '\nOne or more values could not be parsed:\n%s\n' % str(e) params = None + else: + self.working_phil_scope = working_phil_scope return params, msg def onOK(self, e): if self.new: - self.sync_phil_str() - target_phil_str = self.target_phil_str - _, msg = self.parse_trial_phil(target_phil_str) - - if msg is not None: - msg += '\nFix the parameters and press OK again' - msgdlg = wx.MessageDialog(self, - message=msg, - caption='Warning', - style=wx.OK | wx.ICON_EXCLAMATION) - msgdlg.ShowModal() + if not self.sync_phil_scope(): return + target_phil_str = self.phil_scope.fetch_diff(self.working_phil_scope).as_str() comment = self.trial_comment.ctr.GetValue() process_percent = int(self.throttle.ctr.GetValue()) @@ -2985,7 +3041,7 @@ class EditPhilDialog(BaseDialog): def __init__(self, parent, db, read_only=False, phil_scope=None, - phil_str="", + working_phil_scope=None, label_style='bold', content_style='normal', *args, **kwargs): @@ -3029,10 +3085,7 @@ def __init__(self, parent, db, # Disable controls for viewing self.phil_box.SetEditable(False) - if phil_str is None: - phil_str = "" - - self.phil_box.SetValue(phil_str) + self.phil_box.SetValue(phil_scope.fetch_diff(working_phil_scope).as_str()) # Bindings self.Bind(wx.EVT_BUTTON, self.onOK, id=wx.ID_OK) @@ -3042,7 +3095,7 @@ def parse_trial_phil(self, target_phil_str): msg = None params = None try: - trial_params, unused = self.phil_scope.fetch(parse(target_phil_str), track_unused_definitions = True) + working_phil_scope, unused = self.phil_scope.fetch(parse(target_phil_str), track_unused_definitions = True) except Exception as e: msg = '\nParameters incompatible with %s dispatcher:\n%s\n' % (self.db.params.dispatcher, str(e)) else: @@ -3052,10 +3105,12 @@ def parse_trial_phil(self, target_phil_str): msg = 'The following definitions were not recognized:\n%s\n' % msg try: - params = trial_params.extract() + params = working_phil_scope.extract() except Exception as e: if msg is None: msg = "" msg += '\nOne or more values could not be parsed:\n%s\n' % str(e) + else: + self.working_phil_scope = working_phil_scope return params, msg def onOK(self, e): From cedc77bc14c8d119e9769f38d88767690cd152c2 Mon Sep 17 00:00:00 2001 From: "Aaron S. Brewster" Date: Tue, 25 Nov 2025 16:08:32 -0800 Subject: [PATCH 03/12] More trial dialog fixes/controls - Remove extra ok/cancel - Add min spots for hitfinding - Fix rounding in unit cell and roundtripping of phil using working phil --- xfel/ui/components/xfel_gui_dialogs.py | 33 +++++++++++++++----------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/xfel/ui/components/xfel_gui_dialogs.py b/xfel/ui/components/xfel_gui_dialogs.py index a785eef26bd..3a1002be8a1 100644 --- a/xfel/ui/components/xfel_gui_dialogs.py +++ b/xfel/ui/components/xfel_gui_dialogs.py @@ -2730,11 +2730,17 @@ def __init__(self, parent, db, label='Index') self.chk_integrate = wx.CheckBox(self.overall_panel, label='Integrate') - self.overall_chk_sizer = wx.FlexGridSizer(1, 3, 10, 20) - self.overall_chk_sizer.Add(self.chk_find_spots, flag=wx.ALL, border=10) - self.overall_chk_sizer.Add(self.chk_index, flag=wx.ALL, border=10) - self.overall_chk_sizer.Add(self.chk_integrate, flag=wx.ALL, border=10) - self.overall_sizer.Add(self.overall_chk_sizer) + self.min_spots = gctr.TextButtonCtrl(self.overall_panel, + label='Min spots', + label_size=(-1, -1), + label_style='bold', + ghost_button=False) + self.overall_ctrl_sizer = wx.FlexGridSizer(1, 4, 10, 20) + self.overall_ctrl_sizer.Add(self.chk_find_spots, flag=wx.ALL, border=10) + self.overall_ctrl_sizer.Add(self.chk_index, flag=wx.ALL, border=10) + self.overall_ctrl_sizer.Add(self.chk_integrate, flag=wx.ALL, border=10) + self.overall_ctrl_sizer.Add(self.min_spots, flag=wx.ALL, border=10) + self.overall_sizer.Add(self.overall_ctrl_sizer) self.spotfinding_panel = wx.Panel(self) spotfinding_box = wx.StaticBox(self.spotfinding_panel, label='Spotfinding parameters') @@ -2890,9 +2896,10 @@ def sync_controls(self): self.chk_find_spots.SetValue(params.dispatch.find_spots) self.chk_index.SetValue(params.dispatch.index) self.chk_integrate.SetValue(params.dispatch.integrate) + set_value(self.min_spots.ctr, params.dispatch.hit_finder.minimum_number_of_reflections) if params.indexing.known_symmetry.unit_cell: - self.unit_cell.ctr.SetValue(" ".join("%.f"%p for p in params.indexing.known_symmetry.unit_cell.parameters())) + self.unit_cell.ctr.SetValue(str(params.indexing.known_symmetry.unit_cell).strip('()')) else: self.unit_cell.ctr.SetValue("") if params.indexing.known_symmetry.space_group: @@ -2906,6 +2913,9 @@ def sync_phil_scope(self): find_spots = {self.chk_find_spots.GetValue()} index = {self.chk_index.GetValue()} integrate = {self.chk_integrate.GetValue()} + hit_finder {{ + minimum_number_of_reflections = {str_or_none(self.min_spots.ctr)} + }} }} indexing {{ known_symmetry {{ @@ -2978,7 +2988,7 @@ def parse_trial_phil(self, target_phil_str): # Parameter validation msg = None try: - working_phil_scope, unused = self.phil_scope.fetch(parse(target_phil_str), track_unused_definitions = True) + working_phil_scope, unused = self.working_phil_scope.fetch(parse(target_phil_str), track_unused_definitions = True) except Exception as e: msg = '\nParameters incompatible with %s dispatcher:\n%s\n' % (self.db.params.dispatcher, str(e)) else: @@ -3049,6 +3059,7 @@ def __init__(self, parent, db, self.db = db self.read_only = read_only self.phil_scope = phil_scope + self.working_phil_scope = working_phil_scope BaseDialog.__init__(self, parent, label_style=label_style, @@ -3062,12 +3073,6 @@ def __init__(self, parent, db, flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, border=10) - # Dialog control - dialog_box = self.CreateSeparatedButtonSizer(wx.OK | wx.CANCEL) - self.main_sizer.Add(dialog_box, - flag=wx.EXPAND | wx.ALL, - border=10) - # Dialog control if self.read_only: dialog_box = self.CreateSeparatedButtonSizer(wx.OK) @@ -3095,7 +3100,7 @@ def parse_trial_phil(self, target_phil_str): msg = None params = None try: - working_phil_scope, unused = self.phil_scope.fetch(parse(target_phil_str), track_unused_definitions = True) + working_phil_scope, unused = self.working_phil_scope.fetch(parse(target_phil_str), track_unused_definitions = True) except Exception as e: msg = '\nParameters incompatible with %s dispatcher:\n%s\n' % (self.db.params.dispatcher, str(e)) else: From fe70b8f8f46cb5d62ff806834157cc67f08bf457 Mon Sep 17 00:00:00 2001 From: Johannes Blaschke Date: Tue, 25 Nov 2025 16:17:04 -0800 Subject: [PATCH 04/12] Formatting --- xfel/ui/components/xfel_gui_dialogs.py | 100 ++++++++++++++++++++----- 1 file changed, 83 insertions(+), 17 deletions(-) diff --git a/xfel/ui/components/xfel_gui_dialogs.py b/xfel/ui/components/xfel_gui_dialogs.py index 3a1002be8a1..925b118424a 100644 --- a/xfel/ui/components/xfel_gui_dialogs.py +++ b/xfel/ui/components/xfel_gui_dialogs.py @@ -2676,7 +2676,7 @@ class TrialDialog(BaseDialog): def __init__(self, parent, db, new=True, trial=None, - label_style='bold', + label_style='normal', content_style='normal', *args, **kwargs): @@ -2705,7 +2705,7 @@ def __init__(self, parent, db, self.trial_info = gctr.TwoButtonCtrl(self, label='Trial number:', label_size=(100, -1), - label_style='bold', + label_style='normal', button1=True, button1_label='Import PHIL', button1_size=(120, -1), @@ -2716,7 +2716,7 @@ def __init__(self, parent, db, self.trial_comment = gctr.TextButtonCtrl(self, label='Comment:', label_size=(100, -1), - label_style='bold', + label_style='normal', ghost_button=False) self.overall_panel = wx.Panel(self) @@ -2733,7 +2733,7 @@ def __init__(self, parent, db, self.min_spots = gctr.TextButtonCtrl(self.overall_panel, label='Min spots', label_size=(-1, -1), - label_style='bold', + label_style='normal', ghost_button=False) self.overall_ctrl_sizer = wx.FlexGridSizer(1, 4, 10, 20) self.overall_ctrl_sizer.Add(self.chk_find_spots, flag=wx.ALL, border=10) @@ -2747,6 +2747,59 @@ def __init__(self, parent, db, self.spotfinding_sizer = wx.StaticBoxSizer(spotfinding_box) self.spotfinding_panel.SetSizer(self.spotfinding_sizer) + self.min_spot_size = gctr.TextButtonCtrl(self.spotfinding_panel, + label='Min spot size', + label_size=(-1, -1), + label_style='normal', + ghost_button=False) + self.max_spot_size = gctr.TextButtonCtrl(self.spotfinding_panel, + label='Max spot size', + label_size=(-1, -1), + label_style='normal', + ghost_button=False) + self.sigma_background = gctr.TextButtonCtrl(self.spotfinding_panel, + label='Sigma background', + label_size=(-1, -1), + label_style='normal', + ghost_button=False) + self.sigma_strong = gctr.TextButtonCtrl(self.spotfinding_panel, + label='Sigma strong', + label_size=(-1, -1), + label_style='normal', + ghost_button=False) + self.global_threshold = gctr.TextButtonCtrl(self.spotfinding_panel, + label='Global threshold', + label_size=(-1, -1), + label_style='normal', + ghost_button=False) + self.gain = gctr.TextButtonCtrl(self.spotfinding_panel, + label='Gain', + label_size=(-1, -1), + label_style='normal', + ghost_button=False) + self.kernel_size = gctr.TextButtonCtrl(self.spotfinding_panel, + label='Kernel size', + label_size=(-1, -1), + label_style='normal', + ghost_button=False) + self.threshold_algorithm = gctr.ChoiceCtrl(self.spotfinding_panel, + label='Threshold algorithm:', + label_size=(200, -1), + label_style='normal', + ctrl_size=(200, -1), + choices=['dispersion', 'dispersion_extended', 'radial_profile']) + + self.spotfinding_ctrl_sizer = wx.FlexGridSizer(4, 2, 10, 10) + self.spotfinding_ctrl_sizer.Add(self.min_spot_size, flag=wx.ALL, border=10) + self.spotfinding_ctrl_sizer.Add(self.max_spot_size, flag=wx.ALL, border=10) + self.spotfinding_ctrl_sizer.Add(self.sigma_background, flag=wx.ALL, border=10) + self.spotfinding_ctrl_sizer.Add(self.sigma_strong, flag=wx.ALL, border=10) + self.spotfinding_ctrl_sizer.Add(self.global_threshold, flag=wx.ALL, border=10) + self.spotfinding_ctrl_sizer.Add(self.gain, flag=wx.ALL, border=10) + self.spotfinding_ctrl_sizer.Add(self.kernel_size, flag=wx.ALL, border=10) + self.spotfinding_ctrl_sizer.Add(self.threshold_algorithm, flag=wx.ALL, border=10) + self.spotfinding_sizer.Add(self.spotfinding_ctrl_sizer) + self.indexing_panel = wx.Panel(self) indexing_box = wx.StaticBox(self.indexing_panel, label='Indexing parameters') self.indexing_sizer = wx.StaticBoxSizer(indexing_box) @@ -2755,20 +2808,33 @@ def __init__(self, parent, db, self.unit_cell = gctr.TextButtonCtrl(self.indexing_panel, label='Unit cell:', label_size=(100, -1), - label_style='bold', + label_style='normal', ghost_button=False) self.space_group = gctr.TextButtonCtrl(self.indexing_panel, label='Space group:', label_size=(150, -1), - label_style='bold', + label_style='normal', ghost_button=False) - - self.indexing_sizer.Add(self.unit_cell, - flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, - border=10) - self.indexing_sizer.Add(self.space_group, - flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, - border=10) + self.d_min_indexing = gctr.TextButtonCtrl(self.indexing_panel, + label='d_min indexing:', + label_size=(150, -1), + label_style='normal', + ghost_button=False) + self.max_lattices = gctr.TextButtonCtrl(self.indexing_panel, + label='Max lattices:', + label_size=(150, -1), + label_style='normal', + ghost_button=False) + self.chk_subsampling = wx.CheckBox(self.indexing_panel, + label='Reflection subsampling') + + self.indexing_ctrl_sizer = wx.FlexGridSizer(3, 2, 10, 10) + self.indexing_ctrl_sizer.Add(self.unit_cell, flag=wx.ALL, border=10) + self.indexing_ctrl_sizer.Add(self.space_group, flag=wx.ALL, border=10) + self.indexing_ctrl_sizer.Add(self.d_min_indexing, flag=wx.ALL, border=10) + self.indexing_ctrl_sizer.Add(self.max_lattices, flag=wx.ALL, border=10) + self.indexing_ctrl_sizer.Add(self.chk_subsampling, flag=wx.ALL, border=10) + self.indexing_sizer.Add(self.indexing_ctrl_sizer) self.phil_box = gctr.RichTextCtrl(self, style=wx.VSCROLL, size=(-1, 400)) @@ -2785,7 +2851,7 @@ def __init__(self, parent, db, name='trial_throttle', label='Percent events processed:', label_size=(180, -1), - label_style='bold', + label_style='normal', ctrl_size=(150, -1), ctrl_value='100', ctrl_min=1, @@ -2794,7 +2860,7 @@ def __init__(self, parent, db, name='trial_num_bins', label='Number of bins:', label_size=(180, -1), - label_style='bold', + label_style='normal', ctrl_size=(150, -1), ctrl_value='20', ctrl_min=1, @@ -2805,7 +2871,7 @@ def __init__(self, parent, db, label='High res. limit ({}):' ''.format(u'\N{ANGSTROM SIGN}'.encode('utf-8')), label_size=(180, -1), - label_style='bold', + label_style='normal', ctrl_size=(150, -1), ctrl_value=str(d_min), ctrl_min=0.1, @@ -3052,7 +3118,7 @@ def __init__(self, parent, db, read_only=False, phil_scope=None, working_phil_scope=None, - label_style='bold', + label_style='normal', content_style='normal', *args, **kwargs): From c1de5cbc8a81c4b790a68f8b764f3bacc17f0f5d Mon Sep 17 00:00:00 2001 From: Johannes Blaschke Date: Tue, 25 Nov 2025 17:02:00 -0800 Subject: [PATCH 05/12] Remove legacy reference to config file (from pyana) --- xfel/ui/components/xfel_gui_dialogs.py | 44 +------------------------- 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/xfel/ui/components/xfel_gui_dialogs.py b/xfel/ui/components/xfel_gui_dialogs.py index 925b118424a..65acdda4f0b 100644 --- a/xfel/ui/components/xfel_gui_dialogs.py +++ b/xfel/ui/components/xfel_gui_dialogs.py @@ -2103,7 +2103,7 @@ def __getattr__(self, item): elif item in ["extra_phil_str", "calib_dir", "dark_avg_path", "dark_stddev_path", "gain_map_path", "beamx", "beamy", "gain_mask_level", "untrusted_pixel_mask_path", "binning", "energy", "wavelength_offset", "spectrum_eV_per_pixel", "spectrum_eV_offset", - "comment", "config_str", "extra_format_str"]: + "comment", "extra_format_str"]: return None else: raise AttributeError(item) @@ -2133,13 +2133,6 @@ def __getattr__(self, item): # Run block start / end points (choice widgets) - if self.is_lcls: - self.config_panel = wx.Panel(self) - config_box = wx.StaticBox(self.config_panel, label='Configuration') - self.config_sizer = wx.StaticBoxSizer(config_box) - self.config_panel.SetSizer(self.config_sizer) - self.config_panel.Hide() - self.phil_panel = wx.Panel(self) phil_box = wx.StaticBox(self.phil_panel, label='Extra phil parameters') self.phil_sizer = wx.StaticBoxSizer(phil_box) @@ -2157,18 +2150,6 @@ def __getattr__(self, item): self.runblock_sizer = wx.BoxSizer(wx.VERTICAL) self.runblock_panel.SetSizer(self.runblock_sizer) - if self.is_lcls: - # Configuration text ctrl (user can put in anything they want) - self.config = gctr.PHILBox(self.config_panel, - btn_import=True, - btn_import_label='Import Config', - btn_export=False, - btn_default=True, - btn_default_label='Default Config', - ctr_size=(-1, 100), - ctr_value=str(block.config_str)) - self.config_sizer.Add(self.config, 1, flag=wx.EXPAND | wx.ALL, border=10) - # Extra phil self.phil = gctr.PHILBox(self.phil_panel, btn_import=True, @@ -2305,7 +2286,6 @@ def __getattr__(self, item): self.main_sizer.Add(self.phil_panel, flag=wx.EXPAND | wx.ALL, border=10) if self.is_lcls: - self.main_sizer.Add(self.config_panel, flag=wx.EXPAND | wx.ALL, border=10) self.main_sizer.Add(self.format_panel, flag=wx.EXPAND | wx.ALL, border=10) self.runblock_box_sizer.Add(self.runblock_panel) self.main_sizer.Add(self.runblock_box_sizer, flag=wx.EXPAND | wx.ALL, @@ -2326,7 +2306,6 @@ def __getattr__(self, item): id=self.untrusted_path.btn_big.GetId()) self.Bind(wx.EVT_BUTTON, self.onOK, id=wx.ID_OK) - self.fill_in_fields() self.configure_controls() self.Layout() @@ -2340,21 +2319,6 @@ def onAutoEnd(self, e): def onSpecifyEnd(self, e): self.runblocks_end.Enable() - def onImportConfig(self, e): - cfg_dlg = wx.FileDialog(self, - message="Load configuration file", - defaultDir=os.curdir, - defaultFile="*", - wildcard="*", - style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST, - ) - if cfg_dlg.ShowModal() == wx.ID_OK: - config_file = cfg_dlg.GetPaths()[0] - with open(config_file, 'r') as cfg: - cfg_contents = cfg.read() - self.config.ctr.SetValue(cfg_contents) - cfg_dlg.Destroy() - def onImportPhil(self, e): phil_dlg = wx.FileDialog(self, message="Load phil file", @@ -2385,10 +2349,6 @@ def onImportFormat(self, e): self.format.ctr.SetValue(phil_contents) phil_dlg.Destroy() - def onDefaultConfig(self, e): - # TODO: Generate default config parameters (re-do based on pickle / CBF) - pass - def onOK(self, e): try: first = int(self.runblocks_start.ctr.GetValue()) @@ -2440,7 +2400,6 @@ def is_none(string): rg_dict['wavelength_offset']=self.wavelength_offset.wavelength_offset.GetValue() rg_dict['binning']=self.bin_nrg_gain.binning.GetValue() rg_dict['detector_address']=self.address.ctr.GetValue() - rg_dict['config_str']=self.config.ctr.GetValue() rg_dict['extra_format_str']=self.format.ctr.GetValue() rg_dict['spectrum_eV_per_pixel']=self.spectrum_calibration.spectrum_eV_per_pixel.GetValue() rg_dict['spectrum_eV_offset']=self.spectrum_calibration.spectrum_eV_offset.GetValue() @@ -2495,7 +2454,6 @@ def fill_in_fields(self): self.phil.ctr.SetValue(str(last.extra_phil_str)) if self.is_lcls: self.address.ctr.SetValue(str(last.detector_address)) - self.config.ctr.SetValue(str(last.config_str)) self.format.ctr.SetValue(str(last.extra_format_str)) self.beam_xyz.DetZ.SetValue(str(last.detz_parameter)) self.beam_xyz.X.SetValue(str(last.beamx)) From 29ea12236655335fc469cf0c2f151b6d1f4ae539 Mon Sep 17 00:00:00 2001 From: Johannes Blaschke Date: Fri, 16 Jan 2026 12:18:14 -0800 Subject: [PATCH 06/12] First widgets for project management Co-authored-by: Aaron Brewster --- xfel/ui/components/xfel_gui_controls.py | 38 +++++++++++++++++++++++++ xfel/ui/components/xfel_gui_dialogs.py | 16 ++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/xfel/ui/components/xfel_gui_controls.py b/xfel/ui/components/xfel_gui_controls.py index c79ee654c0d..504d00cade2 100644 --- a/xfel/ui/components/xfel_gui_controls.py +++ b/xfel/ui/components/xfel_gui_controls.py @@ -326,6 +326,44 @@ def __init__(self, parent, output_box.AddGrowableCol(1, 1) self.SetSizer(output_box) +class ComboButtonCtrl(CtrlBase): + ''' Generic panel that will place a combo control, with a label and an + optional large button, and an optional bitmap button''' + + def __init__(self, parent, + label='', label_size=(100, -1), + label_style='normal', + text_style=wx.TE_LEFT, + ctrl_size=(200, -1), + big_button=False, + big_button_label='Browse...', + big_button_size=wx.DefaultSize, + ghost_button=True, + value='', **kwargs): + + CtrlBase.__init__(self, parent=parent, label_style=label_style, **kwargs) + + output_box = wx.FlexGridSizer(1, 4, 0, 10) + self.txt = wx.StaticText(self, label=label, size=label_size) + self.txt.SetFont(self.font) + output_box.Add(self.txt) + + self.ctr = wx.ComboBox(self, name=self.Name + "_ctr", style=text_style, size=ctrl_size) + self.ctr.SetValue(value) + output_box.Add(self.ctr, flag=wx.EXPAND) + + self.btn_big = Button(self, name=self.Name + "_btn_big", label=big_button_label, size=big_button_size) + if ghost_button: + output_box.Add(self.btn_big, flag=wx.RESERVE_SPACE_EVEN_IF_HIDDEN) + else: + output_box.Add(self.btn_big) + + if not big_button: + self.btn_big.Hide() + + output_box.AddGrowableCol(1, 1) + self.SetSizer(output_box) + class TwoButtonCtrl(CtrlBase): ''' Generic panel that will place a text control, with a label and an optional large button, and an optional bitmap button''' diff --git a/xfel/ui/components/xfel_gui_dialogs.py b/xfel/ui/components/xfel_gui_dialogs.py index 65acdda4f0b..78a511ffb01 100644 --- a/xfel/ui/components/xfel_gui_dialogs.py +++ b/xfel/ui/components/xfel_gui_dialogs.py @@ -113,6 +113,20 @@ def __init__(self, parent, params, self.main_sizer = wx.BoxSizer(wx.VERTICAL) + # Project management control + self.project = gctr.ComboButtonCtrl(self, + name='project', + label='Project', + label_style='bold', + label_size=(150, -1), + big_button=True, + big_button_size=(160, -1), + ) +# value=self.params.experiment_tag if self.params.experiment_tag is not None else "") + self.main_sizer.Add(self.project, + flag=wx.EXPAND | wx.ALL, + border=10) + # Experiment tag and DB Credentials button self.db_cred = gctr.TextButtonCtrl(self, name='db_cred', @@ -121,7 +135,7 @@ def __init__(self, parent, params, label_size=(150, -1), big_button=True, big_button_label='DB Credentials...', - big_button_size=(130, -1), + big_button_size=(160, -1), value=self.params.experiment_tag if self.params.experiment_tag is not None else "") self.main_sizer.Add(self.db_cred, flag=wx.EXPAND | wx.ALL, From dfbfdd92c2a3574e2d025eafc56e7c80595e7b1b Mon Sep 17 00:00:00 2001 From: Daniel Paley Date: Fri, 20 Mar 2026 16:48:34 -0400 Subject: [PATCH 07/12] gui: apply beamtime UI enhancements and settings file configuration --- xfel/ui/__init__.py | 9 +++++++-- xfel/ui/components/submission_tracker.py | 2 ++ xfel/ui/db/__init__.py | 8 ++++++-- xfel/ui/db/job.py | 2 +- xfel/ui/db/xfel_db.py | 1 + 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/xfel/ui/__init__.py b/xfel/ui/__init__.py index 60dad313742..9096a5d1b14 100644 --- a/xfel/ui/__init__.py +++ b/xfel/ui/__init__.py @@ -1,5 +1,5 @@ from __future__ import absolute_import, division, print_function -import os +import os, sys from iotbx.phil import parse from libtbx.utils import Sorry @@ -148,7 +148,12 @@ master_phil_scope = parse(master_phil_str + db_phil_str, process_includes=True) settings_dir = os.path.join(os.path.expanduser('~'), '.cctbx.xfel') -settings_file = os.path.join(settings_dir, 'settings.phil') +if len(sys.argv)==1: + settings_file = os.path.join(settings_dir, 'settings.phil') +else: + settings_file = os.path.expanduser(sys.argv[1]) + +print('Load settings from', settings_file) known_dials_dispatchers = { 'cctbx.xfel.xtc_process': 'xfel.command_line.xtc_process', diff --git a/xfel/ui/components/submission_tracker.py b/xfel/ui/components/submission_tracker.py index 1bf6075aba4..1b0e1cf818e 100644 --- a/xfel/ui/components/submission_tracker.py +++ b/xfel/ui/components/submission_tracker.py @@ -170,6 +170,8 @@ def __init__(self, queueing_system): "queue interrogator not implemented for %s queueing system"%self.queueing_system) def read_result(self, log_path): + if not log_path: + return "Error reading log file, no path to log" result = easy_run.fully_buffered(command=self.command % log_path) status = "\n".join(result.stdout_lines) error = "\n".join(result.stderr_lines) diff --git a/xfel/ui/db/__init__.py b/xfel/ui/db/__init__.py index dbace530397..ec481fbdbf1 100644 --- a/xfel/ui/db/__init__.py +++ b/xfel/ui/db/__init__.py @@ -10,6 +10,8 @@ def get_run_path(rootpath, trial, rungroup, run, task=None): import os + if trial is None or run is None: + return try: p = os.path.join(rootpath, "r%04d"%int(run.run), "%03d_rg%03d"%(trial.trial, rungroup.id)) except ValueError: @@ -65,7 +67,7 @@ def get_db_connection(params, block=True, autocommit=True): password = params.db.password retry_count = 0 - retry_max = 20 + retry_max = 10 sleep_time = 0.1 while retry_count < retry_max: try: @@ -75,7 +77,8 @@ def get_db_connection(params, block=True, autocommit=True): host=params.db.host, db=params.db.name, port=params.db.port, - autocommit=autocommit + autocommit=autocommit, + connect_timeout=1, ) return dbobj except Exception as e: @@ -89,6 +92,7 @@ def get_db_connection(params, block=True, autocommit=True): print("MySQL can't create a new thread. Retry", retry_count) else: raise e + print(str(e)) import time time.sleep(sleep_time) sleep_time *= 2 diff --git a/xfel/ui/db/job.py b/xfel/ui/db/job.py index 73f25d7fdb3..2c164c0d6c5 100644 --- a/xfel/ui/db/job.py +++ b/xfel/ui/db/job.py @@ -70,7 +70,7 @@ def __setattr__(self, name, value): def get_log_path(self): run_path = get_run_path(self.app.params.output_folder, self.trial, self.rungroup, self.run) - return os.path.join(run_path, "stdout", "out.log") + return os.path.join(run_path, "stdout", "log.out") if run_path else None def submit(self, previous_job = None): raise NotImplementedError("Override me!") diff --git a/xfel/ui/db/xfel_db.py b/xfel/ui/db/xfel_db.py index 246ffc3f7e3..68200ed943e 100644 --- a/xfel/ui/db/xfel_db.py +++ b/xfel/ui/db/xfel_db.py @@ -390,6 +390,7 @@ def execute_query(self, query, commit=True): raise e retry_count += 1 print("Couldn't connect to MYSQL, retry", retry_count) + print(str(e)) time.sleep(sleep_time) sleep_time *= 2 except Exception as e: From ce0cbbc085500308ca811cf87f997c318743a42d Mon Sep 17 00:00:00 2001 From: Daniel Paley Date: Wed, 25 Mar 2026 11:31:56 -0400 Subject: [PATCH 08/12] xfel.ui: use environment variable to share settings file across processes When the GUI is launched with a custom settings file, command-line tools run afterward now correctly use the same settings file. The GUI launcher sets CCTBX_XFEL_SETTINGS environment variable, which inherits to child processes. Both load_cached_settings() and save_cached_settings() check this environment variable first, falling back to the default behavior if not set. Co-Authored-By: Claude Sonnet 4.5 --- xfel/ui/__init__.py | 18 ++++++++++++------ xfel/ui/command_line/xfel_gui_launch.py | 7 +++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/xfel/ui/__init__.py b/xfel/ui/__init__.py index 9096a5d1b14..dd0062afd69 100644 --- a/xfel/ui/__init__.py +++ b/xfel/ui/__init__.py @@ -148,12 +148,6 @@ master_phil_scope = parse(master_phil_str + db_phil_str, process_includes=True) settings_dir = os.path.join(os.path.expanduser('~'), '.cctbx.xfel') -if len(sys.argv)==1: - settings_file = os.path.join(settings_dir, 'settings.phil') -else: - settings_file = os.path.expanduser(sys.argv[1]) - -print('Load settings from', settings_file) known_dials_dispatchers = { 'cctbx.xfel.xtc_process': 'xfel.command_line.xtc_process', @@ -172,6 +166,13 @@ def load_phil_scope_from_dispatcher(dispatcher): return phil_scope def load_cached_settings(scope=None, extract=True): + # Determine which settings file to use + settings_file = os.environ.get('CCTBX_XFEL_SETTINGS') + if settings_file is None: + settings_file = os.path.join(settings_dir, 'settings.phil') + + print('Load settings from', settings_file) + if scope is None: scope = master_phil_scope if os.path.exists(settings_file): @@ -191,6 +192,11 @@ def load_cached_settings(scope=None, extract=True): return scope def save_cached_settings(params): + # Save to the file specified by environment or default + settings_file = os.environ.get('CCTBX_XFEL_SETTINGS') + if settings_file is None: + settings_file = os.path.join(settings_dir, 'settings.phil') + if not os.path.exists(settings_dir): os.makedirs(settings_dir) diff --git a/xfel/ui/command_line/xfel_gui_launch.py b/xfel/ui/command_line/xfel_gui_launch.py index 36135ec498a..b8a90628b5e 100644 --- a/xfel/ui/command_line/xfel_gui_launch.py +++ b/xfel/ui/command_line/xfel_gui_launch.py @@ -72,6 +72,13 @@ def run(args): """) parse(master_phil_str, process_includes = True).show(attributes_level=2) return + + # If custom settings file provided, set environment variable + if len(args) > 0: + import os + custom_settings = os.path.expanduser(args[0]) + os.environ['CCTBX_XFEL_SETTINGS'] = custom_settings + app = MainApp(0) app.MainLoop() From cbe9ee508e771bfdfd4c7a74d36a51d704f74378 Mon Sep 17 00:00:00 2001 From: Daniel Paley Date: Wed, 25 Mar 2026 11:39:33 -0400 Subject: [PATCH 09/12] xfel.ui: simplify to use CCTBX_XFEL_SETTINGS environment variable only Users set CCTBX_XFEL_SETTINGS environment variable directly to specify a custom settings file. All tools (GUI and command-line) read this environment variable. Removed command-line argument parsing for settings files to keep the implementation simple with a single mode of operation. Co-Authored-By: Claude Sonnet 4.5 --- xfel/ui/command_line/xfel_gui_launch.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/xfel/ui/command_line/xfel_gui_launch.py b/xfel/ui/command_line/xfel_gui_launch.py index b8a90628b5e..a874e012df0 100644 --- a/xfel/ui/command_line/xfel_gui_launch.py +++ b/xfel/ui/command_line/xfel_gui_launch.py @@ -73,12 +73,6 @@ def run(args): parse(master_phil_str, process_includes = True).show(attributes_level=2) return - # If custom settings file provided, set environment variable - if len(args) > 0: - import os - custom_settings = os.path.expanduser(args[0]) - os.environ['CCTBX_XFEL_SETTINGS'] = custom_settings - app = MainApp(0) app.MainLoop() From 2cfc9236c81d04135f45a1cce5584bc596be99b6 Mon Sep 17 00:00:00 2001 From: Daniel Paley Date: Wed, 25 Mar 2026 11:48:16 -0400 Subject: [PATCH 10/12] revert whitespace change --- xfel/ui/command_line/xfel_gui_launch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/xfel/ui/command_line/xfel_gui_launch.py b/xfel/ui/command_line/xfel_gui_launch.py index a874e012df0..36135ec498a 100644 --- a/xfel/ui/command_line/xfel_gui_launch.py +++ b/xfel/ui/command_line/xfel_gui_launch.py @@ -72,7 +72,6 @@ def run(args): """) parse(master_phil_str, process_includes = True).show(attributes_level=2) return - app = MainApp(0) app.MainLoop() From 90a3dfe42ba5f35c8b308fdaa43d4c55e28f5410 Mon Sep 17 00:00:00 2001 From: Daniel Paley Date: Thu, 26 Mar 2026 16:34:13 -0400 Subject: [PATCH 11/12] Make upload_mtz lock file path configurable Add drive.lock_file phil parameter to allow users to specify a lock file location when the home directory is not writable (e.g., on compute nodes). Changes: - Add drive.lock_file phil parameter (default None) - Modify Locker class to accept configurable path - Pass lock_file through pydrive2_interface to Locker - Default behavior unchanged (uses ~/.upload_mtz.lock when None) This fixes issues on systems where home directories are not writable from compute nodes, allowing users to specify an alternative writable location. Co-Authored-By: Claude Sonnet 4.5 --- xfel/command_line/upload_mtz.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/xfel/command_line/upload_mtz.py b/xfel/command_line/upload_mtz.py index 9b8d557788f..561f04650cc 100644 --- a/xfel/command_line/upload_mtz.py +++ b/xfel/command_line/upload_mtz.py @@ -27,6 +27,11 @@ .help = Id string of the destination folder. If the folder url is \ https://drive.google.com/drive/u/0/folders/1NlJkfL6CMd1NZIl6Duy23i4G1RM9cNH- , \ then the id is 1NlJkfL6CMd1NZIl6Duy23i4G1RM9cNH- . + lock_file = None + .type = path + .help = Path to lock file for coordinating concurrent uploads. \ +If None, defaults to ~/.upload_mtz.lock. Set this to a writable location \ +if the home directory is not accessible (e.g., on compute nodes). } input { mtz_file = None @@ -78,9 +83,14 @@ def _get_log_fname(mtz_fname): class Locker: """ See https://stackoverflow.com/a/60214222 """ + def __init__(self, lock_file_path=None): + if lock_file_path is None: + lock_file_path = os.path.expanduser('~/.upload_mtz.lock') + self.lock_file_path = lock_file_path + def __enter__(self): try: - self.fp = open(os.path.expanduser('~/.upload_mtz.lock'), 'wb') + self.fp = open(self.lock_file_path, 'wb') except FileNotFoundError: self.fp = None if fcntl and self.fp is not None: @@ -98,7 +108,7 @@ class pydrive2_interface: destination folder. """ - def __init__(self, cred_file, folder_id): + def __init__(self, cred_file, folder_id, lock_file=None): try: from pydrive2.auth import ServiceAccountCredentials, GoogleAuth from pydrive2.drive import GoogleDrive @@ -111,11 +121,12 @@ def __init__(self, cred_file, folder_id): ) self.drive = GoogleDrive(gauth) self.top_folder_id = folder_id + self.lock_file = lock_file def _fetch_or_create_folder(self, fname, parent_id): - with Locker(): + with Locker(self.lock_file): query = { "q": "'{}' in parents and title='{}'".format(parent_id, fname), "supportsTeamDrives": "true", @@ -209,7 +220,8 @@ def run_with_preparsed(params): drive = pydrive2_interface( params.drive.credential_file, - params.drive.shared_folder_id + params.drive.shared_folder_id, + lock_file=params.drive.lock_file ) folders = [dataset_root, version_str] files = [log_path] From 11c541aa7d623f42414e536e4264aacad12deac7 Mon Sep 17 00:00:00 2001 From: Aaron Brewster Date: Tue, 31 Mar 2026 14:02:32 -0700 Subject: [PATCH 12/12] Hide the project management for now (WIP) --- xfel/ui/components/xfel_gui_dialogs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xfel/ui/components/xfel_gui_dialogs.py b/xfel/ui/components/xfel_gui_dialogs.py index 78a511ffb01..a0d3eba124c 100644 --- a/xfel/ui/components/xfel_gui_dialogs.py +++ b/xfel/ui/components/xfel_gui_dialogs.py @@ -126,6 +126,7 @@ def __init__(self, parent, params, self.main_sizer.Add(self.project, flag=wx.EXPAND | wx.ALL, border=10) + self.project.Hide() # WIP # Experiment tag and DB Credentials button self.db_cred = gctr.TextButtonCtrl(self,