Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,29 @@ Please test this on IDAaaS: an ENGINX instance should have MantidWorkbenchNightl
11. Set the ``Override Unit Cell Length`` to ``3.65`` and click ``Refine in GSAS II``, the fit should be better.

12. Tick all the checkboxes: ``Microstrain``, ``Sigma-1`` and ``Gamma (Y)``. An asterisk should appear with an advice tooltip.

Test 12
^^^^^^^
This test covers the multiple data files functionality with multiple banks per file in the ``GSASII`` tab.

Note this test will only work if ``GSASII`` is also installed.
Please test this on IDAaaS: an ENGINX instance should have MantidWorkbenchNightly and ``GSASII`` installed in the expected location.

1. Close and re-open the Engineering Diffraction interface.

2. Go to the ``Calibration`` tab, select ``Create New Calibration`` and un-tick the ``Set Calibration Region of Interest`` option.

3. For the ``Calibration Sample`` # enter ``305738`` and click the ``Calibrate`` button.

4. On the ``Focus`` tab, enter Sample Run # ``305793-305795`` and Vanadium # ``307521`` and click the Focus button. This will generate multiple focused data files.
Change to the GSASII tab. Clear any pre-filled paths.

5. For the ``Instrument Group`` filepath, browse and select the single .prm file output by the calibration (should be ENGINX_305738_all_banks.prm).

6. For the ``Focused Data`` filepath, browse and select multiple .gss files that each contain multiple banks. Ensure all selected files have the same number of banks (e.g., select the all_banks files: ENGINX_305738_305793_all_banks_dSpacing.gss, ENGINX_305738_305794_all_banks_dSpacing.gss, ENGINX_305738_305795_all_banks_dSpacing.gss).

7. For the ``Phase`` filepath, browse to MANTID_INSTALL_DIRECTORY/scripts/Engineering/ENGINX/phase_info/FE_GAMMA.cif. For the ``Project Name`` at the top, enter a string of your choice.

8. Click Refine in ``GSAS II``. After a few seconds, the output fit should be plotted. In the top right of the plot widget, verify that the refined spectrum combobox shows entries for the banks of the last refined data file.

9. Test Error Cases: Try selecting multiple instrument .prm files (should show error message about requiring exactly one instrument file). Try selecting .gss files with different numbers of banks (should show error about inconsistent bank counts). Try selecting single-bank .gss files (should show error about requiring multiple banks per file).
Original file line number Diff line number Diff line change
Expand Up @@ -230,14 +230,14 @@ Project Name
Name of the GSAS project file.

Instrument Group
Path to .prm file produced by the Calibration tab
Path to .prm file produced by the Calibration tab (only one instrument file is supported and will be applied to each data file)

Phase
Path to the .cif file defining the initial crystal structure (more than one path can be supplied, the lattice
parameters will be overridden for the first phase only).

Focused Data
Path to focused .gss files (note it should have the same number of spectra as in .prm file)
Path to focused .gss files (note it should have the same number of spectra as in .prm file and contains multiple banks)

Refinement Method
Only Pawley refinement currently supported
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Support batch refinement of multi-run datasets using a single instrument parameter file in :ref:`GSASII tab <ui engineering gsas>` tab of :ref:`Engineering Diffraction interface<Engineering_Diffraction-ref>` GUI.
- Removed support for mixed single-region and multi-instrument file configurations in :ref:`GSASII tab <ui engineering gsas>` tab of :ref:`Engineering Diffraction interface<Engineering_Diffraction-ref>` GUI.
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,22 @@ def add_phases(project, phase_files):


def add_histograms(data_filenames, project, instruments, number_regions):
if number_regions > len(data_filenames): # many regions in one data and one instrument file
if len(data_filenames) != 1:
raise ValueError("There must be one region/bank per focused data file or many regions/banks in one focused data file")
for loop_region_index in range(1, number_regions + 1):
project.add_powder_histogram(
datafile=os.path.join(data_filenames[0]),
iparams=os.path.join(instruments[0]),
phases=project.phases(),
databank=loop_region_index, # indexing starts at 1
instbank=loop_region_index, # indexing starts at 1
)
else: # one region in each data file
if len(data_filenames) != number_regions:
raise ValueError("There must be one region/bank per focused data file or many regions/banks in one focused data file")
if len(instruments) == len(data_filenames):
for loop_index, loop_data_filename in enumerate(data_filenames):
project.add_powder_histogram(
datafile=os.path.join(loop_data_filename), iparams=os.path.join(instruments[loop_index]), phases=project.phases()
)
elif 1 == len(instruments) < len(data_filenames):
for loop_index, loop_data_filename in enumerate(data_filenames):
project.add_powder_histogram(
datafile=os.path.join(loop_data_filename),
iparams=os.path.join(instruments[0]),
phases=project.phases(),
instbank=loop_index + 1, # indexing starts at 1
)
else:
raise ValueError(
"Calling GSASII from Mantid with multiple instrument files and one focused data file is currently not supported"
)
# Validation checks
if len(data_filenames) != 1:
raise ValueError("You must provide only one data file.")

if len(instruments) != 1:
raise ValueError("You must provide only one instrument file.")

# Add histograms for each bank within that file
for bank_index in range(1, number_regions + 1): # GSAS-II uses 1-based indexing
project.add_powder_histogram(
datafile=os.path.join(data_filenames[0]),
iparams=os.path.join(instruments[0]), # Use the single instrument file
phases=project.phases(),
databank=bank_index, # Bank within this data file
instbank=bank_index, # Corresponding bank in instrument file
)


def add_pawley_reflections(pawley_reflections, project, d_min):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Union, Tuple, TypeAlias
from typing import Dict, List, Optional, Union, Tuple, TypeAlias

import matplotlib.pyplot as plt
from matplotlib.axes import Axes
Expand Down Expand Up @@ -201,6 +201,41 @@ def run_model(
project_name: str,
rb_num: Optional[str] = None,
user_x_limits: Optional[List[List[float]]] = None,
) -> Optional[Dict[str, int]]:
"""
Returns a dictionary mapping data file names to their result counts
"""
data_files = load_parameters[2] # Extract data files list
num_hist = None

for data_file in data_files:
# Create unique project name for each file
file_basename = os.path.splitext(os.path.basename(data_file))[0]
individual_project_name = f"{project_name}_{file_basename}"

# Create modified load_parameters for single file
single_file_load_params = [
load_parameters[0], # instrument_files (reuse same)
load_parameters[1], # phase_filepaths (reuse same)
[data_file], # single data file
]

num_hist = self._run_single_refinement(
single_file_load_params, refinement_parameters, individual_project_name, rb_num, user_x_limits
)

if not num_hist:
return

return num_hist

def _run_single_refinement(
self,
load_parameters: list,
refinement_parameters: list,
project_name: str,
rb_num: Optional[str] = None,
user_x_limits: Optional[List[List[float]]] = None,
) -> Optional[int]:
self.clear_input_components()
if not self.initial_validation(project_name, load_parameters):
Expand All @@ -217,7 +252,8 @@ def run_model(
user_x_limits[1] if isinstance(user_x_limits[1], list) else [user_x_limits[1]],
]

self.validate_x_limits(formatted_limits)
if not self.validate_x_limits(formatted_limits):
return None
if not self.further_validation():
return None

Expand All @@ -236,9 +272,7 @@ def run_model(
return None
self.load_basic_outputs(gsas_result)

if self.state.number_of_regions > self.state.number_histograms:
return self.state.number_of_regions
return self.state.number_histograms
return self.state.number_of_regions

# ===============
# Prepare Inputs
Expand Down Expand Up @@ -427,6 +461,7 @@ def call_subprocess(self, command_string_list: List[str], gsas_binary_paths: Lis

if shell_process.returncode != 0:
logger.error(f"GSAS-II call failed with error: {shell_output[-1]}")

return None
return shell_output
except subprocess.TimeoutExpired:
Expand Down Expand Up @@ -568,32 +603,57 @@ def generate_reflections_from_space_group(self) -> None:
# X Limits
# =========

def get_no_banks(self, prm_file):
with open(prm_file) as f:
for line in f.readlines():
if "BANK" in line and len(line.split()) == 3:
return int(line.split()[-1])

return -1

def understand_data_structure(self) -> None:
if len(self.file_paths.instrument_files) != 1:
logger.error("* You must provide exactly one instrument file.")
return False

self.x_limits.data_x_min = []
self.x_limits.data_x_max = []
number_of_regions = 0
banks_per_file = [] # Track banks per file for validation

for input_file in self.file_paths.data_files:
loop_focused_workspace = LoadGSS(Filename=input_file, OutputWorkspace="GSASII_input_data", EnableLogging=False)
for workspace_index in range(loop_focused_workspace.getNumberHistograms()):
file_bank_count = loop_focused_workspace.getNumberHistograms()
banks_per_file.append(file_bank_count)

no_banks = self.get_no_banks(self.file_paths.instrument_files[0])

if file_bank_count != no_banks:
logger.error("* All data files should have the same number of banks as the instrument file.")
return False

for workspace_index in range(file_bank_count):
self.x_limits.data_x_min.append(loop_focused_workspace.readX(workspace_index)[0])
self.x_limits.data_x_max.append(loop_focused_workspace.readX(workspace_index)[-1])
number_of_regions += 1
DeleteWorkspace(loop_focused_workspace)

expected_total_regions = len(self.file_paths.data_files) * banks_per_file[0]
if number_of_regions != expected_total_regions:
logger.error(f"* Expected {expected_total_regions} total regions, but found {number_of_regions}.")
return False

self.state.number_of_regions = number_of_regions
return True

def validate_x_limits(self, users_limits: Optional[List[List[float]]]) -> bool:
self.understand_data_structure()
if not self.understand_data_structure():
return False
if users_limits:
if len(users_limits[0]) != self.state.number_of_regions:
users_limits[0] *= self.state.number_of_regions
users_limits[1] *= self.state.number_of_regions
self.state.number_histograms = len(self.file_paths.data_files)
if len(self.file_paths.instrument_files) != 1 and len(self.file_paths.instrument_files) != self.state.number_histograms:
logger.error(
f"The number of instrument files ({len(self.file_paths.instrument_files)}) must be 1 "
f"or equal to the number of input histograms {self.state.number_histograms}"
)
return False
if users_limits:
self.x_limits.x_min = [float(k) for k in users_limits[0]]
self.x_limits.x_max = [float(k) for k in users_limits[1]]
Expand Down Expand Up @@ -912,9 +972,13 @@ def create_lattice_parameter_table(self, test: bool = False) -> Optional[Union[N
# ===========

def load_focused_nxs_for_logs(self, filenames: List[str]) -> None:
if len(filenames) == 1 and "all_banks" in filenames[0]:
filenames = [filenames[0].replace("all_banks", "bank_1"), filenames[0].replace("all_banks", "bank_2")]
banks_filenames = []
for filename in filenames:
if "all_banks" in filename:
banks_filenames.extend([filename.replace("all_banks", "bank_1"), filename.replace("all_banks", "bank_2")])
else:
banks_filenames.append(filename)
for filename in banks_filenames:
filename = filename.replace(".gss", ".nxs")
ws_name = _generate_workspace_name(filename, self._suffix)
if ws_name not in self._data_workspaces.get_loaded_workpace_names():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,68 +53,35 @@ def test_add_phases(self):
add_phases(self.project, phase_files)
self.project.add_phase.assert_has_calls([mock.call("file_1"), mock.call("file_2")])

def test_add_histograms_with_single_datafile(self):
def test_add_histograms_with_single_datafile_multiple_banks(self):
data_filenames = ["data_file_1"]
instruments = ["instr_1"]
number_of_regions = 3
add_histograms(data_filenames, self.project, instruments, number_of_regions)
self.project.add_powder_histogram.assert_called()
self.assertEqual(self.project.add_powder_histogram.call_count, 3)

def test_add_histograms_same_number_of_instruments_and_datafiles(self):
self.project.phases.return_value = ["phase_1"]
data_filenames = ["data_file_1", "data_file_2"]
instruments = ["instr_1", "instr_2"]
number_of_regions = 2
add_histograms(data_filenames, self.project, instruments, number_of_regions)
expected_calls = [
mock.call(datafile="data_file_1", iparams="instr_1", phases=["phase_1"]),
mock.call(datafile="data_file_2", iparams="instr_2", phases=["phase_1"]),
]
self.project.add_powder_histogram.assert_has_calls(expected_calls)

def test_add_histograms_more_datafiles_than_instruments(self):
def test_add_histograms_throws_for_more_datafiles_one_instrument(self):
self.project.phases.return_value = ["phase_1"]
data_filenames = ["data_file_1", "data_file_2"]
instruments = ["instr_1"]
number_of_regions = 2
add_histograms(data_filenames, self.project, instruments, number_of_regions)
expected_calls = [
mock.call(datafile="data_file_1", iparams="instr_1", phases=["phase_1"], instbank=1),
mock.call(datafile="data_file_2", iparams="instr_1", phases=["phase_1"], instbank=2),
]
self.project.add_powder_histogram.assert_has_calls(expected_calls)

def test_add_histograms_multiple_regions_for_single_datafile(self):
self.project.phases.return_value = ["phase_1"]
data_filenames = ["data_file_1"]
instruments = ["instr_1", "instr_2"]
number_of_regions = 3
add_histograms(data_filenames, self.project, instruments, number_of_regions)
expected_calls = [
mock.call(datafile="data_file_1", iparams="instr_1", phases=["phase_1"], databank=1, instbank=1),
mock.call(datafile="data_file_1", iparams="instr_1", phases=["phase_1"], databank=2, instbank=2),
mock.call(datafile="data_file_1", iparams="instr_1", phases=["phase_1"], databank=3, instbank=3),
]
self.project.add_powder_histogram.assert_has_calls(expected_calls)

def test_add_histograms_throws_for_more_datafiles_than_number_of_regions(self):
data_filenames = ["data_file_1", "data_file_2"]
instruments = ["instr_1", "instr_2"]
number_of_regions = 3
number_of_regions = 6
with self.assertRaises(ValueError):
add_histograms(data_filenames, self.project, instruments, number_of_regions)

def test_add_histograms_throws_for_less_datafiles_than_number_of_regions(self):
data_filenames = ["data_file_1", "data_file_2"]
instruments = ["instr_1", "instr_2"]
def test_add_histograms_throws_for_single_bank_per_file(self):
"""Test that single bank per file is rejected"""
data_filenames = ["data_file_1"]
instruments = ["instr_1"]
number_of_regions = 1
with self.assertRaises(ValueError):
add_histograms(data_filenames, self.project, instruments, number_of_regions)

def test_add_histograms_throws_for_less_datafiles_than_instruments(self):
data_filenames = ["data_file_1", "data_file_2"]
instruments = ["instr_1", "instr_2", "instr_3"]
number_of_regions = 2
def test_add_histograms_throws_for_multiple_instruments_for_single_datafile(self):
self.project.phases.return_value = ["phase_1"]
data_filenames = ["data_file_1"]
instruments = ["instr_1", "instr_2"]
number_of_regions = 3
with self.assertRaises(ValueError):
add_histograms(data_filenames, self.project, instruments, number_of_regions)

Expand Down
Loading