diff --git a/Framework/PythonInterface/mantid/utils/absorptioncorrutils.py b/Framework/PythonInterface/mantid/utils/absorptioncorrutils.py index 3aaa22e658fe..0bff60c9d97a 100644 --- a/Framework/PythonInterface/mantid/utils/absorptioncorrutils.py +++ b/Framework/PythonInterface/mantid/utils/absorptioncorrutils.py @@ -8,6 +8,7 @@ from mantid.kernel import Logger, Property, PropertyManager from mantid.simpleapi import ( AbsorptionCorrection, + DefineGaugeVolume, DeleteWorkspace, Divide, Load, @@ -24,9 +25,15 @@ import numpy as np import os from functools import wraps +import xml.etree.ElementTree as ET VAN_SAMPLE_DENSITY = 0.0721 _EXTENSIONS_NXS = ["_event.nxs", ".nxs.h5"] +GV_VALS = { + "X": {"x": "0.2", "y": "0.0", "z": "0.0", "t": "90.0", "p": "180.0"}, + "Y": {"x": "0.0", "y": "0.2", "z": "0.0", "t": "90.0", "p": "270.0"}, + "Z": {"x": "0.0", "y": "0.0", "z": "0.2", "t": "180.0", "p": "0.0"}, +} # ---------------------------- # @@ -284,6 +291,11 @@ def calculate_absorption_correction( sample_formula, mass_density, sample_geometry={}, + can_geometry={}, + can_material={}, + gauge_vol="", + container_gauge_vol="", + beam_height=Property.EMPTY_DBL, number_density=Property.EMPTY_DBL, container_shape="PAC06", num_wl_bins=1000, @@ -323,6 +335,11 @@ def calculate_absorption_correction( :param sample_formula: Sample formula to specify the Material for absorption correction :param mass_density: Mass density of the sample to specify the Material for absorption correction :param sample_geometry: Dictionary to specify the sample geometry for absorption correction + :param can_geometry: Dictionary to specify the container geometry for absorption correction + :param can_material: Dictionary to specify the container material for absorption correction + :param gauge_vol: String in XML form to define the volume of the sample visible to the beam + :param container_gauge_vol: String in XML form to define the volume of the container visible to the beam + :param beam_height: Optional beam height to use for absorption correction :param number_density: Optional number density of sample to be added to the Material for absorption correction :param container_shape: Shape definition of container, such as PAC06. :param num_wl_bins: Number of bins for calculating wavelength @@ -344,12 +361,26 @@ def calculate_absorption_correction( material["SampleNumberDensity"] = number_density environment = {} - if container_shape: + find_env = True + if container_shape or (can_geometry and can_material): environment["Name"] = "InAir" - environment["Container"] = container_shape + find_env = False + if not (can_geometry and can_material): + environment["Container"] = container_shape donorWS = create_absorption_input( - filename, props, num_wl_bins, material=material, geometry=sample_geometry, environment=environment, metaws=metaws + filename, + props, + num_wl_bins, + material=material, + geometry=sample_geometry, + can_geometry=can_geometry, + can_material=can_material, + gauge_vol=gauge_vol, + beam_height=beam_height, + environment=environment, + find_environment=find_env, + metaws=metaws, ) # NOTE: Ideally we want to separate cache related task from calculation, @@ -381,6 +412,8 @@ def calculate_absorption_correction( donorWS, abs_method, element_size, + container_gauge_vol=container_gauge_vol, + beam_height=beam_height, prefix_name=absName, cache_dirs=cache_dirs, ms_method=ms_method, @@ -392,6 +425,8 @@ def calc_absorption_corr_using_wksp( donor_wksp, abs_method, element_size=1, + container_gauge_vol="", + beam_height=Property.EMPTY_DBL, prefix_name="", cache_dirs=[], ms_method="", @@ -401,7 +436,7 @@ def calc_absorption_corr_using_wksp( if cache_dirs: log.warning("Empty cache dir found.") # 1. calculate first order absorption correction - abs_s, abs_c = calc_1st_absorption_corr_using_wksp(donor_wksp, abs_method, element_size, prefix_name) + abs_s, abs_c = calc_1st_absorption_corr_using_wksp(donor_wksp, abs_method, element_size, container_gauge_vol, beam_height, prefix_name) # 2. calculate 2nd order absorption correction if ms_method in ["", None, "None"]: log.information("Skip multiple scattering correction as instructed.") @@ -452,6 +487,8 @@ def calc_1st_absorption_corr_using_wksp( donor_wksp, abs_method, element_size=1, + container_gauge_vol="", + beam_height=Property.EMPTY_DBL, prefix_name="", ): """ @@ -461,6 +498,8 @@ def calc_1st_absorption_corr_using_wksp( :param donor_wksp: Input workspace to compute absorption correction on :param abs_method: Type of absorption correction: None, SampleOnly, SampleAndContainer, FullPaalmanPings :param element_size: Size of one side of the integration element cube in mm + :param container_gauge_vol: String in XML form to define the volume of container visible to the beam + :param beam_height: Beam height for defining the gauge volume for container visible to the beam :param prefix_name: Optional prefix of the output workspaces, default is the donor_wksp name. :return: Two workspaces (A_s, A_c), the first for the sample and the second for the container @@ -473,6 +512,13 @@ def calc_1st_absorption_corr_using_wksp( raise RuntimeError("Specified donor workspace not found in the ADS") donor_wksp = mtd[donor_wksp] + def is_valid_xml(xml_string): + try: + ET.fromstring(xml_string) + return True + except ET.ParseError: + return False + absName = donor_wksp.name() if prefix_name != "": absName = prefix_name @@ -482,6 +528,42 @@ def calc_1st_absorption_corr_using_wksp( return absName + "_ass", "" elif abs_method == "SampleAndContainer": AbsorptionCorrection(donor_wksp, OutputWorkspace=absName + "_ass", ScatterFrom="Sample", ElementSize=element_size) + if container_gauge_vol and is_valid_xml(container_gauge_vol): + try: + DefineGaugeVolume(donor_wksp, container_gauge_vol) + except ValueError: + pass + elif container_gauge_vol and beam_height != Property.EMPTY_DBL: + ref_frame = donor_wksp.getInstrument().getReferenceFrame() + up_direction = ref_frame.pointingUpAxis() + info_to_use = GV_VALS[up_direction] + + # Factor '100' here is for unit conversion from cm to m. An extra factor + # of '2' in 'beam_height / 200.0' is to make sure the center of the + # bottom base of the gauge volume is down from the origin by half of the + # gauge volume height so the center of the gauge volume is at the origin. + # This is our assumed location of the beam if only beam height is given. + gauge_vol = """ + + + + + + + """ + gauge_vol = gauge_vol.format( + beam_height / 200.0, + info_to_use["t"], + info_to_use["p"], + info_to_use["x"], + info_to_use["y"], + info_to_use["z"], + float(container_gauge_vol.split()[0]) / 100.0, + float(container_gauge_vol.split()[1]) / 100.0, + beam_height / 100.0, + ) + DefineGaugeVolume(donor_wksp, gauge_vol) + AbsorptionCorrection(donor_wksp, OutputWorkspace=absName + "_acc", ScatterFrom="Container", ElementSize=element_size) return absName + "_ass", absName + "_acc" elif abs_method == "FullPaalmanPings": @@ -499,6 +581,10 @@ def create_absorption_input( num_wl_bins=1000, material={}, geometry={}, + can_geometry={}, + can_material={}, + gauge_vol="", + beam_height=Property.EMPTY_DBL, environment={}, find_environment=True, opt_wl_min=0, @@ -513,6 +599,10 @@ def create_absorption_input( :param num_wl_bins: The number of wavelength bins used for absorption correction :param material: Optional material to use in SetSample :param geometry: Optional geometry to use in SetSample + :param can_geometry: Optional container geometry to use in SetSample + :param can_material: Optional container material to use in SetSample + :param gauge_vol: Optional gauge volume definition, i.e., sample portion visible to the beam. + :param beam_height: Optional beam height to define gauge volume :param environment: Optional environment to use in SetSample :param find_environment: Optional find_environment to control whether to figure out environment automatically. :param opt_wl_min: Optional minimum wavelength. If specified, this is used instead of from the props @@ -611,7 +701,49 @@ def confirmProps(props): # Make sure one is set before calling SetSample if material or geometry or environment: mantid.simpleapi.SetSampleFromLogs( - InputWorkspace=absName, Material=material, Geometry=geometry, Environment=environment, FindEnvironment=find_environment + InputWorkspace=absName, + Material=material, + Geometry=geometry, + ContainerGeometry=can_geometry, + ContainerMaterial=can_material, + Environment=environment, + FindEnvironment=find_environment, ) + if beam_height != Property.EMPTY_DBL and not gauge_vol: + # If the gauge volume is not defined, use the beam height to define it, + # and we will be assuming a cylinder shape of the sample. + ref_frame = mtd[absName].getInstrument().getReferenceFrame() + up_direction = ref_frame.pointingUpAxis() + info_to_use = GV_VALS[up_direction] + gauge_vol = """ + + + + + """ + if isinstance(geometry["Radius"], float): + sam_rad = geometry["Radius"] + else: + sam_rad = geometry["Radius"].value + + # Factor '100' here is for unit conversion from cm to m. An extra factor + # of '2' in 'beam_height / 200.0' is to make sure the center of the + # bottom base of the gauge volume is down from the origin by half of the + # gauge volume height so the center of the gauge volume is at the origin. + # This is our assumed location of the beam if only beam height is given. + gauge_vol = gauge_vol.format( + beam_height / 200.0, + info_to_use["t"], + info_to_use["p"], + info_to_use["x"], + info_to_use["y"], + info_to_use["z"], + sam_rad / 100.0, + beam_height / 100.0, + ) + + if gauge_vol: + DefineGaugeVolume(absName, gauge_vol) + return absName diff --git a/Framework/PythonInterface/plugins/algorithms/SNSPowderReduction.py b/Framework/PythonInterface/plugins/algorithms/SNSPowderReduction.py index 751ab2abe8ac..0b5e9a3e566f 100644 --- a/Framework/PythonInterface/plugins/algorithms/SNSPowderReduction.py +++ b/Framework/PythonInterface/plugins/algorithms/SNSPowderReduction.py @@ -291,6 +291,19 @@ def PyInit(self): ) self.declareProperty("SampleFormula", "", doc="Chemical formula of the sample") self.declareProperty("SampleGeometry", {}, doc="A dictionary of geometry parameters for the sample.") + self.declareProperty("ContainerGeometry", {}, doc="A dictionary of geometry parameters for the container.") + self.declareProperty("ContainerMaterial", {}, doc="A dictionary of material parameters for the container.") + self.declareProperty( + "GaugeVolume", "", "A string in XML form for gauge volume definition indicating sample portion visible to the beam." + ) + self.declareProperty( + "ContainerGaugeVolume", "", "A string in XML form for gauge volume definition indicating container portion visible to the beam." + ) + self.declareProperty( + "BeamHeight", + defaultValue=Property.EMPTY_DBL, + doc="Height of the neutron beam cross section in cm", + ) self.declareProperty( "MeasuredMassDensity", defaultValue=0.1, @@ -423,6 +436,11 @@ def PyExec(self): # noqa self._absMethod = self.getProperty("TypeOfCorrection").value self._sampleFormula = self.getProperty("SampleFormula").value self._sampleGeometry = self.getProperty("SampleGeometry").value + self._containerGeometry = self.getProperty("ContainerGeometry").value + self._containerMaterial = self.getProperty("ContainerMaterial").value + self._gaugeVolume = self.getProperty("GaugeVolume").value + self._containerGaugeVolume = self.getProperty("ContainerGaugeVolume").value + self._beamHeight = self.getProperty("BeamHeight").value self._massDensity = self.getProperty("MeasuredMassDensity").value self._numberDensity = self.getProperty("SampleNumberDensity").value self._containerShape = self.getProperty("ContainerShape").value @@ -524,6 +542,11 @@ def PyExec(self): # noqa self._sampleFormula, # Material for absorption correction self._massDensity, # Mass density of the sample self._sampleGeometry, # Geometry parameters for the sample + self._containerGeometry, # Geometry parameters for the container + self._containerMaterial, # Material parameters for the container + self._gaugeVolume, # Gauge volume definition for sample + self._containerGaugeVolume, # Gauge volume definition for container + self._beamHeight, # Height of the neutron beam cross section in cm self._numberDensity, # Optional number density of sample to be added self._containerShape, # Shape definition of container self._num_wl_bins, # Number of bins: len(ws.readX(0))-1 @@ -1515,6 +1538,7 @@ def _process_vanadium_runs(self, van_run_number_list, samRunIndex, **dummy_focus self._num_wl_bins, material={"ChemicalFormula": "V", "SampleNumberDensity": absorptioncorrutils.VAN_SAMPLE_DENSITY}, geometry={"Shape": "Cylinder", "Height": 7.0, "Radius": self._vanRadius, "Center": [0.0, 0.0, 0.0]}, + beam_height=self._beamHeight, find_environment=False, opt_wl_min=self._wavelengthMin, opt_wl_max=self._wavelengthMax, @@ -1543,6 +1567,9 @@ def _process_vanadium_runs(self, van_run_number_list, samRunIndex, **dummy_focus ) api.RenameWorkspace(abs_v_wsn, "__V_corr_abs") + # Here, we are using a combo of absorption correction with the numerical integration approach and multiple + # scattering correction with the Carpenter approach - `Absorption` param set to `False` below, making sure + # only `__V_corr_ms` will be created without overwriting the already calculated `__V_corr_abs`. api.CalculateCarpenterSampleCorrection( InputWorkspace=absWksp, OutputWorkspaceBaseName="__V_corr", CylinderSampleRadius=self._vanRadius, Absorption=False ) diff --git a/docs/source/release/v6.13.0/Diffraction/Powder/New_features/38887.rst b/docs/source/release/v6.13.0/Diffraction/Powder/New_features/38887.rst new file mode 100644 index 000000000000..ae3b8f82d2a0 --- /dev/null +++ b/docs/source/release/v6.13.0/Diffraction/Powder/New_features/38887.rst @@ -0,0 +1 @@ +- Enable user defined sample and container geometry together with the definition of gauge volume to account for the beam size. Implementation made in :ref:`SNSPowderReduction ` and ``mantid.utils.absorptioncorrutils``.