Skip to content
1 change: 1 addition & 0 deletions Framework/PythonInterface/plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ set(PYTHON_PLUGINS
algorithms/WorkflowAlgorithms/ReflectometryISISLoadAndProcess.py
algorithms/WorkflowAlgorithms/ReflectometryISISSumBanks.py
algorithms/WorkflowAlgorithms/ReflectometryISISCalibration.py
algorithms/WorkflowAlgorithms/ReflectometryISISCreateTransmission.py
algorithms/WorkflowAlgorithms/ResNorm2.py
algorithms/WorkflowAlgorithms/SANS/SANSBeamCentreFinder.py
algorithms/WorkflowAlgorithms/SANS/SANSBeamCentreFinderCore.py
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-# Mantid Repository : https://github.com/mantidproject/mantid
#
# Copyright © 2024 ISIS Rutherford Appleton Laboratory UKRI,
# NScD Oak Ridge National Laboratory, European Spallation Source,
# Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
# SPDX - License - Identifier: GPL - 3.0 +

from mantid.api import (
AlgorithmFactory,
DataProcessorAlgorithm,
MatrixWorkspaceProperty,
PropertyMode,
WorkspaceGroup,
WorkspaceProperty,
)
from mantid.kernel import Direction, StringArrayLengthValidator, StringArrayMandatoryValidator, StringArrayProperty, CompositeValidator


class Prop:
INPUT_RUNS = "InputRuns"
OUTPUT_WS = "OutputWorkspace"
FLOOD_WS = "FloodWorkspace"
BACK_SUB_ROI = "BackgroundProcessingInstructions"
TRANS_ROI = "ProcessingInstructions"
I0_MON_IDX = "I0MonitorIndex"
MON_WAV_MIN = "MonitorIntegrationWavelengthMin"
MON_WAV_MAX = "MonitorIntegrationWavelengthMax"


class ReflectometryISISCreateTransmission(DataProcessorAlgorithm):
_LOAD_ALG = "LoadAndMerge"
_FLOOD_ALG = "ApplyFloodWorkspace"
_BACK_SUB_ALG = "ReflectometryBackgroundSubtraction"
_TRANS_WS_ALG = "CreateTransmissionWorkspaceAuto"

def __init__(self):
"""Initialize an instance of the algorithm."""
DataProcessorAlgorithm.__init__(self)

def category(self):
"""Return the categories of the algorithm."""
return "Reflectometry\\ISIS;Workflow\\Reflectometry"

def name(self):
"""Return the name of the algorithm."""
return "ReflectometryISISCreateTransmission"

def summary(self):
"""Return a summary of the algorithm."""
return "Create a transmission workspace for ISIS reflectometry data, including optional flood and background corrections."

def seeAlso(self):
"""Return a list of related algorithm names."""
return [self._FLOOD_ALG, self._BACK_SUB_ALG, self._TRANS_WS_ALG]

def PyInit(self):
mandatory_runs = CompositeValidator()
mandatory_runs.add(StringArrayMandatoryValidator())
len_validator = StringArrayLengthValidator()
len_validator.setLengthMin(1)
mandatory_runs.add(len_validator)
self.declareProperty(
StringArrayProperty(Prop.INPUT_RUNS, values=[], validator=mandatory_runs),
doc="A list of input run numbers. Multiple runs will be summed after loading",
)

self.copyProperties(self._TRANS_WS_ALG, [Prop.TRANS_ROI, Prop.I0_MON_IDX, Prop.MON_WAV_MIN, Prop.MON_WAV_MAX])

self.declareProperty(
MatrixWorkspaceProperty(Prop.FLOOD_WS, "", direction=Direction.Input, optional=PropertyMode.Optional),
doc="The workspace to be used for the flood correction. If this property is not set then no flood correction is performed.",
)

self.declareProperty(
Prop.BACK_SUB_ROI,
"",
doc=f"The set of workspace indices to be passed to the ProcessingInstructions property of the {self._BACK_SUB_ALG} algorithm."
" If this property is not set then no background subtraction is performed.",
)

self.declareProperty(
WorkspaceProperty(Prop.OUTPUT_WS, "", direction=Direction.Output),
doc="The output transmission workspace",
)

def PyExec(self):
ws = self._load_and_sum_runs(self.getProperty(Prop.INPUT_RUNS).value, self.getPropertyValue(Prop.OUTPUT_WS))

ws = self._apply_flood_correction(ws)
ws = self._apply_background_subtraction(ws)
ws = self._create_transmission_ws(ws)

self.setProperty(Prop.OUTPUT_WS, ws)

def _load_and_sum_runs(self, runs: list[str], output_name: str):
"""Load and sum the input runs"""
self.log().information("Loading and summing the run files")
alg = self.createChildAlgorithm(self._LOAD_ALG, Filename="+".join(runs), LoaderName="LoadNexus", OutputWorkspace=output_name)
alg.setRethrows(True)
alg.setProperty("MergeRunsOptions", {"FailBehaviour": "Stop"})
alg.execute()
return alg.getProperty("OutputWorkspace").value

def _apply_flood_correction(self, workspace):
"""If a flood workspace has been provided then apply a flood correction"""
flood_ws = self.getProperty(Prop.FLOOD_WS).value
if flood_ws is None:
return workspace

self.log().information("Performing flood correction")
args = {"FloodWorkspace": flood_ws}
corrected_ws = self._run_algorithm(workspace, self._FLOOD_ALG, "InputWorkspace", args)

# The flood correction performs a divide, which can result in the workspace being set as a distribution
# if the flood and input workspaces have the same Y units. We need to set the resulting workspace back to
# a non-distribution as it may be dimensionless after the divide, but it shouldn't be a distribution.
if isinstance(corrected_ws, WorkspaceGroup):
for child_ws in corrected_ws:
child_ws.setDistribution(False)
else:
corrected_ws.setDistribution(False)

return corrected_ws

def _apply_background_subtraction(self, workspace):
"""If background processing instructions have been provided then perform a background subtraction"""
if self.getProperty(Prop.BACK_SUB_ROI).isDefault:
return workspace

self.log().information("Performing background subtraction")
args = {
"InputWorkspaceIndexType": "WorkspaceIndex",
"ProcessingInstructions": self.getPropertyValue(Prop.BACK_SUB_ROI),
"BackgroundCalculationMethod": "PerDetectorAverage",
}

try:
return self._run_algorithm(workspace, self._BACK_SUB_ALG, "InputWorkspace", args)
except Exception as ex:
# The error that's printed can be confusing if we don't mention the background subtraction algorithm
self.log().error(f"Error running {self._BACK_SUB_ALG}")
raise RuntimeError(ex)

def _create_transmission_ws(self, workspace):
"""Create the transmission workspace"""
self.log().information("Creating the transmission workspace")
args = {
Prop.TRANS_ROI: self.getPropertyValue(Prop.TRANS_ROI),
Prop.I0_MON_IDX: self.getPropertyValue(Prop.I0_MON_IDX),
Prop.MON_WAV_MIN: self.getPropertyValue(Prop.MON_WAV_MIN),
Prop.MON_WAV_MAX: self.getPropertyValue(Prop.MON_WAV_MAX),
}
return self._run_algorithm(workspace, self._TRANS_WS_ALG, "FirstTransmissionRun", args)

def _run_algorithm(self, workspace, alg_name: str, input_ws_prop_name: str, args: dict):
"""Run the specified algorithm as a child algorithm using the arguments provided.
The input and output workspace properties are both set to be the provided workspace."""

def run_alg(ws):
args.update({input_ws_prop_name: ws, "OutputWorkspace": ws})
alg = self.createChildAlgorithm(alg_name, **args)
alg.setRethrows(True)
alg.execute()
return alg.getProperty("OutputWorkspace").value

# If the input run loads as a workspace group, then using the default workspace group handling of other Mantid algorithms
# prevents us retrieving an output workspace group from this algorithm when it is run as a child (unless we store things
# in the ADS). See issue #38473.
# To avoid this, when we have a workspace group we loop through it, run each algorithm against the child
# workspaces individually and then collect them into a group again at the end
if isinstance(workspace, WorkspaceGroup):
output_grp = WorkspaceGroup()
for child_ws in workspace:
output_grp.addWorkspace(run_alg(child_ws))
return output_grp

return run_alg(workspace)


AlgorithmFactory.subscribe(ReflectometryISISCreateTransmission)
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ set(TEST_PY_FILES
ReflectometryISISSumBanksTest.py
ReflectometryILLSumForegroundTest.py
ReflectometryISISCalibrationTest.py
ReflectometryISISCreateTransmissionTest.py
ReflectometryISISLoadAndProcessTest.py
ReflectometryISISPreprocessTest.py
ResNorm2Test.py
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Mantid Repository : https://github.com/mantidproject/mantid
#
# Copyright © 2024 ISIS Rutherford Appleton Laboratory UKRI,
# NScD Oak Ridge National Laboratory, European Spallation Source,
# Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
# SPDX - License - Identifier: GPL - 3.0 +
import unittest
import numpy as np

from mantid import config
from mantid.api import AnalysisDataService, MatrixWorkspace, WorkspaceGroup
from mantid.simpleapi import CreateWorkspace
from testhelpers import create_algorithm

from plugins.algorithms.WorkflowAlgorithms.ReflectometryISISCreateTransmission import Prop


class ReflectometryISISCreateTransmissionTest(unittest.TestCase):
_CONFIG_KEY_FACILITY = "default.facility"
_CONFIG_KEY_INST = "default.instrument"

_OUTPUT_WS_NAME = "out_ws"

_LOAD_ALG = "LoadAndMerge"
_FLOOD_ALG = "ApplyFloodWorkspace"
_BACK_SUB_ALG = "ReflectometryBackgroundSubtraction"
_TRANS_WS_ALG = "CreateTransmissionWorkspaceAuto"

def setUp(self):
self._oldFacility = config[self._CONFIG_KEY_FACILITY]
if self._oldFacility.strip() == "":
self._oldFacility = "TEST_LIVE"
self._oldInstrument = config[self._CONFIG_KEY_INST]
config[self._CONFIG_KEY_FACILITY] = "ISIS"
config[self._CONFIG_KEY_INST] = "POLREF"

def tearDown(self):
AnalysisDataService.clear()
config[self._CONFIG_KEY_FACILITY] = self._oldFacility
config[self._CONFIG_KEY_INST] = self._oldInstrument

def test_correct_output(self):
output_ws = self._run_algorithm(self._create_args("INTER13460", back_sub_roi=""))
self.assertIsNotNone(output_ws)
self.assertIsInstance(output_ws, MatrixWorkspace)
expected_history = [self._LOAD_ALG, self._FLOOD_ALG, self._TRANS_WS_ALG]
self._check_history(output_ws, expected_history)
self._check_output_data(output_ws, 771, 1.9796095404641e-07, 1.049193056445973e-05)

def test_correct_output_for_multiple_runs(self):
output_ws = self._run_algorithm(self._create_args(["INTER13463", "INTER13464"], back_sub_roi=""))
self.assertIsNotNone(output_ws)
self.assertIsInstance(output_ws, MatrixWorkspace)
expected_history = [self._LOAD_ALG, self._FLOOD_ALG, self._TRANS_WS_ALG]
self._check_history(output_ws, expected_history)
self._check_output_data(output_ws, 771, 0.018547099002195058, 1.2884763705979813e-05)

def test_correct_output_for_workspace_groups(self):
expected_history = [
self._LOAD_ALG,
self._FLOOD_ALG,
self._FLOOD_ALG,
self._BACK_SUB_ALG,
self._BACK_SUB_ALG,
self._TRANS_WS_ALG,
self._TRANS_WS_ALG,
]
output_grp = self._run_workspace_group_test(expected_history=expected_history)
self._check_output_data(output_grp[0], 905, -8.846391366193592e-10, -3.216869587706761e-10)
self._check_output_data(output_grp[1], 905, -4.209521594229452e-10, -3.3676172753835613e-10)

def test_output_workspace_group_returned_when_run_as_child(self):
alg = self._setup_algorithm(self._create_args("14966"))
alg.setChild(True)
alg.execute()
output_ws = alg.getProperty(Prop.OUTPUT_WS).value

self.assertIsNotNone(output_ws)
self.assertIsInstance(output_ws, WorkspaceGroup)

def test_flood_correction_skipped_if_not_requested(self):
expected_history = [self._LOAD_ALG, self._BACK_SUB_ALG, self._BACK_SUB_ALG, self._TRANS_WS_ALG, self._TRANS_WS_ALG]
self._run_workspace_group_test(perform_flood=False, expected_history=expected_history)

def test_background_subtraction_skipped_if_not_requested(self):
expected_history = [self._LOAD_ALG, self._FLOOD_ALG, self._FLOOD_ALG, self._TRANS_WS_ALG, self._TRANS_WS_ALG]
self._run_workspace_group_test(back_sub_roi="", expected_history=expected_history)

def _create_args(self, input_run, perform_flood=True, back_sub_roi="100-200"):
args = {
Prop.INPUT_RUNS: input_run,
Prop.TRANS_ROI: "4",
Prop.I0_MON_IDX: "2",
Prop.MON_WAV_MIN: "2.5",
Prop.MON_WAV_MAX: "10.0",
Prop.OUTPUT_WS: self._OUTPUT_WS_NAME,
}

if perform_flood:
args[Prop.FLOOD_WS] = self._create_flood_ws()

if back_sub_roi:
args[Prop.BACK_SUB_ROI] = back_sub_roi

return args

@staticmethod
def _create_flood_ws():
"""Creates a MatrixWorkspace with a single bin of data. The workspace has 256 spectra with values from
0.0 to 2.56 in steps of ~0.01"""
flood_ws = CreateWorkspace(DataX=[0.0, 1.0], DataY=np.linspace(0.0, 2.56, 256), NSpec=256, UnitX="TOF")
return flood_ws

@staticmethod
def _setup_algorithm(args):
alg = create_algorithm("ReflectometryISISCreateTransmission", **args)
alg.setRethrows(True)
return alg

def _run_algorithm(self, args):
alg = self._setup_algorithm(args)
alg.execute()
return AnalysisDataService.retrieve(self._OUTPUT_WS_NAME)

def _check_history(self, ws, expected):
"""Check if the expected algorithm names are found in the workspace history"""
history = ws.getHistory()
self.assertFalse(history.empty())
child_alg_histories = history.getAlgorithmHistory(history.size() - 1).getChildHistories()
self.assertEqual([alg.name() for alg in child_alg_histories], expected)

def _run_workspace_group_test(self, expected_history, perform_flood=True, back_sub_roi="100-200"):
# Omitting the instrument prefix from the run number should use the default instrument
output_grp = self._run_algorithm(self._create_args("14966", perform_flood, back_sub_roi))
self.assertIsInstance(output_grp, WorkspaceGroup)
self.assertEqual(output_grp.size(), 2)
self._check_history(output_grp[0], expected_history)
return output_grp

def _check_output_data(self, ws, expected_num_bins, expected_first_y, expected_last_y):
self.assertEqual(ws.getNumberHistograms(), 1)
data_y = ws.readY(0)
self.assertEqual(data_y.size, expected_num_bins)
self.assertAlmostEqual(data_y[0], expected_first_y, places=12)
self.assertAlmostEqual(data_y[-1], expected_last_y, places=12)


if __name__ == "__main__":
unittest.main()
1 change: 1 addition & 0 deletions Testing/Data/SystemTest/POLREF00032130.nxs.md5
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1b82a9852a8fc57405bcb94bef5e6e18
1 change: 1 addition & 0 deletions Testing/Data/SystemTest/POLREF00032132.nxs.md5
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
87b8f765015afb7f3a7a25dd400b3105
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
e93ef5c36019462046d31a80ecb6c5c9
Loading
Loading