diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..f7cb7e68 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 340e6499..3917992d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,7 +32,6 @@ docs/generated/ # Data files *.data *.dat -*.csv *.xye *.h5 *.hdf5 @@ -45,3 +44,5 @@ docs/generated/ *.zip *.sqw *.nxspe +/src/ess/loki/examplefiles +/src/ess/loki/batch-gui-legacy diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 00000000..1ee6b1f1 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/ess/.DS_Store b/src/ess/.DS_Store new file mode 100644 index 00000000..23a6821a Binary files /dev/null and b/src/ess/.DS_Store differ diff --git a/src/ess/loki/.DS_Store b/src/ess/loki/.DS_Store new file mode 100644 index 00000000..3a87832f Binary files /dev/null and b/src/ess/loki/.DS_Store differ diff --git a/src/ess/loki/batch-gui-legacy/batchwidget-tabs.py b/src/ess/loki/batch-gui-legacy/batchwidget-tabs.py new file mode 100644 index 00000000..fbe1dc46 --- /dev/null +++ b/src/ess/loki/batch-gui-legacy/batchwidget-tabs.py @@ -0,0 +1,338 @@ +import glob +import os + +import ipywidgets as widgets +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import scipp as sc +from ipydatagrid import DataGrid +from ipyfilechooser import FileChooser +from IPython.display import display + +from ess import loki, sans +from ess.sans.types import * + + +def reduce_loki_batch_preliminary( + sample_run_file: str, + transmission_run_file: str, + background_run_file: str, + empty_beam_file: str, + direct_beam_file: str, + mask_files: list = None, + correct_for_gravity: bool = True, + uncertainty_mode=UncertaintyBroadcastMode.upper_bound, + return_events: bool = False, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + wavelength_n: int = 201, + q_start: float = 0.01, + q_stop: float = 0.3, + q_n: int = 101, +): + if mask_files is None: + mask_files = [] + # Define wavelength and Q bins. + wavelength_bins = sc.linspace( + "wavelength", wavelength_min, wavelength_max, wavelength_n, unit="angstrom" + ) + q_bins = sc.linspace("Q", q_start, q_stop, q_n, unit="1/angstrom") + # Initialize the workflow. + workflow = loki.LokiAtLarmorWorkflow() + if mask_files: + workflow = sans.with_pixel_mask_filenames(workflow, masks=mask_files) + workflow[NeXusDetectorName] = "larmor_detector" + workflow[WavelengthBins] = wavelength_bins + workflow[QBins] = q_bins + workflow[CorrectForGravity] = correct_for_gravity + workflow[UncertaintyBroadcastMode] = uncertainty_mode + workflow[ReturnEvents] = return_events + workflow[Filename[BackgroundRun]] = background_run_file + workflow[Filename[TransmissionRun[BackgroundRun]]] = transmission_run_file + workflow[Filename[EmptyBeamRun]] = empty_beam_file + workflow[DirectBeamFilename] = direct_beam_file + workflow[Filename[SampleRun]] = sample_run_file + workflow[Filename[TransmissionRun[SampleRun]]] = transmission_run_file + center = sans.beam_center_from_center_of_mass(workflow) + workflow[BeamCenter] = center + tf = workflow.compute(TransmissionFraction[SampleRun]) + da = workflow.compute(BackgroundSubtractedIofQ) + return {"transmission": tf, "IofQ": da} + + +def find_file(work_dir, run_number, extension=".nxs"): + pattern = os.path.join(work_dir, f"*{run_number}*{extension}") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find file matching pattern {pattern}") + + +def find_direct_beam(work_dir): + pattern = os.path.join(work_dir, "*direct-beam*.h5") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError( + f"Could not find direct-beam file matching pattern {pattern}" + ) + + +def find_mask_file(work_dir): + pattern = os.path.join(work_dir, "*mask*.xml") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find mask file matching pattern {pattern}") + + +def save_xye_pandas(data_array, filename): + q_vals = data_array.coords["Q"].values + i_vals = data_array.values + if len(q_vals) != len(i_vals): + q_vals = 0.5 * (q_vals[:-1] + q_vals[1:]) + if data_array.variances is not None: + err_vals = np.sqrt(data_array.variances) + if len(err_vals) != len(i_vals): + err_vals = 0.5 * (err_vals[:-1] + err_vals[1:]) + else: + err_vals = np.zeros_like(i_vals) + df = pd.DataFrame({"Q": q_vals, "I(Q)": i_vals, "Error": err_vals}) + df.to_csv(filename, sep=" ", index=False, header=True) + + +class SansBatchReductionWidget: + def __init__(self): + self.csv_chooser = FileChooser(select_dir=False) + self.csv_chooser.title = "Select CSV File" + self.csv_chooser.filter_pattern = "*.csv" + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.ebeam_sans_widget = widgets.Text( + value="", + placeholder="Enter Ebeam SANS run number", + description="Ebeam SANS:", + ) + self.ebeam_trans_widget = widgets.Text( + value="", + placeholder="Enter Ebeam TRANS run number", + description="Ebeam TRANS:", + ) + self.load_csv_button = widgets.Button(description="Load CSV") + self.load_csv_button.on_click(self.load_csv) + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + self.clear_log_button = widgets.Button(description="Clear Log") + self.clear_log_button.on_click(self.clear_log) + self.clear_plots_button = widgets.Button(description="Clear Plots") + self.clear_plots_button.on_click(self.clear_plots) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.main = widgets.VBox( + [ + widgets.HBox( + [self.csv_chooser, self.input_dir_chooser, self.output_dir_chooser] + ), + widgets.HBox([self.ebeam_sans_widget, self.ebeam_trans_widget]), + self.load_csv_button, + self.table, + widgets.HBox( + [self.reduce_button, self.clear_log_button, self.clear_plots_button] + ), + self.log_output, + self.plot_output, + ] + ) + + def clear_log(self, _): + self.log_output.clear_output() + + def clear_plots(self, _): + self.plot_output.clear_output() + + def load_csv(self, _): + csv_path = self.csv_chooser.selected + if not csv_path or not os.path.exists(csv_path): + with self.log_output: + print("CSV file not selected or does not exist.") + return + df = pd.read_csv(csv_path) + self.table.data = df + with self.log_output: + print(f"Loaded reduction table with {len(df)} rows from {csv_path}.") + + def run_reduction(self, _): + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct-beam file:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + return + try: + background_run_file = find_file( + input_dir, self.ebeam_sans_widget.value, extension=".nxs" + ) + empty_beam_file = find_file( + input_dir, self.ebeam_trans_widget.value, extension=".nxs" + ) + with self.log_output: + print("Using empty-beam files:") + print(" Background (Ebeam SANS):", background_run_file) + print(" Empty-beam (Ebeam TRANS):", empty_beam_file) + except Exception as e: + with self.log_output: + print("Error finding empty-beam files:", e) + return + df = self.table.data + for idx, row in df.iterrows(): + sample = row["SAMPLE"] + try: + sample_run_file = find_file( + input_dir, str(row["SANS"]), extension=".nxs" + ) + transmission_run_file = find_file( + input_dir, str(row["TRANS"]), extension=".nxs" + ) + except Exception as e: + with self.log_output: + print(f"Skipping sample {sample}: {e}") + continue + mask_candidate = str(row.get("mask", "")).strip() + mask_file = None + if mask_candidate: + mask_file_candidate = os.path.join(input_dir, f"{mask_candidate}.xml") + if os.path.exists(mask_file_candidate): + mask_file = mask_file_candidate + if mask_file is None: + try: + mask_file = find_mask_file(input_dir) + with self.log_output: + print( + f"Using global mask file: {mask_file} for sample {sample}" + ) + except Exception as e: + with self.log_output: + print(f"Mask file not found for sample {sample}: {e}") + continue + with self.log_output: + print(f"Reducing sample {sample}...") + try: + res = reduce_loki_batch_preliminary( + sample_run_file=sample_run_file, + transmission_run_file=transmission_run_file, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + mask_files=[mask_file], + ) + except Exception as e: + with self.log_output: + print(f"Reduction failed for sample {sample}: {e}") + continue + out_xye = os.path.join( + output_dir, os.path.basename(sample_run_file).replace(".nxs", ".xye") + ) + try: + save_xye_pandas(res["IofQ"], out_xye) + with self.log_output: + print(f"Saved reduced data to {out_xye}") + except Exception as e: + with self.log_output: + print(f"Failed to save reduced data for {sample}: {e}") + # Generate and display Transmission plot. + wavelength_bins = sc.linspace("wavelength", 1.0, 13.0, 201, unit="angstrom") + x_wl = 0.5 * (wavelength_bins.values[:-1] + wavelength_bins.values[1:]) + fig_trans, ax_trans = plt.subplots() + ax_trans.plot(x_wl, res["transmission"].values, marker='o', linestyle='-') + ax_trans.set_title( + f"Transmission: {sample} {os.path.basename(sample_run_file)}" + ) + ax_trans.set_xlabel("Wavelength (Å)") + ax_trans.set_ylabel("Transmission") + plt.tight_layout() + with self.plot_output: + display(fig_trans) + trans_png = os.path.join( + output_dir, + os.path.basename(sample_run_file).replace(".nxs", "_transmission.png"), + ) + fig_trans.savefig(trans_png, dpi=300) + plt.close(fig_trans) + # Generate and display I(Q) plot. + q_bins = sc.linspace("Q", 0.01, 0.3, 101, unit="1/angstrom") + x_q = 0.5 * (q_bins.values[:-1] + q_bins.values[1:]) + fig_iq, ax_iq = plt.subplots() + if res["IofQ"].variances is not None: + yerr = np.sqrt(res["IofQ"].variances) + ax_iq.errorbar( + x_q, res["IofQ"].values, yerr=yerr, marker='o', linestyle='-' + ) + else: + ax_iq.plot(x_q, res["IofQ"].values, marker='o', linestyle='-') + ax_iq.set_title(f"I(Q): {os.path.basename(sample_run_file)} ({sample})") + ax_iq.set_xlabel("Q (Å$^{-1}$)") + ax_iq.set_ylabel("I(Q)") + ax_iq.set_xscale("log") + ax_iq.set_yscale("log") + plt.tight_layout() + with self.plot_output: + display(fig_iq) + iq_png = os.path.join( + output_dir, + os.path.basename(sample_run_file).replace(".nxs", "_IofQ.png"), + ) + fig_iq.savefig(iq_png, dpi=300) + plt.close(fig_iq) + with self.log_output: + print(f"Reduced sample {sample} and saved outputs.") + + @property + def widget(self): + return self.main + + +def save_xye_pandas(data_array, filename): + q_vals = data_array.coords["Q"].values + i_vals = data_array.values + if len(q_vals) != len(i_vals): + q_vals = 0.5 * (q_vals[:-1] + q_vals[1:]) + if data_array.variances is not None: + err_vals = np.sqrt(data_array.variances) + if len(err_vals) != len(i_vals): + err_vals = 0.5 * (err_vals[:-1] + err_vals[1:]) + else: + err_vals = np.zeros_like(i_vals) + df = pd.DataFrame({"Q": q_vals, "I(Q)": i_vals, "Error": err_vals}) + df.to_csv(filename, sep=" ", index=False, header=True) + + +# Build the main tabbed widget. +reduction_widget = SansBatchReductionWidget().widget +direct_beam_widget = widgets.HTML( + "

Direct Beam

Direct beam tab content goes here.

" +) +tabs = widgets.Tab(children=[reduction_widget, direct_beam_widget]) +tabs.set_title(0, "Reduction") +tabs.set_title(1, "Direct Beam") + +# Display the tab widget. +display(tabs) diff --git a/src/ess/loki/batch-gui-legacy/batchwidget.py b/src/ess/loki/batch-gui-legacy/batchwidget.py new file mode 100644 index 00000000..0a85e961 --- /dev/null +++ b/src/ess/loki/batch-gui-legacy/batchwidget.py @@ -0,0 +1,325 @@ +import glob +import os + +import ipywidgets as widgets +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import scipp as sc +from ipydatagrid import DataGrid +from ipyfilechooser import FileChooser +from IPython.display import display + +from ess import loki, sans +from ess.sans.types import * + + +def reduce_loki_batch_preliminary( + sample_run_file: str, + transmission_run_file: str, + background_run_file: str, + empty_beam_file: str, + direct_beam_file: str, + mask_files: list = None, + correct_for_gravity: bool = True, + uncertainty_mode=UncertaintyBroadcastMode.upper_bound, + return_events: bool = False, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + wavelength_n: int = 201, + q_start: float = 0.01, + q_stop: float = 0.3, + q_n: int = 101, +): + if mask_files is None: + mask_files = [] + # Define wavelength and Q bins. + wavelength_bins = sc.linspace( + "wavelength", wavelength_min, wavelength_max, wavelength_n, unit="angstrom" + ) + q_bins = sc.linspace("Q", q_start, q_stop, q_n, unit="1/angstrom") + # Initialize the workflow. + workflow = loki.LokiAtLarmorWorkflow() + if mask_files: + workflow = sans.with_pixel_mask_filenames(workflow, masks=mask_files) + workflow[NeXusDetectorName] = "larmor_detector" + workflow[WavelengthBins] = wavelength_bins + workflow[QBins] = q_bins + workflow[CorrectForGravity] = correct_for_gravity + workflow[UncertaintyBroadcastMode] = uncertainty_mode + workflow[ReturnEvents] = return_events + workflow[Filename[BackgroundRun]] = background_run_file + workflow[Filename[TransmissionRun[BackgroundRun]]] = transmission_run_file + workflow[Filename[EmptyBeamRun]] = empty_beam_file + workflow[DirectBeamFilename] = direct_beam_file + workflow[Filename[SampleRun]] = sample_run_file + workflow[Filename[TransmissionRun[SampleRun]]] = transmission_run_file + center = sans.beam_center_from_center_of_mass(workflow) + workflow[BeamCenter] = center + tf = workflow.compute(TransmissionFraction[SampleRun]) + da = workflow.compute(BackgroundSubtractedIofQ) + return {"transmission": tf, "IofQ": da} + + +def find_file(work_dir, run_number, extension=".nxs"): + pattern = os.path.join(work_dir, f"*{run_number}*{extension}") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find file matching pattern {pattern}") + + +def find_direct_beam(work_dir): + pattern = os.path.join(work_dir, "*direct-beam*.h5") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError( + f"Could not find direct beam file matching pattern {pattern}" + ) + + +def find_mask_file(work_dir): + pattern = os.path.join(work_dir, "*mask*.xml") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find mask file matching pattern {pattern}") + + +def save_xye_pandas(data_array, filename): + q_vals = data_array.coords["Q"].values + i_vals = data_array.values + if len(q_vals) != len(i_vals): + q_vals = 0.5 * (q_vals[:-1] + q_vals[1:]) + if data_array.variances is not None: + err_vals = np.sqrt(data_array.variances) + if len(err_vals) != len(i_vals): + err_vals = 0.5 * (err_vals[:-1] + err_vals[1:]) + else: + err_vals = np.zeros_like(i_vals) + df = pd.DataFrame({"Q": q_vals, "I(Q)": i_vals, "Error": err_vals}) + df.to_csv(filename, sep=" ", index=False, header=True) + + +class SansBatchReductionWidget: + def __init__(self): + self.csv_chooser = FileChooser(select_dir=False) + self.csv_chooser.title = "Select CSV File" + self.csv_chooser.filter_pattern = "*.csv" + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.ebeam_sans_widget = widgets.Text( + value="", + placeholder="Enter Ebeam SANS run number", + description="Ebeam SANS:", + ) + self.ebeam_trans_widget = widgets.Text( + value="", + placeholder="Enter Ebeam TRANS run number", + description="Ebeam TRANS:", + ) + self.load_csv_button = widgets.Button(description="Load CSV") + self.load_csv_button.on_click(self.load_csv) + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + self.clear_log_button = widgets.Button(description="Clear Log") + self.clear_log_button.on_click(self.clear_log) + self.clear_plots_button = widgets.Button(description="Clear Plots") + self.clear_plots_button.on_click(self.clear_plots) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.main = widgets.VBox( + [ + widgets.HBox( + [self.csv_chooser, self.input_dir_chooser, self.output_dir_chooser] + ), + widgets.HBox([self.ebeam_sans_widget, self.ebeam_trans_widget]), + self.load_csv_button, + self.table, + widgets.HBox( + [self.reduce_button, self.clear_log_button, self.clear_plots_button] + ), + self.log_output, + self.plot_output, + ] + ) + + def clear_log(self, _): + self.log_output.clear_output() + + def clear_plots(self, _): + self.plot_output.clear_output() + + def load_csv(self, _): + csv_path = self.csv_chooser.selected + if not csv_path or not os.path.exists(csv_path): + with self.log_output: + print("CSV file not selected or does not exist.") + return + df = pd.read_csv(csv_path) + self.table.data = df + with self.log_output: + print(f"Loaded reduction table with {len(df)} rows from {csv_path}.") + + def run_reduction(self, _): + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct beam file:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct beam file not found:", e) + return + try: + background_run_file = find_file( + input_dir, self.ebeam_sans_widget.value, extension=".nxs" + ) + empty_beam_file = find_file( + input_dir, self.ebeam_trans_widget.value, extension=".nxs" + ) + with self.log_output: + print("Using empty beam files:") + print(" Background (Ebeam SANS):", background_run_file) + print(" Empty beam (Ebeam TRANS):", empty_beam_file) + except Exception as e: + with self.log_output: + print("Error finding empty beam files:", e) + return + df = self.table.data + for idx, row in df.iterrows(): + sample = row["SAMPLE"] + try: + sample_run_file = find_file( + input_dir, str(row["SANS"]), extension=".nxs" + ) + transmission_run_file = find_file( + input_dir, str(row["TRANS"]), extension=".nxs" + ) + except Exception as e: + with self.log_output: + print(f"Skipping sample {sample}: {e}") + continue + mask_candidate = str(row.get("mask", "")).strip() + mask_file = None + if mask_candidate: + mask_file_candidate = os.path.join(input_dir, f"{mask_candidate}.xml") + if os.path.exists(mask_file_candidate): + mask_file = mask_file_candidate + if mask_file is None: + try: + mask_file = find_mask_file(input_dir) + with self.log_output: + print( + f"Using global mask file: {mask_file} for sample {sample}" + ) + except Exception as e: + with self.log_output: + print(f"Mask file not found for sample {sample}: {e}") + continue + with self.log_output: + print(f"Reducing sample {sample}...") + try: + res = reduce_loki_batch_preliminary( + sample_run_file=sample_run_file, + transmission_run_file=transmission_run_file, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + mask_files=[mask_file], + ) + except Exception as e: + with self.log_output: + print(f"Reduction failed for sample {sample}: {e}") + continue + out_xye = os.path.join( + output_dir, os.path.basename(sample_run_file).replace(".nxs", ".xye") + ) + try: + save_xye_pandas(res["IofQ"], out_xye) + with self.log_output: + print(f"Saved reduced data to {out_xye}") + except Exception as e: + with self.log_output: + print(f"Failed to save reduced data for {sample}: {e}") + # Generate and display Transmission plot. + wavelength_bins = sc.linspace("wavelength", 1.0, 13.0, 201, unit="angstrom") + x_wl = 0.5 * (wavelength_bins.values[:-1] + wavelength_bins.values[1:]) + fig_trans, ax_trans = plt.subplots() + ax_trans.plot(x_wl, res["transmission"].values, marker='o', linestyle='-') + ax_trans.set_title( + f"Transmission: {sample} {os.path.basename(sample_run_file)}" + ) + ax_trans.set_xlabel("Wavelength (Å)") + ax_trans.set_ylabel("Transmission") + plt.tight_layout() + # with self.plot_output: + # display(fig_trans) + trans_png = os.path.join( + output_dir, + os.path.basename(sample_run_file).replace(".nxs", "_transmission.png"), + ) + fig_trans.savefig(trans_png, dpi=300) + plt.close(fig_trans) + # Generate and display I(Q) plot. + q_bins = sc.linspace("Q", 0.01, 0.3, 101, unit="1/angstrom") + x_q = 0.5 * (q_bins.values[:-1] + q_bins.values[1:]) + fig_iq, ax_iq = plt.subplots() + if res["IofQ"].variances is not None: + yerr = np.sqrt(res["IofQ"].variances) + ax_iq.errorbar( + x_q, res["IofQ"].values, yerr=yerr, marker='o', linestyle='-' + ) + else: + ax_iq.plot(x_q, res["IofQ"].values, marker='o', linestyle='-') + ax_iq.set_title(f"{os.path.basename(sample_run_file)} ({sample})") + ax_iq.set_xlabel("Q (Å$^{-1}$)") + ax_iq.set_ylabel("I(Q)") + ax_iq.set_xscale("log") + ax_iq.set_yscale("log") + plt.tight_layout() + with self.plot_output: + display(fig_iq) + iq_png = os.path.join( + output_dir, + os.path.basename(sample_run_file).replace(".nxs", "_IofQ.png"), + ) + fig_iq.savefig(iq_png, dpi=300) + plt.close(fig_iq) + with self.log_output: + print(f"Reduced sample {sample} and saved outputs.") + + @property + def widget(self): + return self.main + + +def save_xye_pandas(data_array, filename): + q_vals = data_array.coords["Q"].values + i_vals = data_array.values + if len(q_vals) != len(i_vals): + q_vals = 0.5 * (q_vals[:-1] + q_vals[1:]) + if data_array.variances is not None: + err_vals = np.sqrt(data_array.variances) + if len(err_vals) != len(i_vals): + err_vals = 0.5 * (err_vals[:-1] + err_vals[1:]) + else: + err_vals = np.zeros_like(i_vals) + df = pd.DataFrame({"Q": q_vals, "I(Q)": i_vals, "Error": err_vals}) + df.to_csv(filename, sep=" ", index=False, header=True) diff --git a/src/ess/loki/batch-gui-legacy/batchwidgets.ipynb b/src/ess/loki/batch-gui-legacy/batchwidgets.ipynb new file mode 100644 index 00000000..14da5e6b --- /dev/null +++ b/src/ess/loki/batch-gui-legacy/batchwidgets.ipynb @@ -0,0 +1,36 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import batchwidget\n", + "gui = batchwidget.SansBatchReductionWidget()\n", + "display(gui.widget)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/ess/loki/batch-gui-legacy/tabwidget-050325.py b/src/ess/loki/batch-gui-legacy/tabwidget-050325.py new file mode 100644 index 00000000..1d52f3f0 --- /dev/null +++ b/src/ess/loki/batch-gui-legacy/tabwidget-050325.py @@ -0,0 +1,826 @@ +import os +import glob +import re +import h5py +import pandas as pd +import scipp as sc +import matplotlib.pyplot as plt +import numpy as np +import ipywidgets as widgets +from ipydatagrid import DataGrid +from IPython.display import display +from ipyfilechooser import FileChooser +from ess import sans +from ess import loki +from ess.sans.types import * +from scipp.scipy.interpolate import interp1d +import plopp as pp # used for plotting in direct beam section +import threading +import time + +# ---------------------------- +# Common Utility Functions +# ---------------------------- +def find_file(work_dir, run_number, extension=".nxs"): + pattern = os.path.join(work_dir, f"*{run_number}*{extension}") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find file matching pattern {pattern}") + +def find_direct_beam(work_dir): + pattern = os.path.join(work_dir, "*direct-beam*.h5") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find direct-beam file matching pattern {pattern}") + +def find_mask_file(work_dir): + pattern = os.path.join(work_dir, "*mask*.xml") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find mask file matching pattern {pattern}") + +def save_xye_pandas(data_array, filename): + q_vals = data_array.coords["Q"].values + i_vals = data_array.values + if len(q_vals) != len(i_vals): + q_vals = 0.5 * (q_vals[:-1] + q_vals[1:]) + if data_array.variances is not None: + err_vals = np.sqrt(data_array.variances) + if len(err_vals) != len(i_vals): + err_vals = 0.5 * (err_vals[:-1] + err_vals[1:]) + else: + err_vals = np.zeros_like(i_vals) + df = pd.DataFrame({"Q": q_vals, "I(Q)": i_vals, "Error": err_vals}) + df.to_csv(filename, sep=" ", index=False, header=True) + +def extract_run_number(filename): + m = re.search(r'(\d{4,})', filename) + if m: + return m.group(1) + return "" + +def parse_nx_details(filepath): + details = {} + with h5py.File(filepath, 'r') as f: + if 'nicos_details' in f['entry']: + grp = f['entry']['nicos_details'] + if 'runlabel' in grp: + val = grp['runlabel'][()] + details['runlabel'] = val.decode('utf8') if isinstance(val, bytes) else str(val) + if 'runtype' in grp: + val = grp['runtype'][()] + details['runtype'] = val.decode('utf8') if isinstance(val, bytes) else str(val) + return details + +# ---------------------------- +# Reduction and Plotting Functions +# ---------------------------- +def reduce_loki_batch_preliminary( + sample_run_file: str, + transmission_run_file: str, + background_run_file: str, + empty_beam_file: str, + direct_beam_file: str, + mask_files: list = None, + correct_for_gravity: bool = True, + uncertainty_mode = UncertaintyBroadcastMode.upper_bound, + return_events: bool = False, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + wavelength_n: int = 201, + q_start: float = 0.01, + q_stop: float = 0.3, + q_n: int = 101 +): + if mask_files is None: + mask_files = [] + wavelength_bins = sc.linspace("wavelength", wavelength_min, wavelength_max, wavelength_n, unit="angstrom") + q_bins = sc.linspace("Q", q_start, q_stop, q_n, unit="1/angstrom") + workflow = loki.LokiAtLarmorWorkflow() + if mask_files: + workflow = sans.with_pixel_mask_filenames(workflow, masks=mask_files) + workflow[NeXusDetectorName] = "larmor_detector" + workflow[WavelengthBins] = wavelength_bins + workflow[QBins] = q_bins + workflow[CorrectForGravity] = correct_for_gravity + workflow[UncertaintyBroadcastMode] = uncertainty_mode + workflow[ReturnEvents] = return_events + workflow[Filename[BackgroundRun]] = background_run_file + workflow[Filename[TransmissionRun[BackgroundRun]]] = transmission_run_file + workflow[Filename[EmptyBeamRun]] = empty_beam_file + workflow[DirectBeamFilename] = direct_beam_file + workflow[Filename[SampleRun]] = sample_run_file + workflow[Filename[TransmissionRun[SampleRun]]] = transmission_run_file + center = sans.beam_center_from_center_of_mass(workflow) + workflow[BeamCenter] = center + tf = workflow.compute(TransmissionFraction[SampleRun]) + da = workflow.compute(BackgroundSubtractedIofQ) + return {"transmission": tf, "IofQ": da} + +def save_reduction_plots(res, sample, sample_run_file, lam_min, lam_max, lam_n, q_min, q_max, q_n, output_dir, show=True): + fig, axs = plt.subplots(1, 2, figsize=(8, 4)) + axs[0].set_box_aspect(1) + axs[1].set_box_aspect(1) + title_str = f"{sample} - {os.path.basename(sample_run_file)}" + fig.suptitle(title_str, fontsize=14) + q_bins = sc.linspace("Q", q_min, q_max, q_n, unit="1/angstrom") + x_q = 0.5 * (q_bins.values[:-1] + q_bins.values[1:]) + if res["IofQ"].variances is not None: + yerr = np.sqrt(res["IofQ"].variances) + axs[0].errorbar(x_q, res["IofQ"].values, yerr=yerr, fmt='o', linestyle='none', color='k', alpha=0.5, markerfacecolor='none') + else: + axs[0].scatter(x_q, res["IofQ"].values) + axs[0].set_xlabel("Q (Å$^{-1}$)") + axs[0].set_ylabel("I(Q)") + axs[0].set_xscale("log") + axs[0].set_yscale("log") + wavelength_bins = sc.linspace("wavelength", lam_min, lam_max, lam_n, unit="angstrom") + x_wl = 0.5 * (wavelength_bins.values[:-1] + wavelength_bins.values[1:]) + if res["transmission"].variances is not None: + yerr_tr = np.sqrt(res["transmission"].variances) + axs[1].errorbar(x_wl, res["transmission"].values, yerr=yerr_tr, fmt='^', linestyle='none', color='k', alpha=0.5, markerfacecolor='none') + else: + axs[1].scatter(x_wl, res["transmission"].values) + axs[1].set_xlabel("Wavelength (Å)") + axs[1].set_ylabel("Transmission") + plt.tight_layout() + out_png = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", "_reduced.png")) + fig.savefig(out_png, dpi=300) + if show: + display(fig) + plt.close(fig) + +# ---------------------------- +# Unified Backend Function for Reduction +# ---------------------------- +def perform_reduction_for_sample( + sample_info: dict, + input_dir: str, + output_dir: str, + reduction_params: dict, + background_run_file: str, + empty_beam_file: str, + direct_beam_file: str, + log_func: callable +): + """ + Processes a single sample reduction: + - Finds the necessary run files + - Optionally determines a mask (or finds one automatically) + - Calls the reduction and plotting routines + - Logs all steps via log_func(message) + """ + sample = sample_info.get("SAMPLE", "Unknown") + try: + sample_run_file = find_file(input_dir, str(sample_info["SANS"]), extension=".nxs") + transmission_run_file = find_file(input_dir, str(sample_info["TRANS"]), extension=".nxs") + except Exception as e: + log_func(f"Skipping sample {sample}: {e}") + return None + # Determine mask file. + mask_file = None + mask_candidate = str(sample_info.get("mask", "")).strip() + if mask_candidate: + mask_candidate_file = os.path.join(input_dir, f"{mask_candidate}.xml") + if os.path.exists(mask_candidate_file): + mask_file = mask_candidate_file + if mask_file is None: + try: + mask_file = find_mask_file(input_dir) + log_func(f"Identified mask file: {mask_file} for sample {sample}") + except Exception as e: + log_func(f"Mask file not found for sample {sample}: {e}") + return None + + log_func(f"Reducing sample {sample}...") + try: + res = reduce_loki_batch_preliminary( + sample_run_file=sample_run_file, + transmission_run_file=transmission_run_file, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + mask_files=[mask_file], + wavelength_min=reduction_params["wavelength_min"], + wavelength_max=reduction_params["wavelength_max"], + wavelength_n=reduction_params["wavelength_n"], + q_start=reduction_params["q_start"], + q_stop=reduction_params["q_stop"], + q_n=reduction_params["q_n"] + ) + except Exception as e: + log_func(f"Reduction failed for sample {sample}: {e}") + return None + out_xye = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", ".xye")) + try: + save_xye_pandas(res["IofQ"], out_xye) + log_func(f"Saved reduced data to {out_xye}") + except Exception as e: + log_func(f"Failed to save reduced data for {sample}: {e}") + try: + save_reduction_plots( + res, + sample, + sample_run_file, + reduction_params["wavelength_min"], + reduction_params["wavelength_max"], + reduction_params["wavelength_n"], + reduction_params["q_start"], + reduction_params["q_stop"], + reduction_params["q_n"], + output_dir, + show=True + ) + log_func(f"Saved reduction plot for sample {sample}.") + except Exception as e: + log_func(f"Failed to save reduction plot for {sample}: {e}") + log_func(f"Reduced sample {sample} and saved outputs.") + return res + +# ---------------------------- +# GUI Widgets (Refactored to use Unified Backend) +# ---------------------------- +class SansBatchReductionWidget: + def __init__(self): + self.csv_chooser = FileChooser(select_dir=False) + self.csv_chooser.title = "Select CSV File" + self.csv_chooser.filter_pattern = "*.csv" + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.ebeam_sans_widget = widgets.Text(value="", placeholder="Enter Ebeam SANS run number", description="Ebeam SANS:") + self.ebeam_trans_widget = widgets.Text(value="", placeholder="Enter Ebeam TRANS run number", description="Ebeam TRANS:") + self.wavelength_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.wavelength_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.wavelength_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_start_widget = widgets.FloatText(value=0.01, description="Q start (1/Å):") + self.q_stop_widget = widgets.FloatText(value=0.3, description="Q stop (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + self.load_csv_button = widgets.Button(description="Load CSV") + self.load_csv_button.on_click(self.load_csv) + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + self.clear_log_button = widgets.Button(description="Clear Log") + self.clear_log_button.on_click(lambda _: self.log_output.clear_output()) + self.clear_plots_button = widgets.Button(description="Clear Plots") + self.clear_plots_button.on_click(lambda _: self.plot_output.clear_output()) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.main = widgets.VBox([ + widgets.HBox([self.csv_chooser, self.input_dir_chooser, self.output_dir_chooser]), + widgets.HBox([self.ebeam_sans_widget, self.ebeam_trans_widget]), + widgets.HBox([self.wavelength_min_widget, self.wavelength_max_widget, self.wavelength_n_widget]), + widgets.HBox([self.q_start_widget, self.q_stop_widget, self.q_n_widget]), + self.load_csv_button, + self.table, + widgets.HBox([self.reduce_button, self.clear_log_button, self.clear_plots_button]), + self.log_output, + self.plot_output + ]) + + def load_csv(self, _): + csv_path = self.csv_chooser.selected + if not csv_path or not os.path.exists(csv_path): + with self.log_output: + print("CSV file not selected or does not exist.") + return + df = pd.read_csv(csv_path) + self.table.data = df + with self.log_output: + print(f"Loaded reduction table with {len(df)} rows from {csv_path}.") + + def run_reduction(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct-beam file:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + return + try: + background_run_file = find_file(input_dir, self.ebeam_sans_widget.value, extension=".nxs") + empty_beam_file = find_file(input_dir, self.ebeam_trans_widget.value, extension=".nxs") + with self.log_output: + print("Using empty-beam files:") + print(" Background (Ebeam SANS):", background_run_file) + print(" Empty beam (Ebeam TRANS):", empty_beam_file) + except Exception as e: + with self.log_output: + print("Error finding empty beam files:", e) + return + + reduction_params = { + "wavelength_min": self.wavelength_min_widget.value, + "wavelength_max": self.wavelength_max_widget.value, + "wavelength_n": self.wavelength_n_widget.value, + "q_start": self.q_start_widget.value, + "q_stop": self.q_stop_widget.value, + "q_n": self.q_n_widget.value + } + + df = self.table.data + for idx, row in df.iterrows(): + perform_reduction_for_sample( + sample_info=row, + input_dir=input_dir, + output_dir=output_dir, + reduction_params=reduction_params, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + log_func=lambda msg: print(msg) + ) + + @property + def widget(self): + return self.main + +class SemiAutoReductionWidget: + def __init__(self): + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.scan_button = widgets.Button(description="Scan Directory") + self.scan_button.on_click(self.scan_directory) + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + self.add_row_button = widgets.Button(description="Add Row") + self.add_row_button.on_click(self.add_row) + self.delete_row_button = widgets.Button(description="Delete Last Row") + self.delete_row_button.on_click(self.delete_last_row) + self.lambda_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.lambda_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.lambda_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_min_widget = widgets.FloatText(value=0.01, description="Qmin (1/Å):") + self.q_max_widget = widgets.FloatText(value=0.3, description="Qmax (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + self.empty_beam_sans_text = widgets.Text(value="", description="Ebeam SANS:", disabled=True) + self.empty_beam_trans_text = widgets.Text(value="", description="Ebeam TRANS:", disabled=True) + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + self.clear_log_button = widgets.Button(description="Clear Log") + self.clear_log_button.on_click(lambda _: self.log_output.clear_output()) + self.clear_plots_button = widgets.Button(description="Clear Plots") + self.clear_plots_button.on_click(lambda _: self.plot_output.clear_output()) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.processed = set() + self.main = widgets.VBox([ + widgets.HBox([self.input_dir_chooser, self.output_dir_chooser]), + self.scan_button, + self.table, + widgets.HBox([self.add_row_button, self.delete_row_button]), + widgets.HBox([self.lambda_min_widget, self.lambda_max_widget, self.lambda_n_widget]), + widgets.HBox([self.q_min_widget, self.q_max_widget, self.q_n_widget]), + widgets.HBox([self.empty_beam_sans_text, self.empty_beam_trans_text]), + widgets.HBox([self.reduce_button, self.clear_log_button, self.clear_plots_button]), + self.log_output, + self.plot_output + ]) + + def add_row(self, _): + df = self.table.data + new_row = {col: "" for col in df.columns} if not df.empty else {'SAMPLE': '', 'SANS': '', 'TRANS': ''} + df = df.append(new_row, ignore_index=True) + self.table.data = df + + def delete_last_row(self, _): + df = self.table.data + if not df.empty: + self.table.data = df.iloc[:-1] + + def scan_directory(self, _): + self.log_output.clear_output() + input_dir = self.input_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Invalid input folder.") + return + nxs_files = glob.glob(os.path.join(input_dir, "*.nxs")) + groups = {} + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runlabel' not in details or 'runtype' not in details: + continue + runlabel = details['runlabel'] + runtype = details['runtype'].lower() + run_number = extract_run_number(os.path.basename(f)) + if runlabel not in groups: + groups[runlabel] = {} + groups[runlabel][runtype] = run_number + table_rows = [] + for runlabel, d in groups.items(): + if 'sans' in d and 'trans' in d: + table_rows.append({'SAMPLE': runlabel, 'SANS': d['sans'], 'TRANS': d['trans']}) + df = pd.DataFrame(table_rows) + self.table.data = df + with self.log_output: + print(f"Scanned {len(nxs_files)} files. Found {len(df)} reduction entries.") + ebeam_sans_files = [] + ebeam_trans_files = [] + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runtype' in details: + if details['runtype'].lower() == 'ebeam_sans': + ebeam_sans_files.append(f) + elif details['runtype'].lower() == 'ebeam_trans': + ebeam_trans_files.append(f) + if ebeam_sans_files: + ebeam_sans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_sans_text.value = ebeam_sans_files[0] + else: + self.empty_beam_sans_text.value = "" + if ebeam_trans_files: + ebeam_trans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_trans_text.value = ebeam_trans_files[0] + else: + self.empty_beam_trans_text.value = "" + + def run_reduction(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct-beam file:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + return + background_run_file = self.empty_beam_sans_text.value + empty_beam_file = self.empty_beam_trans_text.value + if not background_run_file or not empty_beam_file: + with self.log_output: + print("Empty beam files not found.") + return + + reduction_params = { + "wavelength_min": self.lambda_min_widget.value, + "wavelength_max": self.lambda_max_widget.value, + "wavelength_n": self.lambda_n_widget.value, + "q_start": self.q_min_widget.value, + "q_stop": self.q_max_widget.value, + "q_n": self.q_n_widget.value + } + + df = self.table.data.copy() + for idx, row in df.iterrows(): + perform_reduction_for_sample( + sample_info=row, + input_dir=input_dir, + output_dir=output_dir, + reduction_params=reduction_params, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + log_func=lambda msg: print(msg) + ) + + @property + def widget(self): + return self.main + +class AutoReductionWidget: + def __init__(self): + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.start_stop_button = widgets.Button(description="Start") + self.start_stop_button.on_click(self.toggle_running) + self.status_label = widgets.Label(value="Stopped") + self.table = DataGrid(pd.DataFrame([]), editable=False, auto_fit_columns=True) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.running = False + self.thread = None + self.processed = set() + self.empty_beam_sans = None + self.empty_beam_trans = None + self.main = widgets.VBox([ + widgets.HBox([self.input_dir_chooser, self.output_dir_chooser]), + widgets.HBox([self.start_stop_button, self.status_label]), + self.table, + self.log_output, + self.plot_output + ]) + + def toggle_running(self, _): + if not self.running: + self.running = True + self.start_stop_button.description = "Stop" + self.status_label.value = "Running" + self.thread = threading.Thread(target=self.background_loop, daemon=True) + self.thread.start() + else: + self.running = False + self.start_stop_button.description = "Start" + self.status_label.value = "Stopped" + + def background_loop(self): + while self.running: + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Invalid input folder. Waiting for valid selection...") + time.sleep(60) + continue + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Invalid output folder. Waiting for valid selection...") + time.sleep(60) + continue + nxs_files = glob.glob(os.path.join(input_dir, "*.nxs")) + groups = {} + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runlabel' not in details or 'runtype' not in details: + continue + runlabel = details['runlabel'] + runtype = details['runtype'].lower() + run_number = extract_run_number(os.path.basename(f)) + if runlabel not in groups: + groups[runlabel] = {} + groups[runlabel][runtype] = run_number + table_rows = [] + for runlabel, d in groups.items(): + if 'sans' in d and 'trans' in d: + table_rows.append({'SAMPLE': runlabel, 'SANS': d['sans'], 'TRANS': d['trans']}) + df = pd.DataFrame(table_rows) + self.table.data = df + with self.log_output: + print(f"Scanned {len(nxs_files)} files. Found {len(df)} reduction entries.") + ebeam_sans_files = [] + ebeam_trans_files = [] + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runtype' in details: + if details['runtype'].lower() == 'ebeam_sans': + ebeam_sans_files.append(f) + elif details['runtype'].lower() == 'ebeam_trans': + ebeam_trans_files.append(f) + if ebeam_sans_files: + ebeam_sans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_sans = ebeam_sans_files[0] + else: + self.empty_beam_sans = None + if ebeam_trans_files: + ebeam_trans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_trans = ebeam_trans_files[0] + else: + self.empty_beam_trans = None + try: + direct_beam_file = find_direct_beam(input_dir) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + time.sleep(60) + continue + for index, row in df.iterrows(): + key = (row["SAMPLE"], row["SANS"], row["TRANS"]) + if key in self.processed: + continue + try: + sample_run_file = find_file(input_dir, row["SANS"], extension=".nxs") + transmission_run_file = find_file(input_dir, row["TRANS"], extension=".nxs") + except Exception as e: + with self.log_output: + print(f"Skipping sample {row['SAMPLE']}: {e}") + continue + try: + mask_file = find_mask_file(input_dir) + with self.log_output: + print(f"Using mask file: {mask_file} for sample {row['SAMPLE']}") + except Exception as e: + with self.log_output: + print(f"Mask file not found for sample {row['SAMPLE']}: {e}") + continue + if not self.empty_beam_sans or not self.empty_beam_trans: + with self.log_output: + print("Empty beam files not found, skipping reduction for sample", row["SAMPLE"]) + continue + with self.log_output: + print(f"Reducing sample {row['SAMPLE']}...") + reduction_params = { + "wavelength_min": 1.0, + "wavelength_max": 13.0, + "wavelength_n": 201, + "q_start": 0.01, + "q_stop": 0.3, + "q_n": 101 + } + perform_reduction_for_sample( + sample_info=row, + input_dir=input_dir, + output_dir=output_dir, + reduction_params=reduction_params, + background_run_file=self.empty_beam_sans, + empty_beam_file=self.empty_beam_trans, + direct_beam_file=direct_beam_file, + log_func=lambda msg: print(msg) + ) + self.processed.add(key) + time.sleep(60) + + @property + def widget(self): + return self.main + +# ---------------------------- +# Direct Beam Functionality and Widget (unchanged) +# ---------------------------- +def compute_direct_beam_local( + mask: str, + sample_sans: str, + background_sans: str, + sample_trans: str, + background_trans: str, + empty_beam: str, + local_Iq_theory: str, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + n_wavelength_bins: int = 50, + n_wavelength_bands: int = 50 +) -> dict: + workflow = loki.LokiAtLarmorWorkflow() + workflow = sans.with_pixel_mask_filenames(workflow, masks=[mask]) + workflow[NeXusDetectorName] = 'larmor_detector' + wl_min = sc.scalar(wavelength_min, unit='angstrom') + wl_max = sc.scalar(wavelength_max, unit='angstrom') + workflow[WavelengthBins] = sc.linspace('wavelength', wl_min, wl_max, n_wavelength_bins + 1) + workflow[WavelengthBands] = sc.linspace('wavelength', wl_min, wl_max, n_wavelength_bands + 1) + workflow[CorrectForGravity] = True + workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound + workflow[ReturnEvents] = False + workflow[QBins] = sc.linspace(dim='Q', start=0.01, stop=0.3, num=101, unit='1/angstrom') + workflow[Filename[SampleRun]] = sample_sans + workflow[Filename[BackgroundRun]] = background_sans + workflow[Filename[TransmissionRun[SampleRun]]] = sample_trans + workflow[Filename[TransmissionRun[BackgroundRun]]] = background_trans + workflow[Filename[EmptyBeamRun]] = empty_beam + center = sans.beam_center_from_center_of_mass(workflow) + print("Computed beam center:", center) + workflow[BeamCenter] = center + Iq_theory = sc.io.load_hdf5(local_Iq_theory) + f = interp1d(Iq_theory, 'Q') + I0 = f(sc.midpoints(workflow.compute(QBins))).data[0] + print("Computed I0:", I0) + results = sans.direct_beam(workflow=workflow, I0=I0, niter=6) + iofq_full = results[-1]['iofq_full'] + iofq_bands = results[-1]['iofq_bands'] + direct_beam_function = results[-1]['direct_beam'] + pp.plot( + {'reference': Iq_theory, 'data': iofq_full}, + color={'reference': 'darkgrey', 'data': 'C0'}, + norm='log', + ) + print("Plotted full-range result vs. theoretical reference.") + return { + 'direct_beam_function': direct_beam_function, + 'iofq_full': iofq_full, + 'Iq_theory': Iq_theory, + } + +class DirectBeamWidget: + def __init__(self): + self.mask_text = widgets.Text(value="", placeholder="Enter mask file path", description="Mask:") + self.sample_sans_text = widgets.Text(value="", placeholder="Enter sample SANS file path", description="Sample SANS:") + self.background_sans_text = widgets.Text(value="", placeholder="Enter background SANS file path", description="Background SANS:") + self.sample_trans_text = widgets.Text(value="", placeholder="Enter sample TRANS file path", description="Sample TRANS:") + self.background_trans_text = widgets.Text(value="", placeholder="Enter background TRANS file path", description="Background TRANS:") + self.empty_beam_text = widgets.Text(value="", placeholder="Enter empty beam file path", description="Empty Beam:") + self.local_Iq_theory_text = widgets.Text(value="", placeholder="Enter I(q) Theory file path", description="I(q) Theory:") + self.db_wavelength_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.db_wavelength_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.db_n_wavelength_bins_widget = widgets.IntText(value=50, description="λ n_bins:") + self.db_n_wavelength_bands_widget = widgets.IntText(value=50, description="λ n_bands:") + self.compute_button = widgets.Button(description="Compute Direct Beam") + self.compute_button.on_click(self.compute_direct_beam) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.main = widgets.VBox([ + self.mask_text, + self.sample_sans_text, + self.background_sans_text, + self.sample_trans_text, + self.background_trans_text, + self.empty_beam_text, + self.local_Iq_theory_text, + widgets.HBox([ + self.db_wavelength_min_widget, + self.db_wavelength_max_widget, + self.db_n_wavelength_bins_widget, + self.db_n_wavelength_bands_widget + ]), + self.compute_button, + self.log_output, + self.plot_output + ]) + + def compute_direct_beam(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + mask = self.mask_text.value + sample_sans = self.sample_sans_text.value + background_sans = self.background_sans_text.value + sample_trans = self.sample_trans_text.value + background_trans = self.background_trans_text.value + empty_beam = self.empty_beam_text.value + local_Iq_theory = self.local_Iq_theory_text.value + wl_min = self.db_wavelength_min_widget.value + wl_max = self.db_wavelength_max_widget.value + n_bins = self.db_n_wavelength_bins_widget.value + n_bands = self.db_n_wavelength_bands_widget.value + with self.log_output: + print("Computing direct beam with:") + print(" Mask:", mask) + print(" Sample SANS:", sample_sans) + print(" Background SANS:", background_sans) + print(" Sample TRANS:", sample_trans) + print(" Background TRANS:", background_trans) + print(" Empty Beam:", empty_beam) + print(" I(q) Theory:", local_Iq_theory) + print(" λ min:", wl_min, "λ max:", wl_max, "n_bins:", n_bins, "n_bands:", n_bands) + try: + results = compute_direct_beam_local( + mask, + sample_sans, + background_sans, + sample_trans, + background_trans, + empty_beam, + local_Iq_theory, + wavelength_min=wl_min, + wavelength_max=wl_max, + n_wavelength_bins=n_bins, + n_wavelength_bands=n_bands + ) + with self.log_output: + print("Direct beam computation complete.") + except Exception as e: + with self.log_output: + print("Error computing direct beam:", e) + + @property + def widget(self): + return self.main + +# ---------------------------- +# Build the Tabbed Widget +# ---------------------------- +reduction_widget = SansBatchReductionWidget().widget +direct_beam_widget = DirectBeamWidget().widget +semi_auto_reduction_widget = SemiAutoReductionWidget().widget +auto_reduction_widget = AutoReductionWidget().widget + +tabs = widgets.Tab(children=[direct_beam_widget, reduction_widget, semi_auto_reduction_widget, auto_reduction_widget]) +tabs.set_title(0, "Direct Beam") +tabs.set_title(1, "Reduction (Manual)") +tabs.set_title(2, "Reduction (Smart)") +tabs.set_title(3, "Reduction (Auto)") + +# display(tabs) diff --git a/src/ess/loki/batch-gui-legacy/tabwidget.py b/src/ess/loki/batch-gui-legacy/tabwidget.py new file mode 100644 index 00000000..0c5f7c02 --- /dev/null +++ b/src/ess/loki/batch-gui-legacy/tabwidget.py @@ -0,0 +1,898 @@ +import glob +import os +import re + +import h5py +import ipywidgets as widgets +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import plopp as pp # used for plotting in direct beam section +import scipp as sc +from ipydatagrid import DataGrid +from ipyfilechooser import FileChooser +from IPython.display import display +from scipp.scipy.interpolate import interp1d + +from ess import loki, sans +from ess.sans.types import * + + +# ---------------------------- +# Reduction Functionality +# ---------------------------- +def reduce_loki_batch_preliminary( + sample_run_file: str, + transmission_run_file: str, + background_run_file: str, + empty_beam_file: str, + direct_beam_file: str, + mask_files: list = None, + correct_for_gravity: bool = True, + uncertainty_mode=UncertaintyBroadcastMode.upper_bound, + return_events: bool = False, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + wavelength_n: int = 201, + q_start: float = 0.01, + q_stop: float = 0.3, + q_n: int = 101, +): + if mask_files is None: + mask_files = [] + # Define wavelength and Q bins. + wavelength_bins = sc.linspace( + "wavelength", wavelength_min, wavelength_max, wavelength_n, unit="angstrom" + ) + q_bins = sc.linspace("Q", q_start, q_stop, q_n, unit="1/angstrom") + # Initialize the workflow. + workflow = loki.LokiAtLarmorWorkflow() + if mask_files: + workflow = sans.with_pixel_mask_filenames(workflow, masks=mask_files) + workflow[NeXusDetectorName] = "larmor_detector" + workflow[WavelengthBins] = wavelength_bins + workflow[QBins] = q_bins + workflow[CorrectForGravity] = correct_for_gravity + workflow[UncertaintyBroadcastMode] = uncertainty_mode + workflow[ReturnEvents] = return_events + workflow[Filename[BackgroundRun]] = background_run_file + workflow[Filename[TransmissionRun[BackgroundRun]]] = transmission_run_file + workflow[Filename[EmptyBeamRun]] = empty_beam_file + workflow[DirectBeamFilename] = direct_beam_file + workflow[Filename[SampleRun]] = sample_run_file + workflow[Filename[TransmissionRun[SampleRun]]] = transmission_run_file + center = sans.beam_center_from_center_of_mass(workflow) + workflow[BeamCenter] = center + tf = workflow.compute(TransmissionFraction[SampleRun]) + da = workflow.compute(BackgroundSubtractedIofQ) + return {"transmission": tf, "IofQ": da} + + +def find_file(work_dir, run_number, extension=".nxs"): + pattern = os.path.join(work_dir, f"*{run_number}*{extension}") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find file matching pattern {pattern}") + + +def find_direct_beam(work_dir): + pattern = os.path.join(work_dir, "*direct-beam*.h5") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError( + f"Could not find direct-beam file matching pattern {pattern}" + ) + + +def find_mask_file(work_dir): + pattern = os.path.join(work_dir, "*mask*.xml") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find mask file matching pattern {pattern}") + + +def save_xye_pandas(data_array, filename): + q_vals = data_array.coords["Q"].values + i_vals = data_array.values + if len(q_vals) != len(i_vals): + q_vals = 0.5 * (q_vals[:-1] + q_vals[1:]) + if data_array.variances is not None: + err_vals = np.sqrt(data_array.variances) + if len(err_vals) != len(i_vals): + err_vals = 0.5 * (err_vals[:-1] + err_vals[1:]) + else: + err_vals = np.zeros_like(i_vals) + df = pd.DataFrame({"Q": q_vals, "I(Q)": i_vals, "Error": err_vals}) + df.to_csv(filename, sep=" ", index=False, header=True) + + +# ---------------------------- +# Helper Functions for Semi-Auto Reduction +# ---------------------------- +def extract_run_number(filename): + m = re.search(r'(\d{4,})', filename) + if m: + return m.group(1) + return "" + + +def parse_nx_details(filepath): + details = {} + with h5py.File(filepath, 'r') as f: + if 'nicos_details' in f['entry']: + grp = f['entry']['nicos_details'] + if 'runlabel' in grp: + val = grp['runlabel'][()] + details['runlabel'] = ( + val.decode('utf8') if isinstance(val, bytes) else str(val) + ) + if 'runtype' in grp: + val = grp['runtype'][()] + details['runtype'] = ( + val.decode('utf8') if isinstance(val, bytes) else str(val) + ) + return details + + +# ---------------------------- +# Semi-Auto Reduction Widget +# ---------------------------- +class SemiAutoReductionWidget: + def __init__(self): + # Only Input and Output Folder choosers are needed. + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + + self.scan_button = widgets.Button(description="Scan Directory") + self.scan_button.on_click(self.scan_directory) + + # DataGrid for auto-generated reduction table; now editable. + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + + # Buttons to add or delete rows from the table. + self.add_row_button = widgets.Button(description="Add Row") + self.add_row_button.on_click(self.add_row) + self.delete_row_button = widgets.Button(description="Delete Last Row") + self.delete_row_button.on_click(self.delete_last_row) + + # Parameter widgets for reduction (lambda and Q parameters) + self.lambda_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.lambda_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.lambda_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_min_widget = widgets.FloatText(value=0.01, description="Qmin (1/Å):") + self.q_max_widget = widgets.FloatText(value=0.3, description="Qmax (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + + # Text fields to display the automatically identified empty-beam files. + self.empty_beam_sans_text = widgets.Text( + value="", description="Ebeam SANS:", disabled=True + ) + self.empty_beam_trans_text = widgets.Text( + value="", description="Ebeam TRANS:", disabled=True + ) + + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + + self.clear_log_button = widgets.Button(description="Clear Log") + self.clear_log_button.on_click(lambda _: self.log_output.clear_output()) + self.clear_plots_button = widgets.Button(description="Clear Plots") + self.clear_plots_button.on_click(lambda _: self.plot_output.clear_output()) + + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + + # Build the layout. + self.main = widgets.VBox( + [ + widgets.HBox([self.input_dir_chooser, self.output_dir_chooser]), + self.scan_button, + self.table, + widgets.HBox([self.add_row_button, self.delete_row_button]), + widgets.HBox( + [ + self.lambda_min_widget, + self.lambda_max_widget, + self.lambda_n_widget, + ] + ), + widgets.HBox([self.q_min_widget, self.q_max_widget, self.q_n_widget]), + widgets.HBox([self.empty_beam_sans_text, self.empty_beam_trans_text]), + widgets.HBox( + [self.reduce_button, self.clear_log_button, self.clear_plots_button] + ), + self.log_output, + self.plot_output, + ] + ) + + def add_row(self, _): + df = self.table.data + # Create a default new row if the DataFrame is empty, otherwise add blank cells. + if df.empty: + new_row = {'SAMPLE': '', 'SANS': '', 'TRANS': ''} + else: + new_row = {col: "" for col in df.columns} + df = df.append(new_row, ignore_index=True) + self.table.data = df + + def delete_last_row(self, _): + df = self.table.data + if not df.empty: + df = df.iloc[:-1] + self.table.data = df + + def scan_directory(self, _): + self.log_output.clear_output() + input_dir = self.input_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Invalid input folder.") + return + nxs_files = glob.glob(os.path.join(input_dir, "*.nxs")) + groups = {} + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runlabel' not in details or 'runtype' not in details: + continue + runlabel = details['runlabel'] + runtype = details['runtype'].lower() + run_number = extract_run_number(os.path.basename(f)) + if runlabel not in groups: + groups[runlabel] = {} + groups[runlabel][runtype] = run_number + table_rows = [] + for runlabel, d in groups.items(): + if 'sans' in d and 'trans' in d: + table_rows.append( + {'SAMPLE': runlabel, 'SANS': d['sans'], 'TRANS': d['trans']} + ) + df = pd.DataFrame(table_rows) + self.table.data = df + with self.log_output: + print(f"Scanned {len(nxs_files)} files. Found {len(df)} reduction entries.") + # Identify empty beam files: + ebeam_sans_files = [] + ebeam_trans_files = [] + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runtype' in details: + if details['runtype'].lower() == 'ebeam_sans': + ebeam_sans_files.append(f) + elif details['runtype'].lower() == 'ebeam_trans': + ebeam_trans_files.append(f) + if ebeam_sans_files: + ebeam_sans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_sans_text.value = ebeam_sans_files[0] + else: + self.empty_beam_sans_text.value = "" + if ebeam_trans_files: + ebeam_trans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_trans_text.value = ebeam_trans_files[0] + else: + self.empty_beam_trans_text.value = "" + + def run_reduction(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct-beam file:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + return + background_run_file = self.empty_beam_sans_text.value + empty_beam_file = self.empty_beam_trans_text.value + if not background_run_file or not empty_beam_file: + with self.log_output: + print("Empty beam files not found.") + return + # Retrieve reduction parameters from widgets. + lam_min = self.lambda_min_widget.value + lam_max = self.lambda_max_widget.value + lam_n = self.lambda_n_widget.value + q_min = self.q_min_widget.value + q_max = self.q_max_widget.value + q_n = self.q_n_widget.value + + df = self.table.data + for idx, row in df.iterrows(): + sample = row["SAMPLE"] + sans_run = row["SANS"] + trans_run = row["TRANS"] + try: + sample_run_file = find_file(input_dir, sans_run, extension=".nxs") + transmission_run_file = find_file( + input_dir, trans_run, extension=".nxs" + ) + except Exception as e: + with self.log_output: + print(f"Skipping sample {sample}: {e}") + continue + try: + mask_file = find_mask_file(input_dir) + with self.log_output: + print(f"Using mask file: {mask_file} for sample {sample}") + except Exception as e: + with self.log_output: + print(f"Mask file not found for sample {sample}: {e}") + continue + with self.log_output: + print(f"Reducing sample {sample}...") + try: + res = reduce_loki_batch_preliminary( + sample_run_file=sample_run_file, + transmission_run_file=transmission_run_file, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + mask_files=[mask_file], + wavelength_min=lam_min, + wavelength_max=lam_max, + wavelength_n=lam_n, + q_start=q_min, + q_stop=q_max, + q_n=q_n, + ) + except Exception as e: + with self.log_output: + print(f"Reduction failed for sample {sample}: {e}") + continue + out_xye = os.path.join( + output_dir, os.path.basename(sample_run_file).replace(".nxs", ".xye") + ) + try: + save_xye_pandas(res["IofQ"], out_xye) + with self.log_output: + print(f"Saved reduced data to {out_xye}") + except Exception as e: + with self.log_output: + print(f"Failed to save reduced data for {sample}: {e}") + wavelength_bins = sc.linspace( + "wavelength", lam_min, lam_max, lam_n, unit="angstrom" + ) + x_wl = 0.5 * (wavelength_bins.values[:-1] + wavelength_bins.values[1:]) + fig_trans, ax_trans = plt.subplots() + ax_trans.plot(x_wl, res["transmission"].values, marker='o', linestyle='-') + ax_trans.set_title( + f"Transmission: {sample} {os.path.basename(sample_run_file)}" + ) + ax_trans.set_xlabel("Wavelength (Å)") + ax_trans.set_ylabel("Transmission") + plt.tight_layout() + with self.plot_output: + display(fig_trans) + trans_png = os.path.join( + output_dir, + os.path.basename(sample_run_file).replace(".nxs", "_transmission.png"), + ) + fig_trans.savefig(trans_png, dpi=300) + plt.close(fig_trans) + q_bins = sc.linspace("Q", q_min, q_max, q_n, unit="1/angstrom") + x_q = 0.5 * (q_bins.values[:-1] + q_bins.values[1:]) + fig_iq, ax_iq = plt.subplots() + if res["IofQ"].variances is not None: + yerr = np.sqrt(res["IofQ"].variances) + ax_iq.errorbar( + x_q, res["IofQ"].values, yerr=yerr, marker='o', linestyle='-' + ) + else: + ax_iq.plot(x_q, res["IofQ"].values, marker='o', linestyle='-') + ax_iq.set_title(f"I(Q): {os.path.basename(sample_run_file)} ({sample})") + ax_iq.set_xlabel("Q (Å$^{-1}$)") + ax_iq.set_ylabel("I(Q)") + ax_iq.set_xscale("log") + ax_iq.set_yscale("log") + plt.tight_layout() + with self.plot_output: + display(fig_iq) + iq_png = os.path.join( + output_dir, + os.path.basename(sample_run_file).replace(".nxs", "_IofQ.png"), + ) + fig_iq.savefig(iq_png, dpi=300) + plt.close(fig_iq) + with self.log_output: + print(f"Reduced sample {sample} and saved outputs.") + + @property + def widget(self): + return self.main + + +# ---------------------------- +# Direct Beam Functionality +# ---------------------------- +def compute_direct_beam_local( + mask: str, + sample_sans: str, + background_sans: str, + sample_trans: str, + background_trans: str, + empty_beam: str, + local_Iq_theory: str, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + n_wavelength_bins: int = 50, + n_wavelength_bands: int = 50, +) -> dict: + """ + Compute the direct beam function for the LoKI detectors using locally stored data. + """ + workflow = loki.LokiAtLarmorWorkflow() + workflow = sans.with_pixel_mask_filenames(workflow, masks=[mask]) + workflow[NeXusDetectorName] = 'larmor_detector' + + wl_min = sc.scalar(wavelength_min, unit='angstrom') + wl_max = sc.scalar(wavelength_max, unit='angstrom') + workflow[WavelengthBins] = sc.linspace( + 'wavelength', wl_min, wl_max, n_wavelength_bins + 1 + ) + workflow[WavelengthBands] = sc.linspace( + 'wavelength', wl_min, wl_max, n_wavelength_bands + 1 + ) + workflow[CorrectForGravity] = True + workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound + workflow[ReturnEvents] = False + workflow[QBins] = sc.linspace( + dim='Q', start=0.01, stop=0.3, num=101, unit='1/angstrom' + ) + + workflow[Filename[SampleRun]] = sample_sans + workflow[Filename[BackgroundRun]] = background_sans + workflow[Filename[TransmissionRun[SampleRun]]] = sample_trans + workflow[Filename[TransmissionRun[BackgroundRun]]] = background_trans + workflow[Filename[EmptyBeamRun]] = empty_beam + + center = sans.beam_center_from_center_of_mass(workflow) + print("Computed beam center:", center) + workflow[BeamCenter] = center + + Iq_theory = sc.io.load_hdf5(local_Iq_theory) + f = interp1d(Iq_theory, 'Q') + I0 = f(sc.midpoints(workflow.compute(QBins))).data[0] + print("Computed I0:", I0) + + results = sans.direct_beam(workflow=workflow, I0=I0, niter=6) + + iofq_full = results[-1]['iofq_full'] + iofq_bands = results[-1]['iofq_bands'] + direct_beam_function = results[-1]['direct_beam'] + + pp.plot( + {'reference': Iq_theory, 'data': iofq_full}, + color={'reference': 'darkgrey', 'data': 'C0'}, + norm='log', + ) + print("Plotted full-range result vs. theoretical reference.") + + return { + 'direct_beam_function': direct_beam_function, + 'iofq_full': iofq_full, + 'Iq_theory': Iq_theory, + } + + +# ---------------------------- +# Widgets for Reduction and Direct Beam +# ---------------------------- +class SansBatchReductionWidget: + def __init__(self): + self.csv_chooser = FileChooser(select_dir=False) + self.csv_chooser.title = "Select CSV File" + self.csv_chooser.filter_pattern = "*.csv" + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.ebeam_sans_widget = widgets.Text( + value="", + placeholder="Enter Ebeam SANS run number", + description="Ebeam SANS:", + ) + self.ebeam_trans_widget = widgets.Text( + value="", + placeholder="Enter Ebeam TRANS run number", + description="Ebeam TRANS:", + ) + # Add GUI widgets for reduction parameters: + self.wavelength_min_widget = widgets.FloatText( + value=1.0, description="λ min (Å):" + ) + self.wavelength_max_widget = widgets.FloatText( + value=13.0, description="λ max (Å):" + ) + self.wavelength_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_start_widget = widgets.FloatText( + value=0.01, description="Q start (1/Å):" + ) + self.q_stop_widget = widgets.FloatText(value=0.3, description="Q stop (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + + self.load_csv_button = widgets.Button(description="Load CSV") + self.load_csv_button.on_click(self.load_csv) + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + self.clear_log_button = widgets.Button(description="Clear Log") + self.clear_log_button.on_click(self.clear_log) + self.clear_plots_button = widgets.Button(description="Clear Plots") + self.clear_plots_button.on_click(self.clear_plots) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.main = widgets.VBox( + [ + widgets.HBox( + [self.csv_chooser, self.input_dir_chooser, self.output_dir_chooser] + ), + widgets.HBox([self.ebeam_sans_widget, self.ebeam_trans_widget]), + # Reduction parameters: + widgets.HBox( + [ + self.wavelength_min_widget, + self.wavelength_max_widget, + self.wavelength_n_widget, + ] + ), + widgets.HBox( + [self.q_start_widget, self.q_stop_widget, self.q_n_widget] + ), + self.load_csv_button, + self.table, + widgets.HBox( + [self.reduce_button, self.clear_log_button, self.clear_plots_button] + ), + self.log_output, + self.plot_output, + ] + ) + + def clear_log(self, _): + self.log_output.clear_output() + + def clear_plots(self, _): + self.plot_output.clear_output() + + def load_csv(self, _): + csv_path = self.csv_chooser.selected + if not csv_path or not os.path.exists(csv_path): + with self.log_output: + print("CSV file not selected or does not exist.") + return + df = pd.read_csv(csv_path) + self.table.data = df + with self.log_output: + print(f"Loaded reduction table with {len(df)} rows from {csv_path}.") + + def run_reduction(self, _): + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct-beam file:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + return + try: + background_run_file = find_file( + input_dir, self.ebeam_sans_widget.value, extension=".nxs" + ) + empty_beam_file = find_file( + input_dir, self.ebeam_trans_widget.value, extension=".nxs" + ) + with self.log_output: + print("Using empty-beam files:") + print(" Background (Ebeam SANS):", background_run_file) + print(" Empty beam (Ebeam TRANS):", empty_beam_file) + except Exception as e: + with self.log_output: + print("Error finding empty beam files:", e) + return + # Retrieve reduction parameters from widgets. + wl_min = self.wavelength_min_widget.value + wl_max = self.wavelength_max_widget.value + wl_n = self.wavelength_n_widget.value + q_start = self.q_start_widget.value + q_stop = self.q_stop_widget.value + q_n = self.q_n_widget.value + df = self.table.data + for idx, row in df.iterrows(): + sample = row["SAMPLE"] + try: + sample_run_file = find_file( + input_dir, str(row["SANS"]), extension=".nxs" + ) + transmission_run_file = find_file( + input_dir, str(row["TRANS"]), extension=".nxs" + ) + except Exception as e: + with self.log_output: + print(f"Skipping sample {sample}: {e}") + continue + mask_candidate = str(row.get("mask", "")).strip() + mask_file = None + if mask_candidate: + mask_file_candidate = os.path.join(input_dir, f"{mask_candidate}.xml") + if os.path.exists(mask_file_candidate): + mask_file = mask_file_candidate + if mask_file is None: + try: + mask_file = find_mask_file(input_dir) + with self.log_output: + print(f"Identified mask file: {mask_file} for sample {sample}") + except Exception as e: + with self.log_output: + print(f"Mask file not found for sample {sample}: {e}") + continue + with self.log_output: + print(f"Reducing sample {sample}...") + try: + res = reduce_loki_batch_preliminary( + sample_run_file=sample_run_file, + transmission_run_file=transmission_run_file, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + mask_files=[mask_file], + wavelength_min=wl_min, + wavelength_max=wl_max, + wavelength_n=wl_n, + q_start=q_start, + q_stop=q_stop, + q_n=q_n, + ) + except Exception as e: + with self.log_output: + print(f"Reduction failed for sample {sample}: {e}") + continue + out_xye = os.path.join( + output_dir, os.path.basename(sample_run_file).replace(".nxs", ".xye") + ) + try: + save_xye_pandas(res["IofQ"], out_xye) + with self.log_output: + print(f"Saved reduced data to {out_xye}") + except Exception as e: + with self.log_output: + print(f"Failed to save reduced data for {sample}: {e}") + wavelength_bins = sc.linspace( + "wavelength", wl_min, wl_max, wl_n, unit="angstrom" + ) + x_wl = 0.5 * (wavelength_bins.values[:-1] + wavelength_bins.values[1:]) + fig_trans, ax_trans = plt.subplots() + ax_trans.plot(x_wl, res["transmission"].values, marker='o', linestyle='-') + ax_trans.set_title( + f"Transmission: {sample} {os.path.basename(sample_run_file)}" + ) + ax_trans.set_xlabel("Wavelength (Å)") + ax_trans.set_ylabel("Transmission") + plt.tight_layout() + with self.plot_output: + display(fig_trans) + trans_png = os.path.join( + output_dir, + os.path.basename(sample_run_file).replace(".nxs", "_transmission.png"), + ) + fig_trans.savefig(trans_png, dpi=300) + plt.close(fig_trans) + q_bins = sc.linspace("Q", q_start, q_stop, q_n, unit="1/angstrom") + x_q = 0.5 * (q_bins.values[:-1] + q_bins.values[1:]) + fig_iq, ax_iq = plt.subplots() + if res["IofQ"].variances is not None: + yerr = np.sqrt(res["IofQ"].variances) + ax_iq.errorbar( + x_q, res["IofQ"].values, yerr=yerr, marker='o', linestyle='-' + ) + else: + ax_iq.plot(x_q, res["IofQ"].values, marker='o', linestyle='-') + ax_iq.set_title(f"I(Q): {os.path.basename(sample_run_file)} ({sample})") + ax_iq.set_xlabel("Q (Å$^{-1}$)") + ax_iq.set_ylabel("I(Q)") + ax_iq.set_xscale("log") + ax_iq.set_yscale("log") + plt.tight_layout() + with self.plot_output: + display(fig_iq) + iq_png = os.path.join( + output_dir, + os.path.basename(sample_run_file).replace(".nxs", "_IofQ.png"), + ) + fig_iq.savefig(iq_png, dpi=300) + plt.close(fig_iq) + with self.log_output: + print(f"Reduced sample {sample} and saved outputs.") + + @property + def widget(self): + return self.main + + +# ---------------------------- +# Direct Beam Widget +# ---------------------------- +class DirectBeamWidget: + def __init__(self): + self.mask_text = widgets.Text( + value="", placeholder="Enter mask file path", description="Mask:" + ) + self.sample_sans_text = widgets.Text( + value="", + placeholder="Enter sample SANS file path", + description="Sample SANS:", + ) + self.background_sans_text = widgets.Text( + value="", + placeholder="Enter background SANS file path", + description="Background SANS:", + ) + self.sample_trans_text = widgets.Text( + value="", + placeholder="Enter sample TRANS file path", + description="Sample TRANS:", + ) + self.background_trans_text = widgets.Text( + value="", + placeholder="Enter background TRANS file path", + description="Background TRANS:", + ) + self.empty_beam_text = widgets.Text( + value="", + placeholder="Enter empty beam file path", + description="Empty Beam:", + ) + self.local_Iq_theory_text = widgets.Text( + value="", + placeholder="Enter I(q) theory file path", + description="I(q) Theory:", + ) + # GUI widgets for direct beam parameters: + self.db_wavelength_min_widget = widgets.FloatText( + value=1.0, description="λ min (Å):" + ) + self.db_wavelength_max_widget = widgets.FloatText( + value=13.0, description="λ max (Å):" + ) + self.db_n_wavelength_bins_widget = widgets.IntText( + value=50, description="λ n_bins:" + ) + self.db_n_wavelength_bands_widget = widgets.IntText( + value=50, description="λ n_bands:" + ) + + self.compute_button = widgets.Button(description="Compute Direct Beam") + self.compute_button.on_click(self.compute_direct_beam) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.main = widgets.VBox( + [ + self.mask_text, + self.sample_sans_text, + self.background_sans_text, + self.sample_trans_text, + self.background_trans_text, + self.empty_beam_text, + self.local_Iq_theory_text, + widgets.HBox( + [ + self.db_wavelength_min_widget, + self.db_wavelength_max_widget, + self.db_n_wavelength_bins_widget, + self.db_n_wavelength_bands_widget, + ] + ), + self.compute_button, + self.log_output, + self.plot_output, + ] + ) + + def compute_direct_beam(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + mask = self.mask_text.value + sample_sans = self.sample_sans_text.value + background_sans = self.background_sans_text.value + sample_trans = self.sample_trans_text.value + background_trans = self.background_trans_text.value + empty_beam = self.empty_beam_text.value + local_Iq_theory = self.local_Iq_theory_text.value + wl_min = self.db_wavelength_min_widget.value + wl_max = self.db_wavelength_max_widget.value + n_bins = self.db_n_wavelength_bins_widget.value + n_bands = self.db_n_wavelength_bands_widget.value + with self.log_output: + print("Computing direct beam with:") + print(" Mask:", mask) + print(" Sample SANS:", sample_sans) + print(" Background SANS:", background_sans) + print(" Sample TRANS:", sample_trans) + print(" Background TRANS:", background_trans) + print(" Empty Beam:", empty_beam) + print(" I(q) Theory:", local_Iq_theory) + print( + " λ min:", + wl_min, + "λ max:", + wl_max, + "n_bins:", + n_bins, + "n_bands:", + n_bands, + ) + try: + results = compute_direct_beam_local( + mask, + sample_sans, + background_sans, + sample_trans, + background_trans, + empty_beam, + local_Iq_theory, + wavelength_min=wl_min, + wavelength_max=wl_max, + n_wavelength_bins=n_bins, + n_wavelength_bands=n_bands, + ) + with self.log_output: + print("Direct beam computation complete.") + except Exception as e: + with self.log_output: + print("Error computing direct beam:", e) + + @property + def widget(self): + return self.main + + +# ---------------------------- +# Build Tabbed Widget +# ---------------------------- +reduction_widget = SansBatchReductionWidget().widget +direct_beam_widget = DirectBeamWidget().widget +semi_auto_reduction_widget = SemiAutoReductionWidget().widget +tabs = widgets.Tab( + children=[direct_beam_widget, reduction_widget, semi_auto_reduction_widget] +) +tabs.set_title(0, "Direct Beam") +tabs.set_title(1, "Reduction (Manual)") +tabs.set_title(2, "Reduction (Smart)") +# tabs.set_title(3, "Reduction (Auto)") + +# Display the tab widget. +# display(tabs) diff --git a/src/ess/loki/batch-gui-legacy/tabwidget050325.py b/src/ess/loki/batch-gui-legacy/tabwidget050325.py new file mode 100644 index 00000000..2c723c51 --- /dev/null +++ b/src/ess/loki/batch-gui-legacy/tabwidget050325.py @@ -0,0 +1,826 @@ +import os +import glob +import re +import h5py +import pandas as pd +import scipp as sc +import matplotlib.pyplot as plt +import numpy as np +import ipywidgets as widgets +from ipydatagrid import DataGrid +from IPython.display import display +from ipyfilechooser import FileChooser +from ess import sans +from ess import loki +from ess.sans.types import * +from scipp.scipy.interpolate import interp1d +import plopp as pp # used for plotting in direct beam section +import threading +import time + +# ---------------------------- +# Common Utility Functions +# ---------------------------- +def find_file(work_dir, run_number, extension=".nxs"): + pattern = os.path.join(work_dir, f"*{run_number}*{extension}") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find file matching pattern {pattern}") + +def find_direct_beam(work_dir): + pattern = os.path.join(work_dir, "*direct-beam*.h5") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find direct-beam file matching pattern {pattern}") + +def find_mask_file(work_dir): + pattern = os.path.join(work_dir, "*mask*.xml") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find mask file matching pattern {pattern}") + +def save_xye_pandas(data_array, filename): + q_vals = data_array.coords["Q"].values + i_vals = data_array.values + if len(q_vals) != len(i_vals): + q_vals = 0.5 * (q_vals[:-1] + q_vals[1:]) + if data_array.variances is not None: + err_vals = np.sqrt(data_array.variances) + if len(err_vals) != len(i_vals): + err_vals = 0.5 * (err_vals[:-1] + err_vals[1:]) + else: + err_vals = np.zeros_like(i_vals) + df = pd.DataFrame({"Q": q_vals, "I(Q)": i_vals, "Error": err_vals}) + df.to_csv(filename, sep=" ", index=False, header=True) + +def extract_run_number(filename): + m = re.search(r'(\d{4,})', filename) + if m: + return m.group(1) + return "" + +def parse_nx_details(filepath): + details = {} + with h5py.File(filepath, 'r') as f: + if 'nicos_details' in f['entry']: + grp = f['entry']['nicos_details'] + if 'runlabel' in grp: + val = grp['runlabel'][()] + details['runlabel'] = val.decode('utf8') if isinstance(val, bytes) else str(val) + if 'runtype' in grp: + val = grp['runtype'][()] + details['runtype'] = val.decode('utf8') if isinstance(val, bytes) else str(val) + return details + +# ---------------------------- +# Reduction and Plotting Functions +# ---------------------------- +def reduce_loki_batch_preliminary( + sample_run_file: str, + transmission_run_file: str, + background_run_file: str, + empty_beam_file: str, + direct_beam_file: str, + mask_files: list = None, + correct_for_gravity: bool = True, + uncertainty_mode = UncertaintyBroadcastMode.upper_bound, + return_events: bool = False, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + wavelength_n: int = 201, + q_start: float = 0.01, + q_stop: float = 0.3, + q_n: int = 101 +): + if mask_files is None: + mask_files = [] + wavelength_bins = sc.linspace("wavelength", wavelength_min, wavelength_max, wavelength_n, unit="angstrom") + q_bins = sc.linspace("Q", q_start, q_stop, q_n, unit="1/angstrom") + workflow = loki.LokiAtLarmorWorkflow() + if mask_files: + workflow = sans.with_pixel_mask_filenames(workflow, masks=mask_files) + workflow[NeXusDetectorName] = "larmor_detector" + workflow[WavelengthBins] = wavelength_bins + workflow[QBins] = q_bins + workflow[CorrectForGravity] = correct_for_gravity + workflow[UncertaintyBroadcastMode] = uncertainty_mode + workflow[ReturnEvents] = return_events + workflow[Filename[BackgroundRun]] = background_run_file + workflow[Filename[TransmissionRun[BackgroundRun]]] = transmission_run_file + workflow[Filename[EmptyBeamRun]] = empty_beam_file + workflow[DirectBeamFilename] = direct_beam_file + workflow[Filename[SampleRun]] = sample_run_file + workflow[Filename[TransmissionRun[SampleRun]]] = transmission_run_file + center = sans.beam_center_from_center_of_mass(workflow) + workflow[BeamCenter] = center + tf = workflow.compute(TransmissionFraction[SampleRun]) + da = workflow.compute(BackgroundSubtractedIofQ) + return {"transmission": tf, "IofQ": da} + +def save_reduction_plots(res, sample, sample_run_file, wavelength_min, wavelength_max, wavelength_n, q_min, q_max, q_n, output_dir, show=True): + fig, axs = plt.subplots(1, 2, figsize=(8, 4)) + axs[0].set_box_aspect(1) + axs[1].set_box_aspect(1) + title_str = f"{sample} - {os.path.basename(sample_run_file)}" + fig.suptitle(title_str, fontsize=14) + q_bins = sc.linspace("Q", q_min, q_max, q_n, unit="1/angstrom") + x_q = 0.5 * (q_bins.values[:-1] + q_bins.values[1:]) + if res["IofQ"].variances is not None: + yerr = np.sqrt(res["IofQ"].variances) + axs[0].errorbar(x_q, res["IofQ"].values, yerr=yerr, fmt='o', linestyle='none', color='k', alpha=0.5, markerfacecolor='none') + else: + axs[0].scatter(x_q, res["IofQ"].values) + axs[0].set_xlabel("Q (Å$^{-1}$)") + axs[0].set_ylabel("I(Q)") + axs[0].set_xscale("log") + axs[0].set_yscale("log") + wavelength_bins = sc.linspace("wavelength", wavelength_min, wavelength_max, wavelength_n, unit="angstrom") + x_wl = 0.5 * (wavelength_bins.values[:-1] + wavelength_bins.values[1:]) + if res["transmission"].variances is not None: + yerr_tr = np.sqrt(res["transmission"].variances) + axs[1].errorbar(x_wl, res["transmission"].values, yerr=yerr_tr, fmt='^', linestyle='none', color='k', alpha=0.5, markerfacecolor='none') + else: + axs[1].scatter(x_wl, res["transmission"].values) + axs[1].set_xlabel("Wavelength (Å)") + axs[1].set_ylabel("Transmission") + plt.tight_layout() + out_png = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", "_reduced.png")) + fig.savefig(out_png, dpi=300) + if show: + display(fig) + plt.close(fig) + +# ---------------------------- +# Unified Backend Function for Reduction +# ---------------------------- +def perform_reduction_for_sample( + sample_info: dict, + input_dir: str, + output_dir: str, + reduction_params: dict, + background_run_file: str, + empty_beam_file: str, + direct_beam_file: str, + log_func: callable +): + """ + Processes a single sample reduction: + - Finds the necessary run files + - Optionally determines a mask (or finds one automatically) + - Calls the reduction and plotting routines + - Logs all steps via log_func(message) + """ + sample = sample_info.get("SAMPLE", "Unknown") + try: + sample_run_file = find_file(input_dir, str(sample_info["SANS"]), extension=".nxs") + transmission_run_file = find_file(input_dir, str(sample_info["TRANS"]), extension=".nxs") + except Exception as e: + log_func(f"Skipping sample {sample}: {e}") + return None + # Determine mask file. + mask_file = None + mask_candidate = str(sample_info.get("mask", "")).strip() + if mask_candidate: + mask_candidate_file = os.path.join(input_dir, f"{mask_candidate}.xml") + if os.path.exists(mask_candidate_file): + mask_file = mask_candidate_file + if mask_file is None: + try: + mask_file = find_mask_file(input_dir) + log_func(f"Identified mask file: {mask_file} for sample {sample}") + except Exception as e: + log_func(f"Mask file not found for sample {sample}: {e}") + return None + + log_func(f"Reducing sample {sample}...") + try: + res = reduce_loki_batch_preliminary( + sample_run_file=sample_run_file, + transmission_run_file=transmission_run_file, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + mask_files=[mask_file], + wavelength_min=reduction_params["wavelength_min"], + wavelength_max=reduction_params["wavelength_max"], + wavelength_n=reduction_params["wavelength_n"], + q_start=reduction_params["q_start"], + q_stop=reduction_params["q_stop"], + q_n=reduction_params["q_n"] + ) + except Exception as e: + log_func(f"Reduction failed for sample {sample}: {e}") + return None + out_xye = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", ".xye")) + try: + save_xye_pandas(res["IofQ"], out_xye) + log_func(f"Saved reduced data to {out_xye}") + except Exception as e: + log_func(f"Failed to save reduced data for {sample}: {e}") + try: + save_reduction_plots( + res, + sample, + sample_run_file, + reduction_params["wavelength_min"], + reduction_params["wavelength_max"], + reduction_params["wavelength_n"], + reduction_params["q_start"], + reduction_params["q_stop"], + reduction_params["q_n"], + output_dir, + show=True + ) + log_func(f"Saved reduction plot for sample {sample}.") + except Exception as e: + log_func(f"Failed to save reduction plot for {sample}: {e}") + log_func(f"Reduced sample {sample} and saved outputs.") + return res + +# ---------------------------- +# GUI Widgets (Refactored to use Unified Backend) +# ---------------------------- +class SansBatchReductionWidget: + def __init__(self): + self.csv_chooser = FileChooser(select_dir=False) + self.csv_chooser.title = "Select CSV File" + self.csv_chooser.filter_pattern = "*.csv" + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.ebeam_sans_widget = widgets.Text(value="", placeholder="Enter Ebeam SANS run number", description="Ebeam SANS:") + self.ebeam_trans_widget = widgets.Text(value="", placeholder="Enter Ebeam TRANS run number", description="Ebeam TRANS:") + self.wavelength_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.wavelength_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.wavelength_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_start_widget = widgets.FloatText(value=0.01, description="Q start (1/Å):") + self.q_stop_widget = widgets.FloatText(value=0.3, description="Q stop (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + self.load_csv_button = widgets.Button(description="Load CSV") + self.load_csv_button.on_click(self.load_csv) + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + self.clear_log_button = widgets.Button(description="Clear Log") + self.clear_log_button.on_click(lambda _: self.log_output.clear_output()) + self.clear_plots_button = widgets.Button(description="Clear Plots") + self.clear_plots_button.on_click(lambda _: self.plot_output.clear_output()) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.main = widgets.VBox([ + widgets.HBox([self.csv_chooser, self.input_dir_chooser, self.output_dir_chooser]), + widgets.HBox([self.ebeam_sans_widget, self.ebeam_trans_widget]), + widgets.HBox([self.wavelength_min_widget, self.wavelength_max_widget, self.wavelength_n_widget]), + widgets.HBox([self.q_start_widget, self.q_stop_widget, self.q_n_widget]), + self.load_csv_button, + self.table, + widgets.HBox([self.reduce_button, self.clear_log_button, self.clear_plots_button]), + self.log_output, + self.plot_output + ]) + + def load_csv(self, _): + csv_path = self.csv_chooser.selected + if not csv_path or not os.path.exists(csv_path): + with self.log_output: + print("CSV file not selected or does not exist.") + return + df = pd.read_csv(csv_path) + self.table.data = df + with self.log_output: + print(f"Loaded reduction table with {len(df)} rows from {csv_path}.") + + def run_reduction(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct-beam file:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + return + try: + background_run_file = find_file(input_dir, self.ebeam_sans_widget.value, extension=".nxs") + empty_beam_file = find_file(input_dir, self.ebeam_trans_widget.value, extension=".nxs") + with self.log_output: + print("Using empty-beam files:") + print(" Background (Ebeam SANS):", background_run_file) + print(" Empty beam (Ebeam TRANS):", empty_beam_file) + except Exception as e: + with self.log_output: + print("Error finding empty beam files:", e) + return + + reduction_params = { + "wavelength_min": self.wavelength_min_widget.value, + "wavelength_max": self.wavelength_max_widget.value, + "wavelength_n": self.wavelength_n_widget.value, + "q_start": self.q_start_widget.value, + "q_stop": self.q_stop_widget.value, + "q_n": self.q_n_widget.value + } + + df = self.table.data + for idx, row in df.iterrows(): + perform_reduction_for_sample( + sample_info=row, + input_dir=input_dir, + output_dir=output_dir, + reduction_params=reduction_params, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + log_func=lambda msg: print(msg) + ) + + @property + def widget(self): + return self.main + +class SemiAutoReductionWidget: + def __init__(self): + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.scan_button = widgets.Button(description="Scan Directory") + self.scan_button.on_click(self.scan_directory) + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + self.add_row_button = widgets.Button(description="Add Row") + self.add_row_button.on_click(self.add_row) + self.delete_row_button = widgets.Button(description="Delete Last Row") + self.delete_row_button.on_click(self.delete_last_row) + self.lambda_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.lambda_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.lambda_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_min_widget = widgets.FloatText(value=0.01, description="Qmin (1/Å):") + self.q_max_widget = widgets.FloatText(value=0.3, description="Qmax (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + self.empty_beam_sans_text = widgets.Text(value="", description="Ebeam SANS:", disabled=True) + self.empty_beam_trans_text = widgets.Text(value="", description="Ebeam TRANS:", disabled=True) + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + self.clear_log_button = widgets.Button(description="Clear Log") + self.clear_log_button.on_click(lambda _: self.log_output.clear_output()) + self.clear_plots_button = widgets.Button(description="Clear Plots") + self.clear_plots_button.on_click(lambda _: self.plot_output.clear_output()) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.processed = set() + self.main = widgets.VBox([ + widgets.HBox([self.input_dir_chooser, self.output_dir_chooser]), + self.scan_button, + self.table, + widgets.HBox([self.add_row_button, self.delete_row_button]), + widgets.HBox([self.lambda_min_widget, self.lambda_max_widget, self.lambda_n_widget]), + widgets.HBox([self.q_min_widget, self.q_max_widget, self.q_n_widget]), + widgets.HBox([self.empty_beam_sans_text, self.empty_beam_trans_text]), + widgets.HBox([self.reduce_button, self.clear_log_button, self.clear_plots_button]), + self.log_output, + self.plot_output + ]) + + def add_row(self, _): + df = self.table.data + new_row = {col: "" for col in df.columns} if not df.empty else {'SAMPLE': '', 'SANS': '', 'TRANS': ''} + df = df.append(new_row, ignore_index=True) + self.table.data = df + + def delete_last_row(self, _): + df = self.table.data + if not df.empty: + self.table.data = df.iloc[:-1] + + def scan_directory(self, _): + self.log_output.clear_output() + input_dir = self.input_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Invalid input folder.") + return + nxs_files = glob.glob(os.path.join(input_dir, "*.nxs")) + groups = {} + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runlabel' not in details or 'runtype' not in details: + continue + runlabel = details['runlabel'] + runtype = details['runtype'].lower() + run_number = extract_run_number(os.path.basename(f)) + if runlabel not in groups: + groups[runlabel] = {} + groups[runlabel][runtype] = run_number + table_rows = [] + for runlabel, d in groups.items(): + if 'sans' in d and 'trans' in d: + table_rows.append({'SAMPLE': runlabel, 'SANS': d['sans'], 'TRANS': d['trans']}) + df = pd.DataFrame(table_rows) + self.table.data = df + with self.log_output: + print(f"Scanned {len(nxs_files)} files. Found {len(df)} reduction entries.") + ebeam_sans_files = [] + ebeam_trans_files = [] + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runtype' in details: + if details['runtype'].lower() == 'ebeam_sans': + ebeam_sans_files.append(f) + elif details['runtype'].lower() == 'ebeam_trans': + ebeam_trans_files.append(f) + if ebeam_sans_files: + ebeam_sans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_sans_text.value = ebeam_sans_files[0] + else: + self.empty_beam_sans_text.value = "" + if ebeam_trans_files: + ebeam_trans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_trans_text.value = ebeam_trans_files[0] + else: + self.empty_beam_trans_text.value = "" + + def run_reduction(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct-beam file:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + return + background_run_file = self.empty_beam_sans_text.value + empty_beam_file = self.empty_beam_trans_text.value + if not background_run_file or not empty_beam_file: + with self.log_output: + print("Empty beam files not found.") + return + + reduction_params = { + "wavelength_min": self.lambda_min_widget.value, + "wavelength_max": self.lambda_max_widget.value, + "wavelength_n": self.lambda_n_widget.value, + "q_start": self.q_min_widget.value, + "q_stop": self.q_max_widget.value, + "q_n": self.q_n_widget.value + } + + df = self.table.data.copy() + for idx, row in df.iterrows(): + perform_reduction_for_sample( + sample_info=row, + input_dir=input_dir, + output_dir=output_dir, + reduction_params=reduction_params, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + log_func=lambda msg: print(msg) + ) + + @property + def widget(self): + return self.main + +class AutoReductionWidget: + def __init__(self): + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.start_stop_button = widgets.Button(description="Start") + self.start_stop_button.on_click(self.toggle_running) + self.status_label = widgets.Label(value="Stopped") + self.table = DataGrid(pd.DataFrame([]), editable=False, auto_fit_columns=True) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.running = False + self.thread = None + self.processed = set() + self.empty_beam_sans = None + self.empty_beam_trans = None + self.main = widgets.VBox([ + widgets.HBox([self.input_dir_chooser, self.output_dir_chooser]), + widgets.HBox([self.start_stop_button, self.status_label]), + self.table, + self.log_output, + self.plot_output + ]) + + def toggle_running(self, _): + if not self.running: + self.running = True + self.start_stop_button.description = "Stop" + self.status_label.value = "Running" + self.thread = threading.Thread(target=self.background_loop, daemon=True) + self.thread.start() + else: + self.running = False + self.start_stop_button.description = "Start" + self.status_label.value = "Stopped" + + def background_loop(self): + while self.running: + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Invalid input folder. Waiting for valid selection...") + time.sleep(60) + continue + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Invalid output folder. Waiting for valid selection...") + time.sleep(60) + continue + nxs_files = glob.glob(os.path.join(input_dir, "*.nxs")) + groups = {} + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runlabel' not in details or 'runtype' not in details: + continue + runlabel = details['runlabel'] + runtype = details['runtype'].lower() + run_number = extract_run_number(os.path.basename(f)) + if runlabel not in groups: + groups[runlabel] = {} + groups[runlabel][runtype] = run_number + table_rows = [] + for runlabel, d in groups.items(): + if 'sans' in d and 'trans' in d: + table_rows.append({'SAMPLE': runlabel, 'SANS': d['sans'], 'TRANS': d['trans']}) + df = pd.DataFrame(table_rows) + self.table.data = df + with self.log_output: + print(f"Scanned {len(nxs_files)} files. Found {len(df)} reduction entries.") + ebeam_sans_files = [] + ebeam_trans_files = [] + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runtype' in details: + if details['runtype'].lower() == 'ebeam_sans': + ebeam_sans_files.append(f) + elif details['runtype'].lower() == 'ebeam_trans': + ebeam_trans_files.append(f) + if ebeam_sans_files: + ebeam_sans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_sans = ebeam_sans_files[0] + else: + self.empty_beam_sans = None + if ebeam_trans_files: + ebeam_trans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_trans = ebeam_trans_files[0] + else: + self.empty_beam_trans = None + try: + direct_beam_file = find_direct_beam(input_dir) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + time.sleep(60) + continue + for index, row in df.iterrows(): + key = (row["SAMPLE"], row["SANS"], row["TRANS"]) + if key in self.processed: + continue + try: + sample_run_file = find_file(input_dir, row["SANS"], extension=".nxs") + transmission_run_file = find_file(input_dir, row["TRANS"], extension=".nxs") + except Exception as e: + with self.log_output: + print(f"Skipping sample {row['SAMPLE']}: {e}") + continue + try: + mask_file = find_mask_file(input_dir) + with self.log_output: + print(f"Using mask file: {mask_file} for sample {row['SAMPLE']}") + except Exception as e: + with self.log_output: + print(f"Mask file not found for sample {row['SAMPLE']}: {e}") + continue + if not self.empty_beam_sans or not self.empty_beam_trans: + with self.log_output: + print("Empty beam files not found, skipping reduction for sample", row["SAMPLE"]) + continue + with self.log_output: + print(f"Reducing sample {row['SAMPLE']}...") + reduction_params = { + "wavelength_min": 1.0, + "wavelength_max": 13.0, + "wavelength_n": 201, + "q_start": 0.01, + "q_stop": 0.3, + "q_n": 101 + } + perform_reduction_for_sample( + sample_info=row, + input_dir=input_dir, + output_dir=output_dir, + reduction_params=reduction_params, + background_run_file=self.empty_beam_sans, + empty_beam_file=self.empty_beam_trans, + direct_beam_file=direct_beam_file, + log_func=lambda msg: print(msg) + ) + self.processed.add(key) + time.sleep(60) + + @property + def widget(self): + return self.main + +# ---------------------------- +# Direct Beam Functionality and Widget (unchanged) +# ---------------------------- +def compute_direct_beam_local( + mask: str, + sample_sans: str, + background_sans: str, + sample_trans: str, + background_trans: str, + empty_beam: str, + local_Iq_theory: str, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + n_wavelength_bins: int = 50, + n_wavelength_bands: int = 50 +) -> dict: + workflow = loki.LokiAtLarmorWorkflow() + workflow = sans.with_pixel_mask_filenames(workflow, masks=[mask]) + workflow[NeXusDetectorName] = 'larmor_detector' + wl_min = sc.scalar(wavelength_min, unit='angstrom') + wl_max = sc.scalar(wavelength_max, unit='angstrom') + workflow[WavelengthBins] = sc.linspace('wavelength', wl_min, wl_max, n_wavelength_bins + 1) + workflow[WavelengthBands] = sc.linspace('wavelength', wl_min, wl_max, n_wavelength_bands + 1) + workflow[CorrectForGravity] = True + workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound + workflow[ReturnEvents] = False + workflow[QBins] = sc.linspace(dim='Q', start=0.01, stop=0.3, num=101, unit='1/angstrom') + workflow[Filename[SampleRun]] = sample_sans + workflow[Filename[BackgroundRun]] = background_sans + workflow[Filename[TransmissionRun[SampleRun]]] = sample_trans + workflow[Filename[TransmissionRun[BackgroundRun]]] = background_trans + workflow[Filename[EmptyBeamRun]] = empty_beam + center = sans.beam_center_from_center_of_mass(workflow) + print("Computed beam center:", center) + workflow[BeamCenter] = center + Iq_theory = sc.io.load_hdf5(local_Iq_theory) + f = interp1d(Iq_theory, 'Q') + I0 = f(sc.midpoints(workflow.compute(QBins))).data[0] + print("Computed I0:", I0) + results = sans.direct_beam(workflow=workflow, I0=I0, niter=6) + iofq_full = results[-1]['iofq_full'] + iofq_bands = results[-1]['iofq_bands'] + direct_beam_function = results[-1]['direct_beam'] + pp.plot( + {'reference': Iq_theory, 'data': iofq_full}, + color={'reference': 'darkgrey', 'data': 'C0'}, + norm='log', + ) + print("Plotted full-range result vs. theoretical reference.") + return { + 'direct_beam_function': direct_beam_function, + 'iofq_full': iofq_full, + 'Iq_theory': Iq_theory, + } + +class DirectBeamWidget: + def __init__(self): + self.mask_text = widgets.Text(value="", placeholder="Enter mask file path", description="Mask:") + self.sample_sans_text = widgets.Text(value="", placeholder="Enter sample SANS file path", description="Sample SANS:") + self.background_sans_text = widgets.Text(value="", placeholder="Enter background SANS file path", description="Background SANS:") + self.sample_trans_text = widgets.Text(value="", placeholder="Enter sample TRANS file path", description="Sample TRANS:") + self.background_trans_text = widgets.Text(value="", placeholder="Enter background TRANS file path", description="Background TRANS:") + self.empty_beam_text = widgets.Text(value="", placeholder="Enter empty beam file path", description="Empty Beam:") + self.local_Iq_theory_text = widgets.Text(value="", placeholder="Enter I(q) Theory file path", description="I(q) Theory:") + self.db_wavelength_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.db_wavelength_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.db_n_wavelength_bins_widget = widgets.IntText(value=50, description="λ n_bins:") + self.db_n_wavelength_bands_widget = widgets.IntText(value=50, description="λ n_bands:") + self.compute_button = widgets.Button(description="Compute Direct Beam") + self.compute_button.on_click(self.compute_direct_beam) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.main = widgets.VBox([ + self.mask_text, + self.sample_sans_text, + self.background_sans_text, + self.sample_trans_text, + self.background_trans_text, + self.empty_beam_text, + self.local_Iq_theory_text, + widgets.HBox([ + self.db_wavelength_min_widget, + self.db_wavelength_max_widget, + self.db_n_wavelength_bins_widget, + self.db_n_wavelength_bands_widget + ]), + self.compute_button, + self.log_output, + self.plot_output + ]) + + def compute_direct_beam(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + mask = self.mask_text.value + sample_sans = self.sample_sans_text.value + background_sans = self.background_sans_text.value + sample_trans = self.sample_trans_text.value + background_trans = self.background_trans_text.value + empty_beam = self.empty_beam_text.value + local_Iq_theory = self.local_Iq_theory_text.value + wl_min = self.db_wavelength_min_widget.value + wl_max = self.db_wavelength_max_widget.value + n_bins = self.db_n_wavelength_bins_widget.value + n_bands = self.db_n_wavelength_bands_widget.value + with self.log_output: + print("Computing direct beam with:") + print(" Mask:", mask) + print(" Sample SANS:", sample_sans) + print(" Background SANS:", background_sans) + print(" Sample TRANS:", sample_trans) + print(" Background TRANS:", background_trans) + print(" Empty Beam:", empty_beam) + print(" I(q) Theory:", local_Iq_theory) + print(" λ min:", wl_min, "λ max:", wl_max, "n_bins:", n_bins, "n_bands:", n_bands) + try: + results = compute_direct_beam_local( + mask, + sample_sans, + background_sans, + sample_trans, + background_trans, + empty_beam, + local_Iq_theory, + wavelength_min=wl_min, + wavelength_max=wl_max, + n_wavelength_bins=n_bins, + n_wavelength_bands=n_bands + ) + with self.log_output: + print("Direct beam computation complete.") + except Exception as e: + with self.log_output: + print("Error computing direct beam:", e) + + @property + def widget(self): + return self.main + +# ---------------------------- +# Build the Tabbed Widget +# ---------------------------- +reduction_widget = SansBatchReductionWidget().widget +direct_beam_widget = DirectBeamWidget().widget +semi_auto_reduction_widget = SemiAutoReductionWidget().widget +auto_reduction_widget = AutoReductionWidget().widget + +tabs = widgets.Tab(children=[direct_beam_widget, reduction_widget, semi_auto_reduction_widget, auto_reduction_widget]) +tabs.set_title(0, "Direct Beam") +tabs.set_title(1, "Reduction (Manual)") +tabs.set_title(2, "Reduction (Smart)") +tabs.set_title(3, "Reduction (Auto)") + +# display(tabs) diff --git a/src/ess/loki/batch-gui-legacy/tabwidgetauto-040325.py b/src/ess/loki/batch-gui-legacy/tabwidgetauto-040325.py new file mode 100644 index 00000000..cad8b49f --- /dev/null +++ b/src/ess/loki/batch-gui-legacy/tabwidgetauto-040325.py @@ -0,0 +1,926 @@ +import os +import glob +import re +import h5py +import pandas as pd +import scipp as sc +import matplotlib.pyplot as plt +import numpy as np +import ipywidgets as widgets +from ipydatagrid import DataGrid +from IPython.display import display +from ipyfilechooser import FileChooser +from ess import sans +from ess import loki +from ess.sans.types import * +from scipp.scipy.interpolate import interp1d +import plopp as pp # used for plotting in direct beam section +import threading +import time + + +# ---------------------------- +# Reduction Functionality +# ---------------------------- +def reduce_loki_batch_preliminary( + sample_run_file: str, + transmission_run_file: str, + background_run_file: str, + empty_beam_file: str, + direct_beam_file: str, + mask_files: list = None, + correct_for_gravity: bool = True, + uncertainty_mode = UncertaintyBroadcastMode.upper_bound, + return_events: bool = False, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + wavelength_n: int = 201, + q_start: float = 0.01, + q_stop: float = 0.3, + q_n: int = 101 +): + if mask_files is None: + mask_files = [] + # Define wavelength and Q bins. + wavelength_bins = sc.linspace("wavelength", wavelength_min, wavelength_max, wavelength_n, unit="angstrom") + q_bins = sc.linspace("Q", q_start, q_stop, q_n, unit="1/angstrom") + # Initialize the workflow. + workflow = loki.LokiAtLarmorWorkflow() + if mask_files: + workflow = sans.with_pixel_mask_filenames(workflow, masks=mask_files) + workflow[NeXusDetectorName] = "larmor_detector" + workflow[WavelengthBins] = wavelength_bins + workflow[QBins] = q_bins + workflow[CorrectForGravity] = correct_for_gravity + workflow[UncertaintyBroadcastMode] = uncertainty_mode + workflow[ReturnEvents] = return_events + workflow[Filename[BackgroundRun]] = background_run_file + workflow[Filename[TransmissionRun[BackgroundRun]]] = transmission_run_file + workflow[Filename[EmptyBeamRun]] = empty_beam_file + workflow[DirectBeamFilename] = direct_beam_file + workflow[Filename[SampleRun]] = sample_run_file + workflow[Filename[TransmissionRun[SampleRun]]] = transmission_run_file + center = sans.beam_center_from_center_of_mass(workflow) + workflow[BeamCenter] = center + tf = workflow.compute(TransmissionFraction[SampleRun]) + da = workflow.compute(BackgroundSubtractedIofQ) + return {"transmission": tf, "IofQ": da} + +def find_file(work_dir, run_number, extension=".nxs"): + pattern = os.path.join(work_dir, f"*{run_number}*{extension}") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find file matching pattern {pattern}") + +def find_direct_beam(work_dir): + pattern = os.path.join(work_dir, "*direct-beam*.h5") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find direct-beam file matching pattern {pattern}") + +def find_mask_file(work_dir): + pattern = os.path.join(work_dir, "*mask*.xml") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find mask file matching pattern {pattern}") + +def save_xye_pandas(data_array, filename): + q_vals = data_array.coords["Q"].values + i_vals = data_array.values + if len(q_vals) != len(i_vals): + q_vals = 0.5 * (q_vals[:-1] + q_vals[1:]) + if data_array.variances is not None: + err_vals = np.sqrt(data_array.variances) + if len(err_vals) != len(i_vals): + err_vals = 0.5 * (err_vals[:-1] + err_vals[1:]) + else: + err_vals = np.zeros_like(i_vals) + df = pd.DataFrame({"Q": q_vals, "I(Q)": i_vals, "Error": err_vals}) + df.to_csv(filename, sep=" ", index=False, header=True) + +# ---------------------------- +# Helper Functions for Semi-Auto Reduction +# ---------------------------- +def extract_run_number(filename): + m = re.search(r'(\d{4,})', filename) + if m: + return m.group(1) + return "" + +def parse_nx_details(filepath): + details = {} + with h5py.File(filepath, 'r') as f: + if 'nicos_details' in f['entry']: + grp = f['entry']['nicos_details'] + if 'runlabel' in grp: + val = grp['runlabel'][()] + details['runlabel'] = val.decode('utf8') if isinstance(val, bytes) else str(val) + if 'runtype' in grp: + val = grp['runtype'][()] + details['runtype'] = val.decode('utf8') if isinstance(val, bytes) else str(val) + return details + +# ---------------------------- +# Common Plotting Function +# ---------------------------- + +def save_reduction_plots(res, sample, sample_run_file, lam_min, lam_max, lam_n, q_min, q_max, q_n, output_dir, show=True): + """ + Creates a figure with 1 row x 2 columns: + - Left subplot: I(Q) (scatter with errorbars, log-log). + - Right subplot: Transmission fraction vs. wavelength (scatter with errorbars). + A single centered title (filename and sample ID) is added above the subplots. + The figure is saved to output_dir with a filename based on sample_run_file. + If show is True, the figure is displayed in the current output. + """ + # Create a figure with two subplots side by side. + fig, axs = plt.subplots(1, 2, figsize=(8, 4)) + + # Force each axis to be square (requires Matplotlib>=3.3) + axs[0].set_box_aspect(1) + axs[1].set_box_aspect(1) + + # Set a centered overall title (filename and sample) + title_str = f"{sample} - {os.path.basename(sample_run_file)}" + fig.suptitle(title_str, fontsize=14) + + # Subplot A: I(Q) + q_bins = sc.linspace("Q", q_min, q_max, q_n, unit="1/angstrom") + x_q = 0.5 * (q_bins.values[:-1] + q_bins.values[1:]) + if res["IofQ"].variances is not None: + yerr = np.sqrt(res["IofQ"].variances) + axs[0].errorbar(x_q, res["IofQ"].values, yerr=yerr, fmt='o', linestyle='none', color='k', alpha=0.5, markerfacecolor='none') + else: + axs[0].scatter(x_q, res["IofQ"].values) + axs[0].set_xlabel("Q (Å$^{-1}$)") + axs[0].set_ylabel("I(Q)") + axs[0].set_xscale("log") + axs[0].set_yscale("log") + + # Subplot B: Transmission vs. Wavelength + wavelength_bins = sc.linspace("wavelength", lam_min, lam_max, lam_n, unit="angstrom") + x_wl = 0.5 * (wavelength_bins.values[:-1] + wavelength_bins.values[1:]) + if res["transmission"].variances is not None: + yerr_tr = np.sqrt(res["transmission"].variances) + axs[1].errorbar(x_wl, res["transmission"].values, yerr=yerr_tr, fmt='^', linestyle='none', color='k', alpha=0.5, markerfacecolor='none') + else: + axs[1].scatter(x_wl, res["transmission"].values) + axs[1].set_xlabel("Wavelength (Å)") + axs[1].set_ylabel("Transmission") + + # Adjust layout to leave room for the suptitle. + plt.tight_layout()#rect=[0, 0, 1, 0.90]) + + # Save the figure. + out_png = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", "_reduced.png")) + fig.savefig(out_png, dpi=300) + + # Display the figure in the GUI if requested. + if show: + display(fig) + + # Close the figure so memory is released. + plt.close(fig) + + + + +# ---------------------------- +# SansBatchReductionWidget (Updated) +# ---------------------------- +class SansBatchReductionWidget: + def __init__(self): + # CSV chooser for pre-loaded reduction table. + self.csv_chooser = FileChooser(select_dir=False) + self.csv_chooser.title = "Select CSV File" + self.csv_chooser.filter_pattern = "*.csv" + # Folder choosers. + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + # Empty-beam run number widgets. + self.ebeam_sans_widget = widgets.Text(value="", placeholder="Enter Ebeam SANS run number", description="Ebeam SANS:") + self.ebeam_trans_widget = widgets.Text(value="", placeholder="Enter Ebeam TRANS run number", description="Ebeam TRANS:") + # Reduction parameter widgets. + self.wavelength_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.wavelength_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.wavelength_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_start_widget = widgets.FloatText(value=0.01, description="Q start (1/Å):") + self.q_stop_widget = widgets.FloatText(value=0.3, description="Q stop (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + # Button to load CSV. + self.load_csv_button = widgets.Button(description="Load CSV") + self.load_csv_button.on_click(self.load_csv) + # DataGrid for the reduction table. + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + # Reduction and clear buttons. + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + self.clear_log_button = widgets.Button(description="Clear Log") + self.clear_log_button.on_click(lambda _: self.log_output.clear_output()) + self.clear_plots_button = widgets.Button(description="Clear Plots") + self.clear_plots_button.on_click(lambda _: self.plot_output.clear_output()) + # Output widgets. + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + # Build layout. + self.main = widgets.VBox([ + widgets.HBox([self.csv_chooser, self.input_dir_chooser, self.output_dir_chooser]), + widgets.HBox([self.ebeam_sans_widget, self.ebeam_trans_widget]), + widgets.HBox([self.wavelength_min_widget, self.wavelength_max_widget, self.wavelength_n_widget]), + widgets.HBox([self.q_start_widget, self.q_stop_widget, self.q_n_widget]), + self.load_csv_button, + self.table, + widgets.HBox([self.reduce_button, self.clear_log_button, self.clear_plots_button]), + self.log_output, + self.plot_output + ]) + + def load_csv(self, _): + csv_path = self.csv_chooser.selected + if not csv_path or not os.path.exists(csv_path): + with self.log_output: + print("CSV file not selected or does not exist.") + return + df = pd.read_csv(csv_path) + self.table.data = df + with self.log_output: + print(f"Loaded reduction table with {len(df)} rows from {csv_path}.") + + def run_reduction(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct-beam file:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + return + try: + background_run_file = find_file(input_dir, self.ebeam_sans_widget.value, extension=".nxs") + empty_beam_file = find_file(input_dir, self.ebeam_trans_widget.value, extension=".nxs") + with self.log_output: + print("Using empty-beam files:") + print(" Background (Ebeam SANS):", background_run_file) + print(" Empty beam (Ebeam TRANS):", empty_beam_file) + except Exception as e: + with self.log_output: + print("Error finding empty beam files:", e) + return + wl_min = self.wavelength_min_widget.value + wl_max = self.wavelength_max_widget.value + wl_n = self.wavelength_n_widget.value + q_start = self.q_start_widget.value + q_stop = self.q_stop_widget.value + q_n = self.q_n_widget.value + + df = self.table.data + for idx, row in df.iterrows(): + sample = row["SAMPLE"] + try: + sample_run_file = find_file(input_dir, str(row["SANS"]), extension=".nxs") + transmission_run_file = find_file(input_dir, str(row["TRANS"]), extension=".nxs") + except Exception as e: + with self.log_output: + print(f"Skipping sample {sample}: {e}") + continue + mask_candidate = str(row.get("mask", "")).strip() + mask_file = None + if mask_candidate: + mask_file_candidate = os.path.join(input_dir, f"{mask_candidate}.xml") + if os.path.exists(mask_file_candidate): + mask_file = mask_file_candidate + if mask_file is None: + try: + mask_file = find_mask_file(input_dir) + with self.log_output: + print(f"Identified mask file: {mask_file} for sample {sample}") + except Exception as e: + with self.log_output: + print(f"Mask file not found for sample {sample}: {e}") + continue + with self.log_output: + print(f"Reducing sample {sample}...") + try: + res = reduce_loki_batch_preliminary( + sample_run_file=sample_run_file, + transmission_run_file=transmission_run_file, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + mask_files=[mask_file], + wavelength_min=wl_min, + wavelength_max=wl_max, + wavelength_n=wl_n, + q_start=q_start, + q_stop=q_stop, + q_n=q_n + ) + except Exception as e: + with self.log_output: + print(f"Reduction failed for sample {sample}: {e}") + continue + out_xye = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", ".xye")) + try: + save_xye_pandas(res["IofQ"], out_xye) + with self.log_output: + print(f"Saved reduced data to {out_xye}") + except Exception as e: + with self.log_output: + print(f"Failed to save reduced data for {sample}: {e}") + try: + with self.plot_output: + save_reduction_plots(res, sample, sample_run_file, lam_min, lam_max, lam_n, q_min, q_max, q_n, output_dir, show=True) + with self.log_output: + print(f"Saved combined reduction plot for sample {sample}.") + except Exception as e: + with self.log_output: + print(f"Failed to save reduction plot for {sample}: {e}") + + #try: + # save_reduction_plots(res, sample, sample_run_file, wl_min, wl_max, wl_n, q_start, q_stop, q_n, output_dir)#, n_bands=5) + # with self.log_output: + # print(f"Saved combined reduction plot for sample {sample}.") + #except Exception as e: + # with self.log_output: + # print(f"Failed to save reduction plot for {sample}: {e}") + with self.log_output: + print(f"Reduced sample {sample} and saved outputs.") + + @property + def widget(self): + return self.main + +# ---------------------------- +# Semi-Auto Reduction Widget (unchanged) +# ---------------------------- +class SemiAutoReductionWidget: + def __init__(self): + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.scan_button = widgets.Button(description="Scan Directory") + self.scan_button.on_click(self.scan_directory) + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + self.add_row_button = widgets.Button(description="Add Row") + self.add_row_button.on_click(self.add_row) + self.delete_row_button = widgets.Button(description="Delete Last Row") + self.delete_row_button.on_click(self.delete_last_row) + self.lambda_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.lambda_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.lambda_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_min_widget = widgets.FloatText(value=0.01, description="Qmin (1/Å):") + self.q_max_widget = widgets.FloatText(value=0.3, description="Qmax (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + self.empty_beam_sans_text = widgets.Text(value="", description="Ebeam SANS:", disabled=True) + self.empty_beam_trans_text = widgets.Text(value="", description="Ebeam TRANS:", disabled=True) + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + self.clear_log_button = widgets.Button(description="Clear Log") + self.clear_log_button.on_click(lambda _: self.log_output.clear_output()) + self.clear_plots_button = widgets.Button(description="Clear Plots") + self.clear_plots_button.on_click(lambda _: self.plot_output.clear_output()) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + + # Add the processed set here: + self.processed = set() + + self.main = widgets.VBox([ + widgets.HBox([self.input_dir_chooser, self.output_dir_chooser]), + self.scan_button, + self.table, + widgets.HBox([self.add_row_button, self.delete_row_button]), + widgets.HBox([self.lambda_min_widget, self.lambda_max_widget, self.lambda_n_widget]), + widgets.HBox([self.q_min_widget, self.q_max_widget, self.q_n_widget]), + widgets.HBox([self.empty_beam_sans_text, self.empty_beam_trans_text]), + widgets.HBox([self.reduce_button, self.clear_log_button, self.clear_plots_button]), + self.log_output, + self.plot_output + ]) + + + def add_row(self, _): + df = self.table.data + if df.empty: + new_row = {'SAMPLE': '', 'SANS': '', 'TRANS': ''} + else: + new_row = {col: "" for col in df.columns} + df = df.append(new_row, ignore_index=True) + self.table.data = df + + def delete_last_row(self, _): + df = self.table.data + if not df.empty: + df = df.iloc[:-1] + self.table.data = df + + def scan_directory(self, _): + self.log_output.clear_output() + input_dir = self.input_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Invalid input folder.") + return + nxs_files = glob.glob(os.path.join(input_dir, "*.nxs")) + groups = {} + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runlabel' not in details or 'runtype' not in details: + continue + runlabel = details['runlabel'] + runtype = details['runtype'].lower() + run_number = extract_run_number(os.path.basename(f)) + if runlabel not in groups: + groups[runlabel] = {} + groups[runlabel][runtype] = run_number + table_rows = [] + for runlabel, d in groups.items(): + if 'sans' in d and 'trans' in d: + table_rows.append({'SAMPLE': runlabel, 'SANS': d['sans'], 'TRANS': d['trans']}) + df = pd.DataFrame(table_rows) + self.table.data = df + with self.log_output: + print(f"Scanned {len(nxs_files)} files. Found {len(df)} reduction entries.") + ebeam_sans_files = [] + ebeam_trans_files = [] + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runtype' in details: + if details['runtype'].lower() == 'ebeam_sans': + ebeam_sans_files.append(f) + elif details['runtype'].lower() == 'ebeam_trans': + ebeam_trans_files.append(f) + if ebeam_sans_files: + ebeam_sans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_sans_text.value = ebeam_sans_files[0] + else: + self.empty_beam_sans_text.value = "" + if ebeam_trans_files: + ebeam_trans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_trans_text.value = ebeam_trans_files[0] + else: + self.empty_beam_trans_text.value = "" + + def run_reduction(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct-beam file:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + return + background_run_file = self.empty_beam_sans_text.value + empty_beam_file = self.empty_beam_trans_text.value + if not background_run_file or not empty_beam_file: + with self.log_output: + print("Empty beam files not found.") + return + lam_min = self.lambda_min_widget.value + lam_max = self.lambda_max_widget.value + lam_n = self.lambda_n_widget.value + q_min = self.q_min_widget.value + q_max = self.q_max_widget.value + q_n = self.q_n_widget.value + + #df = self.table.data + df = self.table.data.copy() + for idx, row in df.iterrows(): + sample = row["SAMPLE"] + sans_run = row["SANS"] + trans_run = row["TRANS"] + try: + sample_run_file = find_file(input_dir, sans_run, extension=".nxs") + transmission_run_file = find_file(input_dir, trans_run, extension=".nxs") + except Exception as e: + with self.log_output: + print(f"Skipping sample {sample}: {e}") + continue + try: + mask_file = find_mask_file(input_dir) + with self.log_output: + print(f"Using mask file: {mask_file} for sample {sample}") + except Exception as e: + with self.log_output: + print(f"Mask file not found for sample {sample}: {e}") + continue + with self.log_output: + print(f"Reducing sample {sample}...") + try: + res = reduce_loki_batch_preliminary( + sample_run_file=sample_run_file, + transmission_run_file=transmission_run_file, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + mask_files=[mask_file], + wavelength_min=lam_min, + wavelength_max=lam_max, + wavelength_n=lam_n, + q_start=q_min, + q_stop=q_max, + q_n=q_n + ) + except Exception as e: + with self.log_output: + print(f"Reduction failed for sample {sample}: {e}") + continue + out_xye = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", ".xye")) + try: + save_xye_pandas(res["IofQ"], out_xye) + with self.log_output: + print(f"Saved reduced data to {out_xye}") + except Exception as e: + with self.log_output: + print(f"Failed to save reduced data for {sample}: {e}") + try: + save_reduction_plots(res, sample, sample_run_file, lam_min, lam_max, lam_n, q_min, q_max, q_n, output_dir)#, n_bands=5) + with self.log_output: + print(f"Saved combined reduction plot for sample {sample}.") + except Exception as e: + with self.log_output: + print(f"Failed to save reduction plot for {sample}: {e}") + with self.log_output: + print(f"Reduced sample {sample} and saved outputs.") + self.processed.add((row["SAMPLE"], row["SANS"], row["TRANS"])) + #time.sleep(1) # small delay between rows + #time.sleep(60) + + @property + def widget(self): + return self.main + +# ---------------------------- +# Direct Beam Functionality and Widget (unchanged) +# ---------------------------- +def compute_direct_beam_local( + mask: str, + sample_sans: str, + background_sans: str, + sample_trans: str, + background_trans: str, + empty_beam: str, + local_Iq_theory: str, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + n_wavelength_bins: int = 50, + n_wavelength_bands: int = 50 +) -> dict: + workflow = loki.LokiAtLarmorWorkflow() + workflow = sans.with_pixel_mask_filenames(workflow, masks=[mask]) + workflow[NeXusDetectorName] = 'larmor_detector' + wl_min = sc.scalar(wavelength_min, unit='angstrom') + wl_max = sc.scalar(wavelength_max, unit='angstrom') + workflow[WavelengthBins] = sc.linspace('wavelength', wl_min, wl_max, n_wavelength_bins + 1) + workflow[WavelengthBands] = sc.linspace('wavelength', wl_min, wl_max, n_wavelength_bands + 1) + workflow[CorrectForGravity] = True + workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound + workflow[ReturnEvents] = False + workflow[QBins] = sc.linspace(dim='Q', start=0.01, stop=0.3, num=101, unit='1/angstrom') + workflow[Filename[SampleRun]] = sample_sans + workflow[Filename[BackgroundRun]] = background_sans + workflow[Filename[TransmissionRun[SampleRun]]] = sample_trans + workflow[Filename[TransmissionRun[BackgroundRun]]] = background_trans + workflow[Filename[EmptyBeamRun]] = empty_beam + center = sans.beam_center_from_center_of_mass(workflow) + print("Computed beam center:", center) + workflow[BeamCenter] = center + Iq_theory = sc.io.load_hdf5(local_Iq_theory) + f = interp1d(Iq_theory, 'Q') + I0 = f(sc.midpoints(workflow.compute(QBins))).data[0] + print("Computed I0:", I0) + results = sans.direct_beam(workflow=workflow, I0=I0, niter=6) + iofq_full = results[-1]['iofq_full'] + iofq_bands = results[-1]['iofq_bands'] + direct_beam_function = results[-1]['direct_beam'] + pp.plot( + {'reference': Iq_theory, 'data': iofq_full}, + color={'reference': 'darkgrey', 'data': 'C0'}, + norm='log', + ) + print("Plotted full-range result vs. theoretical reference.") + return { + 'direct_beam_function': direct_beam_function, + 'iofq_full': iofq_full, + 'Iq_theory': Iq_theory, + } + +class DirectBeamWidget: + def __init__(self): + self.mask_text = widgets.Text(value="", placeholder="Enter mask file path", description="Mask:") + self.sample_sans_text = widgets.Text(value="", placeholder="Enter sample SANS file path", description="Sample SANS:") + self.background_sans_text = widgets.Text(value="", placeholder="Enter background SANS file path", description="Background SANS:") + self.sample_trans_text = widgets.Text(value="", placeholder="Enter sample TRANS file path", description="Sample TRANS:") + self.background_trans_text = widgets.Text(value="", placeholder="Enter background TRANS file path", description="Background TRANS:") + self.empty_beam_text = widgets.Text(value="", placeholder="Enter empty beam file path", description="Empty Beam:") + self.local_Iq_theory_text = widgets.Text(value="", placeholder="Enter I(q) Theory file path", description="I(q) Theory:") + self.db_wavelength_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.db_wavelength_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.db_n_wavelength_bins_widget = widgets.IntText(value=50, description="λ n_bins:") + self.db_n_wavelength_bands_widget = widgets.IntText(value=50, description="λ n_bands:") + self.compute_button = widgets.Button(description="Compute Direct Beam") + self.compute_button.on_click(self.compute_direct_beam) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.main = widgets.VBox([ + self.mask_text, + self.sample_sans_text, + self.background_sans_text, + self.sample_trans_text, + self.background_trans_text, + self.empty_beam_text, + self.local_Iq_theory_text, + widgets.HBox([ + self.db_wavelength_min_widget, + self.db_wavelength_max_widget, + self.db_n_wavelength_bins_widget, + self.db_n_wavelength_bands_widget + ]), + self.compute_button, + self.log_output, + self.plot_output + ]) + + def compute_direct_beam(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + mask = self.mask_text.value + sample_sans = self.sample_sans_text.value + background_sans = self.background_sans_text.value + sample_trans = self.sample_trans_text.value + background_trans = self.background_trans_text.value + empty_beam = self.empty_beam_text.value + local_Iq_theory = self.local_Iq_theory_text.value + wl_min = self.db_wavelength_min_widget.value + wl_max = self.db_wavelength_max_widget.value + n_bins = self.db_n_wavelength_bins_widget.value + n_bands = self.db_n_wavelength_bands_widget.value + with self.log_output: + print("Computing direct beam with:") + print(" Mask:", mask) + print(" Sample SANS:", sample_sans) + print(" Background SANS:", background_sans) + print(" Sample TRANS:", sample_trans) + print(" Background TRANS:", background_trans) + print(" Empty Beam:", empty_beam) + print(" I(q) Theory:", local_Iq_theory) + print(" λ min:", wl_min, "λ max:", wl_max, "n_bins:", n_bins, "n_bands:", n_bands) + try: + results = compute_direct_beam_local( + mask, + sample_sans, + background_sans, + sample_trans, + background_trans, + empty_beam, + local_Iq_theory, + wavelength_min=wl_min, + wavelength_max=wl_max, + n_wavelength_bins=n_bins, + n_wavelength_bands=n_bands + ) + with self.log_output: + print("Direct beam computation complete.") + except Exception as e: + with self.log_output: + print("Error computing direct beam:", e) + + @property + def widget(self): + return self.main + +# ---------------------------- +# Auto Reduction Widget (unchanged, with common plotting call) +# ---------------------------- +class AutoReductionWidget: + def __init__(self): + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.start_stop_button = widgets.Button(description="Start") + self.start_stop_button.on_click(self.toggle_running) + self.status_label = widgets.Label(value="Stopped") + self.table = DataGrid(pd.DataFrame([]), editable=False, auto_fit_columns=True) + self.log_output = widgets.Output() + self.running = False + self.thread = None + self.processed = set() # Track already reduced entries. + self.empty_beam_sans = None + self.empty_beam_trans = None + self.main = widgets.VBox([ + widgets.HBox([self.input_dir_chooser, self.output_dir_chooser]), + widgets.HBox([self.start_stop_button, self.status_label]), + self.table, + self.log_output + ]) + + def toggle_running(self, _): + if not self.running: + self.running = True + self.start_stop_button.description = "Stop" + self.status_label.value = "Running" + self.thread = threading.Thread(target=self.background_loop, daemon=True) + self.thread.start() + else: + self.running = False + self.start_stop_button.description = "Start" + self.status_label.value = "Stopped" + + def background_loop(self): + while self.running: + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Invalid input folder. Waiting for valid selection...") + time.sleep(60) + continue + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Invalid output folder. Waiting for valid selection...") + time.sleep(60) + continue + nxs_files = glob.glob(os.path.join(input_dir, "*.nxs")) + groups = {} + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runlabel' not in details or 'runtype' not in details: + continue + runlabel = details['runlabel'] + runtype = details['runtype'].lower() + run_number = extract_run_number(os.path.basename(f)) + if runlabel not in groups: + groups[runlabel] = {} + groups[runlabel][runtype] = run_number + table_rows = [] + for runlabel, d in groups.items(): + if 'sans' in d and 'trans' in d: + table_rows.append({'SAMPLE': runlabel, 'SANS': d['sans'], 'TRANS': d['trans']}) + df = pd.DataFrame(table_rows) + self.table.data = df + with self.log_output: + print(f"Scanned {len(nxs_files)} files. Found {len(df)} reduction entries.") + ebeam_sans_files = [] + ebeam_trans_files = [] + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runtype' in details: + if details['runtype'].lower() == 'ebeam_sans': + ebeam_sans_files.append(f) + elif details['runtype'].lower() == 'ebeam_trans': + ebeam_trans_files.append(f) + if ebeam_sans_files: + ebeam_sans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_sans = ebeam_sans_files[0] + else: + self.empty_beam_sans = None + if ebeam_trans_files: + ebeam_trans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_trans = ebeam_trans_files[0] + else: + self.empty_beam_trans = None + try: + direct_beam_file = find_direct_beam(input_dir) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + time.sleep(60) + continue + for index, row in df.iterrows(): + key = (row["SAMPLE"], row["SANS"], row["TRANS"]) + if key in self.processed: + continue + try: + sample_run_file = find_file(input_dir, row["SANS"], extension=".nxs") + transmission_run_file = find_file(input_dir, row["TRANS"], extension=".nxs") + except Exception as e: + with self.log_output: + print(f"Skipping sample {row['SAMPLE']}: {e}") + continue + try: + mask_file = find_mask_file(input_dir) + with self.log_output: + print(f"Using mask file: {mask_file} for sample {row['SAMPLE']}") + except Exception as e: + with self.log_output: + print(f"Mask file not found for sample {row['SAMPLE']}: {e}") + continue + if not self.empty_beam_sans or not self.empty_beam_trans: + with self.log_output: + print("Empty beam files not found, skipping reduction for sample", row["SAMPLE"]) + continue + with self.log_output: + print(f"Reducing sample {row['SAMPLE']}...") + try: + res = reduce_loki_batch_preliminary( + sample_run_file=sample_run_file, + transmission_run_file=transmission_run_file, + background_run_file=self.empty_beam_sans, + empty_beam_file=self.empty_beam_trans, + direct_beam_file=direct_beam_file, + mask_files=[mask_file], + wavelength_min=1.0, + wavelength_max=13.0, + wavelength_n=201, + q_start=0.01, + q_stop=0.3, + q_n=101 + ) + except Exception as e: + with self.log_output: + print(f"Reduction failed for sample {row['SAMPLE']}: {e}") + continue + out_xye = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", ".xye")) + try: + save_xye_pandas(res["IofQ"], out_xye) + with self.log_output: + print(f"Saved reduced data to {out_xye}") + except Exception as e: + with self.log_output: + print(f"Failed to save reduced data for {row['SAMPLE']}: {e}") + try: + with self.plot_output: + save_reduction_plots(res, sample, sample_run_file, lam_min, lam_max, lam_n, q_min, q_max, q_n, output_dir, show=True) + with self.log_output: + print(f"Saved combined reduction plot for sample {sample}.") + except Exception as e: + with self.log_output: + print(f"Failed to save reduction plot for {sample}: {e}") + + #try: + # save_reduction_plots(res, row["SAMPLE"], sample_run_file, 1.0, 13.0, 201, 0.01, 0.3, 101, output_dir)#, n_bands=5) + # with self.log_output: + # print(f"Saved combined reduction plot for sample {row['SAMPLE']}.") + #except Exception as e: + # with self.log_output: + # print(f"Failed to save reduction plot for {row['SAMPLE']}: {e}") + with self.log_output: + print(f"Reduced sample {row['SAMPLE']} and saved outputs.") + self.processed.add(key) + time.sleep(60) + + @property + def widget(self): + return self.main + +# ---------------------------- +# Build the Tabbed Widget +# ---------------------------- +reduction_widget = SansBatchReductionWidget().widget +direct_beam_widget = DirectBeamWidget().widget +semi_auto_reduction_widget = SemiAutoReductionWidget().widget +auto_reduction_widget = AutoReductionWidget().widget + +tabs = widgets.Tab(children=[direct_beam_widget, reduction_widget, semi_auto_reduction_widget, auto_reduction_widget]) +tabs.set_title(0, "Direct Beam") +tabs.set_title(1, "Reduction (Manual)") +tabs.set_title(2, "Reduction (Smart)") +tabs.set_title(3, "Reduction (Auto)") + +#display(tabs) diff --git a/src/ess/loki/batch-gui-legacy/tabwidgetauto.py b/src/ess/loki/batch-gui-legacy/tabwidgetauto.py new file mode 100644 index 00000000..e9527b3b --- /dev/null +++ b/src/ess/loki/batch-gui-legacy/tabwidgetauto.py @@ -0,0 +1,926 @@ +import os +import glob +import re +import h5py +import pandas as pd +import scipp as sc +import matplotlib.pyplot as plt +import numpy as np +import ipywidgets as widgets +from ipydatagrid import DataGrid +from IPython.display import display +from ipyfilechooser import FileChooser +from ess import sans +from ess import loki +from ess.sans.types import * +from scipp.scipy.interpolate import interp1d +import plopp as pp # used for plotting in direct beam section +import threading +import time + + +# ---------------------------- +# Reduction Functionality +# ---------------------------- +def reduce_loki_batch_preliminary( + sample_run_file: str, + transmission_run_file: str, + background_run_file: str, + empty_beam_file: str, + direct_beam_file: str, + mask_files: list = None, + correct_for_gravity: bool = True, + uncertainty_mode = UncertaintyBroadcastMode.upper_bound, + return_events: bool = False, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + wavelength_n: int = 201, + q_start: float = 0.01, + q_stop: float = 0.3, + q_n: int = 101 +): + if mask_files is None: + mask_files = [] + # Define wavelength and Q bins. + wavelength_bins = sc.linspace("wavelength", wavelength_min, wavelength_max, wavelength_n, unit="angstrom") + q_bins = sc.linspace("Q", q_start, q_stop, q_n, unit="1/angstrom") + # Initialize the workflow. + workflow = loki.LokiAtLarmorWorkflow() + if mask_files: + workflow = sans.with_pixel_mask_filenames(workflow, masks=mask_files) + workflow[NeXusDetectorName] = "larmor_detector" + workflow[WavelengthBins] = wavelength_bins + workflow[QBins] = q_bins + workflow[CorrectForGravity] = correct_for_gravity + workflow[UncertaintyBroadcastMode] = uncertainty_mode + workflow[ReturnEvents] = return_events + workflow[Filename[BackgroundRun]] = background_run_file + workflow[Filename[TransmissionRun[BackgroundRun]]] = transmission_run_file + workflow[Filename[EmptyBeamRun]] = empty_beam_file + workflow[DirectBeamFilename] = direct_beam_file + workflow[Filename[SampleRun]] = sample_run_file + workflow[Filename[TransmissionRun[SampleRun]]] = transmission_run_file + center = sans.beam_center_from_center_of_mass(workflow) + workflow[BeamCenter] = center + tf = workflow.compute(TransmissionFraction[SampleRun]) + da = workflow.compute(BackgroundSubtractedIofQ) + return {"transmission": tf, "IofQ": da} + +def find_file(work_dir, run_number, extension=".nxs"): + pattern = os.path.join(work_dir, f"*{run_number}*{extension}") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find file matching pattern {pattern}") + +def find_direct_beam(work_dir): + pattern = os.path.join(work_dir, "*direct-beam*.h5") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find direct-beam file matching pattern {pattern}") + +def find_mask_file(work_dir): + pattern = os.path.join(work_dir, "*mask*.xml") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find mask file matching pattern {pattern}") + +def save_xye_pandas(data_array, filename): + q_vals = data_array.coords["Q"].values + i_vals = data_array.values + if len(q_vals) != len(i_vals): + q_vals = 0.5 * (q_vals[:-1] + q_vals[1:]) + if data_array.variances is not None: + err_vals = np.sqrt(data_array.variances) + if len(err_vals) != len(i_vals): + err_vals = 0.5 * (err_vals[:-1] + err_vals[1:]) + else: + err_vals = np.zeros_like(i_vals) + df = pd.DataFrame({"Q": q_vals, "I(Q)": i_vals, "Error": err_vals}) + df.to_csv(filename, sep=" ", index=False, header=True) + +# ---------------------------- +# Helper Functions for Semi-Auto Reduction +# ---------------------------- +def extract_run_number(filename): + m = re.search(r'(\d{4,})', filename) + if m: + return m.group(1) + return "" + +def parse_nx_details(filepath): + details = {} + with h5py.File(filepath, 'r') as f: + if 'nicos_details' in f['entry']: + grp = f['entry']['nicos_details'] + if 'runlabel' in grp: + val = grp['runlabel'][()] + details['runlabel'] = val.decode('utf8') if isinstance(val, bytes) else str(val) + if 'runtype' in grp: + val = grp['runtype'][()] + details['runtype'] = val.decode('utf8') if isinstance(val, bytes) else str(val) + return details + +# ---------------------------- +# Common Plotting Function +# ---------------------------- + +def save_reduction_plots(res, sample, sample_run_file, lam_min, lam_max, lam_n, q_min, q_max, q_n, output_dir, show=True): + """ + Creates a figure with 1 row x 2 columns: + - Left subplot: I(Q) (scatter with errorbars, log-log). + - Right subplot: Transmission fraction vs. wavelength (scatter with errorbars). + A single centered title (filename and sample ID) is added above the subplots. + The figure is saved to output_dir with a filename based on sample_run_file. + If show is True, the figure is displayed in the current output. + """ + # Create a figure with two subplots side by side. + fig, axs = plt.subplots(1, 2, figsize=(8, 4)) + + # Force each axis to be square (requires Matplotlib>=3.3) + axs[0].set_box_aspect(1) + axs[1].set_box_aspect(1) + + # Set a centered overall title (filename and sample) + title_str = f"{sample} - {os.path.basename(sample_run_file)}" + fig.suptitle(title_str, fontsize=14) + + # Subplot A: I(Q) + q_bins = sc.linspace("Q", q_min, q_max, q_n, unit="1/angstrom") + x_q = 0.5 * (q_bins.values[:-1] + q_bins.values[1:]) + if res["IofQ"].variances is not None: + yerr = np.sqrt(res["IofQ"].variances) + axs[0].errorbar(x_q, res["IofQ"].values, yerr=yerr, fmt='o', linestyle='none', color='k', alpha=0.5, markerfacecolor='none') + else: + axs[0].scatter(x_q, res["IofQ"].values) + axs[0].set_xlabel("Q (Å$^{-1}$)") + axs[0].set_ylabel("I(Q)") + axs[0].set_xscale("log") + axs[0].set_yscale("log") + + # Subplot B: Transmission vs. Wavelength + wavelength_bins = sc.linspace("wavelength", lam_min, lam_max, lam_n, unit="angstrom") + x_wl = 0.5 * (wavelength_bins.values[:-1] + wavelength_bins.values[1:]) + if res["transmission"].variances is not None: + yerr_tr = np.sqrt(res["transmission"].variances) + axs[1].errorbar(x_wl, res["transmission"].values, yerr=yerr_tr, fmt='^', linestyle='none', color='k', alpha=0.5, markerfacecolor='none') + else: + axs[1].scatter(x_wl, res["transmission"].values) + axs[1].set_xlabel("Wavelength (Å)") + axs[1].set_ylabel("Transmission") + + # Adjust layout to leave room for the suptitle. + plt.tight_layout(rect=[0, 0, 1, 0.90]) + + # Save the figure. + out_png = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", "_reduced.png")) + fig.savefig(out_png, dpi=300) + + # Display the figure in the GUI if requested. + if show: + display(fig) + + # Close the figure so memory is released. + plt.close(fig) + + + + +# ---------------------------- +# SansBatchReductionWidget (Updated) +# ---------------------------- +class SansBatchReductionWidget: + def __init__(self): + # CSV chooser for pre-loaded reduction table. + self.csv_chooser = FileChooser(select_dir=False) + self.csv_chooser.title = "Select CSV File" + self.csv_chooser.filter_pattern = "*.csv" + # Folder choosers. + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + # Empty-beam run number widgets. + self.ebeam_sans_widget = widgets.Text(value="", placeholder="Enter Ebeam SANS run number", description="Ebeam SANS:") + self.ebeam_trans_widget = widgets.Text(value="", placeholder="Enter Ebeam TRANS run number", description="Ebeam TRANS:") + # Reduction parameter widgets. + self.wavelength_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.wavelength_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.wavelength_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_start_widget = widgets.FloatText(value=0.01, description="Q start (1/Å):") + self.q_stop_widget = widgets.FloatText(value=0.3, description="Q stop (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + # Button to load CSV. + self.load_csv_button = widgets.Button(description="Load CSV") + self.load_csv_button.on_click(self.load_csv) + # DataGrid for the reduction table. + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + # Reduction and clear buttons. + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + self.clear_log_button = widgets.Button(description="Clear Log") + self.clear_log_button.on_click(lambda _: self.log_output.clear_output()) + self.clear_plots_button = widgets.Button(description="Clear Plots") + self.clear_plots_button.on_click(lambda _: self.plot_output.clear_output()) + # Output widgets. + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + # Build layout. + self.main = widgets.VBox([ + widgets.HBox([self.csv_chooser, self.input_dir_chooser, self.output_dir_chooser]), + widgets.HBox([self.ebeam_sans_widget, self.ebeam_trans_widget]), + widgets.HBox([self.wavelength_min_widget, self.wavelength_max_widget, self.wavelength_n_widget]), + widgets.HBox([self.q_start_widget, self.q_stop_widget, self.q_n_widget]), + self.load_csv_button, + self.table, + widgets.HBox([self.reduce_button, self.clear_log_button, self.clear_plots_button]), + self.log_output, + self.plot_output + ]) + + def load_csv(self, _): + csv_path = self.csv_chooser.selected + if not csv_path or not os.path.exists(csv_path): + with self.log_output: + print("CSV file not selected or does not exist.") + return + df = pd.read_csv(csv_path) + self.table.data = df + with self.log_output: + print(f"Loaded reduction table with {len(df)} rows from {csv_path}.") + + def run_reduction(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct-beam file:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + return + try: + background_run_file = find_file(input_dir, self.ebeam_sans_widget.value, extension=".nxs") + empty_beam_file = find_file(input_dir, self.ebeam_trans_widget.value, extension=".nxs") + with self.log_output: + print("Using empty-beam files:") + print(" Background (Ebeam SANS):", background_run_file) + print(" Empty beam (Ebeam TRANS):", empty_beam_file) + except Exception as e: + with self.log_output: + print("Error finding empty beam files:", e) + return + wl_min = self.wavelength_min_widget.value + wl_max = self.wavelength_max_widget.value + wl_n = self.wavelength_n_widget.value + q_start = self.q_start_widget.value + q_stop = self.q_stop_widget.value + q_n = self.q_n_widget.value + + df = self.table.data + for idx, row in df.iterrows(): + sample = row["SAMPLE"] + try: + sample_run_file = find_file(input_dir, str(row["SANS"]), extension=".nxs") + transmission_run_file = find_file(input_dir, str(row["TRANS"]), extension=".nxs") + except Exception as e: + with self.log_output: + print(f"Skipping sample {sample}: {e}") + continue + mask_candidate = str(row.get("mask", "")).strip() + mask_file = None + if mask_candidate: + mask_file_candidate = os.path.join(input_dir, f"{mask_candidate}.xml") + if os.path.exists(mask_file_candidate): + mask_file = mask_file_candidate + if mask_file is None: + try: + mask_file = find_mask_file(input_dir) + with self.log_output: + print(f"Identified mask file: {mask_file} for sample {sample}") + except Exception as e: + with self.log_output: + print(f"Mask file not found for sample {sample}: {e}") + continue + with self.log_output: + print(f"Reducing sample {sample}...") + try: + res = reduce_loki_batch_preliminary( + sample_run_file=sample_run_file, + transmission_run_file=transmission_run_file, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + mask_files=[mask_file], + wavelength_min=wl_min, + wavelength_max=wl_max, + wavelength_n=wl_n, + q_start=q_start, + q_stop=q_stop, + q_n=q_n + ) + except Exception as e: + with self.log_output: + print(f"Reduction failed for sample {sample}: {e}") + continue + out_xye = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", ".xye")) + try: + save_xye_pandas(res["IofQ"], out_xye) + with self.log_output: + print(f"Saved reduced data to {out_xye}") + except Exception as e: + with self.log_output: + print(f"Failed to save reduced data for {sample}: {e}") + try: + with self.plot_output: + save_reduction_plots(res, sample, sample_run_file, lam_min, lam_max, lam_n, q_min, q_max, q_n, output_dir, show=True) + with self.log_output: + print(f"Saved combined reduction plot for sample {sample}.") + except Exception as e: + with self.log_output: + print(f"Failed to save reduction plot for {sample}: {e}") + + #try: + # save_reduction_plots(res, sample, sample_run_file, wl_min, wl_max, wl_n, q_start, q_stop, q_n, output_dir)#, n_bands=5) + # with self.log_output: + # print(f"Saved combined reduction plot for sample {sample}.") + #except Exception as e: + # with self.log_output: + # print(f"Failed to save reduction plot for {sample}: {e}") + with self.log_output: + print(f"Reduced sample {sample} and saved outputs.") + + @property + def widget(self): + return self.main + +# ---------------------------- +# Semi-Auto Reduction Widget (unchanged) +# ---------------------------- +class SemiAutoReductionWidget: + def __init__(self): + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.scan_button = widgets.Button(description="Scan Directory") + self.scan_button.on_click(self.scan_directory) + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + self.add_row_button = widgets.Button(description="Add Row") + self.add_row_button.on_click(self.add_row) + self.delete_row_button = widgets.Button(description="Delete Last Row") + self.delete_row_button.on_click(self.delete_last_row) + self.lambda_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.lambda_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.lambda_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_min_widget = widgets.FloatText(value=0.01, description="Qmin (1/Å):") + self.q_max_widget = widgets.FloatText(value=0.3, description="Qmax (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + self.empty_beam_sans_text = widgets.Text(value="", description="Ebeam SANS:", disabled=True) + self.empty_beam_trans_text = widgets.Text(value="", description="Ebeam TRANS:", disabled=True) + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + self.clear_log_button = widgets.Button(description="Clear Log") + self.clear_log_button.on_click(lambda _: self.log_output.clear_output()) + self.clear_plots_button = widgets.Button(description="Clear Plots") + self.clear_plots_button.on_click(lambda _: self.plot_output.clear_output()) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + + # Add the processed set here: + self.processed = set() + + self.main = widgets.VBox([ + widgets.HBox([self.input_dir_chooser, self.output_dir_chooser]), + self.scan_button, + self.table, + widgets.HBox([self.add_row_button, self.delete_row_button]), + widgets.HBox([self.lambda_min_widget, self.lambda_max_widget, self.lambda_n_widget]), + widgets.HBox([self.q_min_widget, self.q_max_widget, self.q_n_widget]), + widgets.HBox([self.empty_beam_sans_text, self.empty_beam_trans_text]), + widgets.HBox([self.reduce_button, self.clear_log_button, self.clear_plots_button]), + self.log_output, + self.plot_output + ]) + + + def add_row(self, _): + df = self.table.data + if df.empty: + new_row = {'SAMPLE': '', 'SANS': '', 'TRANS': ''} + else: + new_row = {col: "" for col in df.columns} + df = df.append(new_row, ignore_index=True) + self.table.data = df + + def delete_last_row(self, _): + df = self.table.data + if not df.empty: + df = df.iloc[:-1] + self.table.data = df + + def scan_directory(self, _): + self.log_output.clear_output() + input_dir = self.input_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Invalid input folder.") + return + nxs_files = glob.glob(os.path.join(input_dir, "*.nxs")) + groups = {} + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runlabel' not in details or 'runtype' not in details: + continue + runlabel = details['runlabel'] + runtype = details['runtype'].lower() + run_number = extract_run_number(os.path.basename(f)) + if runlabel not in groups: + groups[runlabel] = {} + groups[runlabel][runtype] = run_number + table_rows = [] + for runlabel, d in groups.items(): + if 'sans' in d and 'trans' in d: + table_rows.append({'SAMPLE': runlabel, 'SANS': d['sans'], 'TRANS': d['trans']}) + df = pd.DataFrame(table_rows) + self.table.data = df + with self.log_output: + print(f"Scanned {len(nxs_files)} files. Found {len(df)} reduction entries.") + ebeam_sans_files = [] + ebeam_trans_files = [] + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runtype' in details: + if details['runtype'].lower() == 'ebeam_sans': + ebeam_sans_files.append(f) + elif details['runtype'].lower() == 'ebeam_trans': + ebeam_trans_files.append(f) + if ebeam_sans_files: + ebeam_sans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_sans_text.value = ebeam_sans_files[0] + else: + self.empty_beam_sans_text.value = "" + if ebeam_trans_files: + ebeam_trans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_trans_text.value = ebeam_trans_files[0] + else: + self.empty_beam_trans_text.value = "" + + def run_reduction(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct-beam file:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + return + background_run_file = self.empty_beam_sans_text.value + empty_beam_file = self.empty_beam_trans_text.value + if not background_run_file or not empty_beam_file: + with self.log_output: + print("Empty beam files not found.") + return + lam_min = self.lambda_min_widget.value + lam_max = self.lambda_max_widget.value + lam_n = self.lambda_n_widget.value + q_min = self.q_min_widget.value + q_max = self.q_max_widget.value + q_n = self.q_n_widget.value + + #df = self.table.data + df = self.table.data.copy() + for idx, row in df.iterrows(): + sample = row["SAMPLE"] + sans_run = row["SANS"] + trans_run = row["TRANS"] + try: + sample_run_file = find_file(input_dir, sans_run, extension=".nxs") + transmission_run_file = find_file(input_dir, trans_run, extension=".nxs") + except Exception as e: + with self.log_output: + print(f"Skipping sample {sample}: {e}") + continue + try: + mask_file = find_mask_file(input_dir) + with self.log_output: + print(f"Using mask file: {mask_file} for sample {sample}") + except Exception as e: + with self.log_output: + print(f"Mask file not found for sample {sample}: {e}") + continue + with self.log_output: + print(f"Reducing sample {sample}...") + try: + res = reduce_loki_batch_preliminary( + sample_run_file=sample_run_file, + transmission_run_file=transmission_run_file, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + mask_files=[mask_file], + wavelength_min=lam_min, + wavelength_max=lam_max, + wavelength_n=lam_n, + q_start=q_min, + q_stop=q_max, + q_n=q_n + ) + except Exception as e: + with self.log_output: + print(f"Reduction failed for sample {sample}: {e}") + continue + out_xye = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", ".xye")) + try: + save_xye_pandas(res["IofQ"], out_xye) + with self.log_output: + print(f"Saved reduced data to {out_xye}") + except Exception as e: + with self.log_output: + print(f"Failed to save reduced data for {sample}: {e}") + try: + save_reduction_plots(res, sample, sample_run_file, lam_min, lam_max, lam_n, q_min, q_max, q_n, output_dir)#, n_bands=5) + with self.log_output: + print(f"Saved combined reduction plot for sample {sample}.") + except Exception as e: + with self.log_output: + print(f"Failed to save reduction plot for {sample}: {e}") + with self.log_output: + print(f"Reduced sample {sample} and saved outputs.") + self.processed.add((row["SAMPLE"], row["SANS"], row["TRANS"])) + #time.sleep(1) # small delay between rows + #time.sleep(60) + + @property + def widget(self): + return self.main + +# ---------------------------- +# Direct Beam Functionality and Widget (unchanged) +# ---------------------------- +def compute_direct_beam_local( + mask: str, + sample_sans: str, + background_sans: str, + sample_trans: str, + background_trans: str, + empty_beam: str, + local_Iq_theory: str, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + n_wavelength_bins: int = 50, + n_wavelength_bands: int = 50 +) -> dict: + workflow = loki.LokiAtLarmorWorkflow() + workflow = sans.with_pixel_mask_filenames(workflow, masks=[mask]) + workflow[NeXusDetectorName] = 'larmor_detector' + wl_min = sc.scalar(wavelength_min, unit='angstrom') + wl_max = sc.scalar(wavelength_max, unit='angstrom') + workflow[WavelengthBins] = sc.linspace('wavelength', wl_min, wl_max, n_wavelength_bins + 1) + workflow[WavelengthBands] = sc.linspace('wavelength', wl_min, wl_max, n_wavelength_bands + 1) + workflow[CorrectForGravity] = True + workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound + workflow[ReturnEvents] = False + workflow[QBins] = sc.linspace(dim='Q', start=0.01, stop=0.3, num=101, unit='1/angstrom') + workflow[Filename[SampleRun]] = sample_sans + workflow[Filename[BackgroundRun]] = background_sans + workflow[Filename[TransmissionRun[SampleRun]]] = sample_trans + workflow[Filename[TransmissionRun[BackgroundRun]]] = background_trans + workflow[Filename[EmptyBeamRun]] = empty_beam + center = sans.beam_center_from_center_of_mass(workflow) + print("Computed beam center:", center) + workflow[BeamCenter] = center + Iq_theory = sc.io.load_hdf5(local_Iq_theory) + f = interp1d(Iq_theory, 'Q') + I0 = f(sc.midpoints(workflow.compute(QBins))).data[0] + print("Computed I0:", I0) + results = sans.direct_beam(workflow=workflow, I0=I0, niter=6) + iofq_full = results[-1]['iofq_full'] + iofq_bands = results[-1]['iofq_bands'] + direct_beam_function = results[-1]['direct_beam'] + pp.plot( + {'reference': Iq_theory, 'data': iofq_full}, + color={'reference': 'darkgrey', 'data': 'C0'}, + norm='log', + ) + print("Plotted full-range result vs. theoretical reference.") + return { + 'direct_beam_function': direct_beam_function, + 'iofq_full': iofq_full, + 'Iq_theory': Iq_theory, + } + +class DirectBeamWidget: + def __init__(self): + self.mask_text = widgets.Text(value="", placeholder="Enter mask file path", description="Mask:") + self.sample_sans_text = widgets.Text(value="", placeholder="Enter sample SANS file path", description="Sample SANS:") + self.background_sans_text = widgets.Text(value="", placeholder="Enter background SANS file path", description="Background SANS:") + self.sample_trans_text = widgets.Text(value="", placeholder="Enter sample TRANS file path", description="Sample TRANS:") + self.background_trans_text = widgets.Text(value="", placeholder="Enter background TRANS file path", description="Background TRANS:") + self.empty_beam_text = widgets.Text(value="", placeholder="Enter empty beam file path", description="Empty Beam:") + self.local_Iq_theory_text = widgets.Text(value="", placeholder="Enter I(q) Theory file path", description="I(q) Theory:") + self.db_wavelength_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.db_wavelength_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.db_n_wavelength_bins_widget = widgets.IntText(value=50, description="λ n_bins:") + self.db_n_wavelength_bands_widget = widgets.IntText(value=50, description="λ n_bands:") + self.compute_button = widgets.Button(description="Compute Direct Beam") + self.compute_button.on_click(self.compute_direct_beam) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.main = widgets.VBox([ + self.mask_text, + self.sample_sans_text, + self.background_sans_text, + self.sample_trans_text, + self.background_trans_text, + self.empty_beam_text, + self.local_Iq_theory_text, + widgets.HBox([ + self.db_wavelength_min_widget, + self.db_wavelength_max_widget, + self.db_n_wavelength_bins_widget, + self.db_n_wavelength_bands_widget + ]), + self.compute_button, + self.log_output, + self.plot_output + ]) + + def compute_direct_beam(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + mask = self.mask_text.value + sample_sans = self.sample_sans_text.value + background_sans = self.background_sans_text.value + sample_trans = self.sample_trans_text.value + background_trans = self.background_trans_text.value + empty_beam = self.empty_beam_text.value + local_Iq_theory = self.local_Iq_theory_text.value + wl_min = self.db_wavelength_min_widget.value + wl_max = self.db_wavelength_max_widget.value + n_bins = self.db_n_wavelength_bins_widget.value + n_bands = self.db_n_wavelength_bands_widget.value + with self.log_output: + print("Computing direct beam with:") + print(" Mask:", mask) + print(" Sample SANS:", sample_sans) + print(" Background SANS:", background_sans) + print(" Sample TRANS:", sample_trans) + print(" Background TRANS:", background_trans) + print(" Empty Beam:", empty_beam) + print(" I(q) Theory:", local_Iq_theory) + print(" λ min:", wl_min, "λ max:", wl_max, "n_bins:", n_bins, "n_bands:", n_bands) + try: + results = compute_direct_beam_local( + mask, + sample_sans, + background_sans, + sample_trans, + background_trans, + empty_beam, + local_Iq_theory, + wavelength_min=wl_min, + wavelength_max=wl_max, + n_wavelength_bins=n_bins, + n_wavelength_bands=n_bands + ) + with self.log_output: + print("Direct beam computation complete.") + except Exception as e: + with self.log_output: + print("Error computing direct beam:", e) + + @property + def widget(self): + return self.main + +# ---------------------------- +# Auto Reduction Widget (unchanged, with common plotting call) +# ---------------------------- +class AutoReductionWidget: + def __init__(self): + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.start_stop_button = widgets.Button(description="Start") + self.start_stop_button.on_click(self.toggle_running) + self.status_label = widgets.Label(value="Stopped") + self.table = DataGrid(pd.DataFrame([]), editable=False, auto_fit_columns=True) + self.log_output = widgets.Output() + self.running = False + self.thread = None + self.processed = set() # Track already reduced entries. + self.empty_beam_sans = None + self.empty_beam_trans = None + self.main = widgets.VBox([ + widgets.HBox([self.input_dir_chooser, self.output_dir_chooser]), + widgets.HBox([self.start_stop_button, self.status_label]), + self.table, + self.log_output + ]) + + def toggle_running(self, _): + if not self.running: + self.running = True + self.start_stop_button.description = "Stop" + self.status_label.value = "Running" + self.thread = threading.Thread(target=self.background_loop, daemon=True) + self.thread.start() + else: + self.running = False + self.start_stop_button.description = "Start" + self.status_label.value = "Stopped" + + def background_loop(self): + while self.running: + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Invalid input folder. Waiting for valid selection...") + time.sleep(60) + continue + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Invalid output folder. Waiting for valid selection...") + time.sleep(60) + continue + nxs_files = glob.glob(os.path.join(input_dir, "*.nxs")) + groups = {} + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runlabel' not in details or 'runtype' not in details: + continue + runlabel = details['runlabel'] + runtype = details['runtype'].lower() + run_number = extract_run_number(os.path.basename(f)) + if runlabel not in groups: + groups[runlabel] = {} + groups[runlabel][runtype] = run_number + table_rows = [] + for runlabel, d in groups.items(): + if 'sans' in d and 'trans' in d: + table_rows.append({'SAMPLE': runlabel, 'SANS': d['sans'], 'TRANS': d['trans']}) + df = pd.DataFrame(table_rows) + self.table.data = df + with self.log_output: + print(f"Scanned {len(nxs_files)} files. Found {len(df)} reduction entries.") + ebeam_sans_files = [] + ebeam_trans_files = [] + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runtype' in details: + if details['runtype'].lower() == 'ebeam_sans': + ebeam_sans_files.append(f) + elif details['runtype'].lower() == 'ebeam_trans': + ebeam_trans_files.append(f) + if ebeam_sans_files: + ebeam_sans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_sans = ebeam_sans_files[0] + else: + self.empty_beam_sans = None + if ebeam_trans_files: + ebeam_trans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_trans = ebeam_trans_files[0] + else: + self.empty_beam_trans = None + try: + direct_beam_file = find_direct_beam(input_dir) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + time.sleep(60) + continue + for index, row in df.iterrows(): + key = (row["SAMPLE"], row["SANS"], row["TRANS"]) + if key in self.processed: + continue + try: + sample_run_file = find_file(input_dir, row["SANS"], extension=".nxs") + transmission_run_file = find_file(input_dir, row["TRANS"], extension=".nxs") + except Exception as e: + with self.log_output: + print(f"Skipping sample {row['SAMPLE']}: {e}") + continue + try: + mask_file = find_mask_file(input_dir) + with self.log_output: + print(f"Using mask file: {mask_file} for sample {row['SAMPLE']}") + except Exception as e: + with self.log_output: + print(f"Mask file not found for sample {row['SAMPLE']}: {e}") + continue + if not self.empty_beam_sans or not self.empty_beam_trans: + with self.log_output: + print("Empty beam files not found, skipping reduction for sample", row["SAMPLE"]) + continue + with self.log_output: + print(f"Reducing sample {row['SAMPLE']}...") + try: + res = reduce_loki_batch_preliminary( + sample_run_file=sample_run_file, + transmission_run_file=transmission_run_file, + background_run_file=self.empty_beam_sans, + empty_beam_file=self.empty_beam_trans, + direct_beam_file=direct_beam_file, + mask_files=[mask_file], + wavelength_min=1.0, + wavelength_max=13.0, + wavelength_n=201, + q_start=0.01, + q_stop=0.3, + q_n=101 + ) + except Exception as e: + with self.log_output: + print(f"Reduction failed for sample {row['SAMPLE']}: {e}") + continue + out_xye = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", ".xye")) + try: + save_xye_pandas(res["IofQ"], out_xye) + with self.log_output: + print(f"Saved reduced data to {out_xye}") + except Exception as e: + with self.log_output: + print(f"Failed to save reduced data for {row['SAMPLE']}: {e}") + try: + with self.plot_output: + save_reduction_plots(res, sample, sample_run_file, lam_min, lam_max, lam_n, q_min, q_max, q_n, output_dir, show=True) + with self.log_output: + print(f"Saved combined reduction plot for sample {sample}.") + except Exception as e: + with self.log_output: + print(f"Failed to save reduction plot for {sample}: {e}") + + #try: + # save_reduction_plots(res, row["SAMPLE"], sample_run_file, 1.0, 13.0, 201, 0.01, 0.3, 101, output_dir)#, n_bands=5) + # with self.log_output: + # print(f"Saved combined reduction plot for sample {row['SAMPLE']}.") + #except Exception as e: + # with self.log_output: + # print(f"Failed to save reduction plot for {row['SAMPLE']}: {e}") + with self.log_output: + print(f"Reduced sample {row['SAMPLE']} and saved outputs.") + self.processed.add(key) + time.sleep(60) + + @property + def widget(self): + return self.main + +# ---------------------------- +# Build the Tabbed Widget +# ---------------------------- +reduction_widget = SansBatchReductionWidget().widget +direct_beam_widget = DirectBeamWidget().widget +semi_auto_reduction_widget = SemiAutoReductionWidget().widget +auto_reduction_widget = AutoReductionWidget().widget + +tabs = widgets.Tab(children=[direct_beam_widget, reduction_widget, semi_auto_reduction_widget, auto_reduction_widget]) +tabs.set_title(0, "Direct Beam") +tabs.set_title(1, "Reduction (Manual)") +tabs.set_title(2, "Reduction (Smart)") +tabs.set_title(3, "Reduction (Auto)") + +#display(tabs) diff --git a/src/ess/loki/examplefiles/.DS_Store b/src/ess/loki/examplefiles/.DS_Store new file mode 100644 index 00000000..3d9d7722 Binary files /dev/null and b/src/ess/loki/examplefiles/.DS_Store differ diff --git a/src/ess/loki/examplefiles/mask_new_July2022.xml b/src/ess/loki/examplefiles/mask_new_July2022.xml new file mode 100644 index 00000000..8c9e06ac --- /dev/null +++ b/src/ess/loki/examplefiles/mask_new_July2022.xml @@ -0,0 +1,6 @@ + + + + 1-25158,25532-25670,26044-26182,26556-26694,27068-27206,27580-27718,28092-28230,28604-28742,29116-29254,29628-29766,30140-30278,30652-30790,31164-31302,31676-31814,32188-32326,32700-32838,33212-33350,33724-33862,34236-34374,34748-34886,35260-35398,35772-35910,36284-36422,36796-36934,37308-37446,37820-37958,38332-38470,38844-38982,39356-39494,39868-40006,40380-40518,40892-41030,41404-41542,41916-42054,42428-42566,42940-43078,43452-43590,43964-44102,44476-44614,44988-45126,45500-45638,46012-46150,46524-46662,47036-47174,47548-47686,48060-48198,48572-48710,49084-49222,49596-49734,50108-50246,50620-50758,51132-51270,51644-51782,52156-52294,52668-52806,53180-53318,53692-53830,53986-54020,54204-54854,55008-55045,55228-55366,55522-55555,55740-55878,56036-56066,56252-56390,56548-56578,56764-56902,57058-57091,57276-57414,57566-57607,57788-57926,58078-58119,58300-58438,58590-58631,58812-58950,59102-59143,59324-59462,59614-59655,59836-59974,60126-60167,60348-60486,60638-60679,60860-60998,61156-61186,61372-61510,61673-61692,61884-62022,62184-62205,62396-62534,62692-62722,62908-63046,63202-63236,63420-63558,63713-63748,63932-64070,64228-64258,64444-64582,64956-65094,65468-65606,65980-66118,66492-66630,67004-67142,67516-67654,68028-68166,68540-68678,69052-69190,69564-69702,70076-70214,70588-70726,71100-71238,71612-71750,72124-72262,72636-72774,73148-73286,73660-73798,74172-74310,74684-74822,75196-75334,75708-75846,76220-76358,76732-76870,77244-77382,77756-77894,78268-78406,78780-78918,79292-79430,79804-79942,80316-80454,80828-80966,81340-81478,81852-81990,82364-82502,82876-83014,83388-83526,83900-84038,84412-84550,84924-85062,85436-85574,85948-86086,86460-86598,86972-87110,87484-87622,87996-88134,88508-88646,89020-89158,89532-89670,90044-90182,90556-90694,91068-91206,91580-91718,92092-92230,92604-92742,93116-93254,93628-93766,94140-94278,94652-94790,95164-95302,95676-95814,96188-96326,96700-136262,136636-136774,137148-137286,137660-137798,138172-139334,139708-139846,140220-140358,140732-140870,141244-141382,141756-141894,142268-142406,142780-142918,143292-143430,143804-143942,144316-144454,144828-144966,145340-145478,145852-145990,146364-146502,146876-147014,147388-147526,147900-148038,148412-148550,148924-149062,149436-149574,149948-150086,150460-150598,150972-151110,151484-151622,151996-152134,152508-152646,153020-153158,153532-153670,154044-154182,154556-154694,155068-155206,155580-155718,156092-156230,156604-156742,157116-157254,157628-157766,158140-158278,158652-158790,159164-159302,159676-159814,160188-160326,160700-160838,161212-161350,161724-161862,162236-162374,162748-162886,163260-163398,163772-163910,164284-164422,164796-164934,165308-165446,165820-165958,166332-166470,166844-166982,167356-167494,167868-168006,168380-168518,168676-168706,168892-169030,169186-169220,169404-169542,169698-169731,169916-170054,170212-170242,170428-170566,170729-170748,170940-171078,171242-171259,171452-171590,171748-171778,171964-172102,172254-172295,172476-172614,172766-172807,172988-173126,173278-173319,173500-173638,173790-173831,174012-174150,174302-174343,174524-174662,174814-174855,175036-175174,175326-175367,175548-175686,175842-175875,176060-176198,176356-176386,176572-176710,176868-176898,177084-177222,177378-177412,177596-177734,177888-177925,178108-178246,178400-178438,178620-178758,178913-178948,179132-179270,179644-179782,180156-180294,180668-180806,181180-181318,181692-181830,182204-182342,182716-182854,183228-183366,183740-183878,184252-184390,184764-184902,185276-185414,185788-185926,186300-186438,186812-186950,187324-187462,187836-187974,188348-188486,188860-188998,189372-189510,189884-190022,190396-190534,190908-191046,191420-191558,191932-192070,192444-192582,192956-193094,193468-193606,193980-194118,194492-194630,195004-195142,195516-195654,196028-196166,196540-196678,197052-197190,197564-197702,198076-198214,198588-198726,199100-199238,199612-199750,200124-200262,200636-200774,201148-201286,201660-201798,202172-202310,202684-202822,203196-203334,203708-203846,204220-204358,204732-204870,205244-205382,205756-205894,206268-206406,206780-206918,207292-207430,207804-207942,208316-208454,208828-208966,209340-209478,209852-209990,210364-210502,210876-211014,211388-214086,214460-254534,254908-255046,255420-255558,255932-256070,256444-256582,256956-257094,257468-257606,257980-258118,258492-258630,259004-259142,259516-259654,260028-260166,260540-260678,261052-261190,261564-261702,262076-262214,262588-262726,263100-263238,263612-263750,264124-264262,264636-264774,265148-265286,265660-265798,266172-266310,266684-266822,267196-267334,267708-267846,268220-268358,268732-268870,269244-269382,269756-269894,270268-270406,270780-270918,271292-271430,271804-271942,272316-272454,272828-272966,273340-273478,273852-273990,274364-274502,274876-275014,275388-275526,275900-276038,276412-276550,276924-277062,277436-277574,277948-278086,278460-278598,278972-279110,279484-279622,279996-280134,280508-280646,281020-281158,281532-281670,282044-282182,282556-282694,283068-283206,283368-283389,283580-283718,283876-283906,284092-284230,284388-284418,284604-284742,285116-285254,285628-285766,286140-286278,286652-286790,286942-286983,287164-287302,287460-287490,287676-287814,287972-288002,288188-288326,288484-288514,288700-288838,288991-289030,289212-289350,289508-289538,289724-289862,290020-290050,290236-290374,290528-290565,290748-290886,291044-291074,291260-291398,291556-291586,291772-291910,292068-292098,292284-292422,292575-292615,292796-292934,293092-293122,293308-293446,293604-293634,293820-293958,294332-294470,294844-294982,295356-295494,295868-296006,296172-296186,296380-296518,296892-297030,297404-297542,297916-298054,298428-298566,298940-299078,299452-299590,299964-300102,300476-300614,300988-301126,301500-301638,302012-302150,302524-302662,303036-303174,303548-303686,304060-304198,304572-304710,305084-305222,305596-305734,306108-306246,306620-306758,307132-307270,307644-307782,308156-308294,308668-308806,309180-309318,309692-309830,310204-310342,310716-310854,311228-311366,311740-311878,312252-312390,312764-312902,313276-313414,313788-313926,314300-314438,314812-314950,315324-315462,315836-315974,316348-316486,316860-316998,317372-317510,317884-318022,318396-318534,318908-319046,319420-319558,319932-320070,320444-320582,320956-321094,321468-321606,321980-322118,322492-322630,323004-323142,323516-323654,324028-324166,324540-324678,325052-325190,325564-325702,326076-327750,328124-328262,328636-328774,329148-329286,329660-372806,373180-373318,373692-373830,374204-374342,374716-374854,375228-375366,375740-375878,376252-376390,376764-376902,377276-377414,377788-377926,378300-378438,378812-378950,379324-379462,379836-379974,380348-380486,380860-380998,381372-381510,381884-382022,382396-382534,382908-383046,383420-383558,383932-384070,384444-384582,384956-385094,385468-385606,385980-386118,386492-386630,387004-387142,387516-387654,388028-388166,388540-388678,389052-389190,389564-389702,390076-390214,390588-390726,391100-391238,391612-391750,392124-392262,392636-392774,393148-393286,393660-393798,394172-394310,394684-394822,395196-395334,395708-395846,396220-396358,396732-396870,397244-397382,397756-397894,398268-398406,398780-398918,399292-399430,399804-399942,400316-400454,400828-400966,401340-401478,401636-401666,401852-401990,402148-402178,402364-402502,402660-402690,402876-403014,403172-403202,403388-403526,403684-403714,403900-404038,404196-404226,404412-404550,404708-404738,404924-405062,405220-405250,405436-405574,405732-405762,405948-406086,406244-406274,406460-406598,406756-406786,406972-407110,407268-407298,407484-407622,407780-407810,407996-408134,408292-408322,408508-408646,409020-409158,409532-409670,410044-410182,410556-410694,410852-410882,411068-411206,411364-411394,411580-411718,412092-412230,412604-412742,413116-413254,413628-413766,414140-414278,414652-414790,415164-415302,415676-415814,416188-416326,416700-416838,417212-417350,417724-417862,418236-418374,418748-418886,419260-419398,419772-419910,420284-420422,420796-420934,421308-421446,421820-421958,422332-422470,422844-422982,423356-423494,423868-424006,424380-424518,424892-425030,425404-425542,425916-426054,426428-426566,426940-427078,427452-427590,427964-428102,428476-428614,428988-429126,429500-429638,430012-430150,430524-430662,431036-431174,431548-431686,432060-432198,432572-432710,433084-433222,433596-433734,434108-434246,434620-434758,435132-435270,435644-435782,436156-436294,436668-436806,437180-437318,437692-437830,438204-438342,438716-438854,439228-439366,439740-439878,440252-440390,440764-440902,441276-441414,441788-441926,442300-442438,442812-442950,443324-443462,443836-443974,444348-458752 + + diff --git a/src/ess/loki/examplefiles/nxsmod/.DS_Store b/src/ess/loki/examplefiles/nxsmod/.DS_Store new file mode 100644 index 00000000..9021b034 Binary files /dev/null and b/src/ess/loki/examplefiles/nxsmod/.DS_Store differ diff --git a/src/ess/loki/examplefiles/nxsmod/mask_new_July2022.xml b/src/ess/loki/examplefiles/nxsmod/mask_new_July2022.xml new file mode 100644 index 00000000..8c9e06ac --- /dev/null +++ b/src/ess/loki/examplefiles/nxsmod/mask_new_July2022.xml @@ -0,0 +1,6 @@ + + + + 1-25158,25532-25670,26044-26182,26556-26694,27068-27206,27580-27718,28092-28230,28604-28742,29116-29254,29628-29766,30140-30278,30652-30790,31164-31302,31676-31814,32188-32326,32700-32838,33212-33350,33724-33862,34236-34374,34748-34886,35260-35398,35772-35910,36284-36422,36796-36934,37308-37446,37820-37958,38332-38470,38844-38982,39356-39494,39868-40006,40380-40518,40892-41030,41404-41542,41916-42054,42428-42566,42940-43078,43452-43590,43964-44102,44476-44614,44988-45126,45500-45638,46012-46150,46524-46662,47036-47174,47548-47686,48060-48198,48572-48710,49084-49222,49596-49734,50108-50246,50620-50758,51132-51270,51644-51782,52156-52294,52668-52806,53180-53318,53692-53830,53986-54020,54204-54854,55008-55045,55228-55366,55522-55555,55740-55878,56036-56066,56252-56390,56548-56578,56764-56902,57058-57091,57276-57414,57566-57607,57788-57926,58078-58119,58300-58438,58590-58631,58812-58950,59102-59143,59324-59462,59614-59655,59836-59974,60126-60167,60348-60486,60638-60679,60860-60998,61156-61186,61372-61510,61673-61692,61884-62022,62184-62205,62396-62534,62692-62722,62908-63046,63202-63236,63420-63558,63713-63748,63932-64070,64228-64258,64444-64582,64956-65094,65468-65606,65980-66118,66492-66630,67004-67142,67516-67654,68028-68166,68540-68678,69052-69190,69564-69702,70076-70214,70588-70726,71100-71238,71612-71750,72124-72262,72636-72774,73148-73286,73660-73798,74172-74310,74684-74822,75196-75334,75708-75846,76220-76358,76732-76870,77244-77382,77756-77894,78268-78406,78780-78918,79292-79430,79804-79942,80316-80454,80828-80966,81340-81478,81852-81990,82364-82502,82876-83014,83388-83526,83900-84038,84412-84550,84924-85062,85436-85574,85948-86086,86460-86598,86972-87110,87484-87622,87996-88134,88508-88646,89020-89158,89532-89670,90044-90182,90556-90694,91068-91206,91580-91718,92092-92230,92604-92742,93116-93254,93628-93766,94140-94278,94652-94790,95164-95302,95676-95814,96188-96326,96700-136262,136636-136774,137148-137286,137660-137798,138172-139334,139708-139846,140220-140358,140732-140870,141244-141382,141756-141894,142268-142406,142780-142918,143292-143430,143804-143942,144316-144454,144828-144966,145340-145478,145852-145990,146364-146502,146876-147014,147388-147526,147900-148038,148412-148550,148924-149062,149436-149574,149948-150086,150460-150598,150972-151110,151484-151622,151996-152134,152508-152646,153020-153158,153532-153670,154044-154182,154556-154694,155068-155206,155580-155718,156092-156230,156604-156742,157116-157254,157628-157766,158140-158278,158652-158790,159164-159302,159676-159814,160188-160326,160700-160838,161212-161350,161724-161862,162236-162374,162748-162886,163260-163398,163772-163910,164284-164422,164796-164934,165308-165446,165820-165958,166332-166470,166844-166982,167356-167494,167868-168006,168380-168518,168676-168706,168892-169030,169186-169220,169404-169542,169698-169731,169916-170054,170212-170242,170428-170566,170729-170748,170940-171078,171242-171259,171452-171590,171748-171778,171964-172102,172254-172295,172476-172614,172766-172807,172988-173126,173278-173319,173500-173638,173790-173831,174012-174150,174302-174343,174524-174662,174814-174855,175036-175174,175326-175367,175548-175686,175842-175875,176060-176198,176356-176386,176572-176710,176868-176898,177084-177222,177378-177412,177596-177734,177888-177925,178108-178246,178400-178438,178620-178758,178913-178948,179132-179270,179644-179782,180156-180294,180668-180806,181180-181318,181692-181830,182204-182342,182716-182854,183228-183366,183740-183878,184252-184390,184764-184902,185276-185414,185788-185926,186300-186438,186812-186950,187324-187462,187836-187974,188348-188486,188860-188998,189372-189510,189884-190022,190396-190534,190908-191046,191420-191558,191932-192070,192444-192582,192956-193094,193468-193606,193980-194118,194492-194630,195004-195142,195516-195654,196028-196166,196540-196678,197052-197190,197564-197702,198076-198214,198588-198726,199100-199238,199612-199750,200124-200262,200636-200774,201148-201286,201660-201798,202172-202310,202684-202822,203196-203334,203708-203846,204220-204358,204732-204870,205244-205382,205756-205894,206268-206406,206780-206918,207292-207430,207804-207942,208316-208454,208828-208966,209340-209478,209852-209990,210364-210502,210876-211014,211388-214086,214460-254534,254908-255046,255420-255558,255932-256070,256444-256582,256956-257094,257468-257606,257980-258118,258492-258630,259004-259142,259516-259654,260028-260166,260540-260678,261052-261190,261564-261702,262076-262214,262588-262726,263100-263238,263612-263750,264124-264262,264636-264774,265148-265286,265660-265798,266172-266310,266684-266822,267196-267334,267708-267846,268220-268358,268732-268870,269244-269382,269756-269894,270268-270406,270780-270918,271292-271430,271804-271942,272316-272454,272828-272966,273340-273478,273852-273990,274364-274502,274876-275014,275388-275526,275900-276038,276412-276550,276924-277062,277436-277574,277948-278086,278460-278598,278972-279110,279484-279622,279996-280134,280508-280646,281020-281158,281532-281670,282044-282182,282556-282694,283068-283206,283368-283389,283580-283718,283876-283906,284092-284230,284388-284418,284604-284742,285116-285254,285628-285766,286140-286278,286652-286790,286942-286983,287164-287302,287460-287490,287676-287814,287972-288002,288188-288326,288484-288514,288700-288838,288991-289030,289212-289350,289508-289538,289724-289862,290020-290050,290236-290374,290528-290565,290748-290886,291044-291074,291260-291398,291556-291586,291772-291910,292068-292098,292284-292422,292575-292615,292796-292934,293092-293122,293308-293446,293604-293634,293820-293958,294332-294470,294844-294982,295356-295494,295868-296006,296172-296186,296380-296518,296892-297030,297404-297542,297916-298054,298428-298566,298940-299078,299452-299590,299964-300102,300476-300614,300988-301126,301500-301638,302012-302150,302524-302662,303036-303174,303548-303686,304060-304198,304572-304710,305084-305222,305596-305734,306108-306246,306620-306758,307132-307270,307644-307782,308156-308294,308668-308806,309180-309318,309692-309830,310204-310342,310716-310854,311228-311366,311740-311878,312252-312390,312764-312902,313276-313414,313788-313926,314300-314438,314812-314950,315324-315462,315836-315974,316348-316486,316860-316998,317372-317510,317884-318022,318396-318534,318908-319046,319420-319558,319932-320070,320444-320582,320956-321094,321468-321606,321980-322118,322492-322630,323004-323142,323516-323654,324028-324166,324540-324678,325052-325190,325564-325702,326076-327750,328124-328262,328636-328774,329148-329286,329660-372806,373180-373318,373692-373830,374204-374342,374716-374854,375228-375366,375740-375878,376252-376390,376764-376902,377276-377414,377788-377926,378300-378438,378812-378950,379324-379462,379836-379974,380348-380486,380860-380998,381372-381510,381884-382022,382396-382534,382908-383046,383420-383558,383932-384070,384444-384582,384956-385094,385468-385606,385980-386118,386492-386630,387004-387142,387516-387654,388028-388166,388540-388678,389052-389190,389564-389702,390076-390214,390588-390726,391100-391238,391612-391750,392124-392262,392636-392774,393148-393286,393660-393798,394172-394310,394684-394822,395196-395334,395708-395846,396220-396358,396732-396870,397244-397382,397756-397894,398268-398406,398780-398918,399292-399430,399804-399942,400316-400454,400828-400966,401340-401478,401636-401666,401852-401990,402148-402178,402364-402502,402660-402690,402876-403014,403172-403202,403388-403526,403684-403714,403900-404038,404196-404226,404412-404550,404708-404738,404924-405062,405220-405250,405436-405574,405732-405762,405948-406086,406244-406274,406460-406598,406756-406786,406972-407110,407268-407298,407484-407622,407780-407810,407996-408134,408292-408322,408508-408646,409020-409158,409532-409670,410044-410182,410556-410694,410852-410882,411068-411206,411364-411394,411580-411718,412092-412230,412604-412742,413116-413254,413628-413766,414140-414278,414652-414790,415164-415302,415676-415814,416188-416326,416700-416838,417212-417350,417724-417862,418236-418374,418748-418886,419260-419398,419772-419910,420284-420422,420796-420934,421308-421446,421820-421958,422332-422470,422844-422982,423356-423494,423868-424006,424380-424518,424892-425030,425404-425542,425916-426054,426428-426566,426940-427078,427452-427590,427964-428102,428476-428614,428988-429126,429500-429638,430012-430150,430524-430662,431036-431174,431548-431686,432060-432198,432572-432710,433084-433222,433596-433734,434108-434246,434620-434758,435132-435270,435644-435782,436156-436294,436668-436806,437180-437318,437692-437830,438204-438342,438716-438854,439228-439366,439740-439878,440252-440390,440764-440902,441276-441414,441788-441926,442300-442438,442812-442950,443324-443462,443836-443974,444348-458752 + + diff --git a/src/ess/loki/examplefiles/nxsmod/out/.DS_Store b/src/ess/loki/examplefiles/nxsmod/out/.DS_Store new file mode 100644 index 00000000..5008ddfc Binary files /dev/null and b/src/ess/loki/examplefiles/nxsmod/out/.DS_Store differ diff --git a/src/ess/loki/examplefiles/nxsmodscript/.DS_Store b/src/ess/loki/examplefiles/nxsmodscript/.DS_Store new file mode 100644 index 00000000..535d3061 Binary files /dev/null and b/src/ess/loki/examplefiles/nxsmodscript/.DS_Store differ diff --git a/src/ess/loki/examplefiles/nxsmodscript/mask_new_July2022.xml b/src/ess/loki/examplefiles/nxsmodscript/mask_new_July2022.xml new file mode 100644 index 00000000..8c9e06ac --- /dev/null +++ b/src/ess/loki/examplefiles/nxsmodscript/mask_new_July2022.xml @@ -0,0 +1,6 @@ + + + + 1-25158,25532-25670,26044-26182,26556-26694,27068-27206,27580-27718,28092-28230,28604-28742,29116-29254,29628-29766,30140-30278,30652-30790,31164-31302,31676-31814,32188-32326,32700-32838,33212-33350,33724-33862,34236-34374,34748-34886,35260-35398,35772-35910,36284-36422,36796-36934,37308-37446,37820-37958,38332-38470,38844-38982,39356-39494,39868-40006,40380-40518,40892-41030,41404-41542,41916-42054,42428-42566,42940-43078,43452-43590,43964-44102,44476-44614,44988-45126,45500-45638,46012-46150,46524-46662,47036-47174,47548-47686,48060-48198,48572-48710,49084-49222,49596-49734,50108-50246,50620-50758,51132-51270,51644-51782,52156-52294,52668-52806,53180-53318,53692-53830,53986-54020,54204-54854,55008-55045,55228-55366,55522-55555,55740-55878,56036-56066,56252-56390,56548-56578,56764-56902,57058-57091,57276-57414,57566-57607,57788-57926,58078-58119,58300-58438,58590-58631,58812-58950,59102-59143,59324-59462,59614-59655,59836-59974,60126-60167,60348-60486,60638-60679,60860-60998,61156-61186,61372-61510,61673-61692,61884-62022,62184-62205,62396-62534,62692-62722,62908-63046,63202-63236,63420-63558,63713-63748,63932-64070,64228-64258,64444-64582,64956-65094,65468-65606,65980-66118,66492-66630,67004-67142,67516-67654,68028-68166,68540-68678,69052-69190,69564-69702,70076-70214,70588-70726,71100-71238,71612-71750,72124-72262,72636-72774,73148-73286,73660-73798,74172-74310,74684-74822,75196-75334,75708-75846,76220-76358,76732-76870,77244-77382,77756-77894,78268-78406,78780-78918,79292-79430,79804-79942,80316-80454,80828-80966,81340-81478,81852-81990,82364-82502,82876-83014,83388-83526,83900-84038,84412-84550,84924-85062,85436-85574,85948-86086,86460-86598,86972-87110,87484-87622,87996-88134,88508-88646,89020-89158,89532-89670,90044-90182,90556-90694,91068-91206,91580-91718,92092-92230,92604-92742,93116-93254,93628-93766,94140-94278,94652-94790,95164-95302,95676-95814,96188-96326,96700-136262,136636-136774,137148-137286,137660-137798,138172-139334,139708-139846,140220-140358,140732-140870,141244-141382,141756-141894,142268-142406,142780-142918,143292-143430,143804-143942,144316-144454,144828-144966,145340-145478,145852-145990,146364-146502,146876-147014,147388-147526,147900-148038,148412-148550,148924-149062,149436-149574,149948-150086,150460-150598,150972-151110,151484-151622,151996-152134,152508-152646,153020-153158,153532-153670,154044-154182,154556-154694,155068-155206,155580-155718,156092-156230,156604-156742,157116-157254,157628-157766,158140-158278,158652-158790,159164-159302,159676-159814,160188-160326,160700-160838,161212-161350,161724-161862,162236-162374,162748-162886,163260-163398,163772-163910,164284-164422,164796-164934,165308-165446,165820-165958,166332-166470,166844-166982,167356-167494,167868-168006,168380-168518,168676-168706,168892-169030,169186-169220,169404-169542,169698-169731,169916-170054,170212-170242,170428-170566,170729-170748,170940-171078,171242-171259,171452-171590,171748-171778,171964-172102,172254-172295,172476-172614,172766-172807,172988-173126,173278-173319,173500-173638,173790-173831,174012-174150,174302-174343,174524-174662,174814-174855,175036-175174,175326-175367,175548-175686,175842-175875,176060-176198,176356-176386,176572-176710,176868-176898,177084-177222,177378-177412,177596-177734,177888-177925,178108-178246,178400-178438,178620-178758,178913-178948,179132-179270,179644-179782,180156-180294,180668-180806,181180-181318,181692-181830,182204-182342,182716-182854,183228-183366,183740-183878,184252-184390,184764-184902,185276-185414,185788-185926,186300-186438,186812-186950,187324-187462,187836-187974,188348-188486,188860-188998,189372-189510,189884-190022,190396-190534,190908-191046,191420-191558,191932-192070,192444-192582,192956-193094,193468-193606,193980-194118,194492-194630,195004-195142,195516-195654,196028-196166,196540-196678,197052-197190,197564-197702,198076-198214,198588-198726,199100-199238,199612-199750,200124-200262,200636-200774,201148-201286,201660-201798,202172-202310,202684-202822,203196-203334,203708-203846,204220-204358,204732-204870,205244-205382,205756-205894,206268-206406,206780-206918,207292-207430,207804-207942,208316-208454,208828-208966,209340-209478,209852-209990,210364-210502,210876-211014,211388-214086,214460-254534,254908-255046,255420-255558,255932-256070,256444-256582,256956-257094,257468-257606,257980-258118,258492-258630,259004-259142,259516-259654,260028-260166,260540-260678,261052-261190,261564-261702,262076-262214,262588-262726,263100-263238,263612-263750,264124-264262,264636-264774,265148-265286,265660-265798,266172-266310,266684-266822,267196-267334,267708-267846,268220-268358,268732-268870,269244-269382,269756-269894,270268-270406,270780-270918,271292-271430,271804-271942,272316-272454,272828-272966,273340-273478,273852-273990,274364-274502,274876-275014,275388-275526,275900-276038,276412-276550,276924-277062,277436-277574,277948-278086,278460-278598,278972-279110,279484-279622,279996-280134,280508-280646,281020-281158,281532-281670,282044-282182,282556-282694,283068-283206,283368-283389,283580-283718,283876-283906,284092-284230,284388-284418,284604-284742,285116-285254,285628-285766,286140-286278,286652-286790,286942-286983,287164-287302,287460-287490,287676-287814,287972-288002,288188-288326,288484-288514,288700-288838,288991-289030,289212-289350,289508-289538,289724-289862,290020-290050,290236-290374,290528-290565,290748-290886,291044-291074,291260-291398,291556-291586,291772-291910,292068-292098,292284-292422,292575-292615,292796-292934,293092-293122,293308-293446,293604-293634,293820-293958,294332-294470,294844-294982,295356-295494,295868-296006,296172-296186,296380-296518,296892-297030,297404-297542,297916-298054,298428-298566,298940-299078,299452-299590,299964-300102,300476-300614,300988-301126,301500-301638,302012-302150,302524-302662,303036-303174,303548-303686,304060-304198,304572-304710,305084-305222,305596-305734,306108-306246,306620-306758,307132-307270,307644-307782,308156-308294,308668-308806,309180-309318,309692-309830,310204-310342,310716-310854,311228-311366,311740-311878,312252-312390,312764-312902,313276-313414,313788-313926,314300-314438,314812-314950,315324-315462,315836-315974,316348-316486,316860-316998,317372-317510,317884-318022,318396-318534,318908-319046,319420-319558,319932-320070,320444-320582,320956-321094,321468-321606,321980-322118,322492-322630,323004-323142,323516-323654,324028-324166,324540-324678,325052-325190,325564-325702,326076-327750,328124-328262,328636-328774,329148-329286,329660-372806,373180-373318,373692-373830,374204-374342,374716-374854,375228-375366,375740-375878,376252-376390,376764-376902,377276-377414,377788-377926,378300-378438,378812-378950,379324-379462,379836-379974,380348-380486,380860-380998,381372-381510,381884-382022,382396-382534,382908-383046,383420-383558,383932-384070,384444-384582,384956-385094,385468-385606,385980-386118,386492-386630,387004-387142,387516-387654,388028-388166,388540-388678,389052-389190,389564-389702,390076-390214,390588-390726,391100-391238,391612-391750,392124-392262,392636-392774,393148-393286,393660-393798,394172-394310,394684-394822,395196-395334,395708-395846,396220-396358,396732-396870,397244-397382,397756-397894,398268-398406,398780-398918,399292-399430,399804-399942,400316-400454,400828-400966,401340-401478,401636-401666,401852-401990,402148-402178,402364-402502,402660-402690,402876-403014,403172-403202,403388-403526,403684-403714,403900-404038,404196-404226,404412-404550,404708-404738,404924-405062,405220-405250,405436-405574,405732-405762,405948-406086,406244-406274,406460-406598,406756-406786,406972-407110,407268-407298,407484-407622,407780-407810,407996-408134,408292-408322,408508-408646,409020-409158,409532-409670,410044-410182,410556-410694,410852-410882,411068-411206,411364-411394,411580-411718,412092-412230,412604-412742,413116-413254,413628-413766,414140-414278,414652-414790,415164-415302,415676-415814,416188-416326,416700-416838,417212-417350,417724-417862,418236-418374,418748-418886,419260-419398,419772-419910,420284-420422,420796-420934,421308-421446,421820-421958,422332-422470,422844-422982,423356-423494,423868-424006,424380-424518,424892-425030,425404-425542,425916-426054,426428-426566,426940-427078,427452-427590,427964-428102,428476-428614,428988-429126,429500-429638,430012-430150,430524-430662,431036-431174,431548-431686,432060-432198,432572-432710,433084-433222,433596-433734,434108-434246,434620-434758,435132-435270,435644-435782,436156-436294,436668-436806,437180-437318,437692-437830,438204-438342,438716-438854,439228-439366,439740-439878,440252-440390,440764-440902,441276-441414,441788-441926,442300-442438,442812-442950,443324-443462,443836-443974,444348-458752 + + diff --git a/src/ess/loki/examplefiles/nxsmodscript/out/.DS_Store b/src/ess/loki/examplefiles/nxsmodscript/out/.DS_Store new file mode 100644 index 00000000..5008ddfc Binary files /dev/null and b/src/ess/loki/examplefiles/nxsmodscript/out/.DS_Store differ diff --git a/src/ess/loki/examplefiles/nxsmodscript/timed-test/60339-2022-02-28_2215_mod_IofQ.png b/src/ess/loki/examplefiles/nxsmodscript/timed-test/60339-2022-02-28_2215_mod_IofQ.png new file mode 100644 index 00000000..f966603f Binary files /dev/null and b/src/ess/loki/examplefiles/nxsmodscript/timed-test/60339-2022-02-28_2215_mod_IofQ.png differ diff --git a/src/ess/loki/examplefiles/nxsmodscript/timed-test/60339-2022-02-28_2215_mod_transmission.png b/src/ess/loki/examplefiles/nxsmodscript/timed-test/60339-2022-02-28_2215_mod_transmission.png new file mode 100644 index 00000000..4bd562b1 Binary files /dev/null and b/src/ess/loki/examplefiles/nxsmodscript/timed-test/60339-2022-02-28_2215_mod_transmission.png differ diff --git a/src/ess/loki/examplefiles/nxsmodscript/timed-test/mask_new_July2022.xml b/src/ess/loki/examplefiles/nxsmodscript/timed-test/mask_new_July2022.xml new file mode 100644 index 00000000..8c9e06ac --- /dev/null +++ b/src/ess/loki/examplefiles/nxsmodscript/timed-test/mask_new_July2022.xml @@ -0,0 +1,6 @@ + + + + 1-25158,25532-25670,26044-26182,26556-26694,27068-27206,27580-27718,28092-28230,28604-28742,29116-29254,29628-29766,30140-30278,30652-30790,31164-31302,31676-31814,32188-32326,32700-32838,33212-33350,33724-33862,34236-34374,34748-34886,35260-35398,35772-35910,36284-36422,36796-36934,37308-37446,37820-37958,38332-38470,38844-38982,39356-39494,39868-40006,40380-40518,40892-41030,41404-41542,41916-42054,42428-42566,42940-43078,43452-43590,43964-44102,44476-44614,44988-45126,45500-45638,46012-46150,46524-46662,47036-47174,47548-47686,48060-48198,48572-48710,49084-49222,49596-49734,50108-50246,50620-50758,51132-51270,51644-51782,52156-52294,52668-52806,53180-53318,53692-53830,53986-54020,54204-54854,55008-55045,55228-55366,55522-55555,55740-55878,56036-56066,56252-56390,56548-56578,56764-56902,57058-57091,57276-57414,57566-57607,57788-57926,58078-58119,58300-58438,58590-58631,58812-58950,59102-59143,59324-59462,59614-59655,59836-59974,60126-60167,60348-60486,60638-60679,60860-60998,61156-61186,61372-61510,61673-61692,61884-62022,62184-62205,62396-62534,62692-62722,62908-63046,63202-63236,63420-63558,63713-63748,63932-64070,64228-64258,64444-64582,64956-65094,65468-65606,65980-66118,66492-66630,67004-67142,67516-67654,68028-68166,68540-68678,69052-69190,69564-69702,70076-70214,70588-70726,71100-71238,71612-71750,72124-72262,72636-72774,73148-73286,73660-73798,74172-74310,74684-74822,75196-75334,75708-75846,76220-76358,76732-76870,77244-77382,77756-77894,78268-78406,78780-78918,79292-79430,79804-79942,80316-80454,80828-80966,81340-81478,81852-81990,82364-82502,82876-83014,83388-83526,83900-84038,84412-84550,84924-85062,85436-85574,85948-86086,86460-86598,86972-87110,87484-87622,87996-88134,88508-88646,89020-89158,89532-89670,90044-90182,90556-90694,91068-91206,91580-91718,92092-92230,92604-92742,93116-93254,93628-93766,94140-94278,94652-94790,95164-95302,95676-95814,96188-96326,96700-136262,136636-136774,137148-137286,137660-137798,138172-139334,139708-139846,140220-140358,140732-140870,141244-141382,141756-141894,142268-142406,142780-142918,143292-143430,143804-143942,144316-144454,144828-144966,145340-145478,145852-145990,146364-146502,146876-147014,147388-147526,147900-148038,148412-148550,148924-149062,149436-149574,149948-150086,150460-150598,150972-151110,151484-151622,151996-152134,152508-152646,153020-153158,153532-153670,154044-154182,154556-154694,155068-155206,155580-155718,156092-156230,156604-156742,157116-157254,157628-157766,158140-158278,158652-158790,159164-159302,159676-159814,160188-160326,160700-160838,161212-161350,161724-161862,162236-162374,162748-162886,163260-163398,163772-163910,164284-164422,164796-164934,165308-165446,165820-165958,166332-166470,166844-166982,167356-167494,167868-168006,168380-168518,168676-168706,168892-169030,169186-169220,169404-169542,169698-169731,169916-170054,170212-170242,170428-170566,170729-170748,170940-171078,171242-171259,171452-171590,171748-171778,171964-172102,172254-172295,172476-172614,172766-172807,172988-173126,173278-173319,173500-173638,173790-173831,174012-174150,174302-174343,174524-174662,174814-174855,175036-175174,175326-175367,175548-175686,175842-175875,176060-176198,176356-176386,176572-176710,176868-176898,177084-177222,177378-177412,177596-177734,177888-177925,178108-178246,178400-178438,178620-178758,178913-178948,179132-179270,179644-179782,180156-180294,180668-180806,181180-181318,181692-181830,182204-182342,182716-182854,183228-183366,183740-183878,184252-184390,184764-184902,185276-185414,185788-185926,186300-186438,186812-186950,187324-187462,187836-187974,188348-188486,188860-188998,189372-189510,189884-190022,190396-190534,190908-191046,191420-191558,191932-192070,192444-192582,192956-193094,193468-193606,193980-194118,194492-194630,195004-195142,195516-195654,196028-196166,196540-196678,197052-197190,197564-197702,198076-198214,198588-198726,199100-199238,199612-199750,200124-200262,200636-200774,201148-201286,201660-201798,202172-202310,202684-202822,203196-203334,203708-203846,204220-204358,204732-204870,205244-205382,205756-205894,206268-206406,206780-206918,207292-207430,207804-207942,208316-208454,208828-208966,209340-209478,209852-209990,210364-210502,210876-211014,211388-214086,214460-254534,254908-255046,255420-255558,255932-256070,256444-256582,256956-257094,257468-257606,257980-258118,258492-258630,259004-259142,259516-259654,260028-260166,260540-260678,261052-261190,261564-261702,262076-262214,262588-262726,263100-263238,263612-263750,264124-264262,264636-264774,265148-265286,265660-265798,266172-266310,266684-266822,267196-267334,267708-267846,268220-268358,268732-268870,269244-269382,269756-269894,270268-270406,270780-270918,271292-271430,271804-271942,272316-272454,272828-272966,273340-273478,273852-273990,274364-274502,274876-275014,275388-275526,275900-276038,276412-276550,276924-277062,277436-277574,277948-278086,278460-278598,278972-279110,279484-279622,279996-280134,280508-280646,281020-281158,281532-281670,282044-282182,282556-282694,283068-283206,283368-283389,283580-283718,283876-283906,284092-284230,284388-284418,284604-284742,285116-285254,285628-285766,286140-286278,286652-286790,286942-286983,287164-287302,287460-287490,287676-287814,287972-288002,288188-288326,288484-288514,288700-288838,288991-289030,289212-289350,289508-289538,289724-289862,290020-290050,290236-290374,290528-290565,290748-290886,291044-291074,291260-291398,291556-291586,291772-291910,292068-292098,292284-292422,292575-292615,292796-292934,293092-293122,293308-293446,293604-293634,293820-293958,294332-294470,294844-294982,295356-295494,295868-296006,296172-296186,296380-296518,296892-297030,297404-297542,297916-298054,298428-298566,298940-299078,299452-299590,299964-300102,300476-300614,300988-301126,301500-301638,302012-302150,302524-302662,303036-303174,303548-303686,304060-304198,304572-304710,305084-305222,305596-305734,306108-306246,306620-306758,307132-307270,307644-307782,308156-308294,308668-308806,309180-309318,309692-309830,310204-310342,310716-310854,311228-311366,311740-311878,312252-312390,312764-312902,313276-313414,313788-313926,314300-314438,314812-314950,315324-315462,315836-315974,316348-316486,316860-316998,317372-317510,317884-318022,318396-318534,318908-319046,319420-319558,319932-320070,320444-320582,320956-321094,321468-321606,321980-322118,322492-322630,323004-323142,323516-323654,324028-324166,324540-324678,325052-325190,325564-325702,326076-327750,328124-328262,328636-328774,329148-329286,329660-372806,373180-373318,373692-373830,374204-374342,374716-374854,375228-375366,375740-375878,376252-376390,376764-376902,377276-377414,377788-377926,378300-378438,378812-378950,379324-379462,379836-379974,380348-380486,380860-380998,381372-381510,381884-382022,382396-382534,382908-383046,383420-383558,383932-384070,384444-384582,384956-385094,385468-385606,385980-386118,386492-386630,387004-387142,387516-387654,388028-388166,388540-388678,389052-389190,389564-389702,390076-390214,390588-390726,391100-391238,391612-391750,392124-392262,392636-392774,393148-393286,393660-393798,394172-394310,394684-394822,395196-395334,395708-395846,396220-396358,396732-396870,397244-397382,397756-397894,398268-398406,398780-398918,399292-399430,399804-399942,400316-400454,400828-400966,401340-401478,401636-401666,401852-401990,402148-402178,402364-402502,402660-402690,402876-403014,403172-403202,403388-403526,403684-403714,403900-404038,404196-404226,404412-404550,404708-404738,404924-405062,405220-405250,405436-405574,405732-405762,405948-406086,406244-406274,406460-406598,406756-406786,406972-407110,407268-407298,407484-407622,407780-407810,407996-408134,408292-408322,408508-408646,409020-409158,409532-409670,410044-410182,410556-410694,410852-410882,411068-411206,411364-411394,411580-411718,412092-412230,412604-412742,413116-413254,413628-413766,414140-414278,414652-414790,415164-415302,415676-415814,416188-416326,416700-416838,417212-417350,417724-417862,418236-418374,418748-418886,419260-419398,419772-419910,420284-420422,420796-420934,421308-421446,421820-421958,422332-422470,422844-422982,423356-423494,423868-424006,424380-424518,424892-425030,425404-425542,425916-426054,426428-426566,426940-427078,427452-427590,427964-428102,428476-428614,428988-429126,429500-429638,430012-430150,430524-430662,431036-431174,431548-431686,432060-432198,432572-432710,433084-433222,433596-433734,434108-434246,434620-434758,435132-435270,435644-435782,436156-436294,436668-436806,437180-437318,437692-437830,438204-438342,438716-438854,439228-439366,439740-439878,440252-440390,440764-440902,441276-441414,441788-441926,442300-442438,442812-442950,443324-443462,443836-443974,444348-458752 + + diff --git a/src/ess/loki/examplefiles/out/.DS_Store b/src/ess/loki/examplefiles/out/.DS_Store new file mode 100644 index 00000000..a457438e Binary files /dev/null and b/src/ess/loki/examplefiles/out/.DS_Store differ diff --git a/src/ess/loki/tabwidget-visa-scicat.py b/src/ess/loki/tabwidget-visa-scicat.py new file mode 100644 index 00000000..05907c3d --- /dev/null +++ b/src/ess/loki/tabwidget-visa-scicat.py @@ -0,0 +1,1028 @@ +import os +import glob +import re +import h5py +import pandas as pd +import scipp as sc +import matplotlib.pyplot as plt +import numpy as np +import ipywidgets as widgets +from ipydatagrid import DataGrid +from IPython.display import display +from ipyfilechooser import FileChooser +from ess import sans +from ess import loki +from ess.sans.types import * +from scipp.scipy.interpolate import interp1d +import plopp as pp +import threading +import time +from ipywidgets import Layout +import csv +from scitacean import Client, Dataset, Attachment, Thumbnail +from scitacean.transfer.copy import CopyFileTransfer +from scitacean.transfer.select import SelectFileTransfer + +# ---------------------------- +# Utility Functions +# ---------------------------- +def find_file(work_dir, run_number, extension=".nxs"): + pattern = os.path.join(work_dir, f"*{run_number}*{extension}") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find file matching pattern {pattern}") + +def find_direct_beam(work_dir): #Find the direct beam automagically + pattern = os.path.join(work_dir, "*direct-beam*.h5") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find direct-beam file matching pattern {pattern}") + +def find_mask_file(work_dir): #Find the mask automagically + pattern = os.path.join(work_dir, "*mask*.xml") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find mask file matching pattern {pattern}") + +def save_xye_pandas(data_array, filename): ###Note here this needs to be 'fixed' / updated to use scipp io – ideally I want a nxcansas and xye saved for each file, but I struggled with the syntax and just did it in pandas as a first pass + q_vals = data_array.coords["Q"].values + i_vals = data_array.values + if len(q_vals) != len(i_vals): + q_vals = 0.5 * (q_vals[:-1] + q_vals[1:]) + if data_array.variances is not None: + err_vals = np.sqrt(data_array.variances) + if len(err_vals) != len(i_vals): + err_vals = 0.5 * (err_vals[:-1] + err_vals[1:]) + else: + err_vals = np.zeros_like(i_vals) + df = pd.DataFrame({"Q": q_vals, "I(Q)": i_vals, "Error": err_vals}) + df.to_csv(filename, sep=" ", index=False, header=True) + +def extract_run_number(filename): + m = re.search(r'(\d{4,})', filename) + if m: + return m.group(1) + return "" + +def parse_nx_details(filepath): #For finding/grouping files by common title assigned by NICOS, e.g. 'runlabel' and 'runtype' + details = {} + with h5py.File(filepath, 'r') as f: + if 'nicos_details' in f['entry']: + grp = f['entry']['nicos_details'] + if 'runlabel' in grp: + val = grp['runlabel'][()] + details['runlabel'] = val.decode('utf8') if isinstance(val, bytes) else str(val) + if 'runtype' in grp: + val = grp['runtype'][()] + details['runtype'] = val.decode('utf8') if isinstance(val, bytes) else str(val) + return details + +# ---------------------------- +# Colour Mapping From Filename + +def string_to_colour(input_str): + if not input_str: + return "#000000" # Empty input = black + total = 0 + for ch in input_str: + if ch.isalpha(): + total += ord(ch.lower()) - ord('a') + 1 # a=1, b=2, ..., z=26 + elif ch.isdigit(): + total += 1 + int(ch) * (25/9) # Maps '0' to 1 and '9' to 26 + # Special characters equal 0 + avg = total / len(input_str) + norm = max(0, min(1, avg / 26)) # Average and normalise to [0,1] + rgba = plt.get_cmap('flag')(norm) #prism + return '#{:02x}{:02x}{:02x}'.format(int(rgba[0]*255), + int(rgba[1]*255), + int(rgba[2]*255)) + + +# ---------------------------- +# Reduction and Plotting Functions +# ---------------------------- +def reduce_loki_batch_preliminary( + sample_run_file: str, + transmission_run_file: str, + background_run_file: str, + empty_beam_file: str, + direct_beam_file: str, + mask_files: list = None, + correct_for_gravity: bool = True, + uncertainty_mode = UncertaintyBroadcastMode.upper_bound, + return_events: bool = False, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + wavelength_n: int = 201, + q_start: float = 0.01, + q_stop: float = 0.3, + q_n: int = 101 +): + if mask_files is None: + mask_files = [] + wavelength_bins = sc.linspace("wavelength", wavelength_min, wavelength_max, wavelength_n, unit="angstrom") + q_bins = sc.linspace("Q", q_start, q_stop, q_n, unit="1/angstrom") + workflow = loki.LokiAtLarmorWorkflow() + if mask_files: + workflow = sans.with_pixel_mask_filenames(workflow, masks=mask_files) + workflow[NeXusDetectorName] = "larmor_detector" + workflow[WavelengthBins] = wavelength_bins + workflow[QBins] = q_bins + workflow[CorrectForGravity] = correct_for_gravity + workflow[UncertaintyBroadcastMode] = uncertainty_mode + workflow[ReturnEvents] = return_events + workflow[Filename[BackgroundRun]] = background_run_file + workflow[Filename[TransmissionRun[BackgroundRun]]] = transmission_run_file + workflow[Filename[EmptyBeamRun]] = empty_beam_file + workflow[DirectBeamFilename] = direct_beam_file + workflow[Filename[SampleRun]] = sample_run_file + workflow[Filename[TransmissionRun[SampleRun]]] = transmission_run_file + center = sans.beam_center_from_center_of_mass(workflow) + workflow[BeamCenter] = center + tf = workflow.compute(TransmissionFraction[SampleRun]) + da = workflow.compute(BackgroundSubtractedIofQ) + return {"transmission": tf, "IofQ": da} + +def save_reduction_plots(res, sample, sample_run_file, wavelength_min, wavelength_max, wavelength_n, q_min, q_max, q_n, output_dir, show=True): + fig, axs = plt.subplots(1, 2, figsize=(6, 3)) + axs[0].set_box_aspect(1) + axs[1].set_box_aspect(1) + title_str = f"{sample} - {os.path.basename(sample_run_file)}" + fig.suptitle(title_str, fontsize=12) + q_bins = sc.linspace("Q", q_min, q_max, q_n, unit="1/angstrom") + x_q = 0.5 * (q_bins.values[:-1] + q_bins.values[1:]) + if res["IofQ"].variances is not None: + yerr = np.sqrt(res["IofQ"].variances) + #axs[0].errorbar(x_q, res["IofQ"].values, yerr=yerr, fmt='o', linestyle='none', color='k', alpha=0.5, markerfacecolor='none') + axs[0].errorbar(x_q, res["IofQ"].values, yerr=yerr, fmt='o', linestyle='none', color='k', alpha=0.5, markerfacecolor=string_to_colour(sample), ecolor='k', markersize=6) + + else: + axs[0].scatter(x_q, res["IofQ"].values) + axs[0].set_xlabel("Q (Å$^{-1}$)") + axs[0].set_ylabel("I(Q)") + axs[0].set_xscale("log") + axs[0].set_yscale("log") + wavelength_bins = sc.linspace("wavelength", wavelength_min, wavelength_max, wavelength_n, unit="angstrom") + x_wl = 0.5 * (wavelength_bins.values[:-1] + wavelength_bins.values[1:]) + if res["transmission"].variances is not None: + yerr_tr = np.sqrt(res["transmission"].variances) + #axs[1].errorbar(x_wl, res["transmission"].values, yerr=yerr_tr, fmt='^', linestyle='none', color='k', alpha=0.5, markerfacecolor='none') + axs[1].errorbar(x_wl, res["transmission"].values, yerr=yerr_tr, fmt='^', linestyle='none', color='k', alpha=0.5, markerfacecolor=string_to_colour(sample), ecolor='k', markersize=6) + + + else: + axs[1].scatter(x_wl, res["transmission"].values) + axs[1].set_xlabel("Wavelength (Å)") + axs[1].set_ylabel("Transmission") + plt.tight_layout() + out_png = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", "_reduced.png")) + fig.savefig(out_png, dpi=300) + if show: + display(fig) + plt.close(fig) + +# ---------------------------- +# Unified "Backend" Function for Reduction +# ---------------------------- +def perform_reduction_for_sample( + sample_info: dict, + input_dir: str, + output_dir: str, + reduction_params: dict, + background_run_file: str, + empty_beam_file: str, + direct_beam_file: str, + log_func: callable +): + """ + Processes a single sample reduction: + - Finds the necessary run files + - Optionally determines a mask (or finds one automatically) + - Calls the reduction and plotting routines + - Logs all steps via log_func(message) ### edited to just print statements - does logfunc work correctly with voila??? + """ + sample = sample_info.get("SAMPLE", "Unknown") + try: + sample_run_file = find_file(input_dir, str(sample_info["SANS"]), extension=".nxs") + transmission_run_file = find_file(input_dir, str(sample_info["TRANS"]), extension=".nxs") + except Exception as e: + log_func(f"Skipping sample {sample}: {e}") + #print(f"Skipping sample {sample}: {e}") + + return None + # Determine mask file. + mask_file = None + mask_candidate = str(sample_info.get("mask", "")).strip() + if mask_candidate: + mask_candidate_file = os.path.join(input_dir, f"{mask_candidate}.xml") + if os.path.exists(mask_candidate_file): + mask_file = mask_candidate_file + if mask_file is None: + try: + mask_file = find_mask_file(input_dir) + log_func(f"Using mask: {mask_file} for sample {sample}") + #print(f"Identified mask file: {mask_file} for sample {sample}") + + except Exception as e: + log_func(f"Mask file not found for sample {sample}: {e}") + #print(f"Mask file not found for sample {sample}: {e}") + + return None + + log_func(f"Reducing sample {sample}...") + #print(f"Reducing sample {sample}...") + + try: + res = reduce_loki_batch_preliminary( + sample_run_file=sample_run_file, + transmission_run_file=transmission_run_file, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + mask_files=[mask_file], + wavelength_min=reduction_params["wavelength_min"], + wavelength_max=reduction_params["wavelength_max"], + wavelength_n=reduction_params["wavelength_n"], + q_start=reduction_params["q_start"], + q_stop=reduction_params["q_stop"], + q_n=reduction_params["q_n"] + ) + except Exception as e: + log_func(f"Reduction failed for sample {sample}: {e}") + #print(f"Reduction failed for sample {sample}: {e}") + return None + out_xye = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", ".xye")) + try: + save_xye_pandas(res["IofQ"], out_xye) + log_func(f"Saved reduced data to {out_xye}") + #print(f"Saved reduced data to {out_xye}") + + except Exception as e: + log_func(f"Failed to save reduced data for {sample}: {e}") + #print(f"Failed to save reduced data for {sample}: {e}") + try: + save_reduction_plots( + res, + sample, + sample_run_file, + reduction_params["wavelength_min"], + reduction_params["wavelength_max"], + reduction_params["wavelength_n"], + reduction_params["q_start"], + reduction_params["q_stop"], + reduction_params["q_n"], + output_dir, + show=True + ) + log_func(f"Saved reduction plot for sample {sample}.") + except Exception as e: + log_func(f"Failed to save reduction plot for {sample}: {e}") + #log_func(f"Reduced sample {sample} and saved outputs.") + return res +# print(f"Saved reduction plot for sample {sample}.") +# except Exception as e: +# print(f"Failed to save reduction plot for {sample}: {e}") +# print(f"Reduced sample {sample} and saved outputs.") +# return res + + +########################################################################################### + +############################################################################################## + +# ---------------------------- +# GUI Widgets +# ---------------------------- +class SansBatchReductionWidget: + def __init__(self): + # File Choosers for CSV, input dir, output dir + self.csv_chooser = FileChooser(select_dir=False) + self.csv_chooser.title = "Select CSV File" + self.csv_chooser.filter_pattern = "*.csv" + + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + + # Remove references to Ebeam SANS/TRANS widgets + # (since these are now specified per row in the CSV). + + # Reduction parameter widgets + self.wavelength_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.wavelength_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.wavelength_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_start_widget = widgets.FloatText(value=0.01, description="Q start (1/Å):") + self.q_stop_widget = widgets.FloatText(value=0.3, description="Q stop (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + + # Button to load CSV + self.load_csv_button = widgets.Button(description="Load CSV") + self.load_csv_button.on_click(self.load_csv) + + # Table to display/edit CSV data + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + + # Button to run reduction + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + + # (Optional) log/plot outputs + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + + # Main layout + self.main = widgets.VBox([ + widgets.HBox([self.csv_chooser, self.input_dir_chooser, self.output_dir_chooser]), + widgets.HBox([self.wavelength_min_widget, self.wavelength_max_widget, self.wavelength_n_widget]), + widgets.HBox([self.q_start_widget, self.q_stop_widget, self.q_n_widget]), + self.load_csv_button, + self.table, + widgets.HBox([self.reduce_button]), + self.log_output, + self.plot_output + ]) + + def load_csv(self, _): + """Loads the CSV file into the DataGrid.""" + csv_path = self.csv_chooser.selected + if not csv_path or not os.path.exists(csv_path): + with self.log_output: + print("CSV file not selected or does not exist.") + return + df = pd.read_csv(csv_path) + self.table.data = df + with self.log_output: + print(f"Loaded reduction table with {len(df)} rows from {csv_path}.") + + def run_reduction(self, _): + """Loops over each row of the CSV table, finds input files, and performs the reduction.""" + # Clear old log/plot outputs + self.log_output.clear_output() + self.plot_output.clear_output() + + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + + # Read current table data + df = self.table.data + + # Reduction parameters + reduction_params = { + "wavelength_min": self.wavelength_min_widget.value, + "wavelength_max": self.wavelength_max_widget.value, + "wavelength_n": self.wavelength_n_widget.value, + "q_start": self.q_start_widget.value, + "q_stop": self.q_stop_widget.value, + "q_n": self.q_n_widget.value + } + + # Loop over each row of the CSV table + for idx, row in df.iterrows(): + try: + # Use the CSV columns to find file paths + sample_run_file = find_file(input_dir, str(row["SANS"]), extension=".nxs") + transmission_run_file = find_file(input_dir, str(row["TRANS"]), extension=".nxs") + background_run_file = find_file(input_dir, str(row["Ebeam_SANS"]), extension=".nxs") + empty_beam_file = find_file(input_dir, str(row["Ebeam_TRANS"]), extension=".nxs") + + # For mask and direct beam, we assume the CSV filename is relative to input_dir + mask_file = os.path.join(input_dir, str(row["mask"])) + direct_beam_file = os.path.join(input_dir, str(row["direct_beam"])) + + except Exception as e: + # If something fails, log and skip this row + with self.log_output: + print(f"Error finding input files for row {idx} ({row['SAMPLE']}): {e}") + continue + + # Create a mini dict for the sample info + sample_info = { + "SAMPLE": row["SAMPLE"], + "SANS": row["SANS"], + "TRANS": row["TRANS"], + # We store the mask as a column, so pass it along + "mask": mask_file + } + + # Call the actual reduction + perform_reduction_for_sample( + sample_info=sample_info, + input_dir=input_dir, + output_dir=output_dir, + reduction_params=reduction_params, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + log_func=lambda msg: print(msg) + ) + + @property + def widget(self): + """Return the main widget layout.""" + return self.main + +class SemiAutoReductionWidget: + def __init__(self): + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.scan_button = widgets.Button(description="Scan Directory") + self.scan_button.on_click(self.scan_directory) + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + self.add_row_button = widgets.Button(description="Add Row") + self.add_row_button.on_click(self.add_row) + self.delete_row_button = widgets.Button(description="Delete Last Row") + self.delete_row_button.on_click(self.delete_last_row) + self.lambda_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.lambda_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.lambda_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_min_widget = widgets.FloatText(value=0.01, description="Qmin (1/Å):") + self.q_max_widget = widgets.FloatText(value=0.3, description="Qmax (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + self.empty_beam_sans_text = widgets.Text(value="", description="Ebeam SANS:", disabled=True) + self.empty_beam_trans_text = widgets.Text(value="", description="Ebeam TRANS:", disabled=True) + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + #self.clear_log_button = widgets.Button(description="Clear Log") + #self.clear_log_button.on_click(lambda _: self.log_output.clear_output()) + #self.clear_plots_button = widgets.Button(description="Clear Plots") + #self.clear_plots_button.on_click(lambda _: self.plot_output.clear_output()) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.processed = set() + self.main = widgets.VBox([ + widgets.HBox([self.input_dir_chooser, self.output_dir_chooser]), + self.scan_button, + self.table, + widgets.HBox([self.add_row_button, self.delete_row_button]), + widgets.HBox([self.lambda_min_widget, self.lambda_max_widget, self.lambda_n_widget]), + widgets.HBox([self.q_min_widget, self.q_max_widget, self.q_n_widget]), + widgets.HBox([self.empty_beam_sans_text, self.empty_beam_trans_text]), + widgets.HBox([self.reduce_button]),# self.clear_log_button, self.clear_plots_button]), + #self.log_output, + #self.plot_output + ]) + + def add_row(self, _): + df = self.table.data + new_row = {col: "" for col in df.columns} if not df.empty else {'SAMPLE': '', 'SANS': '', 'TRANS': ''} + df = df.append(new_row, ignore_index=True) + self.table.data = df + + def delete_last_row(self, _): + df = self.table.data + if not df.empty: + self.table.data = df.iloc[:-1] + + def scan_directory(self, _): + self.log_output.clear_output() + input_dir = self.input_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Invalid input folder.") + return + nxs_files = glob.glob(os.path.join(input_dir, "*.nxs")) + groups = {} + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runlabel' not in details or 'runtype' not in details: + continue + runlabel = details['runlabel'] + runtype = details['runtype'].lower() + run_number = extract_run_number(os.path.basename(f)) + if runlabel not in groups: + groups[runlabel] = {} + groups[runlabel][runtype] = run_number + table_rows = [] + for runlabel, d in groups.items(): + if 'sans' in d and 'trans' in d: + table_rows.append({'SAMPLE': runlabel, 'SANS': d['sans'], 'TRANS': d['trans']}) + df = pd.DataFrame(table_rows) + self.table.data = df + with self.log_output: + print(f"Scanned {len(nxs_files)} files. Found {len(df)} reduction entries.") + ebeam_sans_files = [] + ebeam_trans_files = [] + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runtype' in details: + if details['runtype'].lower() == 'ebeam_sans': + ebeam_sans_files.append(f) + elif details['runtype'].lower() == 'ebeam_trans': + ebeam_trans_files.append(f) + if ebeam_sans_files: + ebeam_sans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_sans_text.value = ebeam_sans_files[0] + else: + self.empty_beam_sans_text.value = "" + if ebeam_trans_files: + ebeam_trans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_trans_text.value = ebeam_trans_files[0] + else: + self.empty_beam_trans_text.value = "" + + def run_reduction(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct beam:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct beam file not found:", e) + return + background_run_file = self.empty_beam_sans_text.value + empty_beam_file = self.empty_beam_trans_text.value + if not background_run_file or not empty_beam_file: + with self.log_output: + print("Empty beam files not found.") + return + + reduction_params = { + "wavelength_min": self.lambda_min_widget.value, + "wavelength_max": self.lambda_max_widget.value, + "wavelength_n": self.lambda_n_widget.value, + "q_start": self.q_min_widget.value, + "q_stop": self.q_max_widget.value, + "q_n": self.q_n_widget.value + } + + #df = self.table.data.copy() + df = self.table.data.drop_duplicates(subset=['SAMPLE', 'SANS', 'TRANS']) + for idx, row in df.iterrows(): + perform_reduction_for_sample( + sample_info=row, + input_dir=input_dir, + output_dir=output_dir, + reduction_params=reduction_params, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + log_func=lambda msg: print(msg) + ) + + @property + def widget(self): + return self.main + +class AutoReductionWidget: + def __init__(self): + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.start_stop_button = widgets.Button(description="Start") + self.start_stop_button.on_click(self.toggle_running) + self.status_label = widgets.Label(value="Stopped") + self.table = DataGrid(pd.DataFrame([]), editable=False, auto_fit_columns=True) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.running = False + self.thread = None + self.processed = set() + self.empty_beam_sans = None + self.empty_beam_trans = None + self.main = widgets.VBox([ + widgets.HBox([self.input_dir_chooser, self.output_dir_chooser]), + widgets.HBox([self.start_stop_button, self.status_label]), + self.table, + self.log_output, + self.plot_output + ]) + # SciCat settings – adjust these as needed. + self.token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NmRhYjhmYzFiNThkNDFlYTM4OTc5MzIiLCJ1c2VybmFtZSI6Imh0dHBzOi8vbG9naW4uZXNzLmV1X29saXZlcmhhbW1vbmQiLCJlbWFpbCI6Im9saXZlci5oYW1tb25kQGVzcy5ldSIsImF1dGhTdHJhdGVneSI6Im9pZGMiLCJfX3YiOjAsImlkIjoiNjZkYWI4ZmMxYjU4ZDQxZWEzODk3OTMyIiwidXNlcklkIjoiNjZkYWI4ZmMxYjU4ZDQxZWEzODk3OTMyIiwiaWF0IjoxNzQyOTk2Mzc4LCJleHAiOjE3NDI5OTk5Nzh9.YytBMfX0p971InDFs0cSkfoVP92RvpgE_Vu9K_OLbiY' + self.scicat_url = 'https://staging.scicat.ess.eu/api/v3' + self.scicat_source_folder = '/scratch/oliverhammond/LARMOR/nxs/out/scicat' + + def toggle_running(self, _): + if not self.running: + self.running = True + self.start_stop_button.description = "Stop" + self.status_label.value = "Running" + self.thread = threading.Thread(target=self.background_loop, daemon=True) + self.thread.start() + else: + self.running = False + self.start_stop_button.description = "Start" + self.status_label.value = "Stopped" + + def background_loop(self): + while self.running: + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Invalid input folder. Waiting for valid selection...") + time.sleep(60) + continue + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Invalid output folder. Waiting for valid selection...") + time.sleep(60) + continue + nxs_files = glob.glob(os.path.join(input_dir, "*.nxs")) + groups = {} + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runlabel' not in details or 'runtype' not in details: + continue + runlabel = details['runlabel'] + runtype = details['runtype'].lower() + run_number = extract_run_number(os.path.basename(f)) + if runlabel not in groups: + groups[runlabel] = {} + groups[runlabel][runtype] = run_number + table_rows = [] + for runlabel, d in groups.items(): + if 'sans' in d and 'trans' in d: + table_rows.append({'SAMPLE': runlabel, 'SANS': d['sans'], 'TRANS': d['trans']}) + df = pd.DataFrame(table_rows) + self.table.data = df + with self.log_output: + print(f"Scanned {len(nxs_files)} files. Found {len(df)} reduction entries.") + ebeam_sans_files = [] + ebeam_trans_files = [] + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runtype' in details: + if details['runtype'].lower() == 'ebeam_sans': + ebeam_sans_files.append(f) + elif details['runtype'].lower() == 'ebeam_trans': + ebeam_trans_files.append(f) + if ebeam_sans_files: + ebeam_sans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_sans = ebeam_sans_files[0] + else: + self.empty_beam_sans = None + if ebeam_trans_files: + ebeam_trans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_trans = ebeam_trans_files[0] + else: + self.empty_beam_trans = None + try: + direct_beam_file = find_direct_beam(input_dir) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + time.sleep(60) + continue + for index, row in df.iterrows(): + key = (row["SAMPLE"], row["SANS"], row["TRANS"]) + if key in self.processed: + continue + try: + sample_run_file = find_file(input_dir, row["SANS"], extension=".nxs") + transmission_run_file = find_file(input_dir, row["TRANS"], extension=".nxs") + except Exception as e: + with self.log_output: + print(f"Skipping sample {row['SAMPLE']}: {e}") + continue + try: + mask_file = find_mask_file(input_dir) + with self.log_output: + print(f"Using mask file: {mask_file} for sample {row['SAMPLE']}") + except Exception as e: + with self.log_output: + print(f"Mask file not found for sample {row['SAMPLE']}: {e}") + continue + if not self.empty_beam_sans or not self.empty_beam_trans: + with self.log_output: + print("Empty beam files not found, skipping reduction for sample", row["SAMPLE"]) + continue + with self.log_output: + print(f"Reducing sample {row['SAMPLE']}...") + reduction_params = { + "wavelength_min": 1.0, + "wavelength_max": 13.0, + "wavelength_n": 201, + "q_start": 0.01, + "q_stop": 0.3, + "q_n": 101 + } + perform_reduction_for_sample( + sample_info=row, + input_dir=input_dir, + output_dir=output_dir, + reduction_params=reduction_params, + background_run_file=self.empty_beam_sans, + empty_beam_file=self.empty_beam_trans, + direct_beam_file=direct_beam_file, + log_func=lambda msg: print(msg) + ) + self.processed.add(key) + # Call the uploader function using the run number/name from the reduction table. + self.upload_dataset(run=row["SANS"], sample_name=row["SAMPLE"], metadata_file='uos_metadata.csv') + time.sleep(60) + + def upload_dataset(self, run, sample_name, metadata_file='uos_metadata.csv'): + """ + Uploads a reduced dataset to SciCat using files in the widget's output directory. + Metadata is combined from the reduction process (run, sample_name) and additional + arguments provided in a CSV file as a proxy for metadata provided by the user office. + + The CSV file should have a header with these columns: + contact_email, owner_email, investigator, owner, owner_group, description + + Parameters: + run (str): The run number to search for and use in the dataset (same as tabular input). + sample_name (str): The sample name to include in the dataset metadata (from nxs file). + metadata_file (str): Path to the CSV file containing additional metadata. + """ + # Read metadata from the CSV file. + try: + with open(metadata_file, newline='') as csvfile: + reader = csv.DictReader(csvfile) + row = next(reader) + except Exception as e: + with self.log_output: + print(f"Error reading metadata CSV file '{metadata_file}': {e}") + return + + contact_email = row.get('contact_email', 'default@example.com') + owner_email = row.get('owner_email', contact_email) + investigator = row.get('investigator', 'Unknown') + owner = row.get('owner', 'Unknown') + owner_group = row.get('owner_group', 'ess') + description = row.get('description', '') + + # Use the output directory from the widget to search for the reduced files. + file_folder = self.output_dir_chooser.selected + if not file_folder or not os.path.isdir(file_folder): + with self.log_output: + print("Invalid output folder selected for uploading.") + return + + # Initialize the SciCat client. + client = Client.from_token( + url=self.scicat_url, + token=self.token, + file_transfer=SelectFileTransfer([CopyFileTransfer()]) + ) + + # Use glob to find the .xye and .png files based on the run number. + xye_pattern = os.path.join(file_folder, f"*{run}*.xye") + png_pattern = os.path.join(file_folder, f"*{run}*_reduced.png") + xye_files = glob.glob(xye_pattern) + png_files = glob.glob(png_pattern) + + if not xye_files: + with self.log_output: + print(f"No .xye file found for run {run}.") + return + if not png_files: + with self.log_output: + print(f"No .png file found for run {run}.") + return + + xye_file = xye_files[0] # Use first matching file. + png_file = png_files[0] # Use first matching file. + + # Construct the dataset object with combined metadata. + dataset = Dataset( + type='derived', + contact_email=contact_email, + owner_email=owner_email, + input_datasets=[], + investigator=investigator, + owner=owner, + owner_group=owner_group, + access_groups=[owner_group], + source_folder=self.scicat_source_folder, + used_software=['esssans'], + name=f"{run}.xye", # Derived from run number. + description=description, + run_number=run, + meta={'sample_name': {'value': sample_name, 'unit': ''}} + ) + + # Add the primary .xye file. + dataset.add_local_files(xye_file) + + # Add the attachment (thumbnail). + dataset.attachments.append( + Attachment( + caption=f"Reduced I(Q) and transmission for {dataset.name}", + owner_group=owner_group, + thumbnail=Thumbnail.load_file(png_file) + ) + ) + + # Upload the dataset. + client.upload_new_dataset_now(dataset) + with self.log_output: + print(f"Uploaded dataset for run {run} using files:\n - {xye_file}\n - {png_file}") + + @property + def widget(self): + return self.main + +# ---------------------------- +# Direct Beam stuff +# ---------------------------- +def compute_direct_beam_local( + mask: str, + sample_sans: str, + background_sans: str, + sample_trans: str, + background_trans: str, + empty_beam: str, + local_Iq_theory: str, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + n_wavelength_bins: int = 50, + n_wavelength_bands: int = 50 +) -> dict: + workflow = loki.LokiAtLarmorWorkflow() + workflow = sans.with_pixel_mask_filenames(workflow, masks=[mask]) + workflow[NeXusDetectorName] = 'larmor_detector' + wl_min = sc.scalar(wavelength_min, unit='angstrom') + wl_max = sc.scalar(wavelength_max, unit='angstrom') + workflow[WavelengthBins] = sc.linspace('wavelength', wl_min, wl_max, n_wavelength_bins + 1) + workflow[WavelengthBands] = sc.linspace('wavelength', wl_min, wl_max, n_wavelength_bands + 1) + workflow[CorrectForGravity] = True + workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound + workflow[ReturnEvents] = False + workflow[QBins] = sc.linspace(dim='Q', start=0.01, stop=0.3, num=101, unit='1/angstrom') + workflow[Filename[SampleRun]] = sample_sans + workflow[Filename[BackgroundRun]] = background_sans + workflow[Filename[TransmissionRun[SampleRun]]] = sample_trans + workflow[Filename[TransmissionRun[BackgroundRun]]] = background_trans + workflow[Filename[EmptyBeamRun]] = empty_beam + center = sans.beam_center_from_center_of_mass(workflow) + print("Computed beam center:", center) + workflow[BeamCenter] = center + Iq_theory = sc.io.load_hdf5(local_Iq_theory) + f = interp1d(Iq_theory, 'Q') + I0 = f(sc.midpoints(workflow.compute(QBins))).data[0] + print("Computed I0:", I0) + results = sans.direct_beam(workflow=workflow, I0=I0, niter=6) + iofq_full = results[-1]['iofq_full'] + iofq_bands = results[-1]['iofq_bands'] + direct_beam_function = results[-1]['direct_beam'] + pp.plot( + {'reference': Iq_theory, 'data': iofq_full}, + color={'reference': 'darkgrey', 'data': 'C0'}, + norm='log', + ) + print("Plotted full-range result vs. theoretical reference.") + return { + 'direct_beam_function': direct_beam_function, + 'iofq_full': iofq_full, + 'Iq_theory': Iq_theory, + } + +class DirectBeamWidget: + def __init__(self): + self.mask_text = widgets.Text(value="", placeholder="Enter mask file path", description="Mask:") + self.sample_sans_text = widgets.Text(value="", placeholder="Enter sample SANS file path", description="Sample SANS:") + self.background_sans_text = widgets.Text(value="", placeholder="Enter background SANS file path", description="Background SANS:") + self.sample_trans_text = widgets.Text(value="", placeholder="Enter sample TRANS file path", description="Sample TRANS:") + self.background_trans_text = widgets.Text(value="", placeholder="Enter background TRANS file path", description="Background TRANS:") + self.empty_beam_text = widgets.Text(value="", placeholder="Enter empty beam file path", description="Empty Beam:") + self.local_Iq_theory_text = widgets.Text(value="", placeholder="Enter I(q) Theory file path", description="I(q) Theory:") + self.db_wavelength_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.db_wavelength_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.db_n_wavelength_bins_widget = widgets.IntText(value=50, description="λ n_bins:") + self.db_n_wavelength_bands_widget = widgets.IntText(value=50, description="λ n_bands:") + self.compute_button = widgets.Button(description="Compute Direct Beam") + self.compute_button.on_click(self.compute_direct_beam) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.main = widgets.VBox([ + self.mask_text, + self.sample_sans_text, + self.background_sans_text, + self.sample_trans_text, + self.background_trans_text, + self.empty_beam_text, + self.local_Iq_theory_text, + widgets.HBox([ + self.db_wavelength_min_widget, + self.db_wavelength_max_widget, + self.db_n_wavelength_bins_widget, + self.db_n_wavelength_bands_widget + ]), + self.compute_button, + self.log_output, + self.plot_output + ]) + + def compute_direct_beam(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + mask = self.mask_text.value + sample_sans = self.sample_sans_text.value + background_sans = self.background_sans_text.value + sample_trans = self.sample_trans_text.value + background_trans = self.background_trans_text.value + empty_beam = self.empty_beam_text.value + local_Iq_theory = self.local_Iq_theory_text.value + wl_min = self.db_wavelength_min_widget.value + wl_max = self.db_wavelength_max_widget.value + n_bins = self.db_n_wavelength_bins_widget.value + n_bands = self.db_n_wavelength_bands_widget.value + with self.log_output: + print("Computing direct beam with:") + print(" Mask:", mask) + print(" Sample SANS:", sample_sans) + print(" Background SANS:", background_sans) + print(" Sample TRANS:", sample_trans) + print(" Background TRANS:", background_trans) + print(" Empty Beam:", empty_beam) + print(" I(q) Theory:", local_Iq_theory) + print(" λ min:", wl_min, "λ max:", wl_max, "n_bins:", n_bins, "n_bands:", n_bands) + try: + results = compute_direct_beam_local( + mask, + sample_sans, + background_sans, + sample_trans, + background_trans, + empty_beam, + local_Iq_theory, + wavelength_min=wl_min, + wavelength_max=wl_max, + n_wavelength_bins=n_bins, + n_wavelength_bands=n_bands + ) + with self.log_output: + print("Direct beam computation complete.") + except Exception as e: + with self.log_output: + print("Error computing direct beam:", e) + + @property + def widget(self): + return self.main + +# ---------------------------- +# Build it +# ---------------------------- +#reduction_widget = SansBatchReductionWidget().widget +#direct_beam_widget = DirectBeamWidget().widget +#semi_auto_reduction_widget = SemiAutoReductionWidget().widget +#auto_reduction_widget = AutoReductionWidget().widget + +#tabs = widgets.Tab(children=[direct_beam_widget, reduction_widget, semi_auto_reduction_widget, auto_reduction_widget]) +#tabs.set_title(0, "Direct Beam") +#tabs.set_title(1, "Reduction (Manual)") +#tabs.set_title(2, "Reduction (Smart)") +#tabs.set_title(3, "Reduction (Auto)") + +reduction_widget = SansBatchReductionWidget().widget +#direct_beam_widget = DirectBeamWidget().widget +semi_auto_reduction_widget = SemiAutoReductionWidget().widget +auto_reduction_widget = AutoReductionWidget().widget + +tabs = widgets.Tab(children=[reduction_widget, semi_auto_reduction_widget, auto_reduction_widget]) +#tabs.set_title(0, "Direct Beam") +tabs.set_title(0, "Reduction (Manual)") +#tabs.set_title(2, "Reduction (Smart)") +tabs.set_title(1, "Reduction (Smart)") +tabs.set_title(2, "Reduction (Auto)") + + +# display(tabs) +# voila /src/ess/loki/tabwidget.ipynb #--theme=dark + diff --git a/src/ess/loki/tabwidget.ipynb b/src/ess/loki/tabwidget.ipynb new file mode 100644 index 00000000..81508ea3 --- /dev/null +++ b/src/ess/loki/tabwidget.ipynb @@ -0,0 +1,57 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7024d6d63db745e98d2b39f691d4e559", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Tab(children=(VBox(children=(Text(value='', description='Mask:', placeholder='Enter mask file path'), Text(val…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from tabwidget import tabs\n", + "display(tabs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/src/ess/loki/tabwidget.py b/src/ess/loki/tabwidget.py new file mode 100644 index 00000000..8346cc3a --- /dev/null +++ b/src/ess/loki/tabwidget.py @@ -0,0 +1,871 @@ +import os +import glob +import re +import h5py +import pandas as pd +import scipp as sc +import matplotlib.pyplot as plt +import numpy as np +import ipywidgets as widgets +from ipydatagrid import DataGrid +from IPython.display import display +from ipyfilechooser import FileChooser +from ess import sans +from ess import loki +from ess.sans.types import * +from scipp.scipy.interpolate import interp1d +import plopp as pp +import threading +import time +from ipywidgets import Layout + +# ---------------------------- +# Utility Functions +# ---------------------------- +def find_file(work_dir, run_number, extension=".nxs"): + pattern = os.path.join(work_dir, f"*{run_number}*{extension}") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find file matching pattern {pattern}") + +def find_direct_beam(work_dir): #Find the direct beam automagically + pattern = os.path.join(work_dir, "*direct-beam*.h5") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find direct beam file matching pattern {pattern}") + +def find_mask_file(work_dir): #Find the mask automagically + pattern = os.path.join(work_dir, "*mask*.xml") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find mask file matching pattern {pattern}") + +def save_xye_pandas(data_array, filename): ###Note here this needs to be 'fixed' / updated to use scipp io – ideally I want a nxcansas and xye saved for each file, but I struggled with the syntax and just did it in pandas as a first pass + q_vals = data_array.coords["Q"].values + i_vals = data_array.values + if len(q_vals) != len(i_vals): + q_vals = 0.5 * (q_vals[:-1] + q_vals[1:]) + if data_array.variances is not None: + err_vals = np.sqrt(data_array.variances) + if len(err_vals) != len(i_vals): + err_vals = 0.5 * (err_vals[:-1] + err_vals[1:]) + else: + err_vals = np.zeros_like(i_vals) + df = pd.DataFrame({"Q": q_vals, "I(Q)": i_vals, "Error": err_vals}) + df.to_csv(filename, sep=" ", index=False, header=True) + +def extract_run_number(filename): + m = re.search(r'(\d{4,})', filename) + if m: + return m.group(1) + return "" + +def parse_nx_details(filepath): #For finding/grouping files by common title assigned by NICOS, e.g. 'runlabel' and 'runtype' + details = {} + with h5py.File(filepath, 'r') as f: + if 'nicos_details' in f['entry']: + grp = f['entry']['nicos_details'] + if 'runlabel' in grp: + val = grp['runlabel'][()] + details['runlabel'] = val.decode('utf8') if isinstance(val, bytes) else str(val) + if 'runtype' in grp: + val = grp['runtype'][()] + details['runtype'] = val.decode('utf8') if isinstance(val, bytes) else str(val) + return details + +# ---------------------------- +# Colour Mapping From Filename + +def string_to_colour(input_str): + if not input_str: + return "#000000" # Empty input = black + total = 0 + for ch in input_str: + if ch.isalpha(): + total += ord(ch.lower()) - ord('a') + 1 # a=1, b=2, ..., z=26 + elif ch.isdigit(): + total += 1 + int(ch) * (25/9) # Maps '0' to 1 and '9' to 26 + # Special characters equal 0 + avg = total / len(input_str) + norm = max(0, min(1, avg / 26)) # Average and normalise to [0,1] + rgba = plt.get_cmap('flag')(norm) #prism + return '#{:02x}{:02x}{:02x}'.format(int(rgba[0]*255), + int(rgba[1]*255), + int(rgba[2]*255)) + + +# ---------------------------- +# Reduction and Plotting Functions +# ---------------------------- +def reduce_loki_batch_preliminary( + sample_run_file: str, + transmission_run_file: str, + background_run_file: str, + empty_beam_file: str, + direct_beam_file: str, + mask_files: list = None, + correct_for_gravity: bool = True, + uncertainty_mode = UncertaintyBroadcastMode.upper_bound, + return_events: bool = False, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + wavelength_n: int = 201, + q_start: float = 0.01, + q_stop: float = 0.3, + q_n: int = 101 +): + if mask_files is None: + mask_files = [] + wavelength_bins = sc.linspace("wavelength", wavelength_min, wavelength_max, wavelength_n, unit="angstrom") + q_bins = sc.linspace("Q", q_start, q_stop, q_n, unit="1/angstrom") + workflow = loki.LokiAtLarmorWorkflow() + if mask_files: + workflow = sans.with_pixel_mask_filenames(workflow, masks=mask_files) + workflow[NeXusDetectorName] = "larmor_detector" + workflow[WavelengthBins] = wavelength_bins + workflow[QBins] = q_bins + workflow[CorrectForGravity] = correct_for_gravity + workflow[UncertaintyBroadcastMode] = uncertainty_mode + workflow[ReturnEvents] = return_events + workflow[Filename[BackgroundRun]] = background_run_file + workflow[Filename[TransmissionRun[BackgroundRun]]] = transmission_run_file + workflow[Filename[EmptyBeamRun]] = empty_beam_file + workflow[DirectBeamFilename] = direct_beam_file + workflow[Filename[SampleRun]] = sample_run_file + workflow[Filename[TransmissionRun[SampleRun]]] = transmission_run_file + center = sans.beam_center_from_center_of_mass(workflow) + workflow[BeamCenter] = center + tf = workflow.compute(TransmissionFraction[SampleRun]) + da = workflow.compute(BackgroundSubtractedIofQ) + return {"transmission": tf, "IofQ": da} + +def save_reduction_plots(res, sample, sample_run_file, wavelength_min, wavelength_max, wavelength_n, q_min, q_max, q_n, output_dir, show=True): + fig, axs = plt.subplots(1, 2, figsize=(8, 4)) + axs[0].set_box_aspect(1) + axs[1].set_box_aspect(1) + title_str = f"{sample} - {os.path.basename(sample_run_file)}" + fig.suptitle(title_str, fontsize=14) + q_bins = sc.linspace("Q", q_min, q_max, q_n, unit="1/angstrom") + x_q = 0.5 * (q_bins.values[:-1] + q_bins.values[1:]) + if res["IofQ"].variances is not None: + yerr = np.sqrt(res["IofQ"].variances) + #axs[0].errorbar(x_q, res["IofQ"].values, yerr=yerr, fmt='o', linestyle='none', color='k', alpha=0.5, markerfacecolor='none') + axs[0].errorbar(x_q, res["IofQ"].values, yerr=yerr, fmt='o', linestyle='none', color='k', alpha=0.5, markerfacecolor=string_to_colour(sample), ecolor='k', markersize=6) + + else: + axs[0].scatter(x_q, res["IofQ"].values) + axs[0].set_xlabel("Q (Å$^{-1}$)") + axs[0].set_ylabel("I(Q)") + axs[0].set_xscale("log") + axs[0].set_yscale("log") + wavelength_bins = sc.linspace("wavelength", wavelength_min, wavelength_max, wavelength_n, unit="angstrom") + x_wl = 0.5 * (wavelength_bins.values[:-1] + wavelength_bins.values[1:]) + if res["transmission"].variances is not None: + yerr_tr = np.sqrt(res["transmission"].variances) + #axs[1].errorbar(x_wl, res["transmission"].values, yerr=yerr_tr, fmt='^', linestyle='none', color='k', alpha=0.5, markerfacecolor='none') + axs[1].errorbar(x_wl, res["transmission"].values, yerr=yerr_tr, fmt='^', linestyle='none', color='k', alpha=0.5, markerfacecolor=string_to_colour(sample), ecolor='k', markersize=6) + + + else: + axs[1].scatter(x_wl, res["transmission"].values) + axs[1].set_xlabel("Wavelength (Å)") + axs[1].set_ylabel("Transmission") + plt.tight_layout() + out_png = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", "_reduced.png")) + fig.savefig(out_png, dpi=300) + if show: + display(fig) + plt.close(fig) + +# ---------------------------- +# Unified "Backend" Function for Reduction +# ---------------------------- +def perform_reduction_for_sample( + sample_info: dict, + input_dir: str, + output_dir: str, + reduction_params: dict, + background_run_file: str, + empty_beam_file: str, + direct_beam_file: str, + #log_func: callable +): + """ + Processes a single sample reduction: + - Finds the necessary run files + - Optionally determines a mask (or finds one automatically) + - Calls the reduction and plotting routines + - Logs all steps via log_func(message) ### edited to just print statements - does logfunc work correctly with voila??? + """ + sample = sample_info.get("SAMPLE", "Unknown") + try: + sample_run_file = find_file(input_dir, str(sample_info["SANS"]), extension=".nxs") + transmission_run_file = find_file(input_dir, str(sample_info["TRANS"]), extension=".nxs") + except Exception as e: + #log_func(f"Skipping sample {sample}: {e}") + print(f"Skipping sample {sample}: {e}") + + return None + # Determine mask file. + mask_file = None + mask_candidate = str(sample_info.get("mask", "")).strip() + if mask_candidate: + mask_candidate_file = os.path.join(input_dir, f"{mask_candidate}.xml") + if os.path.exists(mask_candidate_file): + mask_file = mask_candidate_file + if mask_file is None: + try: + mask_file = find_mask_file(input_dir) + #log_func(f"Identified mask file: {mask_file} for sample {sample}") + print(f"Identified mask file: {mask_file} for sample {sample}") + + except Exception as e: + #log_func(f"Mask file not found for sample {sample}: {e}") + print(f"Mask file not found for sample {sample}: {e}") + + return None + + #log_func(f"Reducing sample {sample}...") + print(f"Reducing sample {sample}...") + + try: + res = reduce_loki_batch_preliminary( + sample_run_file=sample_run_file, + transmission_run_file=transmission_run_file, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + mask_files=[mask_file], + wavelength_min=reduction_params["wavelength_min"], + wavelength_max=reduction_params["wavelength_max"], + wavelength_n=reduction_params["wavelength_n"], + q_start=reduction_params["q_start"], + q_stop=reduction_params["q_stop"], + q_n=reduction_params["q_n"] + ) + except Exception as e: + #log_func(f"Reduction failed for sample {sample}: {e}") + print(f"Reduction failed for sample {sample}: {e}") + return None + out_xye = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", ".xye")) + try: + save_xye_pandas(res["IofQ"], out_xye) + #log_func(f"Saved reduced data to {out_xye}") + print(f"Saved reduced data to {out_xye}") + + except Exception as e: + #log_func(f"Failed to save reduced data for {sample}: {e}") + print(f"Failed to save reduced data for {sample}: {e}") + try: + save_reduction_plots( + res, + sample, + sample_run_file, + reduction_params["wavelength_min"], + reduction_params["wavelength_max"], + reduction_params["wavelength_n"], + reduction_params["q_start"], + reduction_params["q_stop"], + reduction_params["q_n"], + output_dir, + show=True + ) +# log_func(f"Saved reduction plot for sample {sample}.") +# except Exception as e: +# log_func(f"Failed to save reduction plot for {sample}: {e}") +# log_func(f"Reduced sample {sample} and saved outputs.") +# return res + print(f"Saved reduction plot for sample {sample}.") + except Exception as e: + print(f"Failed to save reduction plot for {sample}: {e}") + print(f"Reduced sample {sample} and saved outputs.") + return res + +# ---------------------------- +# GUI Widgets +# ---------------------------- +class SansBatchReductionWidget: + def __init__(self): + self.csv_chooser = FileChooser(select_dir=False) + self.csv_chooser.title = "Select CSV File" + self.csv_chooser.filter_pattern = "*.csv" + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.ebeam_sans_widget = widgets.Text(value="", placeholder="Enter Ebeam SANS run number", description="Ebeam SANS:") + self.ebeam_trans_widget = widgets.Text(value="", placeholder="Enter Ebeam TRANS run number", description="Ebeam TRANS:") + self.wavelength_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.wavelength_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.wavelength_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_start_widget = widgets.FloatText(value=0.01, description="Q start (1/Å):") + self.q_stop_widget = widgets.FloatText(value=0.3, description="Q stop (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + self.load_csv_button = widgets.Button(description="Load CSV") + self.load_csv_button.on_click(self.load_csv) + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + self.clear_log_button = widgets.Button(description="Clear Log") + self.clear_log_button.on_click(lambda _: self.log_output.clear_output()) + self.clear_plots_button = widgets.Button(description="Clear Plots") + self.clear_plots_button.on_click(lambda _: self.plot_output.clear_output()) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.main = widgets.VBox([ + widgets.HBox([self.csv_chooser, self.input_dir_chooser, self.output_dir_chooser]), + widgets.HBox([self.ebeam_sans_widget, self.ebeam_trans_widget]), + widgets.HBox([self.wavelength_min_widget, self.wavelength_max_widget, self.wavelength_n_widget]), + widgets.HBox([self.q_start_widget, self.q_stop_widget, self.q_n_widget]), + self.load_csv_button, + self.table, + widgets.HBox([self.reduce_button, self.clear_log_button, self.clear_plots_button]), + self.log_output, + self.plot_output + ]) + + def load_csv(self, _): + csv_path = self.csv_chooser.selected + if not csv_path or not os.path.exists(csv_path): + with self.log_output: + print("CSV file not selected or does not exist.") + return + df = pd.read_csv(csv_path) + self.table.data = df + with self.log_output: + print(f"Loaded reduction table with {len(df)} rows from {csv_path}.") + + def run_reduction(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct beam file:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct beam file not found:", e) + return + try: + background_run_file = find_file(input_dir, self.ebeam_sans_widget.value, extension=".nxs") + empty_beam_file = find_file(input_dir, self.ebeam_trans_widget.value, extension=".nxs") + with self.log_output: + print("Using empty beam files:") + print(" Background (Ebeam SANS):", background_run_file) + print(" Empty beam (Ebeam TRANS):", empty_beam_file) + except Exception as e: + with self.log_output: + print("Error finding empty beam files:", e) + return + + reduction_params = { + "wavelength_min": self.wavelength_min_widget.value, + "wavelength_max": self.wavelength_max_widget.value, + "wavelength_n": self.wavelength_n_widget.value, + "q_start": self.q_start_widget.value, + "q_stop": self.q_stop_widget.value, + "q_n": self.q_n_widget.value + } + + df = self.table.data + for idx, row in df.iterrows(): + perform_reduction_for_sample( + sample_info=row, + input_dir=input_dir, + output_dir=output_dir, + reduction_params=reduction_params, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + #log_func=lambda msg: print(msg) + ) + + @property + def widget(self): + return self.main + +class SemiAutoReductionWidget: + def __init__(self): + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.scan_button = widgets.Button(description="Scan Directory") + self.scan_button.on_click(self.scan_directory) + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + self.add_row_button = widgets.Button(description="Add Row") + self.add_row_button.on_click(self.add_row) + self.delete_row_button = widgets.Button(description="Delete Last Row") + self.delete_row_button.on_click(self.delete_last_row) + self.lambda_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.lambda_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.lambda_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_min_widget = widgets.FloatText(value=0.01, description="Qmin (1/Å):") + self.q_max_widget = widgets.FloatText(value=0.3, description="Qmax (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + self.empty_beam_sans_text = widgets.Text(value="", description="Ebeam SANS:", disabled=True) + self.empty_beam_trans_text = widgets.Text(value="", description="Ebeam TRANS:", disabled=True) + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + #self.clear_log_button = widgets.Button(description="Clear Log") + #self.clear_log_button.on_click(lambda _: self.log_output.clear_output()) + #self.clear_plots_button = widgets.Button(description="Clear Plots") + #self.clear_plots_button.on_click(lambda _: self.plot_output.clear_output()) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.processed = set() + self.main = widgets.VBox([ + widgets.HBox([self.input_dir_chooser, self.output_dir_chooser]), + self.scan_button, + self.table, + widgets.HBox([self.add_row_button, self.delete_row_button]), + widgets.HBox([self.lambda_min_widget, self.lambda_max_widget, self.lambda_n_widget]), + widgets.HBox([self.q_min_widget, self.q_max_widget, self.q_n_widget]), + widgets.HBox([self.empty_beam_sans_text, self.empty_beam_trans_text]), + widgets.HBox([self.reduce_button]),# self.clear_log_button, self.clear_plots_button]), + self.log_output, + self.plot_output + ]) + + def add_row(self, _): + df = self.table.data + new_row = {col: "" for col in df.columns} if not df.empty else {'SAMPLE': '', 'SANS': '', 'TRANS': ''} + df = df.append(new_row, ignore_index=True) + self.table.data = df + + def delete_last_row(self, _): + df = self.table.data + if not df.empty: + self.table.data = df.iloc[:-1] + + def scan_directory(self, _): + self.log_output.clear_output() + input_dir = self.input_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Invalid input folder.") + return + nxs_files = glob.glob(os.path.join(input_dir, "*.nxs")) + groups = {} + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runlabel' not in details or 'runtype' not in details: + continue + runlabel = details['runlabel'] + runtype = details['runtype'].lower() + run_number = extract_run_number(os.path.basename(f)) + if runlabel not in groups: + groups[runlabel] = {} + groups[runlabel][runtype] = run_number + table_rows = [] + for runlabel, d in groups.items(): + if 'sans' in d and 'trans' in d: + table_rows.append({'SAMPLE': runlabel, 'SANS': d['sans'], 'TRANS': d['trans']}) + df = pd.DataFrame(table_rows) + self.table.data = df + with self.log_output: + print(f"Scanned {len(nxs_files)} files. Found {len(df)} reduction entries.") + ebeam_sans_files = [] + ebeam_trans_files = [] + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runtype' in details: + if details['runtype'].lower() == 'ebeam_sans': + ebeam_sans_files.append(f) + elif details['runtype'].lower() == 'ebeam_trans': + ebeam_trans_files.append(f) + if ebeam_sans_files: + ebeam_sans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_sans_text.value = ebeam_sans_files[0] + else: + self.empty_beam_sans_text.value = "" + if ebeam_trans_files: + ebeam_trans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_trans_text.value = ebeam_trans_files[0] + else: + self.empty_beam_trans_text.value = "" + + def run_reduction(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct beam file:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct beam file not found:", e) + return + background_run_file = self.empty_beam_sans_text.value + empty_beam_file = self.empty_beam_trans_text.value + if not background_run_file or not empty_beam_file: + with self.log_output: + print("Empty beam files not found.") + return + + reduction_params = { + "wavelength_min": self.lambda_min_widget.value, + "wavelength_max": self.lambda_max_widget.value, + "wavelength_n": self.lambda_n_widget.value, + "q_start": self.q_min_widget.value, + "q_stop": self.q_max_widget.value, + "q_n": self.q_n_widget.value + } + + df = self.table.data.copy() + for idx, row in df.iterrows(): + perform_reduction_for_sample( + sample_info=row, + input_dir=input_dir, + output_dir=output_dir, + reduction_params=reduction_params, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + #log_func=lambda msg: print(msg) + ) + + @property + def widget(self): + return self.main + +class AutoReductionWidget: + def __init__(self): + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.start_stop_button = widgets.Button(description="Start") + self.start_stop_button.on_click(self.toggle_running) + self.status_label = widgets.Label(value="Stopped") + self.table = DataGrid(pd.DataFrame([]), editable=False, auto_fit_columns=True) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.running = False + self.thread = None + self.processed = set() + self.empty_beam_sans = None + self.empty_beam_trans = None + self.main = widgets.VBox([ + widgets.HBox([self.input_dir_chooser, self.output_dir_chooser]), + widgets.HBox([self.start_stop_button, self.status_label]), + self.table, + self.log_output, + self.plot_output + ]) + + def toggle_running(self, _): + if not self.running: + self.running = True + self.start_stop_button.description = "Stop" + self.status_label.value = "Running" + self.thread = threading.Thread(target=self.background_loop, daemon=True) + self.thread.start() + else: + self.running = False + self.start_stop_button.description = "Start" + self.status_label.value = "Stopped" + + def background_loop(self): + while self.running: + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Invalid input folder. Waiting for valid selection...") + time.sleep(60) + continue + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Invalid output folder. Waiting for valid selection...") + time.sleep(60) + continue + nxs_files = glob.glob(os.path.join(input_dir, "*.nxs")) + groups = {} + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runlabel' not in details or 'runtype' not in details: + continue + runlabel = details['runlabel'] + runtype = details['runtype'].lower() + run_number = extract_run_number(os.path.basename(f)) + if runlabel not in groups: + groups[runlabel] = {} + groups[runlabel][runtype] = run_number + table_rows = [] + for runlabel, d in groups.items(): + if 'sans' in d and 'trans' in d: + table_rows.append({'SAMPLE': runlabel, 'SANS': d['sans'], 'TRANS': d['trans']}) + df = pd.DataFrame(table_rows) + self.table.data = df + with self.log_output: + print(f"Scanned {len(nxs_files)} files. Found {len(df)} reduction entries.") + ebeam_sans_files = [] + ebeam_trans_files = [] + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runtype' in details: + if details['runtype'].lower() == 'ebeam_sans': + ebeam_sans_files.append(f) + elif details['runtype'].lower() == 'ebeam_trans': + ebeam_trans_files.append(f) + if ebeam_sans_files: + ebeam_sans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_sans = ebeam_sans_files[0] + else: + self.empty_beam_sans = None + if ebeam_trans_files: + ebeam_trans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_trans = ebeam_trans_files[0] + else: + self.empty_beam_trans = None + try: + direct_beam_file = find_direct_beam(input_dir) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + time.sleep(60) + continue + for index, row in df.iterrows(): + key = (row["SAMPLE"], row["SANS"], row["TRANS"]) + if key in self.processed: + continue + try: + sample_run_file = find_file(input_dir, row["SANS"], extension=".nxs") + transmission_run_file = find_file(input_dir, row["TRANS"], extension=".nxs") + except Exception as e: + with self.log_output: + print(f"Skipping sample {row['SAMPLE']}: {e}") + continue + try: + mask_file = find_mask_file(input_dir) + with self.log_output: + print(f"Using mask file: {mask_file} for sample {row['SAMPLE']}") + except Exception as e: + with self.log_output: + print(f"Mask file not found for sample {row['SAMPLE']}: {e}") + continue + if not self.empty_beam_sans or not self.empty_beam_trans: + with self.log_output: + print("Empty beam files not found, skipping reduction for sample", row["SAMPLE"]) + continue + with self.log_output: + print(f"Reducing sample {row['SAMPLE']}...") + reduction_params = { + "wavelength_min": 1.0, + "wavelength_max": 13.0, + "wavelength_n": 201, + "q_start": 0.01, + "q_stop": 0.3, + "q_n": 101 + } + perform_reduction_for_sample( + sample_info=row, + input_dir=input_dir, + output_dir=output_dir, + reduction_params=reduction_params, + background_run_file=self.empty_beam_sans, + empty_beam_file=self.empty_beam_trans, + direct_beam_file=direct_beam_file, + #log_func=lambda msg: print(msg) + ) + self.processed.add(key) + time.sleep(60) + + @property + def widget(self): + return self.main + +# ---------------------------- +# Direct Beam stuff +# ---------------------------- +def compute_direct_beam_local( + mask: str, + sample_sans: str, + background_sans: str, + sample_trans: str, + background_trans: str, + empty_beam: str, + local_Iq_theory: str, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + n_wavelength_bins: int = 50, + n_wavelength_bands: int = 50 +) -> dict: + workflow = loki.LokiAtLarmorWorkflow() + workflow = sans.with_pixel_mask_filenames(workflow, masks=[mask]) + workflow[NeXusDetectorName] = 'larmor_detector' + wl_min = sc.scalar(wavelength_min, unit='angstrom') + wl_max = sc.scalar(wavelength_max, unit='angstrom') + workflow[WavelengthBins] = sc.linspace('wavelength', wl_min, wl_max, n_wavelength_bins + 1) + workflow[WavelengthBands] = sc.linspace('wavelength', wl_min, wl_max, n_wavelength_bands + 1) + workflow[CorrectForGravity] = True + workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound + workflow[ReturnEvents] = False + workflow[QBins] = sc.linspace(dim='Q', start=0.01, stop=0.3, num=101, unit='1/angstrom') + workflow[Filename[SampleRun]] = sample_sans + workflow[Filename[BackgroundRun]] = background_sans + workflow[Filename[TransmissionRun[SampleRun]]] = sample_trans + workflow[Filename[TransmissionRun[BackgroundRun]]] = background_trans + workflow[Filename[EmptyBeamRun]] = empty_beam + center = sans.beam_center_from_center_of_mass(workflow) + print("Computed beam center:", center) + workflow[BeamCenter] = center + Iq_theory = sc.io.load_hdf5(local_Iq_theory) + f = interp1d(Iq_theory, 'Q') + I0 = f(sc.midpoints(workflow.compute(QBins))).data[0] + print("Computed I0:", I0) + results = sans.direct_beam(workflow=workflow, I0=I0, niter=6) + iofq_full = results[-1]['iofq_full'] + iofq_bands = results[-1]['iofq_bands'] + direct_beam_function = results[-1]['direct_beam'] + pp.plot( + {'reference': Iq_theory, 'data': iofq_full}, + color={'reference': 'darkgrey', 'data': 'C0'}, + norm='log', + ) + print("Plotted full-range result vs. theoretical reference.") + return { + 'direct_beam_function': direct_beam_function, + 'iofq_full': iofq_full, + 'Iq_theory': Iq_theory, + } + +class DirectBeamWidget: + def __init__(self): + self.mask_text = widgets.Text(value="", placeholder="Enter mask file path", description="Mask:") + self.sample_sans_text = widgets.Text(value="", placeholder="Enter sample SANS file path", description="Sample SANS:") + self.background_sans_text = widgets.Text(value="", placeholder="Enter background SANS file path", description="Background SANS:") + self.sample_trans_text = widgets.Text(value="", placeholder="Enter sample TRANS file path", description="Sample TRANS:") + self.background_trans_text = widgets.Text(value="", placeholder="Enter background TRANS file path", description="Background TRANS:") + self.empty_beam_text = widgets.Text(value="", placeholder="Enter empty beam file path", description="Empty Beam:") + self.local_Iq_theory_text = widgets.Text(value="", placeholder="Enter I(q) Theory file path", description="I(q) Theory:") + self.db_wavelength_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.db_wavelength_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.db_n_wavelength_bins_widget = widgets.IntText(value=50, description="λ n_bins:") + self.db_n_wavelength_bands_widget = widgets.IntText(value=50, description="λ n_bands:") + self.compute_button = widgets.Button(description="Compute Direct Beam") + self.compute_button.on_click(self.compute_direct_beam) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.main = widgets.VBox([ + self.mask_text, + self.sample_sans_text, + self.background_sans_text, + self.sample_trans_text, + self.background_trans_text, + self.empty_beam_text, + self.local_Iq_theory_text, + widgets.HBox([ + self.db_wavelength_min_widget, + self.db_wavelength_max_widget, + self.db_n_wavelength_bins_widget, + self.db_n_wavelength_bands_widget + ]), + self.compute_button, + self.log_output, + self.plot_output + ]) + + def compute_direct_beam(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + mask = self.mask_text.value + sample_sans = self.sample_sans_text.value + background_sans = self.background_sans_text.value + sample_trans = self.sample_trans_text.value + background_trans = self.background_trans_text.value + empty_beam = self.empty_beam_text.value + local_Iq_theory = self.local_Iq_theory_text.value + wl_min = self.db_wavelength_min_widget.value + wl_max = self.db_wavelength_max_widget.value + n_bins = self.db_n_wavelength_bins_widget.value + n_bands = self.db_n_wavelength_bands_widget.value + with self.log_output: + print("Computing direct beam with:") + print(" Mask:", mask) + print(" Sample SANS:", sample_sans) + print(" Background SANS:", background_sans) + print(" Sample TRANS:", sample_trans) + print(" Background TRANS:", background_trans) + print(" Empty Beam:", empty_beam) + print(" I(q) Theory:", local_Iq_theory) + print(" λ min:", wl_min, "λ max:", wl_max, "n_bins:", n_bins, "n_bands:", n_bands) + try: + results = compute_direct_beam_local( + mask, + sample_sans, + background_sans, + sample_trans, + background_trans, + empty_beam, + local_Iq_theory, + wavelength_min=wl_min, + wavelength_max=wl_max, + n_wavelength_bins=n_bins, + n_wavelength_bands=n_bands + ) + with self.log_output: + print("Direct beam computation complete.") + except Exception as e: + with self.log_output: + print("Error computing direct beam:", e) + + @property + def widget(self): + return self.main + +# ---------------------------- +# Build it +# ---------------------------- +reduction_widget = SansBatchReductionWidget().widget +direct_beam_widget = DirectBeamWidget().widget +semi_auto_reduction_widget = SemiAutoReductionWidget().widget +auto_reduction_widget = AutoReductionWidget().widget + +tabs = widgets.Tab(children=[direct_beam_widget, reduction_widget, semi_auto_reduction_widget, auto_reduction_widget]) +tabs.set_title(0, "Direct Beam") +tabs.set_title(1, "Reduction (Manual)") +tabs.set_title(2, "Reduction (Smart)") +tabs.set_title(3, "Reduction (Auto)") + +# display(tabs) +# voila /src/ess/loki/tabwidget.ipynb #--theme=dark \ No newline at end of file diff --git a/src/ess/loki/tabwidgetvisascicat.py b/src/ess/loki/tabwidgetvisascicat.py new file mode 100644 index 00000000..05907c3d --- /dev/null +++ b/src/ess/loki/tabwidgetvisascicat.py @@ -0,0 +1,1028 @@ +import os +import glob +import re +import h5py +import pandas as pd +import scipp as sc +import matplotlib.pyplot as plt +import numpy as np +import ipywidgets as widgets +from ipydatagrid import DataGrid +from IPython.display import display +from ipyfilechooser import FileChooser +from ess import sans +from ess import loki +from ess.sans.types import * +from scipp.scipy.interpolate import interp1d +import plopp as pp +import threading +import time +from ipywidgets import Layout +import csv +from scitacean import Client, Dataset, Attachment, Thumbnail +from scitacean.transfer.copy import CopyFileTransfer +from scitacean.transfer.select import SelectFileTransfer + +# ---------------------------- +# Utility Functions +# ---------------------------- +def find_file(work_dir, run_number, extension=".nxs"): + pattern = os.path.join(work_dir, f"*{run_number}*{extension}") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find file matching pattern {pattern}") + +def find_direct_beam(work_dir): #Find the direct beam automagically + pattern = os.path.join(work_dir, "*direct-beam*.h5") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find direct-beam file matching pattern {pattern}") + +def find_mask_file(work_dir): #Find the mask automagically + pattern = os.path.join(work_dir, "*mask*.xml") + files = glob.glob(pattern) + if files: + return files[0] + else: + raise FileNotFoundError(f"Could not find mask file matching pattern {pattern}") + +def save_xye_pandas(data_array, filename): ###Note here this needs to be 'fixed' / updated to use scipp io – ideally I want a nxcansas and xye saved for each file, but I struggled with the syntax and just did it in pandas as a first pass + q_vals = data_array.coords["Q"].values + i_vals = data_array.values + if len(q_vals) != len(i_vals): + q_vals = 0.5 * (q_vals[:-1] + q_vals[1:]) + if data_array.variances is not None: + err_vals = np.sqrt(data_array.variances) + if len(err_vals) != len(i_vals): + err_vals = 0.5 * (err_vals[:-1] + err_vals[1:]) + else: + err_vals = np.zeros_like(i_vals) + df = pd.DataFrame({"Q": q_vals, "I(Q)": i_vals, "Error": err_vals}) + df.to_csv(filename, sep=" ", index=False, header=True) + +def extract_run_number(filename): + m = re.search(r'(\d{4,})', filename) + if m: + return m.group(1) + return "" + +def parse_nx_details(filepath): #For finding/grouping files by common title assigned by NICOS, e.g. 'runlabel' and 'runtype' + details = {} + with h5py.File(filepath, 'r') as f: + if 'nicos_details' in f['entry']: + grp = f['entry']['nicos_details'] + if 'runlabel' in grp: + val = grp['runlabel'][()] + details['runlabel'] = val.decode('utf8') if isinstance(val, bytes) else str(val) + if 'runtype' in grp: + val = grp['runtype'][()] + details['runtype'] = val.decode('utf8') if isinstance(val, bytes) else str(val) + return details + +# ---------------------------- +# Colour Mapping From Filename + +def string_to_colour(input_str): + if not input_str: + return "#000000" # Empty input = black + total = 0 + for ch in input_str: + if ch.isalpha(): + total += ord(ch.lower()) - ord('a') + 1 # a=1, b=2, ..., z=26 + elif ch.isdigit(): + total += 1 + int(ch) * (25/9) # Maps '0' to 1 and '9' to 26 + # Special characters equal 0 + avg = total / len(input_str) + norm = max(0, min(1, avg / 26)) # Average and normalise to [0,1] + rgba = plt.get_cmap('flag')(norm) #prism + return '#{:02x}{:02x}{:02x}'.format(int(rgba[0]*255), + int(rgba[1]*255), + int(rgba[2]*255)) + + +# ---------------------------- +# Reduction and Plotting Functions +# ---------------------------- +def reduce_loki_batch_preliminary( + sample_run_file: str, + transmission_run_file: str, + background_run_file: str, + empty_beam_file: str, + direct_beam_file: str, + mask_files: list = None, + correct_for_gravity: bool = True, + uncertainty_mode = UncertaintyBroadcastMode.upper_bound, + return_events: bool = False, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + wavelength_n: int = 201, + q_start: float = 0.01, + q_stop: float = 0.3, + q_n: int = 101 +): + if mask_files is None: + mask_files = [] + wavelength_bins = sc.linspace("wavelength", wavelength_min, wavelength_max, wavelength_n, unit="angstrom") + q_bins = sc.linspace("Q", q_start, q_stop, q_n, unit="1/angstrom") + workflow = loki.LokiAtLarmorWorkflow() + if mask_files: + workflow = sans.with_pixel_mask_filenames(workflow, masks=mask_files) + workflow[NeXusDetectorName] = "larmor_detector" + workflow[WavelengthBins] = wavelength_bins + workflow[QBins] = q_bins + workflow[CorrectForGravity] = correct_for_gravity + workflow[UncertaintyBroadcastMode] = uncertainty_mode + workflow[ReturnEvents] = return_events + workflow[Filename[BackgroundRun]] = background_run_file + workflow[Filename[TransmissionRun[BackgroundRun]]] = transmission_run_file + workflow[Filename[EmptyBeamRun]] = empty_beam_file + workflow[DirectBeamFilename] = direct_beam_file + workflow[Filename[SampleRun]] = sample_run_file + workflow[Filename[TransmissionRun[SampleRun]]] = transmission_run_file + center = sans.beam_center_from_center_of_mass(workflow) + workflow[BeamCenter] = center + tf = workflow.compute(TransmissionFraction[SampleRun]) + da = workflow.compute(BackgroundSubtractedIofQ) + return {"transmission": tf, "IofQ": da} + +def save_reduction_plots(res, sample, sample_run_file, wavelength_min, wavelength_max, wavelength_n, q_min, q_max, q_n, output_dir, show=True): + fig, axs = plt.subplots(1, 2, figsize=(6, 3)) + axs[0].set_box_aspect(1) + axs[1].set_box_aspect(1) + title_str = f"{sample} - {os.path.basename(sample_run_file)}" + fig.suptitle(title_str, fontsize=12) + q_bins = sc.linspace("Q", q_min, q_max, q_n, unit="1/angstrom") + x_q = 0.5 * (q_bins.values[:-1] + q_bins.values[1:]) + if res["IofQ"].variances is not None: + yerr = np.sqrt(res["IofQ"].variances) + #axs[0].errorbar(x_q, res["IofQ"].values, yerr=yerr, fmt='o', linestyle='none', color='k', alpha=0.5, markerfacecolor='none') + axs[0].errorbar(x_q, res["IofQ"].values, yerr=yerr, fmt='o', linestyle='none', color='k', alpha=0.5, markerfacecolor=string_to_colour(sample), ecolor='k', markersize=6) + + else: + axs[0].scatter(x_q, res["IofQ"].values) + axs[0].set_xlabel("Q (Å$^{-1}$)") + axs[0].set_ylabel("I(Q)") + axs[0].set_xscale("log") + axs[0].set_yscale("log") + wavelength_bins = sc.linspace("wavelength", wavelength_min, wavelength_max, wavelength_n, unit="angstrom") + x_wl = 0.5 * (wavelength_bins.values[:-1] + wavelength_bins.values[1:]) + if res["transmission"].variances is not None: + yerr_tr = np.sqrt(res["transmission"].variances) + #axs[1].errorbar(x_wl, res["transmission"].values, yerr=yerr_tr, fmt='^', linestyle='none', color='k', alpha=0.5, markerfacecolor='none') + axs[1].errorbar(x_wl, res["transmission"].values, yerr=yerr_tr, fmt='^', linestyle='none', color='k', alpha=0.5, markerfacecolor=string_to_colour(sample), ecolor='k', markersize=6) + + + else: + axs[1].scatter(x_wl, res["transmission"].values) + axs[1].set_xlabel("Wavelength (Å)") + axs[1].set_ylabel("Transmission") + plt.tight_layout() + out_png = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", "_reduced.png")) + fig.savefig(out_png, dpi=300) + if show: + display(fig) + plt.close(fig) + +# ---------------------------- +# Unified "Backend" Function for Reduction +# ---------------------------- +def perform_reduction_for_sample( + sample_info: dict, + input_dir: str, + output_dir: str, + reduction_params: dict, + background_run_file: str, + empty_beam_file: str, + direct_beam_file: str, + log_func: callable +): + """ + Processes a single sample reduction: + - Finds the necessary run files + - Optionally determines a mask (or finds one automatically) + - Calls the reduction and plotting routines + - Logs all steps via log_func(message) ### edited to just print statements - does logfunc work correctly with voila??? + """ + sample = sample_info.get("SAMPLE", "Unknown") + try: + sample_run_file = find_file(input_dir, str(sample_info["SANS"]), extension=".nxs") + transmission_run_file = find_file(input_dir, str(sample_info["TRANS"]), extension=".nxs") + except Exception as e: + log_func(f"Skipping sample {sample}: {e}") + #print(f"Skipping sample {sample}: {e}") + + return None + # Determine mask file. + mask_file = None + mask_candidate = str(sample_info.get("mask", "")).strip() + if mask_candidate: + mask_candidate_file = os.path.join(input_dir, f"{mask_candidate}.xml") + if os.path.exists(mask_candidate_file): + mask_file = mask_candidate_file + if mask_file is None: + try: + mask_file = find_mask_file(input_dir) + log_func(f"Using mask: {mask_file} for sample {sample}") + #print(f"Identified mask file: {mask_file} for sample {sample}") + + except Exception as e: + log_func(f"Mask file not found for sample {sample}: {e}") + #print(f"Mask file not found for sample {sample}: {e}") + + return None + + log_func(f"Reducing sample {sample}...") + #print(f"Reducing sample {sample}...") + + try: + res = reduce_loki_batch_preliminary( + sample_run_file=sample_run_file, + transmission_run_file=transmission_run_file, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + mask_files=[mask_file], + wavelength_min=reduction_params["wavelength_min"], + wavelength_max=reduction_params["wavelength_max"], + wavelength_n=reduction_params["wavelength_n"], + q_start=reduction_params["q_start"], + q_stop=reduction_params["q_stop"], + q_n=reduction_params["q_n"] + ) + except Exception as e: + log_func(f"Reduction failed for sample {sample}: {e}") + #print(f"Reduction failed for sample {sample}: {e}") + return None + out_xye = os.path.join(output_dir, os.path.basename(sample_run_file).replace(".nxs", ".xye")) + try: + save_xye_pandas(res["IofQ"], out_xye) + log_func(f"Saved reduced data to {out_xye}") + #print(f"Saved reduced data to {out_xye}") + + except Exception as e: + log_func(f"Failed to save reduced data for {sample}: {e}") + #print(f"Failed to save reduced data for {sample}: {e}") + try: + save_reduction_plots( + res, + sample, + sample_run_file, + reduction_params["wavelength_min"], + reduction_params["wavelength_max"], + reduction_params["wavelength_n"], + reduction_params["q_start"], + reduction_params["q_stop"], + reduction_params["q_n"], + output_dir, + show=True + ) + log_func(f"Saved reduction plot for sample {sample}.") + except Exception as e: + log_func(f"Failed to save reduction plot for {sample}: {e}") + #log_func(f"Reduced sample {sample} and saved outputs.") + return res +# print(f"Saved reduction plot for sample {sample}.") +# except Exception as e: +# print(f"Failed to save reduction plot for {sample}: {e}") +# print(f"Reduced sample {sample} and saved outputs.") +# return res + + +########################################################################################### + +############################################################################################## + +# ---------------------------- +# GUI Widgets +# ---------------------------- +class SansBatchReductionWidget: + def __init__(self): + # File Choosers for CSV, input dir, output dir + self.csv_chooser = FileChooser(select_dir=False) + self.csv_chooser.title = "Select CSV File" + self.csv_chooser.filter_pattern = "*.csv" + + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + + # Remove references to Ebeam SANS/TRANS widgets + # (since these are now specified per row in the CSV). + + # Reduction parameter widgets + self.wavelength_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.wavelength_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.wavelength_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_start_widget = widgets.FloatText(value=0.01, description="Q start (1/Å):") + self.q_stop_widget = widgets.FloatText(value=0.3, description="Q stop (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + + # Button to load CSV + self.load_csv_button = widgets.Button(description="Load CSV") + self.load_csv_button.on_click(self.load_csv) + + # Table to display/edit CSV data + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + + # Button to run reduction + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + + # (Optional) log/plot outputs + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + + # Main layout + self.main = widgets.VBox([ + widgets.HBox([self.csv_chooser, self.input_dir_chooser, self.output_dir_chooser]), + widgets.HBox([self.wavelength_min_widget, self.wavelength_max_widget, self.wavelength_n_widget]), + widgets.HBox([self.q_start_widget, self.q_stop_widget, self.q_n_widget]), + self.load_csv_button, + self.table, + widgets.HBox([self.reduce_button]), + self.log_output, + self.plot_output + ]) + + def load_csv(self, _): + """Loads the CSV file into the DataGrid.""" + csv_path = self.csv_chooser.selected + if not csv_path or not os.path.exists(csv_path): + with self.log_output: + print("CSV file not selected or does not exist.") + return + df = pd.read_csv(csv_path) + self.table.data = df + with self.log_output: + print(f"Loaded reduction table with {len(df)} rows from {csv_path}.") + + def run_reduction(self, _): + """Loops over each row of the CSV table, finds input files, and performs the reduction.""" + # Clear old log/plot outputs + self.log_output.clear_output() + self.plot_output.clear_output() + + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + + # Read current table data + df = self.table.data + + # Reduction parameters + reduction_params = { + "wavelength_min": self.wavelength_min_widget.value, + "wavelength_max": self.wavelength_max_widget.value, + "wavelength_n": self.wavelength_n_widget.value, + "q_start": self.q_start_widget.value, + "q_stop": self.q_stop_widget.value, + "q_n": self.q_n_widget.value + } + + # Loop over each row of the CSV table + for idx, row in df.iterrows(): + try: + # Use the CSV columns to find file paths + sample_run_file = find_file(input_dir, str(row["SANS"]), extension=".nxs") + transmission_run_file = find_file(input_dir, str(row["TRANS"]), extension=".nxs") + background_run_file = find_file(input_dir, str(row["Ebeam_SANS"]), extension=".nxs") + empty_beam_file = find_file(input_dir, str(row["Ebeam_TRANS"]), extension=".nxs") + + # For mask and direct beam, we assume the CSV filename is relative to input_dir + mask_file = os.path.join(input_dir, str(row["mask"])) + direct_beam_file = os.path.join(input_dir, str(row["direct_beam"])) + + except Exception as e: + # If something fails, log and skip this row + with self.log_output: + print(f"Error finding input files for row {idx} ({row['SAMPLE']}): {e}") + continue + + # Create a mini dict for the sample info + sample_info = { + "SAMPLE": row["SAMPLE"], + "SANS": row["SANS"], + "TRANS": row["TRANS"], + # We store the mask as a column, so pass it along + "mask": mask_file + } + + # Call the actual reduction + perform_reduction_for_sample( + sample_info=sample_info, + input_dir=input_dir, + output_dir=output_dir, + reduction_params=reduction_params, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + log_func=lambda msg: print(msg) + ) + + @property + def widget(self): + """Return the main widget layout.""" + return self.main + +class SemiAutoReductionWidget: + def __init__(self): + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.scan_button = widgets.Button(description="Scan Directory") + self.scan_button.on_click(self.scan_directory) + self.table = DataGrid(pd.DataFrame([]), editable=True, auto_fit_columns=True) + self.add_row_button = widgets.Button(description="Add Row") + self.add_row_button.on_click(self.add_row) + self.delete_row_button = widgets.Button(description="Delete Last Row") + self.delete_row_button.on_click(self.delete_last_row) + self.lambda_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.lambda_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.lambda_n_widget = widgets.IntText(value=201, description="λ n_bins:") + self.q_min_widget = widgets.FloatText(value=0.01, description="Qmin (1/Å):") + self.q_max_widget = widgets.FloatText(value=0.3, description="Qmax (1/Å):") + self.q_n_widget = widgets.IntText(value=101, description="Q n_bins:") + self.empty_beam_sans_text = widgets.Text(value="", description="Ebeam SANS:", disabled=True) + self.empty_beam_trans_text = widgets.Text(value="", description="Ebeam TRANS:", disabled=True) + self.reduce_button = widgets.Button(description="Reduce") + self.reduce_button.on_click(self.run_reduction) + #self.clear_log_button = widgets.Button(description="Clear Log") + #self.clear_log_button.on_click(lambda _: self.log_output.clear_output()) + #self.clear_plots_button = widgets.Button(description="Clear Plots") + #self.clear_plots_button.on_click(lambda _: self.plot_output.clear_output()) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.processed = set() + self.main = widgets.VBox([ + widgets.HBox([self.input_dir_chooser, self.output_dir_chooser]), + self.scan_button, + self.table, + widgets.HBox([self.add_row_button, self.delete_row_button]), + widgets.HBox([self.lambda_min_widget, self.lambda_max_widget, self.lambda_n_widget]), + widgets.HBox([self.q_min_widget, self.q_max_widget, self.q_n_widget]), + widgets.HBox([self.empty_beam_sans_text, self.empty_beam_trans_text]), + widgets.HBox([self.reduce_button]),# self.clear_log_button, self.clear_plots_button]), + #self.log_output, + #self.plot_output + ]) + + def add_row(self, _): + df = self.table.data + new_row = {col: "" for col in df.columns} if not df.empty else {'SAMPLE': '', 'SANS': '', 'TRANS': ''} + df = df.append(new_row, ignore_index=True) + self.table.data = df + + def delete_last_row(self, _): + df = self.table.data + if not df.empty: + self.table.data = df.iloc[:-1] + + def scan_directory(self, _): + self.log_output.clear_output() + input_dir = self.input_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Invalid input folder.") + return + nxs_files = glob.glob(os.path.join(input_dir, "*.nxs")) + groups = {} + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runlabel' not in details or 'runtype' not in details: + continue + runlabel = details['runlabel'] + runtype = details['runtype'].lower() + run_number = extract_run_number(os.path.basename(f)) + if runlabel not in groups: + groups[runlabel] = {} + groups[runlabel][runtype] = run_number + table_rows = [] + for runlabel, d in groups.items(): + if 'sans' in d and 'trans' in d: + table_rows.append({'SAMPLE': runlabel, 'SANS': d['sans'], 'TRANS': d['trans']}) + df = pd.DataFrame(table_rows) + self.table.data = df + with self.log_output: + print(f"Scanned {len(nxs_files)} files. Found {len(df)} reduction entries.") + ebeam_sans_files = [] + ebeam_trans_files = [] + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runtype' in details: + if details['runtype'].lower() == 'ebeam_sans': + ebeam_sans_files.append(f) + elif details['runtype'].lower() == 'ebeam_trans': + ebeam_trans_files.append(f) + if ebeam_sans_files: + ebeam_sans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_sans_text.value = ebeam_sans_files[0] + else: + self.empty_beam_sans_text.value = "" + if ebeam_trans_files: + ebeam_trans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_trans_text.value = ebeam_trans_files[0] + else: + self.empty_beam_trans_text.value = "" + + def run_reduction(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Input folder is not valid.") + return + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Output folder is not valid.") + return + try: + direct_beam_file = find_direct_beam(input_dir) + with self.log_output: + print("Using direct beam:", direct_beam_file) + except Exception as e: + with self.log_output: + print("Direct beam file not found:", e) + return + background_run_file = self.empty_beam_sans_text.value + empty_beam_file = self.empty_beam_trans_text.value + if not background_run_file or not empty_beam_file: + with self.log_output: + print("Empty beam files not found.") + return + + reduction_params = { + "wavelength_min": self.lambda_min_widget.value, + "wavelength_max": self.lambda_max_widget.value, + "wavelength_n": self.lambda_n_widget.value, + "q_start": self.q_min_widget.value, + "q_stop": self.q_max_widget.value, + "q_n": self.q_n_widget.value + } + + #df = self.table.data.copy() + df = self.table.data.drop_duplicates(subset=['SAMPLE', 'SANS', 'TRANS']) + for idx, row in df.iterrows(): + perform_reduction_for_sample( + sample_info=row, + input_dir=input_dir, + output_dir=output_dir, + reduction_params=reduction_params, + background_run_file=background_run_file, + empty_beam_file=empty_beam_file, + direct_beam_file=direct_beam_file, + log_func=lambda msg: print(msg) + ) + + @property + def widget(self): + return self.main + +class AutoReductionWidget: + def __init__(self): + self.input_dir_chooser = FileChooser(select_dir=True) + self.input_dir_chooser.title = "Select Input Folder" + self.output_dir_chooser = FileChooser(select_dir=True) + self.output_dir_chooser.title = "Select Output Folder" + self.start_stop_button = widgets.Button(description="Start") + self.start_stop_button.on_click(self.toggle_running) + self.status_label = widgets.Label(value="Stopped") + self.table = DataGrid(pd.DataFrame([]), editable=False, auto_fit_columns=True) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.running = False + self.thread = None + self.processed = set() + self.empty_beam_sans = None + self.empty_beam_trans = None + self.main = widgets.VBox([ + widgets.HBox([self.input_dir_chooser, self.output_dir_chooser]), + widgets.HBox([self.start_stop_button, self.status_label]), + self.table, + self.log_output, + self.plot_output + ]) + # SciCat settings – adjust these as needed. + self.token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NmRhYjhmYzFiNThkNDFlYTM4OTc5MzIiLCJ1c2VybmFtZSI6Imh0dHBzOi8vbG9naW4uZXNzLmV1X29saXZlcmhhbW1vbmQiLCJlbWFpbCI6Im9saXZlci5oYW1tb25kQGVzcy5ldSIsImF1dGhTdHJhdGVneSI6Im9pZGMiLCJfX3YiOjAsImlkIjoiNjZkYWI4ZmMxYjU4ZDQxZWEzODk3OTMyIiwidXNlcklkIjoiNjZkYWI4ZmMxYjU4ZDQxZWEzODk3OTMyIiwiaWF0IjoxNzQyOTk2Mzc4LCJleHAiOjE3NDI5OTk5Nzh9.YytBMfX0p971InDFs0cSkfoVP92RvpgE_Vu9K_OLbiY' + self.scicat_url = 'https://staging.scicat.ess.eu/api/v3' + self.scicat_source_folder = '/scratch/oliverhammond/LARMOR/nxs/out/scicat' + + def toggle_running(self, _): + if not self.running: + self.running = True + self.start_stop_button.description = "Stop" + self.status_label.value = "Running" + self.thread = threading.Thread(target=self.background_loop, daemon=True) + self.thread.start() + else: + self.running = False + self.start_stop_button.description = "Start" + self.status_label.value = "Stopped" + + def background_loop(self): + while self.running: + input_dir = self.input_dir_chooser.selected + output_dir = self.output_dir_chooser.selected + if not input_dir or not os.path.isdir(input_dir): + with self.log_output: + print("Invalid input folder. Waiting for valid selection...") + time.sleep(60) + continue + if not output_dir or not os.path.isdir(output_dir): + with self.log_output: + print("Invalid output folder. Waiting for valid selection...") + time.sleep(60) + continue + nxs_files = glob.glob(os.path.join(input_dir, "*.nxs")) + groups = {} + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runlabel' not in details or 'runtype' not in details: + continue + runlabel = details['runlabel'] + runtype = details['runtype'].lower() + run_number = extract_run_number(os.path.basename(f)) + if runlabel not in groups: + groups[runlabel] = {} + groups[runlabel][runtype] = run_number + table_rows = [] + for runlabel, d in groups.items(): + if 'sans' in d and 'trans' in d: + table_rows.append({'SAMPLE': runlabel, 'SANS': d['sans'], 'TRANS': d['trans']}) + df = pd.DataFrame(table_rows) + self.table.data = df + with self.log_output: + print(f"Scanned {len(nxs_files)} files. Found {len(df)} reduction entries.") + ebeam_sans_files = [] + ebeam_trans_files = [] + for f in nxs_files: + try: + details = parse_nx_details(f) + except Exception: + continue + if 'runtype' in details: + if details['runtype'].lower() == 'ebeam_sans': + ebeam_sans_files.append(f) + elif details['runtype'].lower() == 'ebeam_trans': + ebeam_trans_files.append(f) + if ebeam_sans_files: + ebeam_sans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_sans = ebeam_sans_files[0] + else: + self.empty_beam_sans = None + if ebeam_trans_files: + ebeam_trans_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) + self.empty_beam_trans = ebeam_trans_files[0] + else: + self.empty_beam_trans = None + try: + direct_beam_file = find_direct_beam(input_dir) + except Exception as e: + with self.log_output: + print("Direct-beam file not found:", e) + time.sleep(60) + continue + for index, row in df.iterrows(): + key = (row["SAMPLE"], row["SANS"], row["TRANS"]) + if key in self.processed: + continue + try: + sample_run_file = find_file(input_dir, row["SANS"], extension=".nxs") + transmission_run_file = find_file(input_dir, row["TRANS"], extension=".nxs") + except Exception as e: + with self.log_output: + print(f"Skipping sample {row['SAMPLE']}: {e}") + continue + try: + mask_file = find_mask_file(input_dir) + with self.log_output: + print(f"Using mask file: {mask_file} for sample {row['SAMPLE']}") + except Exception as e: + with self.log_output: + print(f"Mask file not found for sample {row['SAMPLE']}: {e}") + continue + if not self.empty_beam_sans or not self.empty_beam_trans: + with self.log_output: + print("Empty beam files not found, skipping reduction for sample", row["SAMPLE"]) + continue + with self.log_output: + print(f"Reducing sample {row['SAMPLE']}...") + reduction_params = { + "wavelength_min": 1.0, + "wavelength_max": 13.0, + "wavelength_n": 201, + "q_start": 0.01, + "q_stop": 0.3, + "q_n": 101 + } + perform_reduction_for_sample( + sample_info=row, + input_dir=input_dir, + output_dir=output_dir, + reduction_params=reduction_params, + background_run_file=self.empty_beam_sans, + empty_beam_file=self.empty_beam_trans, + direct_beam_file=direct_beam_file, + log_func=lambda msg: print(msg) + ) + self.processed.add(key) + # Call the uploader function using the run number/name from the reduction table. + self.upload_dataset(run=row["SANS"], sample_name=row["SAMPLE"], metadata_file='uos_metadata.csv') + time.sleep(60) + + def upload_dataset(self, run, sample_name, metadata_file='uos_metadata.csv'): + """ + Uploads a reduced dataset to SciCat using files in the widget's output directory. + Metadata is combined from the reduction process (run, sample_name) and additional + arguments provided in a CSV file as a proxy for metadata provided by the user office. + + The CSV file should have a header with these columns: + contact_email, owner_email, investigator, owner, owner_group, description + + Parameters: + run (str): The run number to search for and use in the dataset (same as tabular input). + sample_name (str): The sample name to include in the dataset metadata (from nxs file). + metadata_file (str): Path to the CSV file containing additional metadata. + """ + # Read metadata from the CSV file. + try: + with open(metadata_file, newline='') as csvfile: + reader = csv.DictReader(csvfile) + row = next(reader) + except Exception as e: + with self.log_output: + print(f"Error reading metadata CSV file '{metadata_file}': {e}") + return + + contact_email = row.get('contact_email', 'default@example.com') + owner_email = row.get('owner_email', contact_email) + investigator = row.get('investigator', 'Unknown') + owner = row.get('owner', 'Unknown') + owner_group = row.get('owner_group', 'ess') + description = row.get('description', '') + + # Use the output directory from the widget to search for the reduced files. + file_folder = self.output_dir_chooser.selected + if not file_folder or not os.path.isdir(file_folder): + with self.log_output: + print("Invalid output folder selected for uploading.") + return + + # Initialize the SciCat client. + client = Client.from_token( + url=self.scicat_url, + token=self.token, + file_transfer=SelectFileTransfer([CopyFileTransfer()]) + ) + + # Use glob to find the .xye and .png files based on the run number. + xye_pattern = os.path.join(file_folder, f"*{run}*.xye") + png_pattern = os.path.join(file_folder, f"*{run}*_reduced.png") + xye_files = glob.glob(xye_pattern) + png_files = glob.glob(png_pattern) + + if not xye_files: + with self.log_output: + print(f"No .xye file found for run {run}.") + return + if not png_files: + with self.log_output: + print(f"No .png file found for run {run}.") + return + + xye_file = xye_files[0] # Use first matching file. + png_file = png_files[0] # Use first matching file. + + # Construct the dataset object with combined metadata. + dataset = Dataset( + type='derived', + contact_email=contact_email, + owner_email=owner_email, + input_datasets=[], + investigator=investigator, + owner=owner, + owner_group=owner_group, + access_groups=[owner_group], + source_folder=self.scicat_source_folder, + used_software=['esssans'], + name=f"{run}.xye", # Derived from run number. + description=description, + run_number=run, + meta={'sample_name': {'value': sample_name, 'unit': ''}} + ) + + # Add the primary .xye file. + dataset.add_local_files(xye_file) + + # Add the attachment (thumbnail). + dataset.attachments.append( + Attachment( + caption=f"Reduced I(Q) and transmission for {dataset.name}", + owner_group=owner_group, + thumbnail=Thumbnail.load_file(png_file) + ) + ) + + # Upload the dataset. + client.upload_new_dataset_now(dataset) + with self.log_output: + print(f"Uploaded dataset for run {run} using files:\n - {xye_file}\n - {png_file}") + + @property + def widget(self): + return self.main + +# ---------------------------- +# Direct Beam stuff +# ---------------------------- +def compute_direct_beam_local( + mask: str, + sample_sans: str, + background_sans: str, + sample_trans: str, + background_trans: str, + empty_beam: str, + local_Iq_theory: str, + wavelength_min: float = 1.0, + wavelength_max: float = 13.0, + n_wavelength_bins: int = 50, + n_wavelength_bands: int = 50 +) -> dict: + workflow = loki.LokiAtLarmorWorkflow() + workflow = sans.with_pixel_mask_filenames(workflow, masks=[mask]) + workflow[NeXusDetectorName] = 'larmor_detector' + wl_min = sc.scalar(wavelength_min, unit='angstrom') + wl_max = sc.scalar(wavelength_max, unit='angstrom') + workflow[WavelengthBins] = sc.linspace('wavelength', wl_min, wl_max, n_wavelength_bins + 1) + workflow[WavelengthBands] = sc.linspace('wavelength', wl_min, wl_max, n_wavelength_bands + 1) + workflow[CorrectForGravity] = True + workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound + workflow[ReturnEvents] = False + workflow[QBins] = sc.linspace(dim='Q', start=0.01, stop=0.3, num=101, unit='1/angstrom') + workflow[Filename[SampleRun]] = sample_sans + workflow[Filename[BackgroundRun]] = background_sans + workflow[Filename[TransmissionRun[SampleRun]]] = sample_trans + workflow[Filename[TransmissionRun[BackgroundRun]]] = background_trans + workflow[Filename[EmptyBeamRun]] = empty_beam + center = sans.beam_center_from_center_of_mass(workflow) + print("Computed beam center:", center) + workflow[BeamCenter] = center + Iq_theory = sc.io.load_hdf5(local_Iq_theory) + f = interp1d(Iq_theory, 'Q') + I0 = f(sc.midpoints(workflow.compute(QBins))).data[0] + print("Computed I0:", I0) + results = sans.direct_beam(workflow=workflow, I0=I0, niter=6) + iofq_full = results[-1]['iofq_full'] + iofq_bands = results[-1]['iofq_bands'] + direct_beam_function = results[-1]['direct_beam'] + pp.plot( + {'reference': Iq_theory, 'data': iofq_full}, + color={'reference': 'darkgrey', 'data': 'C0'}, + norm='log', + ) + print("Plotted full-range result vs. theoretical reference.") + return { + 'direct_beam_function': direct_beam_function, + 'iofq_full': iofq_full, + 'Iq_theory': Iq_theory, + } + +class DirectBeamWidget: + def __init__(self): + self.mask_text = widgets.Text(value="", placeholder="Enter mask file path", description="Mask:") + self.sample_sans_text = widgets.Text(value="", placeholder="Enter sample SANS file path", description="Sample SANS:") + self.background_sans_text = widgets.Text(value="", placeholder="Enter background SANS file path", description="Background SANS:") + self.sample_trans_text = widgets.Text(value="", placeholder="Enter sample TRANS file path", description="Sample TRANS:") + self.background_trans_text = widgets.Text(value="", placeholder="Enter background TRANS file path", description="Background TRANS:") + self.empty_beam_text = widgets.Text(value="", placeholder="Enter empty beam file path", description="Empty Beam:") + self.local_Iq_theory_text = widgets.Text(value="", placeholder="Enter I(q) Theory file path", description="I(q) Theory:") + self.db_wavelength_min_widget = widgets.FloatText(value=1.0, description="λ min (Å):") + self.db_wavelength_max_widget = widgets.FloatText(value=13.0, description="λ max (Å):") + self.db_n_wavelength_bins_widget = widgets.IntText(value=50, description="λ n_bins:") + self.db_n_wavelength_bands_widget = widgets.IntText(value=50, description="λ n_bands:") + self.compute_button = widgets.Button(description="Compute Direct Beam") + self.compute_button.on_click(self.compute_direct_beam) + self.log_output = widgets.Output() + self.plot_output = widgets.Output() + self.main = widgets.VBox([ + self.mask_text, + self.sample_sans_text, + self.background_sans_text, + self.sample_trans_text, + self.background_trans_text, + self.empty_beam_text, + self.local_Iq_theory_text, + widgets.HBox([ + self.db_wavelength_min_widget, + self.db_wavelength_max_widget, + self.db_n_wavelength_bins_widget, + self.db_n_wavelength_bands_widget + ]), + self.compute_button, + self.log_output, + self.plot_output + ]) + + def compute_direct_beam(self, _): + self.log_output.clear_output() + self.plot_output.clear_output() + mask = self.mask_text.value + sample_sans = self.sample_sans_text.value + background_sans = self.background_sans_text.value + sample_trans = self.sample_trans_text.value + background_trans = self.background_trans_text.value + empty_beam = self.empty_beam_text.value + local_Iq_theory = self.local_Iq_theory_text.value + wl_min = self.db_wavelength_min_widget.value + wl_max = self.db_wavelength_max_widget.value + n_bins = self.db_n_wavelength_bins_widget.value + n_bands = self.db_n_wavelength_bands_widget.value + with self.log_output: + print("Computing direct beam with:") + print(" Mask:", mask) + print(" Sample SANS:", sample_sans) + print(" Background SANS:", background_sans) + print(" Sample TRANS:", sample_trans) + print(" Background TRANS:", background_trans) + print(" Empty Beam:", empty_beam) + print(" I(q) Theory:", local_Iq_theory) + print(" λ min:", wl_min, "λ max:", wl_max, "n_bins:", n_bins, "n_bands:", n_bands) + try: + results = compute_direct_beam_local( + mask, + sample_sans, + background_sans, + sample_trans, + background_trans, + empty_beam, + local_Iq_theory, + wavelength_min=wl_min, + wavelength_max=wl_max, + n_wavelength_bins=n_bins, + n_wavelength_bands=n_bands + ) + with self.log_output: + print("Direct beam computation complete.") + except Exception as e: + with self.log_output: + print("Error computing direct beam:", e) + + @property + def widget(self): + return self.main + +# ---------------------------- +# Build it +# ---------------------------- +#reduction_widget = SansBatchReductionWidget().widget +#direct_beam_widget = DirectBeamWidget().widget +#semi_auto_reduction_widget = SemiAutoReductionWidget().widget +#auto_reduction_widget = AutoReductionWidget().widget + +#tabs = widgets.Tab(children=[direct_beam_widget, reduction_widget, semi_auto_reduction_widget, auto_reduction_widget]) +#tabs.set_title(0, "Direct Beam") +#tabs.set_title(1, "Reduction (Manual)") +#tabs.set_title(2, "Reduction (Smart)") +#tabs.set_title(3, "Reduction (Auto)") + +reduction_widget = SansBatchReductionWidget().widget +#direct_beam_widget = DirectBeamWidget().widget +semi_auto_reduction_widget = SemiAutoReductionWidget().widget +auto_reduction_widget = AutoReductionWidget().widget + +tabs = widgets.Tab(children=[reduction_widget, semi_auto_reduction_widget, auto_reduction_widget]) +#tabs.set_title(0, "Direct Beam") +tabs.set_title(0, "Reduction (Manual)") +#tabs.set_title(2, "Reduction (Smart)") +tabs.set_title(1, "Reduction (Smart)") +tabs.set_title(2, "Reduction (Auto)") + + +# display(tabs) +# voila /src/ess/loki/tabwidget.ipynb #--theme=dark +