Skip to content
Snippets Groups Projects
Code owners
ConvertSpectrumAxis2.cpp 11.80 KiB
#include "MantidAlgorithms/ConvertSpectrumAxis2.h"
#include "MantidAPI/InstrumentValidator.h"
#include "MantidAPI/NumericAxis.h"
#include "MantidAPI/Run.h"
#include "MantidAPI/SpectraAxisValidator.h"
#include "MantidAPI/SpectrumInfo.h"
#include "MantidAPI/WorkspaceFactory.h"
#include "MantidGeometry/Instrument.h"
#include "MantidGeometry/Instrument/DetectorInfo.h"
#include "MantidKernel/BoundedValidator.h"
#include "MantidKernel/CompositeValidator.h"
#include "MantidKernel/ListValidator.h"
#include "MantidKernel/Unit.h"
#include "MantidKernel/UnitConversion.h"
#include "MantidKernel/UnitFactory.h"
#include "MantidTypes/SpectrumDefinition.h"

#include <cfloat>

constexpr double rad2deg = 180.0 / M_PI;

namespace Mantid {
namespace Algorithms {
// Register the algorithm into the AlgorithmFactory
DECLARE_ALGORITHM(ConvertSpectrumAxis2)
using namespace Kernel;
using namespace API;
using namespace Geometry;

void ConvertSpectrumAxis2::init() {
  // Validator for Input Workspace
  auto wsVal = boost::make_shared<CompositeValidator>();
  wsVal->add<SpectraAxisValidator>();
  wsVal->add<InstrumentValidator>();

  declareProperty(make_unique<WorkspaceProperty<>>("InputWorkspace", "",
                                                   Direction::Input, wsVal),
                  "The name of the input workspace.");
  declareProperty(make_unique<WorkspaceProperty<>>("OutputWorkspace", "",
                                                   Direction::Output),
                  "The name to use for the output workspace.");
  std::vector<std::string> targetOptions(7);
  targetOptions[0] = "Theta";
  targetOptions[1] = "SignedTheta";
  targetOptions[2] = "ElasticQ";
  targetOptions[3] = "ElasticQSquared";
  targetOptions[4] = "theta";
  targetOptions[5] = "signed_theta";
  targetOptions[6] = "ElasticDSpacing";

  declareProperty(
      "Target", "", boost::make_shared<StringListValidator>(targetOptions),
      "The unit to which spectrum axis is converted to - \"theta\" (for the "
      "angle in degrees), Q or Q^2, where elastic Q is evaluated at EFixed. "
      "Note that 'theta' and 'signed_theta' are there for compatibility "
      "purposes; they are the same as 'Theta' and 'SignedTheta' respectively");
  std::vector<std::string> eModeOptions;
  eModeOptions.emplace_back("Direct");
  eModeOptions.emplace_back("Indirect");
  declareProperty("EMode", "Direct",
                  boost::make_shared<StringListValidator>(eModeOptions),
                  "Some unit conversions require this value to be set "
                  "(\"Direct\" or \"Indirect\")");
  auto mustBePositive = boost::make_shared<BoundedValidator<double>>();
  mustBePositive->setLower(0.0);
  declareProperty("EFixed", EMPTY_DBL(), mustBePositive,
                  "Value of fixed energy in meV : EI (EMode=Direct) or EF "
                  "(EMode=Indirect))");

  declareProperty("OrderAxis", true,
                  "Whether or not to sort the resulting"
                  " spectrum axis.");
}

void ConvertSpectrumAxis2::exec() {
  // Get the input workspace.
  API::MatrixWorkspace_sptr inputWS = getProperty("InputWorkspace");
  // Assign value to the member variable storing the number of histograms.
  const size_t nHist = inputWS->getNumberHistograms();

  // The unit to convert to.
  const std::string unitTarget = getProperty("Target");

  // Whether needs to be ordered
  m_toOrder = getProperty("OrderAxis");

  size_t nProgress = nHist;
  if (m_toOrder) {
    // we will need to loop twice, once to build the indexMap,
    // once to copy over the spectra and set the output
    nProgress *= 2;
  } else {
    m_axis.reserve(nHist);
  }

  Progress progress(this, 0.0, 1.0, nProgress);

  // Call the functions to convert to the different forms of theta or Q.
  if (unitTarget == "theta" || unitTarget == "Theta" ||
      unitTarget == "signed_theta" || unitTarget == "SignedTheta") {
    createThetaMap(progress, unitTarget, inputWS);
  } else if (unitTarget == "ElasticQ" || unitTarget == "ElasticQSquared" ||
             unitTarget == "ElasticDSpacing") {
    createElasticQMap(progress, unitTarget, inputWS);
  }

  // Create an output workspace and set the property for it.
  MatrixWorkspace_sptr outputWS =
      createOutputWorkspace(progress, unitTarget, inputWS);
  setProperty("OutputWorkspace", outputWS);
}

/** Converts X axis to theta representation
 * @param progress :: Progress indicator
 * @param targetUnit :: Target conversion unit
 * @param inputWS :: Input Workspace
 */
void ConvertSpectrumAxis2::createThetaMap(API::Progress &progress,
                                          const std::string &targetUnit,
                                          API::MatrixWorkspace_sptr &inputWS) {
  // Not sure about default, previously there was a call to a null function?
  bool signedTheta = false;
  if (targetUnit == "signed_theta" || targetUnit == "SignedTheta") {
    signedTheta = true;
  } else if (targetUnit == "theta" || targetUnit == "Theta") {
    signedTheta = false;
  }

  bool warningGiven = false;

  const auto &spectrumInfo = inputWS->spectrumInfo();
  for (size_t i = 0; i < spectrumInfo.size(); ++i) {
    if (!spectrumInfo.hasDetectors(i)) {
      if (!warningGiven)
        g_log.warning("The instrument definition is incomplete - spectra "
                      "dropped from output");
      warningGiven = true;
      continue;
    }
    if (!spectrumInfo.isMonitor(i)) {
      if (signedTheta)
        emplaceIndexMap(spectrumInfo.signedTwoTheta(i) * rad2deg, i);
      else
        emplaceIndexMap(spectrumInfo.twoTheta(i) * rad2deg, i);
    } else {
      emplaceIndexMap(0.0, i);
    }

    progress.report("Converting to theta...");
  }
}

/** Convert X axis to Elastic Q representation
 * @param progress :: Progress indicator
 * @param targetUnit :: Target conversion unit
 * @param inputWS :: Input workspace
 */
void ConvertSpectrumAxis2::createElasticQMap(
    API::Progress &progress, const std::string &targetUnit,
    API::MatrixWorkspace_sptr &inputWS) {

  const std::string emodeStr = getProperty("EMode");
  int emode = 0;
  if (emodeStr == "Direct")
    emode = 1;
  else if (emodeStr == "Indirect")
    emode = 2;

  const auto &spectrumInfo = inputWS->spectrumInfo();
  const auto &detectorInfo = inputWS->detectorInfo();
  const size_t nHist = spectrumInfo.size();
  for (size_t i = 0; i < nHist; i++) {
    double theta(0.0), efixed(0.0);
    if (!spectrumInfo.isMonitor(i)) {
      theta = 0.5 * spectrumInfo.twoTheta(i);
      /*
       * Two assumptions made in the following code.
       * 1. Getting the detector index of the first detector in the spectrum
       * definition is enough (this should be completely safe).
       * 2. That the time index is not important (first element of pair only
       * accessed). i.e we are not performing scanning. Step scanning is not
       * supported at the time of writing.
       */
      const auto detectorIndex = spectrumInfo.spectrumDefinition(i)[0].first;
      efixed = getEfixed(detectorIndex, detectorInfo, *inputWS,
                         emode); // get efixed
    } else {
      theta = DBL_MIN;
      efixed = DBL_MIN;
    }

    // Convert to MomentumTransfer
    double elasticQInAngstroms =
        Kernel::UnitConversion::convertToElasticQ(theta, efixed);

    if (targetUnit == "ElasticQ") {
      emplaceIndexMap(elasticQInAngstroms, i);
    } else if (targetUnit == "ElasticQSquared") {
      // The QSquared value.
      double elasticQSquaredInAngstroms =
          elasticQInAngstroms * elasticQInAngstroms;

      emplaceIndexMap(elasticQSquaredInAngstroms, i);
    } else if (targetUnit == "ElasticDSpacing") {
      double elasticDSpacing = 2 * M_PI / elasticQInAngstroms;
      emplaceIndexMap(elasticDSpacing, i);
    }

    progress.report("Converting to " + targetUnit);
  }
}

/** Create the final output workspace after converting the X axis
 * @returns the final output workspace
 *
 * @param progress :: Progress indicator
 * @param targetUnit :: Target conversion unit
 * @param inputWS :: Input workspace
 */
MatrixWorkspace_sptr ConvertSpectrumAxis2::createOutputWorkspace(
    API::Progress &progress, const std::string &targetUnit,
    API::MatrixWorkspace_sptr &inputWS) {

  MatrixWorkspace_sptr outputWorkspace = nullptr;
  NumericAxis *newAxis = nullptr;
  if (m_toOrder) {
    // Can not re-use the input one because the spectra are re-ordered.
    outputWorkspace = WorkspaceFactory::Instance().create(
        inputWS, m_indexMap.size(), inputWS->x(0).size(), inputWS->y(0).size());
    std::vector<double> axis;
    axis.reserve(m_indexMap.size());
    for (const auto &it : m_indexMap) {
      axis.emplace_back(it.first);
    }
    newAxis = new NumericAxis(std::move(axis));
  } else {
    // If there is no reordering we can simply clone.
    outputWorkspace = inputWS->clone();
    newAxis = new NumericAxis(m_axis);
  }
  outputWorkspace->replaceAxis(1, newAxis);

  // Set the units of the axis.
  if (targetUnit == "theta" || targetUnit == "Theta" ||
      targetUnit == "signed_theta" || targetUnit == "SignedTheta") {
    newAxis->unit() = boost::make_shared<Units::Degrees>();
  } else if (targetUnit == "ElasticQ") {
    newAxis->unit() = UnitFactory::Instance().create("MomentumTransfer");
  } else if (targetUnit == "ElasticQSquared") {
    newAxis->unit() = UnitFactory::Instance().create("QSquared");
  } else if (targetUnit == "ElasticDSpacing") {
    newAxis->unit() = UnitFactory::Instance().create("dSpacing");
  }

  // Note that this is needed only for ordered case
  if (m_toOrder) {
    size_t currentIndex = 0;
    std::multimap<double, size_t>::const_iterator it;
    for (it = m_indexMap.begin(); it != m_indexMap.end(); ++it) {
      // Copy over the data.
      outputWorkspace->setHistogram(currentIndex,
                                    inputWS->histogram(it->second));
      // We can keep the spectrum numbers etc.
      outputWorkspace->getSpectrum(currentIndex)
          .copyInfoFrom(inputWS->getSpectrum(it->second));
      ++currentIndex;
      progress.report("Setting output spectrum #" +
                      std::to_string(currentIndex));
    }
  }

  return outputWorkspace;
}

double ConvertSpectrumAxis2::getEfixed(
    const size_t detectorIndex, const Geometry::DetectorInfo &detectorInfo,
    const Mantid::API::MatrixWorkspace &inputWS, const int emode) const {
  double efixed(0);
  double efixedProp = getProperty("Efixed");
  Mantid::detid_t detectorID = detectorInfo.detectorIDs()[detectorIndex];
  if (efixedProp != EMPTY_DBL()) {
    efixed = efixedProp;
    g_log.debug() << "Detector: " << detectorID << " Efixed: " << efixed
                  << "\n";
  } else {
    if (emode == 1) {
      if (inputWS.run().hasProperty("Ei")) {
        efixed = inputWS.run().getLogAsSingleValue("Ei");
      } else {
        throw std::invalid_argument("Could not retrieve Efixed from the "
                                    "workspace. Please provide a value.");
      }
    } else if (emode == 2) {

      const auto &detectorSingle = detectorInfo.detector(detectorIndex);

      std::vector<double> efixedVec =
          detectorSingle.getNumberParameter("Efixed");

      if (!efixedVec.empty()) {
        efixed = efixedVec.at(0);
        g_log.debug() << "Detector: " << detectorID << " EFixed: " << efixed
                      << "\n";
      } else {
        g_log.warning() << "Efixed could not be found for detector "
                        << detectorID << ", please provide a value\n";
        throw std::invalid_argument("Could not retrieve Efixed from the "
                                    "detector. Please provide a value.");
      }
    }
  }
  return efixed;
}

/** Emplaces inside the ordered or unordered index registry
 * @param value :: value to insert
 * @param wsIndex :: workspace index
 */
void ConvertSpectrumAxis2::emplaceIndexMap(double value, size_t wsIndex) {
  if (m_toOrder) {
    m_indexMap.emplace(value, wsIndex);
  } else {
    m_axis.emplace_back(value);
  }
}

} // namespace Algorithms
} // namespace Mantid