From d51717b92a1eccfe5b3f55a0234163c798cd9b3e Mon Sep 17 00:00:00 2001
From: Marina Ganeva <m.ganeva@fz-juelich.de>
Date: Tue, 13 Oct 2015 17:12:58 +0200
Subject: [PATCH] TOFTOF Vanadium correction algorithm.

---
 .../algorithms/ComputeCalibrationCoefVan.py   | 164 ++++++++++++++++++
 .../plugins/algorithms/mlzutils.py            |  81 +++++++++
 .../python/plugins/algorithms/CMakeLists.txt  |   1 +
 .../ComputeCalibrationCoefVanTest.py          |  90 ++++++++++
 .../ComputeCalibrationCoefVan-v1.rst          |  83 +++++++++
 5 files changed, 419 insertions(+)
 create mode 100644 Framework/PythonInterface/plugins/algorithms/ComputeCalibrationCoefVan.py
 create mode 100644 Framework/PythonInterface/test/python/plugins/algorithms/ComputeCalibrationCoefVanTest.py
 create mode 100644 docs/source/algorithms/ComputeCalibrationCoefVan-v1.rst

diff --git a/Framework/PythonInterface/plugins/algorithms/ComputeCalibrationCoefVan.py b/Framework/PythonInterface/plugins/algorithms/ComputeCalibrationCoefVan.py
new file mode 100644
index 00000000000..e950ee02e2e
--- /dev/null
+++ b/Framework/PythonInterface/plugins/algorithms/ComputeCalibrationCoefVan.py
@@ -0,0 +1,164 @@
+from mantid.api import PythonAlgorithm, AlgorithmFactory, WorkspaceProperty, Progress, InstrumentValidator
+from mantid.kernel import Direction
+import mantid.simpleapi as api
+import numpy as np
+from scipy import integrate
+import scipy as sp
+import mlzutils
+
+
+# Test for keyword Vanadium or vanadium in Title - compulsory entries
+class ComputeCalibrationCoefVan(PythonAlgorithm):
+    """ Calculate coefficients to normalize by Vanadium and correct Debye Waller factor
+    """
+    def __init__(self):
+        """
+        Init
+        """
+        PythonAlgorithm.__init__(self)
+        self.vanaws = None
+        self.defaultT = 293.0       # K, default temperature if not given
+        self.Mvan = 50.942          # [g/mol], Vanadium molar mass
+        self.DebyeT = 389.0         # K, Debye temperature for Vanadium
+
+    def category(self):
+        """ Return category
+        """
+        return "PythonAlgorithms;CorrectionFunctions\\EfficiencyCorrections"
+
+    def name(self):
+        """ Return summary
+        """
+        return "ComputeCalibrationCoefVan"
+
+    def summary(self):
+        return "Calculate coefficients for detector efficiency correction using the Vanadium data."
+
+    def PyInit(self):
+        """ Declare properties
+        """
+        self.declareProperty(WorkspaceProperty("VanadiumWorkspace", "", direction=Direction.Input,
+                                               validator=InstrumentValidator()),
+                             "Input Vanadium workspace")
+        self.declareProperty(WorkspaceProperty("OutputWorkspace", "", direction=Direction.Output),
+                             "Name the workspace that will contain the calibration coefficients")
+        return
+
+    def validateInputs(self):
+        issues = dict()
+        inws = self.getProperty("VanadiumWorkspace").value
+        run = inws.getRun()
+
+        if not run.hasProperty('wavelength'):
+            issues['VanadiumWorkspace'] = "Input workspace must have wavelength sample log."
+        else:
+            try:
+                float(run.getProperty('wavelength').value)
+            except ValueError:
+                issues['VanadiumWorkspace'] = "Invalid value for wavelength sample log. Wavelength must be a number."
+
+        outws = self.getPropertyValue("OutputWorkspace")
+        if not outws:
+            issues["OutputWorkspace"] = "Name of the output workspace must not be empty."
+
+        return issues
+
+    def get_temperature(self):
+        """
+        tries to get temperature from the sample logs
+        in the case of fail, default value is returned
+        """
+        run = self.vanaws.getRun()
+        if not run.hasProperty('temperature'):
+            self.log().warning("Temperature sample log is not present in " + self.vanaws.getName() +
+                               "T=293K is assumed for Debye-Waller factor.")
+            return self.defaultT
+        try:
+            temperature = float(run.getProperty('temperature').value)
+        except ValueError, err:
+            self.log().warning("Error of getting temperature: " + err +
+                               "T=293K is assumed for Debye-Waller factor.")
+            return self.defaultT
+
+        return temperature
+
+    def PyExec(self):
+        """ Main execution body
+        """
+        self.vanaws = self.getProperty("VanadiumWorkspace").value       # returns workspace instance
+        outws_name = self.getPropertyValue("OutputWorkspace")           # returns workspace name (string)
+        nhist = self.vanaws.getNumberHistograms()
+        prog_reporter = Progress(self, start=0.0, end=1.0, nreports=nhist+1)
+
+        # calculate array of Debye-Waller factors
+        dwf = self.calculate_dwf()
+
+        # for each detector: fit gaussian to get peak_centre and fwhm
+        # sum data in the range [peak_centre - 3*fwhm, peak_centre + 3*fwhm]
+        dataX = self.vanaws.readX(0)
+        coefY = np.zeros(nhist)
+        coefE = np.zeros(nhist)
+        for idx in range(nhist):
+            prog_reporter.report("Setting %dth spectrum" % idx)
+            dataY = self.vanaws.readY(idx)
+            if np.max(dataY) != 0:
+                dataE = self.vanaws.readE(idx)
+                peak_centre, sigma = mlzutils.do_fit_gaussian(self.vanaws, idx, self.log(), cleanup_fit=False)
+                fwhm = sigma*2.*np.sqrt(2.*np.log(2.))
+                idxmin = (np.fabs(dataX-peak_centre+3.*fwhm)).argmin()
+                idxmax = (np.fabs(dataX-peak_centre-3.*fwhm)).argmin()
+                self.log().debug("Sigma=%f, centre=%f, fwhm=%f, idxmin=%d, idxmax=%d" % (sigma, peak_centre, fwhm, idxmin, idxmax))
+                coefY[idx] = dwf[idx]*sum(dataY[idxmin:idxmax+1])
+                coefE[idx] = dwf[idx]*sum(dataE[idxmin:idxmax+1])
+            else:
+                coefY[idx] = 0.
+                coefE[idx] = 0.
+
+        # create X array, X data are the same for all detectors, so
+        coefX = np.zeros(nhist)
+        coefX.fill(dataX[0])
+
+        api.CreateWorkspace(OutputWorkspace=outws_name, DataX=coefX, DataY=coefY, DataE=coefE, NSpec=nhist, UnitX="TOF")
+        outws = api.AnalysisDataService.retrieve(outws_name)
+        api.CopyLogs(self.vanaws, outws)
+
+        self.log().information("Workspace with calibration coefficients " + outws.getName() + " has been created.")
+
+        self.setProperty("OutputWorkspace", outws)
+
+    def calculate_dwf(self):
+        """
+        Calculates Debye-Waller factor according to
+        Sears and Shelley Acta Cryst. A 47, 441 (1991)
+        """
+        run = self.vanaws.getRun()
+        nhist = self.vanaws.getNumberHistograms()
+        thetasort = np.zeros(nhist)  # theta in radians !!!NOT 2Theta
+
+        instrument = self.vanaws.getInstrument()
+        detID_offset = 0
+        try:
+            instrument.getDetector(0)
+        except RuntimeError:
+            detID_offset = 1
+
+        for i in range(nhist):
+            det = instrument.getDetector(i + detID_offset)
+            thetasort[i] = 0.5*self.vanaws.detectorSignedTwoTheta(det)
+
+        temperature = self.get_temperature()                    # T in K
+        wlength = float(run.getLogData('wavelength').value)     # Wavelength, Angstrom
+        mass_vana = 0.001*self.Mvan/sp.constants.N_A            # Vanadium mass, kg
+        temp_ratio = temperature/self.DebyeT
+
+        if temp_ratio < 1.e-3:
+            integral = 0.5
+        else:
+            integral = integrate.quad(lambda x: x/sp.tanh(0.5*x/temp_ratio), 0, 1)[0]
+
+        msd = 3.*sp.constants.hbar**2/(2.*mass_vana*sp.constants.k * self.DebyeT)*integral*1.e20
+        return np.exp(-msd*(4.*sp.pi*sp.sin(thetasort)/wlength)**2)
+
+
+# Register algorithm with Mantid.
+AlgorithmFactory.subscribe(ComputeCalibrationCoefVan)
diff --git a/Framework/PythonInterface/plugins/algorithms/mlzutils.py b/Framework/PythonInterface/plugins/algorithms/mlzutils.py
index 3b69302522b..0636fdf4168 100644
--- a/Framework/PythonInterface/plugins/algorithms/mlzutils.py
+++ b/Framework/PythonInterface/plugins/algorithms/mlzutils.py
@@ -1,4 +1,6 @@
+# pylint: disable=assignment-from-none
 import mantid.simpleapi as api
+import numpy as np
 
 
 def cleanup(wslist):
@@ -154,3 +156,82 @@ def compare_mandatory(wslist, plist, logger, tolerance=0.01):
                 "Workspaces: " + ", ".join(wslist) + "\n Values: " + str(properties)
             logger.error(message)
             raise RuntimeError(message)
+
+
+def remove_fit_workspaces(prefix):
+    ws1 = prefix + '_Parameters'
+    ws2 = prefix + '_NormalisedCovarianceMatrix'
+    cleanup([ws1, ws2])
+
+
+def do_fit_gaussian(workspace, index, logger, cleanup_fit=True):
+    """
+    Calculates guess values on peak centre, sigma and peak height.
+    Uses them as an input to run a fit algorithm
+        @ param workspace --- input workspace
+        @ param index --- the spectrum with which WorkspaceIndex to fit
+        @ param cleanup --- if False, the intermediate workspaces created by Fit algorithm will not be removed
+        @ returns peak_centre --- fitted peak centre
+        @ returns sigma --- fitted sigma
+    """
+    nhist = workspace.getNumberHistograms()
+    if index > nhist:
+        message = "Index " + str(index) + " is out of range for the workspace " + workspace.getName()
+        logger.error(message)
+        raise RuntimeError(message)
+
+    x_values = np.array(workspace.readX(index))
+    y_values = np.array(workspace.readY(index))
+
+    # get peak centre position
+    imax = np.argmax(y_values)
+    height = y_values[imax]
+
+    # check for zero or negative signal
+    if height <= 0:
+        logger.warning("Workspace %s, detector %d has maximum <= 0" % (workspace.getName(), index))
+        return [0, 0]
+
+    try_centre = x_values[imax]
+
+    # guess sigma
+    indices = np.argwhere(y_values > 0.5*height)
+    nentries = len(indices)
+    if nentries < 3:
+        message = "Spectrum " + str(index) + " in workspace " + workspace.getName() +\
+            " has too narrow peak. Cannot guess sigma. Check your data."
+        logger.error(message)
+        raise RuntimeError(message)
+    # fwhm = sigma * (2.*np.sqrt(2.*np.log(2.)))
+    fwhm = np.fabs(x_values[indices[nentries - 1, 0]] - x_values[indices[0, 0]])
+    sigma = fwhm/(2.*np.sqrt(2.*np.log(2.)))
+
+    # execute Fit algorithm
+    myfunc = 'name=Gaussian, Height='+str(height)+', PeakCentre='+str(try_centre)+', Sigma='+str(sigma)
+    startX = try_centre - 3.0*fwhm
+    endX = try_centre + 3.0*fwhm
+    prefix = "Fit" + workspace.getName() + str(index)
+    retvals = api.Fit(InputWorkspace=workspace.getName(), WorkspaceIndex=index, StartX=startX, EndX=endX,
+                      Function=myfunc, Output=prefix, OutputParametersOnly=True)
+    if not retvals or len(retvals) < 4:
+        message = "For detector " + str(index) + " in workspace " + workspace.getName() +\
+            " failed to retrieve fit results. Input guess parameters are " + str(myfunc)
+        logger.error(message)
+        if cleanup_fit:
+            remove_fit_workspaces(prefix)
+        raise RuntimeError(message)
+
+    fitStatus = retvals[0]
+    paramTable = retvals[3]
+    if fitStatus != 'success':
+        message = "For detector " + str(index) + " in workspace " + workspace.getName() +\
+            "fit has been terminated with status " + fitStatus + ". Input guess parameters are " + str(myfunc)
+        logger.error(message)
+        if cleanup_fit:
+            remove_fit_workspaces(prefix)
+        raise RuntimeError(message)
+    result = paramTable.column(1)[1:3]
+    if cleanup_fit:
+        remove_fit_workspaces(prefix)
+    # return list: [peak_centre, sigma]
+    return result
diff --git a/Framework/PythonInterface/test/python/plugins/algorithms/CMakeLists.txt b/Framework/PythonInterface/test/python/plugins/algorithms/CMakeLists.txt
index c12c040cddb..7b7edae2fa4 100644
--- a/Framework/PythonInterface/test/python/plugins/algorithms/CMakeLists.txt
+++ b/Framework/PythonInterface/test/python/plugins/algorithms/CMakeLists.txt
@@ -8,6 +8,7 @@ set ( TEST_PY_FILES
   CalculateSampleTransmissionTest.py
   CheckForSampleLogsTest.py
   ConjoinSpectraTest.py
+  ComputeCalibrationCoefVanTest.py
   CorrectLogTimesTest.py
   CreateLeBailFitInputTest.py
   CreateMDTest.py
diff --git a/Framework/PythonInterface/test/python/plugins/algorithms/ComputeCalibrationCoefVanTest.py b/Framework/PythonInterface/test/python/plugins/algorithms/ComputeCalibrationCoefVanTest.py
new file mode 100644
index 00000000000..dcf27845ab1
--- /dev/null
+++ b/Framework/PythonInterface/test/python/plugins/algorithms/ComputeCalibrationCoefVanTest.py
@@ -0,0 +1,90 @@
+import unittest
+from mantid.simpleapi import DeleteWorkspace, CreateSampleWorkspace, AddSampleLog, EditInstrumentGeometry,\
+    CloneWorkspace, CheckWorkspacesMatch
+from testhelpers import run_algorithm
+from mantid.api import AnalysisDataService
+from scipy.constants import N_A, hbar, k
+import numpy as np
+
+
+class ComputeCalibrationCoefVanTest(unittest.TestCase):
+    def setUp(self):
+        # input_ws = Load(Filename="TOFTOFTestdata.nxs")
+        input_ws = CreateSampleWorkspace(Function="User Defined",
+                                         UserDefinedFunction="name=LinearBackground, A0=0.3;name=Gaussian, \
+                                         PeakCentre=5, Height=10, Sigma=0.3", NumBanks=2, BankPixelWidth=1,
+                                         XMin=0, XMax=10, BinWidth=0.1, BankDistanceFromSample=4.0)
+        self._input_ws = input_ws
+        AddSampleLog(self._input_ws, LogName='wavelength', LogText='4.0', LogType='Number', LogUnit='Angstrom')
+
+    def test_output(self):
+        outputWorkspaceName = "output_ws"
+        alg_test = run_algorithm("ComputeCalibrationCoefVan", VanadiumWorkspace=self._input_ws,
+                                 OutputWorkspace=outputWorkspaceName)
+        self.assertTrue(alg_test.isExecuted())
+        wsoutput = AnalysisDataService.retrieve(outputWorkspaceName)
+
+        # Output = Vanadium ws
+        self.assertEqual(wsoutput.getRun().getLogData('run_title').value,
+                         self._input_ws.getRun().getLogData('run_title').value)
+
+        # Structure of output workspace
+        for i in range(wsoutput.getNumberHistograms()):
+            self.assertIsNotNone(wsoutput.readY(i)[0])
+            for j in range(1, wsoutput.blocksize()):
+                self.assertEqual(wsoutput.readY(i)[j], wsoutput.readY(i)[0])
+
+        # Size of output workspace
+        self.assertEqual(wsoutput.getNumberHistograms(), self._input_ws.getNumberHistograms())
+
+        DeleteWorkspace(wsoutput)
+        return
+
+    def test_sum(self):
+        outputWorkspaceName = "output_ws"
+        alg_test = run_algorithm("ComputeCalibrationCoefVan", VanadiumWorkspace=self._input_ws,
+                                 OutputWorkspace=outputWorkspaceName)
+        self.assertTrue(alg_test.isExecuted())
+        wsoutput = AnalysisDataService.retrieve(outputWorkspaceName)
+
+        # check whether sum is calculated correctly, for theta=0, dwf=1
+        y_sum = sum(self._input_ws.readY(0)[27:75])
+        self.assertAlmostEqual(y_sum, wsoutput.readY(0)[0])
+
+        DeleteWorkspace(wsoutput)
+
+    def test_dwf(self):
+        outputWorkspaceName = "output_ws"
+
+        # change theta to make dwf != 1
+        EditInstrumentGeometry(self._input_ws, L2="4,8", Polar="0,15", Azimuthal="0,0", DetectorIDs="1,2")
+        alg_test = run_algorithm("ComputeCalibrationCoefVan", VanadiumWorkspace=self._input_ws,
+                                 OutputWorkspace=outputWorkspaceName)
+        self.assertTrue(alg_test.isExecuted())
+        wsoutput = AnalysisDataService.retrieve(outputWorkspaceName)
+
+        # check dwf calculation
+        y_sum = sum(self._input_ws.readY(1)[27:75])
+        det2 = self._input_ws.getInstrument().getDetector(2)
+        mvan = 0.001*50.942/N_A
+        Bcoef = 4.736767162094296*1e+20*hbar*hbar/(2.0*mvan*k*389.0)
+        dwf = np.exp(-1.0*Bcoef*(4.0*np.pi*np.sin(0.5*self._input_ws.detectorSignedTwoTheta(det2))/4.0)**2)
+        self.assertAlmostEqual(y_sum*dwf, wsoutput.readY(1)[0])
+
+        DeleteWorkspace(wsoutput)
+
+    def test_input_not_modified(self):
+        backup = CloneWorkspace(self._input_ws)
+        outputWorkspaceName = "output_ws"
+        alg_test = run_algorithm("ComputeCalibrationCoefVan", VanadiumWorkspace=self._input_ws,
+                                 OutputWorkspace=outputWorkspaceName)
+        self.assertTrue(alg_test.isExecuted())
+        self.assertEqual("Success!", CheckWorkspacesMatch(backup, self._input_ws))
+        DeleteWorkspace(backup)
+
+    def tearDown(self):
+        if AnalysisDataService.doesExist(self._input_ws.getName()):
+            DeleteWorkspace(self._input_ws)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/docs/source/algorithms/ComputeCalibrationCoefVan-v1.rst b/docs/source/algorithms/ComputeCalibrationCoefVan-v1.rst
new file mode 100644
index 00000000000..aebe3a32a6b
--- /dev/null
+++ b/docs/source/algorithms/ComputeCalibrationCoefVan-v1.rst
@@ -0,0 +1,83 @@
+.. algorithm::
+
+.. summary::
+
+.. alias::
+
+.. properties::
+
+Description
+-----------
+
+Algorithm creates a workspace with  detector sensitivity correction coefficients using the given Vanadium workspace. The correction coefficients are calculated as follows.
+
+1. Calculate the Debye-Waller factor according to Sears and Shelley *Acta Cryst. A* **47**, 441 (1991):
+
+   :math:`D_i = \exp\left(-B_i\cdot\frac{4\pi\sin\theta_i}{\lambda^2}\right)`
+
+   :math:`B_i = \frac{3\hbar^2\cdot 10^{20}}{2m_VkT_m}\cdot J(y)`
+
+   where :math:`J(y) = 0.5` if :math:`y < 10^{-3}`, otherwise
+
+   :math:`J(y) = \int_0^1 x\cdot\mathrm{coth}\left(\frac{x}{2y}\right)\,\mathrm{d}x`
+
+   where :math:`y=T/T_m` is the ratio of the temperature during the experiment :math:`T` to the Debye temperature :math:`T_m = 389K`, :math:`m_V` is the Vanadium atomic mass (in kg) and :math:`\theta_i` is the polar angle of the i-th detector retrieved using the *detectorSignedTwoTheta* function.
+
+.. warning::
+
+    If sample log *temperature* is not present in the given Vanadium workspace or temperature is set to an invalid value, T=293K will be taken for the Debye-Waller factor calculation. Algorithm will produce warning in this case.
+
+2. Perform Gaussian fit of the data to find out the position of the peak centre and FWHM. These values are used to calculate sum :math:`S_i` as
+
+   :math:`S_i = \sum_{x = x_C - 3\,\mathrm{fwhm}}^{x_C + 3\,\mathrm{fwhm}} Y_i(x)`
+
+   where :math:`x_C` is the peak centre position and :math:`Y_i(x)` is the coresponding to :math:`x` :math:`Y` value for i-th detector.
+
+3. Finally, the correction coefficients :math:`K_i` are calculated as
+
+   :math:`K_i = D_i\times S_i`
+
+Workspace containing these correction coefficients is created as an output and can be used as a RHS workspace in :ref:`algm-Divide` to apply correction to the LHS workspace.
+
+.. note::
+    
+    If gaussian fit fails, algorithm terminates with an error message. The error message contains name of the workspace and detector number.
+
+Restrictions on the input workspace
+###################################
+
+The valid input workspace:
+
+- must have an instrument set
+- must have a *wavelength* sample log
+
+
+Usage
+-----
+
+**Example**
+
+.. testcode:: ExComputeCalibrationCoefVan
+
+    # load Vanadium data
+    wsVana = LoadMLZ(Filename='TOFTOFTestdata.nxs')
+    # calculate correction coefficients
+    wsCoefs = ComputeCalibrationCoefVan(wsVana)
+    print 'Spectrum 4 of the output workspace is filled with: ', round(wsCoefs.readY(999)[0])
+
+    # wsCoefs can be used as rhs with Divide algorithm to apply correction to the data 
+    wsCorr = wsVana/wsCoefs
+    print 'Spectrum 4 of the input workspace is filled with: ', round(wsVana.readY(999)[0], 1)
+    print 'Spectrum 4 of the corrected workspace is filled with: ', round(wsCorr.readY(999)[0], 5)
+
+Output:    
+
+.. testoutput:: ExComputeCalibrationCoefVan
+
+    Spectrum 4 of the output workspace is filled with:  6596.0
+    Spectrum 4 of the input workspace is filled with:  1.0
+    Spectrum 4 of the corrected workspace is filled with:  0.00015
+
+.. categories::
+
+.. sourcelink::
-- 
GitLab