\(\renewcommand\AA{\unicode{x212B}}\)

ISIS Single Crystal Diffraction Reduction Scripts

Introduction

The reduction of single-crystal diffraction data from the SXD and WISH instruments at ISIS is achieved with the use of instrument specific classes (SXD and WishSX respectively).

The instrument specific classes inherit from a base class BaseSX that contains code common to the workflow of all single-crystal diffraction reduction instruments - the user shouldn’t interact directly with this class.

Basic Usage

During beamtime, users may want to load data, find peaks and optimise UB matrices etc. without performing the time-consuming normalisation and correction procedures necessary when reducing the data for refinement.

These simple operations have been made concise with the use of static methods in the reduction classes - i.e. the user can call these methods without a creating an instance of the class. These static methods can be combined in a script with mantid algorithms in the usual way.

Here is an example using the SXD class that finds peaks and then removes duplicates

from mantid.simpleapi import *
from Diffraction.single_crystal.sxd import SXD

ws = Load(Filename='SXD33335.nxs', OutputWorkspace='SXD33335')

# find peaks using SXD static method - determines peak threshold from
# the ratio of local variance over mean
peaks_ws = SXD.find_sx_peaks(ws, ThresholdVarianceOverMean=2.0)
SXD.remove_duplicate_peaks_by_qlab(peaks_ws, q_tol=0.05)

# find a UB
FindUBUsingFFT(PeaksWorkspace=peaks_ws, MinD=1, MaxD=10)

# use another static method to remove duplicates by hkl index
SXD.remove_duplicate_peaks_by_hkl(peaks_ws)

SXD Workflow

1. Detector Calibration

The positions of the SXD detector panels require calibration - typically using an NaCl crystal (standard sample). The updated positions are stored in an .xml file, the path to the file is returned by calibrate_sxd_panels.

sxd = SXD()
wsname = sxd.load_run(32863)
peaks_ws = sxd.find_sx_peaks(wsname)
sxd.remove_peaks_on_detector_edge(peaks_ws, 2)

# initial UB
FindUBUsingFFT(PeaksWorkspace=peaks_ws , MinD=1, MaxD=10)
# force lattice parameters to be correct
SelectCellWithForm(PeaksWorkspace=peaks_ws, FormNumber=1, Apply=True, Tolerance=0.15)
OptimizeLatticeForCellType(PeaksWorkspace=peaks_ws, Apply=True, Tolerance=0.15)
IndexPeaks(PeaksWorkspace=peaks_ws, Tolerance=0.15, CommonUBForAll=False)
CalculateUMatrix(PeaksWorkspace=peaks_ws, a=5.6402, b=5.6402, c=5.6402,
                 alpha=90, beta=90, gamma=90)

save_dir = r"<put-correct-save-path>"
detcal_path = sxd.calibrate_sxd_panels(wsname, peaks_ws, save_dir, tol=0.2)

2. Reduce a sequence of runs

Once the detectors have been calibrated a sequence of runs can be reduced.

a. Setting up the SXD object

In order to reduce a sequence of runs, an instance of the SXD class needs to be initiated with a vanadium and empty run number and optionally the detector calibration file from 1. Detector Calibration.

If sample absorption is to be corrected for, the sample shape/geometry and material can be set using set_sample as in SetSample v1 - note the default units for the number density are formula units per cubic Angstrom.

Typically each run in a sequence corresponds to a different sample orientation, in which case it is necessary to set the goniometer axes using set_goniometer as defined in SetGoniometer v1. The goniometer angles are provided later.

sxd = SXD(vanadium_runno=32857, empty_runno=32856, detcal_path=detcal_path)
sxd.set_sample(Geometry={'Shape': 'CSG', 'Value': sxd.sphere_shape},
               Material={'ChemicalFormula': 'Na Cl', 'SampleNumberDensity': 0.0223})
sxd.set_goniometer_axes([0,1,0,1])  # ccw rotation around vertical

b. Running the reduction

The reduction proceeds as follows:

  • Process vanadium

  • Load and normalise data

  • Find peaks

  • Optimise a UB

  • Integrate peaks

  • Export peaks for refinement

from mantid.simpleapi import *
from os import path
from Diffraction.single_crystal.base_sx import PEAK_TYPE, INTEGRATION_TYPE
from Diffraction.single_crystal.sxd import SXD

# Setup SXD
###########

sxd = SXD(vanadium_runno=32857, empty_runno=32856, detcal_path=detcal_path)
sxd.set_sample(Geometry={'Shape': 'CSG', 'Value': sxd.sphere_shape},
               Material={'ChemicalFormula': 'Na Cl', 'SampleNumberDensity': 0.0223})
sxd.set_goniometer_axes([0,1,0,1])  # ccw rotation around vertical

# load and normalise data
#########################

runs = range(32863,32865)
sxd.process_vanadium()
sxd.process_data(runs, "wccr") # wccr is the goniometer motor log name - one arg for each axis added
# if there was no log value then the angles have to be set with a sequence e.g.
# sxd.process_data(runs, [0,45])

# Find peaks and optimise UBs
##############################

for run in runs:
    ws = sxd.get_ws(run)
    peaks = sxd.find_sx_peaks(ws)
    sxd.remove_peaks_on_detector_edge(peaks, 2)
    sxd.set_peaks(run, peaks)
# find UB with consistent indexing across runs
sxd.find_ub_using_lattice_params(global_B=True, tol=0.15,
                                 a=5.6402, b=5.6402, c=5.6402,
                                 alpha=90, beta=90, gamma=90)
sxd.calibrate_sample_pos(tol=0.15)  # calibrates each run independently

# Integrate and save
####################

# input arguments for skew integration
save_dir = "/babylon/Public/UserName"
skew_args= {'UseNearestPeak': True, 'IntegrateIfOnEdge': False,
            'LorentzCorrection': True,
            'NVacanciesMax': 2, 'NPixPerVacancyMin': 2, 'NTOFBinsMin': 2,
            'UpdatePeakPosition': True, 'OptimiseMask': True,
            'GetTOFWindowFromBackToBackParams': False,
            'BackScatteringTOFResolution': 0.06, 'ThetaWidth': 1.5,
            'ScaleThetaWidthByWavelength': True,
            'OptimiseXWindowSize': True, 'ThresholdIoverSigma': 15}

# integrate and save each run
peak_type = PEAK_TYPE.FOUND
integration_type = INTEGRATION_TYPE.SKEW
for run in runs:
    skew_args = {**skew_args, 'OutputFile': path.join(save_dir, f"{run}_{peak_type.value}_int.pdf")}
    sxd.integrate_data(integration_type, peak_type, run=run, **skew_args)
    sxd.remove_non_integrated_peaks(sxd.get_peaks(run, peak_type, integration_type))
    sxd.calc_absorption_weighted_path_lengths(peak_type, integration_type, run=run, ApplyCorrection=True)
    sxd.save_peak_table(run, peak_type, integration_type, save_dir, save_format='SHELX')

# save combined table
sxd.save_all_peaks(peak_type, integration_type, save_dir=save_dir, save_format='SHELX')

WISH Workflow

1. Preliminary integration

During beamtime users may want to check that a run has been counted sufficiently. This involves saving the data with a different file extension e.g. .s01.

This example shows how to load data with specific file extension and perform an integration in Q-space.

from mantid.simpleapi import *
from os import path
from Diffraction.wish.wishSX import WishSX

# integration parameters
intPeaksMDArgs = {'ellipsoid': True, 'fixQAxis': True, 'fixMajorAxisLength': True, 'useCentroid': True, 'MaskEdgeTubes': False}

ws = WishSX.load_run(run, file_ext=".s01")
# convert data to Q for integration
WishSX.mask_detector_edges(ws, nedge=16, ntubes=2)
wsMD = WishSX.convert_ws_to_MD(ws, frame="Q (lab frame)")
# find peaks and integrate
peaks = WishSX.find_sx_peaks(ws)
peaks_int = WishSX().integrate_peaks_MD_optimal_radius(wsMD, peaks, peaks+"_int", ws=ws, **intPeaksMDArgs)

Note that the method integrate_peaks_MD_optimal_radius requires an instance of the class WishSX() - this is because the optimal radius for the integration depends on the instrument, but the method is defined in the base class as it is common to both SXD and WISH.

The default file extension (used in process_vanadium and process_data) is stored as an attribute on the class which can be set at instantiation as follows

wish = WishSX(file_ext=".s01")

It can also set at any point directly (e.g. after the vanadium run has been processed and to reduce a single run for preliminary refinement).

wish.file_ext = ".s01"

2. Finding the UBs for a sequence of runs

The WISH workflow finds UBs before the reduction and exports the UB matrix for every run post rotation by the goniometer matrix. The UBs are then loaded in the subsequent reduction, which then doesn’t require the goniometer information, and the predicted peak positions are integrated (in contrast to SXD where the found peaks are integrated)..

Here is an example that finds peaks, integrates them in Q-space and filters by I/sigma to only use the strongest peaks in the UB optimisation. The data were collected using the WISH goniometer with 2 axes of rotation (phi and omega), in this case a positive angle (omega) correposnds to a counter-clockwise rotation around the vertical axis as viewed from the top - i.e. the goniometer axis is Axis0=str(omegas[irun])+',0,1,0,1'. For data collected in the Newport with only a vertical axis of rotation a positive angle corresponds to a clockwise rotation and the correct goniometer axis is Axis0=str(omegas[irun])+',0,1,0,-1' (note the sign of the last number has changed).

from mantid.simpleapi import *
from os import path
from Diffraction.wish.wishSX import WishSX

# lattice parameters
a, b, c, alpha, beta, gamma = 12.2738, 12.2738, 12.2738, 90.0, 90.0, 90.0

# integration parameters
intPeaksMDArgs = {'ellipsoid': True, 'fixQAxis': True, 'fixMajorAxisLength': True, 'useCentroid': True, 'MaskEdgeTubes': False}

# goniometer angles (one for each run)
omegas = [7.0, 50.0, 153.0]
phis = [181.0, 247.0, 93.0]
runs = range(42730, 42733)

pk_ws_list = []
for irun, run in enumerate(runs):
    ws = WishSX.load_run(run)
    SetGoniometer(Workspace=ws, Axis0=str(omegas[irun])+',0,1,0,1',
                  Axis1=str(phis[irun]) + ',1,1,0,1')
    # SetGoniometer(Workspace=ws, Axis0=str(omegas[irun])+',0,1,0,-1') # if using newport
    # convert data to Q for integration
    WishSX.mask_detector_edges(ws, nedge=16, ntubes=2)
    wsMD = WishSX.convert_ws_to_MD(ws, frame="Q (lab frame)")
    # find peaks and integrate
    peaks = WishSX.find_sx_peaks(ws)
    peaks_int = WishSX().integrate_peaks_MD_optimal_radius(wsMD, peaks, peaks+"_int", ws=ws, **intPeaksMDArgs)
    peaks_int = peaks_int.name()
    # filter to get strong peaks only
    FilterPeaks(InputWorkspace=peaks_int, OutputWorkspace=peaks_int, FilterVariable="Signal/Noise",
                FilterValue=10, Operator=">")
    # get a rough UB and keep indexed
    FindUBUsingFFT(peaks_int, MinD=1, MaxD=20)
    SelectCellOfType(PeaksWorkspace=peaks_int, Centering='I', Apply=True)
    IndexPeaks(peaks_int, RoundHKLs=True, CommonUBForAll=False)
    CalculateUMatrix(peaks_int, a=a, b=b, c=c, alpha=alpha, beta=beta, gamma=gamma)
    WishSX.remove_unindexed_peaks(peaks_int)
    pk_ws_list.append(peaks_int)

# find a UB per run with consistent indexing across the sequence
FindGlobalBMatrix(PeakWorkspaces=pk_ws_list, a=a, b=b, c=c, alpha=alpha, beta=beta, gamma=gamma, Tolerance=0.15)

# save the UBs
save_dir = "/babylon/Public/UserName"
for irun, peaks in enumerate(pk_ws_list):
    SaveIsawUB(InputWorkspace=peaks, Filename=path.join(save_dir,  f'{runs[irun]}.mat'),
               RotateByGoniometerMatrix=True)

3. Reduce a sequence of runs

After a UB matrix has been optimised for each run, the entire sequence of runs can now be reduced.

The method predict_peaks predicts peaks based on the enum class PEAK_TYPE:

  • FOUND

  • PREDICT

  • PREDICT_SAT

The method takes the keyword arguments for PredictPeaks v1 and PredictFractionalPeaks v1 for PEAK_TYPE.PREDICT and PEAK_TYPE.PREDICT_SAT respectively.

The integration algorithm called by integrate_data will integrate the peak table based on the PEAK_TYPE supplied using an agorithm determined by the enum class INTEGRATION_TYPE:

  • MD

  • MD_OPTIMAL_RADIUS

  • SKEW

The Q-space integration methods are MD a MD_OPTIMAL_RADIUS - the latter estimates an appropriate peak radius for each peak, the former method requires the user to supply the peak radius to integrate_data using keyword arguments as in IntegratePeaksMD v2.

a. Skew Integration (no satellites)

from mantid.simpleapi import *
from os import path
from Diffraction.single_crystal.base_sx import PEAK_TYPE, INTEGRATION_TYPE
from Diffraction.wish.wishSX import WishSX

# Setup WishSX
##############

wish = WishSX(vanadium_runno=43526)
# set sample material
sphere = '''<sphere id="sphere">
            <centre x="0.0"  y="0.0" z="0.0" />
            <radius val="0.0009"/>
            </sphere>'''  # sphere radius 0.9mm
wish.set_sample(Geometry={'Shape': 'CSG', 'Value': sphere_GGG},
                Material={'ChemicalFormula': 'Ca3-Ga2-Ge3-O12', 'SampleNumberDensity': 0.001086})

# load vanadium
###############

wish.process_vanadium()

# Integrate and save
####################

save_dir = "/babylon/Public/UserName"
# integration parameters
intPeaksSkewArgs = {'UseNearestPeak': True, 'IntegrateIfOnEdge': False,
                    'NRowsEdge': 5, 'NColsEdge':2,
                    'NVacanciesMax': 2, 'NPixPerVacancyMin': 2, 'NTOFBinsMin': 2,
                    'UpdatePeakPosition': True, 'OptimiseMask': True,
                    'GetTOFWindowFromBackToBackParams': True, 'NFWHM': 10,
                    'LorentzCorrection': True, 'ScaleThetaWidthByWavelength': True,
                    'OptimiseXWindowSize':True, 'ThresholdIoverSigma': 80}

for run in range(42730, 42733):
    wish.process_data([run])
    # load UB
    wish.load_isaw_ub(path.join(save_dir, f'{run}.mat'), tol=0.15, run=run)
    # predict peaks
    wish.predict_peaks(MinDSpacing=0.75, WavelengthMin=0.85,
                       ReflectionCondition='Body centred', run=run)
    # Integrate
    skew_kwargs = {'OutputFile': path.join(save_dir, f'{run}_integrated.pdf'),
                   **intPeaksSkewArgs}
    wish.integrate_data(INTEGRATION_TYPE.SKEW, PEAK_TYPE.PREDICT, run=run, **skew_kwargs)
    wish.save_peak_table(run, PEAK_TYPE.PREDICT, INTEGRATION_TYPE.SKEW,
                         save_dir=save_dir, save_format="Jana")
    wish.delete_run_data(run)  # delete raw workspace to save memory

wish.save_all_peaks(PEAK_TYPE.FOUND, INTEGRATION_TYPE.SKEW, save_dir=save_dir, save_format="jana")

b. Q Integration (with satellites)

from mantid.simpleapi import *
from os import path
from Diffraction.single_crystal.base_sx import PEAK_TYPE, INTEGRATION_TYPE
from Diffraction.wish.wishSX import WishSX

# Setup WishSX
##############
wish = WishSX(vanadium_runno=43526)
# set sample material
sphere = '''<sphere id="sphere">
            <centre x="0.0"  y="0.0" z="0.0" />
            <radius val="0.0009"/>
            </sphere>'''  # sphere radius 0.9mm
wish.set_sample(Geometry={'Shape': 'CSG', 'Value': sphere},
                Material={'ChemicalFormula': 'Ca3-Ga2-Ge3-O12', 'SampleNumberDensity': 0.001086})

# load vanadium
###############

wish.process_vanadium()

# Load runs one at a time and integrate
#######################################

save_dir = "/babylon/Public/UserName"
# integration parameters
intPeaksMDArgs = {'ellipsoid': True, 'fixQAxis': True, 'fixMajorAxisLength': True, 'useCentroid': True, 'MaskEdgeTubes': False}

for run in range(42730, 42733):
    wish.process_data([run])
    # load UB
    wish.load_isaw_ub(path.join(save_dir, f'{run}.mat'), tol=0.15, run=run)
    # predict peaks
    # main
    wish.predict_peaks(MinDSpacing=0.75, WavelengthMin=0.85,
                       ReflectionCondition='Body centred', run=run)
    # satellite
    wish.predict_peaks(peak_type=PEAK_TYPE.SATELLITE, run=run,
                       ModVector1="0.5,0,0", ModVector2="0,0.5,0", ModVector3="0,0,0.5",
                       MaxOrder=1, CrossTerms=False, RequirePeaksOnDetector=True,
                       ReflectionCondition='Body centred')
    # filter satellite peaks by wavelength and d-spacing
    sat_peaks = self.get_peaks(run, PEAK_TYPE.PREDICT_SAT)
    FilterPeaks(InputWorkspace=sat_peaks, OutputWorkspace=sat_peaks, FilterVariable="Wavelength",
                FilterValue=0.85, Operator=">")
    FilterPeaks(InputWorkspace=sat_peaks, OutputWorkspace=sat_peaks, FilterVariable="DSpacing",
                FilterValue=0.75, Operator=">")
    # Integrate and save
    wish.convert_to_MD(run=run)  # convert to QLab (default)
    for peak_type in [PEAK_TYPE.PREDICT, PREDICT_SAT]:
        wish.integrate_data(INTEGRATION_TYPE.MD_OPTIMAL_RADIUS, peak_type, run=run, **intPeaksMDArgs)
        wish.save_peak_table(run, peak_type, INTEGRATION_TYPE.MD_OPTIMAL_RADIUS,
                             save_dir=save_dir, save_format="Jana")
    wish.delete_run_data(run, del_MD=False)  # delete raw workspace to save memory

for peak_type in [PEAK_TYPE.PREDICT, PREDICT_SAT]:
    wish.save_all_peaks(peak_type, INTEGRATION_TYPE.MD_OPTIMAL_RADIUS, save_dir=save_dir, save_format="jana")

Category: Techniques