From 40063a87c8b64416510bba59740037e4884d8b25 Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Tue, 15 Apr 2025 11:52:21 +0200 Subject: [PATCH 1/7] fix: take reference settings into consideration --- src/ess/reflectometry/gui.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/ess/reflectometry/gui.py b/src/ess/reflectometry/gui.py index 87c9b8e1..ed136e8e 100644 --- a/src/ess/reflectometry/gui.py +++ b/src/ess/reflectometry/gui.py @@ -654,13 +654,13 @@ def display_results(self): if len(df) == 0: self.log('There was nothing to display') return - try: - results = [ - next(v for (m, _), v in self.results.items() if m == key) - for key in (tuple(row) for _, row in df.iterrows()) - ] - except StopIteration: - # No results were found for the selected row + results = [ + self.results[key] + for _, row in df.iterrows() + if (key := self.get_row_key(row)) in self.results + ] + if len(results) < len(df): + # No results were found for some of the selected rows. # It hasn't been computed yet, so compute it and try again. self.run_workflow() self.display_results() @@ -692,6 +692,7 @@ def get_unique_names(df): results, norm='log', figsize=(12, 6), + vmin=1e-6, ) ] ) From 03b742a9f8f798a195e5c32673128aee59e869ad Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Tue, 15 Apr 2025 13:23:21 +0200 Subject: [PATCH 2/7] fix: take reference settings into consideration + add column renderers with rounding --- src/ess/reflectometry/gui.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/ess/reflectometry/gui.py b/src/ess/reflectometry/gui.py index ed136e8e..83c8d70e 100644 --- a/src/ess/reflectometry/gui.py +++ b/src/ess/reflectometry/gui.py @@ -8,7 +8,7 @@ import pandas as pd import plopp as pp import scipp as sc -from ipydatagrid import DataGrid, VegaExpr +from ipydatagrid import DataGrid, TextRenderer, VegaExpr from IPython.display import display from ipytree import Node, Tree @@ -239,6 +239,8 @@ def set_table_colors(self, table): if self.get_row_key(row) == row_key: expr += template.format(i=i, reduced_color="'lightgreen'") expr += "default_value" + for renderer in table.renderers.values(): + renderer.background_color = VegaExpr(expr) table.default_renderer.background_color = VegaExpr(expr) @staticmethod @@ -252,6 +254,18 @@ def set_result(self, metadata, result): self.set_table_colors(self.custom_reduction_table) self.set_table_colors(self.reference_table) + def get_renderers_for_reduction_table(self): + return {} + + def get_renderers_for_reference_table(self): + return {} + + def get_renderers_for_custom_reduction_table(self): + return {} + + def get_renderers_for_runs_table(self): + return {} + def log(self, message): out = widgets.Output() with out: @@ -304,6 +318,7 @@ def __init__(self): auto_fit_columns=True, column_visibility={"key": False}, selection_mode="cell", + renderers=self.get_renderers_for_runs_table(), ) self.reduction_table = DataGrid( pd.DataFrame([]), @@ -311,6 +326,7 @@ def __init__(self): auto_fit_columns=True, column_visibility={"key": False}, selection_mode="cell", + renderers=self.get_renderers_for_reduction_table(), ) self.reference_table = DataGrid( pd.DataFrame([]), @@ -318,6 +334,7 @@ def __init__(self): auto_fit_columns=True, column_visibility={"key": False}, selection_mode="cell", + renderers=self.get_renderers_for_reference_table(), ) self.custom_reduction_table = DataGrid( pd.DataFrame([]), @@ -325,6 +342,7 @@ def __init__(self): auto_fit_columns=True, column_visibility={"key": False}, selection_mode="cell", + renderers=self.get_renderers_for_custom_reduction_table(), ) self.runs_table.on_cell_change(self.sync) @@ -564,6 +582,21 @@ def read_meta_data(self, path): "Angle": f['entry1']['Amor']['master_parameters']['mu']['value'][0, 0], } + def get_renderers_for_reduction_table(self): + return { + 'Angle': TextRenderer(text_value=VegaExpr("format(cell.value, ',.3f')")) + } + + def get_renderers_for_custom_reduction_table(self): + return { + 'Angle': TextRenderer(text_value=VegaExpr("format(cell.value, ',.3f')")) + } + + def get_renderers_for_runs_table(self): + return { + 'Angle': TextRenderer(text_value=VegaExpr("format(cell.value, ',.3f')")) + } + @staticmethod def _merge_old_and_new_state(new, old, on, how='left'): old = old if on in old else old.assign(**{on: None}) From f93c6320c433af815d757c9383379e66a5b8df92 Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Tue, 15 Apr 2025 14:39:27 +0200 Subject: [PATCH 3/7] feat: add detector view --- src/ess/reflectometry/gui.py | 81 +++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/src/ess/reflectometry/gui.py b/src/ess/reflectometry/gui.py index 83c8d70e..42482afa 100644 --- a/src/ess/reflectometry/gui.py +++ b/src/ess/reflectometry/gui.py @@ -14,9 +14,12 @@ from ess import amor from ess.amor.types import ChopperPhase +from ess.reflectometry.figures import wavelength_z_figure from ess.reflectometry.types import ( + Filename, QBins, ReducedReference, + ReducibleData, ReferenceRun, ReflectivityOverQ, SampleRun, @@ -28,6 +31,72 @@ from ess.reflectometry.workflow import with_filenames +class DetectorView: + def __init__(self, runs_table: DataGrid, run_to_filepath: Callable[[str], str]): + self.runs_table = runs_table + self.run_to_filepath = run_to_filepath + self.runs_table.observe(self.run_workflow, names='selections') + self.plot_log = widgets.VBox([]) + self.working_label = widgets.Label( + "working...", layout=widgets.Layout(display='none') + ) + self.widget = widgets.HBox( + [ + widgets.VBox( + [ + widgets.Label("Runs Table"), + self.runs_table, + ], + layout={"width": "35%"}, + ), + widgets.VBox( + [ + widgets.Label("Wavelength z-index counts distribution"), + self.plot_log, + self.working_label, + ], + layout={"width": "60%"}, + ), + ] + ) + + def run_workflow(self, _): + self.working_label.layout.display = '' + selections = self.runs_table.selections + + if not selections: + return + + row_idx = selections[0]['r1'] + run = self.runs_table.data.iloc[row_idx]['Run'] + + workflow = amor.AmorWorkflow() + workflow[SampleSize[SampleRun]] = sc.scalar(10, unit='mm') + workflow[SampleSize[ReferenceRun]] = sc.scalar(10, unit='mm') + + workflow[ChopperPhase[ReferenceRun]] = sc.scalar(7.5, unit='deg') + workflow[ChopperPhase[SampleRun]] = sc.scalar(7.5, unit='deg') + + workflow[YIndexLimits] = (0, 64) + workflow[ZIndexLimits] = (0, 16 * 32) + workflow[WavelengthBins] = sc.geomspace( + 'wavelength', + 2, + 13.5, + 2001, + unit='angstrom', + ) + workflow[Filename[SampleRun]] = self.run_to_filepath(run) + da = workflow.compute(ReducibleData[SampleRun]) + da.bins.data[...] = sc.scalar(1.0, variance=1.0, unit=da.bins.unit) + da.bins.unit = 'counts' + da.masks.clear() + da.bins.masks.clear() + p = wavelength_z_figure(da, wavelength_bins=workflow.compute(WavelengthBins)) + self.plot_log.children = (p,) + self.working_label.layout.display = 'none' + + class NexusExplorer: def __init__(self, runs_table: DataGrid, run_to_filepath: Callable[[str], str]): self.runs_table = runs_table @@ -571,8 +640,16 @@ class AmorBatchReductionGUI(ReflectometryBatchReductionGUI): def __init__(self): super().__init__() self.nexus_explorer = NexusExplorer(self.runs_table, self.get_filepath_from_run) - self.tabs.children = (*self.tabs.children, self.nexus_explorer.widget) - self.tabs.set_title(len(self.tabs.children) - 1, "Nexus Explorer") + self.detector_display = DetectorView( + self.runs_table, self.get_filepath_from_run + ) + self.tabs.children = ( + *self.tabs.children, + self.nexus_explorer.widget, + self.detector_display.widget, + ) + self.tabs.set_title(len(self.tabs.children) - 2, "Nexus Explorer") + self.tabs.set_title(len(self.tabs.children) - 1, "Detector View") def read_meta_data(self, path): with h5py.File(path) as f: From 0ea4e3f8af3456267406911bb360153ee50b6dac Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Tue, 15 Apr 2025 14:53:54 +0200 Subject: [PATCH 4/7] feat: group by angle in reference table --- src/ess/reflectometry/gui.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/ess/reflectometry/gui.py b/src/ess/reflectometry/gui.py index 42482afa..2990f38c 100644 --- a/src/ess/reflectometry/gui.py +++ b/src/ess/reflectometry/gui.py @@ -731,13 +731,15 @@ def sync_reference_table(self, db): df = db["user_runs"] df = ( df[df["Sample"] == "sm5"][~df["Exclude"]] - .groupby(["Sample"], as_index=False) + .groupby(["Sample", "Angle"], as_index=False) .agg(Runs=("Run", tuple)) - .sort_values(by="Sample") + .sort_values(["Sample", "Angle"]) ) # We don't want changes to Sample # in the user_reference table to persist - user_reference = db['user_reference'].drop(columns=["Sample"], errors='ignore') + user_reference = db['user_reference'].drop( + columns=["Sample", "Angle"], errors='ignore' + ) df = self._merge_old_and_new_state(df, user_reference, on='Runs') self._setdefault(df, "Ymin", 17) self._setdefault(df, "Ymax", 47) @@ -745,8 +747,8 @@ def sync_reference_table(self, db): self._setdefault(df, "Zmax", 380) self._setdefault(df, "Lmin", 3.0) self._setdefault(df, "Lmax", 12.5) - df = self._ordercolumns(df, 'Sample', 'Runs') - return df.sort_values(by="Sample") + df = self._ordercolumns(df, 'Sample', 'Angle', 'Runs') + return df.sort_values(["Sample", "Angle"]) def sync_custom_reduction_table(self): df = self.custom_reduction_table.data.copy() From 63ab0a1b042eadd12c02273dbc87e48acf8345dc Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Tue, 22 Apr 2025 09:16:10 +0200 Subject: [PATCH 5/7] fix: safer plot retry + bug in 'in progress' message --- src/ess/reflectometry/gui.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/ess/reflectometry/gui.py b/src/ess/reflectometry/gui.py index 2990f38c..fb31ed27 100644 --- a/src/ess/reflectometry/gui.py +++ b/src/ess/reflectometry/gui.py @@ -38,7 +38,7 @@ def __init__(self, runs_table: DataGrid, run_to_filepath: Callable[[str], str]): self.runs_table.observe(self.run_workflow, names='selections') self.plot_log = widgets.VBox([]) self.working_label = widgets.Label( - "working...", layout=widgets.Layout(display='none') + "...working", layout=widgets.Layout(display='none') ) self.widget = widgets.HBox( [ @@ -51,9 +51,13 @@ def __init__(self, runs_table: DataGrid, run_to_filepath: Callable[[str], str]): ), widgets.VBox( [ - widgets.Label("Wavelength z-index counts distribution"), + widgets.HBox( + [ + widgets.Label("Wavelength z-index counts distribution"), + self.working_label, + ] + ), self.plot_log, - self.working_label, ], layout={"width": "60%"}, ), @@ -61,12 +65,11 @@ def __init__(self, runs_table: DataGrid, run_to_filepath: Callable[[str], str]): ) def run_workflow(self, _): - self.working_label.layout.display = '' selections = self.runs_table.selections - if not selections: return + self.working_label.layout.display = '' row_idx = selections[0]['r1'] run = self.runs_table.data.iloc[row_idx]['Run'] @@ -766,17 +769,17 @@ def display_results(self): if len(df) == 0: self.log('There was nothing to display') return - results = [ - self.results[key] - for _, row in df.iterrows() - if (key := self.get_row_key(row)) in self.results - ] - if len(results) < len(df): + for _ in range(2): + results = [ + self.results[key] + for _, row in df.iterrows() + if (key := self.get_row_key(row)) in self.results + ] + if len(results) == len(df): + break # No results were found for some of the selected rows. # It hasn't been computed yet, so compute it and try again. self.run_workflow() - self.display_results() - return def get_unique_names(df): # Create labels with Sample name and runs From eb1406fef68299a4129b632641aabcd2a8bcaa99 Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Wed, 23 Apr 2025 12:11:57 +0200 Subject: [PATCH 6/7] feat: only rerun detector view when the detector view is in tab --- src/ess/reflectometry/gui.py | 174 ++++++++++++++++++++--------------- 1 file changed, 100 insertions(+), 74 deletions(-) diff --git a/src/ess/reflectometry/gui.py b/src/ess/reflectometry/gui.py index fb31ed27..f3cf31e2 100644 --- a/src/ess/reflectometry/gui.py +++ b/src/ess/reflectometry/gui.py @@ -11,6 +11,7 @@ from ipydatagrid import DataGrid, TextRenderer, VegaExpr from IPython.display import display from ipytree import Node, Tree +from traitlets import Bool from ess import amor from ess.amor.types import ChopperPhase @@ -31,42 +32,56 @@ from ess.reflectometry.workflow import with_filenames -class DetectorView: - def __init__(self, runs_table: DataGrid, run_to_filepath: Callable[[str], str]): +class DetectorView(widgets.HBox): + is_active_tab = Bool(False).tag(sync=True) + + def __init__( + self, runs_table: DataGrid, run_to_filepath: Callable[[str], str], **kwargs + ): + super().__init__([], **kwargs) self.runs_table = runs_table self.run_to_filepath = run_to_filepath - self.runs_table.observe(self.run_workflow, names='selections') self.plot_log = widgets.VBox([]) self.working_label = widgets.Label( "...working", layout=widgets.Layout(display='none') ) - self.widget = widgets.HBox( - [ - widgets.VBox( - [ - widgets.Label("Runs Table"), - self.runs_table, - ], - layout={"width": "35%"}, - ), - widgets.VBox( - [ - widgets.HBox( - [ - widgets.Label("Wavelength z-index counts distribution"), - self.working_label, - ] - ), - self.plot_log, - ], - layout={"width": "60%"}, - ), - ] - ) + self.children = ( + widgets.VBox( + [ + widgets.Label("Runs Table"), + self.runs_table, + ], + layout={"width": "35%"}, + ), + widgets.VBox( + [ + widgets.HBox( + [ + widgets.Label("Wavelength z-index counts distribution"), + self.working_label, + ] + ), + self.plot_log, + ], + layout={"width": "60%"}, + ), + ) + + def run_when_selected_row_changes(change): + if not change['old'] or change['old'][0]['r1'] != change['new'][0]['r1']: + self.run_workflow() + + self.runs_table.observe(run_when_selected_row_changes, names='selections') + + def run_when_active_tab(change): + if change['new']: + self.run_workflow() + + self.observe(run_when_active_tab, 'is_active_tab') - def run_workflow(self, _): + def run_workflow(self): selections = self.runs_table.selections - if not selections: + if not self.is_active_tab or not selections: return self.working_label.layout.display = '' @@ -100,8 +115,12 @@ def run_workflow(self, _): self.working_label.layout.display = 'none' -class NexusExplorer: - def __init__(self, runs_table: DataGrid, run_to_filepath: Callable[[str], str]): +class NexusExplorer(widgets.VBox): + def __init__( + self, runs_table: DataGrid, run_to_filepath: Callable[[str], str], **kwargs + ): + kwargs.setdefault('layout', {"width": "100%"}) + super().__init__(**kwargs) self.runs_table = runs_table self.run_to_filepath = run_to_filepath @@ -128,47 +147,44 @@ def __init__(self, runs_table: DataGrid, run_to_filepath: Callable[[str], str]): self.nexus_tree.observe(self.on_tree_select, names='selected_nodes') # Create the Nexus Explorer tab content - self.widget = widgets.VBox( - [ - widgets.Label("Nexus Explorer"), - widgets.HBox( - [ - widgets.VBox( - [ - widgets.Label("Runs Table"), - self.runs_table, - ], - layout={"width": "30%"}, - ), - widgets.VBox( - [ - widgets.Label("File Structure"), - widgets.VBox( - [self.nexus_tree], - layout=widgets.Layout( - width='100%', - height='600px', - min_height='100px', # Min resize height - max_height='1000px', # Max resize height - overflow_y='scroll', - border='1px solid lightgray', - resize='vertical', # Add resize handle - ), + self.children = ( + widgets.Label("Nexus Explorer"), + widgets.HBox( + [ + widgets.VBox( + [ + widgets.Label("Runs Table"), + self.runs_table, + ], + layout={"width": "30%"}, + ), + widgets.VBox( + [ + widgets.Label("File Structure"), + widgets.VBox( + [self.nexus_tree], + layout=widgets.Layout( + width='100%', + height='600px', + min_height='100px', # Min resize height + max_height='1000px', # Max resize height + overflow_y='scroll', + border='1px solid lightgray', + resize='vertical', # Add resize handle ), - ], - layout={"width": "35%"}, - ), - widgets.VBox( - [ - widgets.Label("Content"), - self.nexus_content, - ], - layout={"width": "35%"}, - ), - ] - ), - ], - layout={"width": "100%"}, + ), + ], + layout={"width": "35%"}, + ), + widgets.VBox( + [ + widgets.Label("Content"), + self.nexus_content, + ], + layout={"width": "35%"}, + ), + ] + ), ) def create_hdf5_tree(self, filepath): @@ -607,6 +623,16 @@ def delete_row(_): self.tabs.set_title(1, "Settings") self.tabs.set_title(2, "Log") + def on_tab_change(change): + old = self.tabs.children[change['old']] + new = self.tabs.children[change['new']] + if hasattr(old, 'is_active_tab'): + old.is_active_tab = False + if hasattr(new, 'is_active_tab'): + new.is_active_tab = True + + self.tabs.observe(on_tab_change, names='selected_index') + self.main = widgets.VBox( [ self.proposal_number_box, @@ -648,11 +674,11 @@ def __init__(self): ) self.tabs.children = ( *self.tabs.children, - self.nexus_explorer.widget, - self.detector_display.widget, + self.nexus_explorer, + self.detector_display, ) - self.tabs.set_title(len(self.tabs.children) - 2, "Nexus Explorer") - self.tabs.set_title(len(self.tabs.children) - 1, "Detector View") + # Empty titles are automatically added for the new children + self.tabs.titles = [*self.tabs.titles[:-2], "Nexus Explorer", "Detector View"] def read_meta_data(self, path): with h5py.File(path) as f: From c923cb219e39cb5e14f7ec8700474e6ac8641102 Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Wed, 23 Apr 2025 14:37:45 +0200 Subject: [PATCH 7/7] fix: set titles --- src/ess/reflectometry/gui.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ess/reflectometry/gui.py b/src/ess/reflectometry/gui.py index f3cf31e2..814c2e54 100644 --- a/src/ess/reflectometry/gui.py +++ b/src/ess/reflectometry/gui.py @@ -619,9 +619,7 @@ def delete_row(_): tab_settings, tab_log, ] - self.tabs.set_title(0, "Reduce") - self.tabs.set_title(1, "Settings") - self.tabs.set_title(2, "Log") + self.tabs.titles = ["Reduce", "Settings", "Log"] def on_tab_change(change): old = self.tabs.children[change['old']]