Skip to content
Snippets Groups Projects
ExperimentInfo.cpp 60 KiB
Newer Older
// Mantid Repository : https://github.com/mantidproject/mantid
//
// Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
//     NScD Oak Ridge National Laboratory, European Spallation Source
//     & Institut Laue - Langevin
// SPDX - License - Identifier: GPL - 3.0 +
#include "MantidAPI/ExperimentInfo.h"
#include "MantidAPI/InstrumentDataService.h"
#include "MantidAPI/ResizeRectangularDetectorHelper.h"
#include "MantidAPI/Run.h"
#include "MantidAPI/Sample.h"
#include "MantidAPI/SpectrumInfo.h"
#include "MantidGeometry/Crystal/OrientedLattice.h"
Owen Arnold's avatar
Owen Arnold committed
#include "MantidGeometry/ICompAssembly.h"
#include "MantidGeometry/IComponent.h"
Owen Arnold's avatar
Owen Arnold committed
#include "MantidGeometry/IDetector.h"
#include "MantidGeometry/Instrument/ComponentInfo.h"
#include "MantidGeometry/Instrument/Detector.h"
#include "MantidGeometry/Instrument/DetectorInfo.h"
#include "MantidGeometry/Instrument/InstrumentDefinitionParser.h"
#include "MantidGeometry/Instrument/ParComponentFactory.h"
#include "MantidGeometry/Instrument/ParameterFactory.h"
#include "MantidGeometry/Instrument/ParameterMap.h"
#include "MantidGeometry/Instrument/RectangularDetector.h"
#include "MantidGeometry/Instrument/XMLInstrumentParameter.h"
Owen Arnold's avatar
Owen Arnold committed
#include "MantidBeamline/ComponentInfo.h"
#include "MantidBeamline/DetectorInfo.h"
#include "MantidBeamline/SpectrumInfo.h"
#include "MantidKernel/ConfigService.h"
#include "MantidKernel/DateAndTime.h"
#include "MantidKernel/EigenConversionHelpers.h"
#include "MantidKernel/IPropertyManager.h"
#include "MantidKernel/InstrumentInfo.h"
#include "MantidKernel/StringTokenizer.h"
#include "MantidKernel/Strings.h"
#include "MantidTypes/SpectrumDefinition.h"

#include <boost/algorithm/string.hpp>
#include <boost/make_shared.hpp>
#include <boost/regex.hpp>
#include <Poco/DirectoryIterator.h>
#include <Poco/Path.h>
#include <Poco/SAX/Attributes.h>
#include <Poco/SAX/ContentHandler.h>
#include <Poco/SAX/SAXParser.h>
#include <nexus/NeXusException.hpp>
#include <tuple>
using namespace Mantid::Geometry;
using namespace Mantid::Types::Core;
namespace Mantid {
namespace API {
namespace {
/// static logger object
Kernel::Logger g_log("ExperimentInfo");

// used to terminate SAX process
class DummyException {
public:
  std::string m_validFrom;
  std::string m_validTo;
  DummyException(const std::string &validFrom, const std::string &validTo)
      : m_validFrom(validFrom), m_validTo(validTo) {}
};
// SAX content handler for grapping stuff quickly from IDF
class myContentHandler : public Poco::XML::ContentHandler {
  void startElement(const XMLString & /*uri*/, const XMLString &localName,
                    const XMLString & /*qname*/,
                    const Attributes &attrList) override {
    if (localName == "instrument" || localName == "parameter-file") {
      throw DummyException(
          static_cast<std::string>(attrList.getValue("", "valid-from")),
          static_cast<std::string>(attrList.getValue("", "valid-to")));
    }
  }
  void endElement(const XMLString & /*uri*/, const XMLString & /*localName*/,
                  const XMLString & /*qname*/) override {}
  void startDocument() override {}
  void endDocument() override {}
  void characters(const XMLChar /*ch*/[], int /*start*/,
                  int /*length*/) override {}
  void endPrefixMapping(const XMLString & /*prefix*/) override {}
  void ignorableWhitespace(const XMLChar /*ch*/[], int /*start*/,
                           int /*length*/) override {}
  void processingInstruction(const XMLString & /*target*/,
                             const XMLString & /*data*/) override {}
  void setDocumentLocator(const Locator * /*loc*/) override {}
  void skippedEntity(const XMLString & /*name*/) override {}
  void startPrefixMapping(const XMLString & /*prefix*/,
                          const XMLString & /*uri*/) override {}
} // namespace

/** Constructor
 */
ExperimentInfo::ExperimentInfo()
    : m_parmap(new ParameterMap()), sptr_instrument(new Instrument()) {
  m_parmap->setInstrument(sptr_instrument.get());

/**
 * Constructs the object from a copy if the input. This leaves the new mutex
 * unlocked.
 * @param source The source object from which to initialize
 */
ExperimentInfo::ExperimentInfo(const ExperimentInfo &source) {
  this->copyExperimentInfoFrom(&source);
  setSpectrumDefinitions(source.spectrumInfo().sharedSpectrumDefinitions());
// Defined as default in source for forward declaration with std::unique_ptr.
ExperimentInfo::~ExperimentInfo() = default;

/** Copy the experiment info data from another ExperimentInfo instance,
 * e.g. a MatrixWorkspace.
 * @param other :: the source from which to copy ExperimentInfo
 */
void ExperimentInfo::copyExperimentInfoFrom(const ExperimentInfo *other) {
  m_sample = other->m_sample;
  m_run = other->m_run;
  this->setInstrument(other->getInstrument());
  // We do not copy Beamline::SpectrumInfo (which contains detector grouping
  // information) for now:
  // - For MatrixWorkspace, grouping information is still stored in ISpectrum
  //   and should not be overridden (copy is done in ExperimentInfo ctor, but
  //   not here since we just copy the experiment data).
  // - For cached groupings (for MDWorkspaces), grouping was not copied in the
  //   old implementation either.
}

/** Clone this ExperimentInfo class into a new one
 */
ExperimentInfo *ExperimentInfo::cloneExperimentInfo() const {
}

/// @returns A human-readable description of the object
const std::string ExperimentInfo::toString() const {
  try {
    populateIfNotLoaded();
  } catch (std::exception &) {
    // Catch any errors so that the string returned has as much information
    // as possible
  }

  std::ostringstream out;

  Geometry::Instrument_const_sptr inst = this->getInstrument();
  const auto instName = inst->getName();
  out << "Instrument: ";
  if (!instName.empty()) {
    out << instName << " ("
        << inst->getValidFromDate().toFormattedString("%Y-%b-%d") << " to "
        << inst->getValidToDate().toFormattedString("%Y-%b-%d") << ")";
    const auto instFilename = inst->getFilename();
    if (!instFilename.empty()) {
      out << "Instrument from: " << instFilename;
      out << "\n";
    }
  } else {
    out << "None";
  // parameter files loaded
  auto paramFileVector =
      this->constInstrumentParameters().getParameterFilenames();
  for (auto &itFilename : paramFileVector) {
    out << "Parameters from: " << itFilename;
  std::string runStart = getAvailableWorkspaceStartDate();
  std::string runEnd = getAvailableWorkspaceEndDate();
  std::string msgNA = "not available";
  if (runStart.empty())
    runStart = msgNA;
  if (runEnd.empty())
    runEnd = msgNA;
  out << "Run start: " << runStart << "\n";
  out << "Run end:  " << runEnd
      << "\n"; // note extra space for pseudo/approx-alignment

  if (this->sample().hasOrientedLattice()) {
    const Geometry::OrientedLattice &latt = this->sample().getOrientedLattice();
    out << "Sample: a " << std::fixed << std::setprecision(1) << latt.a()
        << ", b " << latt.b() << ", c " << latt.c();
    out << "; alpha " << std::fixed << std::setprecision(0) << latt.alpha()
        << ", beta " << latt.beta() << ", gamma " << latt.gamma();
    out << "\n";
// Helpers for setInstrument and getInstrument
namespace {
void checkDetectorInfoSize(const Instrument &instr,
                           const Geometry::DetectorInfo &detInfo) {
  const auto numDets = instr.getNumberDetectors();
  if (numDets != detInfo.size())
    throw std::runtime_error("ExperimentInfo: size mismatch between "
                             "DetectorInfo and number of detectors in "
                             "instrument");
}
} // namespace
/** Set the instrument
 * @param instr :: Shared pointer to an instrument.
 */
void ExperimentInfo::setInstrument(const Instrument_const_sptr &instr) {
  m_spectrumInfoWrapper = nullptr;

  // Detector IDs that were previously dropped because they were not part of the
  // instrument may now suddenly be valid, so we have to reinitialize the
  // detector grouping. Also the index corresponding to specific IDs may have
  // changed.
  if (sptr_instrument !=
      (instr->isParametrized() ? instr->baseInstrument() : instr))
    invalidateAllSpectrumDefinitions();
  if (instr->isParametrized()) {
    sptr_instrument = instr->baseInstrument();
    // We take a *copy* of the ParameterMap since we are modifying it by setting
    // a pointer to our DetectorInfo, and in case it contains legacy parameters
    // such as positions or rotations.
    m_parmap = boost::make_shared<ParameterMap>(*instr->getParameterMap());
  } else {
    sptr_instrument = instr;
    m_parmap = boost::make_shared<ParameterMap>();
  m_parmap->setInstrument(sptr_instrument.get());
}

/** Get a shared pointer to the parametrized instrument associated with this
 *workspace
 *
 *  @return The instrument class
 */
Instrument_const_sptr ExperimentInfo::getInstrument() const {
  populateIfNotLoaded();
  checkDetectorInfoSize(*sptr_instrument, detectorInfo());
  return Geometry::ParComponentFactory::createInstrument(sptr_instrument,
                                                         m_parmap);
}

/**  Returns a new copy of the instrument parameters
 *    @return a (new) copy of the instruments parameter map
 */
Geometry::ParameterMap &ExperimentInfo::instrumentParameters() {
  return *m_parmap;
}

/**  Returns a const reference to the instrument parameters.
 *    @return a const reference to the instrument ParameterMap.
 */
const Geometry::ParameterMap &ExperimentInfo::instrumentParameters() const {
  return *m_parmap;
}

/**  Returns a const reference to the instrument parameters.
 *    @return a const reference to the instrument ParameterMap.
 */
const Geometry::ParameterMap &
ExperimentInfo::constInstrumentParameters() const {
  return *m_parmap;
}

namespace {
///@cond

/// Used for storing info about "r-position", "t-position" and "p-position"
/// parameters
/// These are translated to X,Y,Z and so must all be processed together
struct RTP {
  RTP() : radius(0.0), haveRadius(false), theta(0.0), phi(0.0) {}
  double radius;
  bool haveRadius;
  double theta;
  double phi;
};

struct ParameterValue {
  ParameterValue(const Geometry::XMLInstrumentParameter &paramInfo,
                 const API::Run &run)
      : info(paramInfo), runData(run) {}

  operator double() {
    if (info.m_logfileID.empty())
      return boost::lexical_cast<double>(info.m_value);
    else
      return info.createParamValue(
          runData.getTimeSeriesProperty<double>(info.m_logfileID));
  operator int() { return boost::lexical_cast<int>(info.m_value); }
  operator bool() {
    if (boost::iequals(info.m_value, "true"))
      return true;
    else if (boost::iequals(info.m_value, "yes"))
      return true;
    else
      return false;
  const Geometry::XMLInstrumentParameter &info;
  const Run &runData;
};
///@endcond
} // namespace
namespace {
bool isPositionParameter(const std::string &name) {
  return ParameterMap::pos() == name;
}

bool isRotationParameter(const std::string &name) {
  return ParameterMap::rot() == name;
}

bool isScaleParameter(const std::string &name) {
  return (name == "scalex" || name == "scaley");
}

bool isRedundantPosOrRot(const std::string &name) {
  // Check size first as a small optimization.
  return (name.size() == 4) &&
         (name == "posx" || name == "posy" || name == "posz" ||
          name == "rotx" || name == "roty" || name == "rotz");
}

template <class T>
T getParam(const std::string &paramType, const std::string &paramValue) {
  const std::string name = "dummy";
  auto param = ParameterFactory::create(paramType, name);
  param->fromString(paramValue);
  return param->value<T>();
void updatePosition(ComponentInfo &componentInfo, const IComponent *component,
                    const V3D &newRelPos) {
  const auto compIndex = componentInfo.indexOf(component->getComponentID());
  V3D position = newRelPos;
  if (componentInfo.hasParent(compIndex)) {
    const auto parentIndex = componentInfo.parent(compIndex);
    componentInfo.rotation(parentIndex).rotate(position);
    position += componentInfo.position(parentIndex);
  }
  componentInfo.setPosition(compIndex, position);
}

void updateRotation(ComponentInfo &componentInfo, const IComponent *component,
                    const Quat &newRelRot) {
  const auto compIndex = componentInfo.indexOf(component->getComponentID());

  auto rotation = newRelRot;
  if (componentInfo.hasParent(compIndex)) {
    const auto parentIndex = componentInfo.parent(compIndex);
    rotation = componentInfo.rotation(parentIndex) * newRelRot;
  }
  componentInfo.setRotation(compIndex, rotation);
void adjustPositionsFromScaleFactor(ComponentInfo &componentInfo,
                                    const IComponent *component,
                                    const std::string &paramName,
                                    double factor) {
  double ScaleX = 1.0;
  double ScaleY = 1.0;
  if (paramName == "scalex")
    ScaleX = factor;
  else
    ScaleY = factor;
  applyRectangularDetectorScaleToComponentInfo(
      componentInfo, component->getComponentID(), ScaleX, ScaleY);
} // namespace
/** Add parameters to the instrument parameter map that are defined in
 * instrument
 *   definition file or parameter file, which may contain parameters that
 * require logfile data to be available. Logs must be loaded before running this
 * method.
 */
void ExperimentInfo::populateInstrumentParameters() {

  // Reference to the run
  const auto &runData = run();

  // Get pointer to parameter map that we may add parameters to and information
  // about
  // the parameters that my be specified in the instrument definition file (IDF)
  Geometry::ParameterMap &paramMap = instrumentParameters();
  Geometry::ParameterMap paramMapForPosAndRot;

  // Get instrument and sample
  auto &componentInfo = mutableComponentInfo();
  const auto parInstrument = getInstrument();
  const auto instrument = parInstrument->baseInstrument();
  const auto &paramInfoFromIDF = instrument->getLogfileCache();

  std::map<const IComponent *, RTP> rtpParams;

  // In this loop position and rotation parameters are inserted into the
  // temporary map paramMapForPosAndRot. In the subsequent loop, after all
  // parameters have been parsed, we update positions and rotations in
  // DetectorInfo and the temporary map goes out of scope. The main reason for
  // this is that ParameterMap will then take care of assembling parameters for
  // individual position or rotation components into a vector or quaternion. In
  // particular, we cannot directly change DetectorInfo since the order of
  // rotation components is not guaranteed.
  for (const auto &item : paramInfoFromIDF) {
    const auto &nameComp = item.first;
    const auto &paramInfo = item.second;
    const std::string &paramN = nameComp.first;
    try {
      // Special case where user has specified r-position,t-position, and/or
      // p-position.
      // We need to know all three first to calculate a set of X,Y,Z
      if (paramN.compare(1, 9, "-position") == 0) {
        auto &rtpValues = rtpParams[paramInfo->m_component]; // If not found,
                                                             // constructs
                                                             // default
        double value = ParameterValue(*paramInfo, runData);
        if (paramN.compare(0, 1, "r") == 0) {
          rtpValues.radius = value;
          rtpValues.haveRadius = true;
        } else if (paramN.compare(0, 1, "t") == 0)
        else if (paramN.compare(0, 1, "p") == 0)
        if (rtpValues.haveRadius) {
          V3D pos;
          pos.spherical(rtpValues.radius, rtpValues.theta, rtpValues.phi);
          paramMapForPosAndRot.addV3D(paramInfo->m_component,
                                      ParameterMap::pos(), pos);
        populateWithParameter(paramMap, paramMapForPosAndRot, paramN,
                              *paramInfo, runData);
    } catch (std::exception &exc) {
      g_log.information() << "Unable to add component parameter '"
                          << nameComp.first << "'. Error: " << exc.what();
      continue;
  for (const auto &item : paramMapForPosAndRot) {
    if (isPositionParameter(item.second->name())) {
      const auto newRelPos = item.second->value<V3D>();
      updatePosition(componentInfo, item.first, newRelPos);
    } else if (isRotationParameter(item.second->name())) {
      const auto newRelRot = item.second->value<Quat>();
      updateRotation(componentInfo, item.first, newRelRot);
    }
    // Parameters for individual components (x,y,z) are ignored. ParameterMap
    // did compute the correct compound positions and rotations internally.
  }
  // Special case RectangularDetector: Parameters scalex and scaley affect pixel
  // positions.
  for (const auto &item : paramMap) {
    if (isScaleParameter(item.second->name()))
      adjustPositionsFromScaleFactor(componentInfo, item.first,
                                     item.second->name(),
                                     item.second->value<double>());
  }
  // paramMapForPosAndRot goes out of scope, dropping all position and rotation
  // parameters of detectors (parameters for non-detector components have been
  // inserted into paramMap via DetectorInfo::setPosition(IComponent *)).
/** Sets the number of detector groups.
 *
 * This method should not need to be called explicitly. The number of detector
 * groups will be set either when initializing a MatrixWorkspace, or by calling
 * `cacheDetectorGroupings` for an ExperimentInfo stored in an MDWorkspace. */
void ExperimentInfo::setNumberOfDetectorGroups(const size_t count) const {
  if (m_spectrumInfo)
    m_spectrumDefinitionNeedsUpdate.clear();
  m_spectrumDefinitionNeedsUpdate.resize(count, 1);
  m_spectrumInfo = std::make_unique<Beamline::SpectrumInfo>(count);
/** Returns the number of detector groups.
 *
 * For MatrixWorkspace this is equal to getNumberHistograms() (after
 *initialization). */
size_t ExperimentInfo::numberOfDetectorGroups() const {
  return m_spectrumDefinitionNeedsUpdate.size();
}

/** Sets the detector grouping for the spectrum with the given `index`.
 *
 * This method should not need to be called explicitly. Groupings are updated
 * automatically when modifying detector IDs in a workspace (via ISpectrum). */
void ExperimentInfo::setDetectorGrouping(
    const size_t index, const std::set<detid_t> &detIDs) const {
  for (const auto detID : detIDs) {
    try {
      const size_t detIndex = detectorInfo().indexOf(detID);
      specDef.add(detIndex);
    } catch (std::out_of_range &) {
      // Silently strip bad detector IDs
  m_spectrumInfo->setSpectrumDefinition(index, std::move(specDef));
  m_spectrumDefinitionNeedsUpdate.at(index) = 0;
/** Update detector grouping for spectrum with given index.
 * This method is called when the detector grouping stored in SpectrumDefinition
 * at `index` in Beamline::SpectrumInfo is not initialized or outdated. The
 * implementation throws, since no grouping information for update is available
 * when grouping comes from a call to `cacheDetectorGroupings`. This method is
 * overridden in MatrixWorkspace. */
void ExperimentInfo::updateCachedDetectorGrouping(
    const size_t /*unused*/) const {
  throw std::runtime_error("ExperimentInfo::updateCachedDetectorGrouping: "
                           "Cannot update -- grouping information not "
                           "available");
}

/** Get a constant reference to the Sample associated with this workspace.
 * @return const reference to Sample object
 */
const Sample &ExperimentInfo::sample() const {
  return *m_sample;
}

/** Get a reference to the Sample associated with this workspace.
 *  This non-const method will copy the sample if it is shared between
 *  more than one workspace, and the reference returned will be to the copy.
 * @return reference to sample object
 */
Sample &ExperimentInfo::mutableSample() {
  return m_sample.access();
}

/** Get a constant reference to the Run object associated with this workspace.
 * @return const reference to run object
 */
const Run &ExperimentInfo::run() const {
  return *m_run;
}

/** Get a reference to the Run object associated with this workspace.
 *  This non-const method will copy the Run object if it is shared between
 *  more than one workspace, and the reference returned will be to the copy.
 * @return reference to Run object
 */
Run &ExperimentInfo::mutableRun() {
  return m_run.access();
}

/// Set the run object. Use in particular to clear run without copying old run.
void ExperimentInfo::setSharedRun(Kernel::cow_ptr<Run> run) {
  m_run = std::move(run);
/// Return the cow ptr of the run
Kernel::cow_ptr<Run> ExperimentInfo::sharedRun() { return m_run; }

/**
 * Get an experimental log either by log name or by type, e.g.
 *   - temperature_log
 *   - chopper_speed_log
 * The logs are first checked for one matching the given string and if that
 * fails then the instrument is checked for a parameter of the same name
 * and if this exists then its value is assume to be the actual log required
 * @param log :: A string giving either a specific log name or instrument
 * parameter whose
 * value is to be retrieved
 * @return A pointer to the property
 */
Kernel::Property *ExperimentInfo::getLog(const std::string &log) const {
  try {
    return run().getProperty(log);
  } catch (Kernel::Exception::NotFoundError &) {
    // No log with that name
  // If the instrument has a parameter with that name then take the value as a
  // log name
  const std::string logName =
      constInstrumentParameters().getString(sptr_instrument.get(), log);
  if (logName.empty()) {
    throw std::invalid_argument(
        "ExperimentInfo::getLog - No instrument parameter named \"" + log +
        "\". Cannot access full log name");
  return run().getProperty(logName);
}

/**
 * Get an experimental log as a single value either by log name or by type. @see
 * getLog
 * @param log :: A string giving either a specific log name or instrument
 * parameter whose
 * value is to be retrieved
 * @return A pointer to the property
 */
double ExperimentInfo::getLogAsSingleValue(const std::string &log) const {
  try {
    return run().getPropertyAsSingleValue(log);
  } catch (Kernel::Exception::NotFoundError &) {
    // No log with that name
  // If the instrument has a parameter with that name then take the value as a
  // log name
  const std::string logName =
      constInstrumentParameters().getString(sptr_instrument.get(), log);
  if (logName.empty()) {
    throw std::invalid_argument(
        "ExperimentInfo::getLog - No instrument parameter named \"" + log +
        "\". Cannot access full log name");
  return run().getPropertyAsSingleValue(logName);
}

/** Utility method to get the run number
 *
 * @return the run number (int) or 0 if not found.
 */
int ExperimentInfo::getRunNumber() const {
  const Run &thisRun = run();
  if (!thisRun.hasProperty("run_number")) {
    // No run_number property, default to 0
  } else {
    Property *prop = m_run->getProperty("run_number");
    if (prop) {
      // Use the string representation. That way both a string and a number
      // property will work.
      int val;
      if (Strings::convert(prop->value(), val))
        return val;
      else
        return 0;
  return 0;
}

/**
 * Returns the emode for this run. It first searchs the run logs for a
 * "deltaE-mode" log and falls back to
 * the instrument if one is not found. If neither exist then the run is
 * considered Elastic.
 * @return The emode enum for the energy transfer mode of this run. Currently
 * checks the sample log & instrument in this order
 */
Kernel::DeltaEMode::Type ExperimentInfo::getEMode() const {
  static const char *emodeTag = "deltaE-mode";
  std::string emodeStr;
  if (run().hasProperty(emodeTag)) {
    emodeStr = run().getPropertyValueAsType<std::string>(emodeTag);
  } else if (sptr_instrument && constInstrumentParameters().contains(
                                    sptr_instrument.get(), emodeTag)) {
    Geometry::Parameter_sptr param =
        constInstrumentParameters().get(sptr_instrument.get(), emodeTag);
    emodeStr = param->asString();
  } else {
    return Kernel::DeltaEMode::Elastic;
  return Kernel::DeltaEMode::fromString(emodeStr);
}

/**
 * Easy access to the efixed value for this run & detector ID
 * @param detID :: The detector ID to ask for the efixed mode (ignored in Direct
 * & Elastic mode). The
 * detector with ID matching that given is pulled from the instrument with this
 * method and it will
 * throw a Exception::NotFoundError if the ID is unknown.
 * @return The current EFixed value
 */
double ExperimentInfo::getEFixed(const detid_t detID) const {
  IDetector_const_sptr det = getInstrument()->getDetector(detID);
  return getEFixed(det);
}

/**
 * Easy access to the efixed value for this run & detector
 * @param detector :: The detector object to ask for the efixed mode. Only
 * required for Indirect mode
 * @return The current efixed value
 */
double ExperimentInfo::getEFixed(
    const boost::shared_ptr<const Geometry::IDetector> detector) const {
  Kernel::DeltaEMode::Type emode = getEMode();
  if (emode == Kernel::DeltaEMode::Direct) {
    try {
      return this->run().getPropertyValueAsType<double>("Ei");
    } catch (Kernel::Exception::NotFoundError &) {
      throw std::runtime_error(
          "Experiment logs do not contain an Ei value. Have you run GetEi?");
    }
  } else if (emode == Kernel::DeltaEMode::Indirect) {
    if (!detector)
      throw std::runtime_error("ExperimentInfo::getEFixed - Indirect mode "
                               "efixed requested without a valid detector.");
    Parameter_sptr par =
        constInstrumentParameters().getRecursive(detector.get(), "Efixed");
    if (par) {
      return par->value<double>();
    } else {
      std::vector<double> efixedVec = detector->getNumberParameter("Efixed");
      if (efixedVec.empty()) {
        int detid = detector->getID();
        IDetector_const_sptr detectorSingle =
            getInstrument()->getDetector(detid);
        efixedVec = detectorSingle->getNumberParameter("Efixed");
      if (!efixedVec.empty()) {
        return efixedVec.at(0);
      } else {
        std::ostringstream os;
        os << "ExperimentInfo::getEFixed - Indirect mode efixed requested but "
              "detector has no Efixed parameter attached. ID="
           << detector->getID();
        throw std::runtime_error(os.str());
  } else {
    throw std::runtime_error("ExperimentInfo::getEFixed - EFixed requested for "
                             "elastic mode, don't know what to do!");
}

void ExperimentInfo::setEFixed(const detid_t detID, const double value) {
  IDetector_const_sptr det = getInstrument()->getDetector(detID);
  Geometry::ParameterMap &pmap = instrumentParameters();
  pmap.addDouble(det.get(), "Efixed", value);
}

/** Return workspace start date as an ISO 8601 string. If this info not stored
 *in workspace the
 *   method returns current date. This date is used for example to retrieve the
 *instrument file.
 *
 *  @return workspace start date as a string (current time if start date not
 *available)
 */
std::string ExperimentInfo::getWorkspaceStartDate() const {
  std::string date;
  try {
    date = run().startTime().toISO8601String();
  } catch (std::runtime_error &) {
    g_log.information("run_start/start_time not stored in workspace. Default "
                      "to current date.");
    date = Types::Core::DateAndTime::getCurrentTime().toISO8601String();
  return date;
}

/** Return workspace start date as a formatted string (strftime, as
 *  returned by Types::Core::DateAndTime) string, if available. If
 *  unavailable, an empty string is returned
 *
 *  @return workspace start date as a string (empty if no date available)
 */
std::string ExperimentInfo::getAvailableWorkspaceStartDate() const {
  std::string date;
  try {
    date = run().startTime().toFormattedString();
  } catch (std::runtime_error &) {
    g_log.information("Note: run_start/start_time not stored in workspace.");
  }
  return date;
}

/** Return workspace end date as a formatted string (strftime style,
 *  as returned by Kernel::DateAdnTime) string, if available. If
 *  unavailable, an empty string is returned
 *
 *  @return workspace end date as a string (empty if no date available)
 */
std::string ExperimentInfo::getAvailableWorkspaceEndDate() const {
  std::string date;
  try {
    date = run().endTime().toFormattedString();
  } catch (std::runtime_error &) {
    g_log.information("Note: run_start/start_time not stored in workspace.");
  }
  return date;
}

/** Return from an IDF the values of the valid-from and valid-to attributes
 *
 *  @param IDFfilename :: Full path of an IDF
 *  @param[out] outValidFrom :: Used to return valid-from date
 *  @param[out] outValidTo :: Used to return valid-to date
 */
void ExperimentInfo::getValidFromTo(const std::string &IDFfilename,
                                    std::string &outValidFrom,
                                    std::string &outValidTo) {
  SAXParser pParser;
  // Create on stack to ensure deletion. Relies on pParser also being local
  // variable.
  myContentHandler conHand;
  pParser.setContentHandler(&conHand);

  try {
    pParser.parse(IDFfilename);
  } catch (DummyException &e) {
    outValidFrom = e.m_validFrom;
    outValidTo = e.m_validTo;
  } catch (...) {
    // should throw some sensible here
  }
}

/** Compile a list of files in compliance with name pattern-matching, file
 * format, and date-stamp constraints
 *
 * Ideally, the valid-from and valid-to of any valid file should encapsulate
 * argument date. If this is not possible, then the file with the most recent
 * valid-from stamp is selected.
 *
 * @param prefix :: the name of a valid file must begin with this pattern
 * @param fileFormats :: the extension of a valid file must be one of these
 * formats
 * @param directoryNames :: search only in these directories
 * @param date :: the valid-from and valid-to of a valid file should encapsulate
 * this date
 * @return list of absolute paths for each valid file
 */
std::vector<std::string> ExperimentInfo::getResourceFilenames(
    const std::string &prefix, const std::vector<std::string> &fileFormats,
    const std::vector<std::string> &directoryNames, const std::string &date) {
  if (date.empty()) {
    // Just use the current date
Hahn, Steven's avatar
Hahn, Steven committed
    g_log.debug() << "No date specified, using current date and time.\n";
    const std::string now =
        Types::Core::DateAndTime::getCurrentTime().toISO8601String();
    // Recursively call this method, but with all parameters.
    return ExperimentInfo::getResourceFilenames(prefix, fileFormats,
                                                directoryNames, now);
  // Join all the file formats into a single string
  std::stringstream ss;
  ss << "(";
  for (size_t i = 0; i < fileFormats.size(); ++i) {
    if (i != 0)
      ss << "|";
    ss << fileFormats[i];
  }
  ss << ")";
  const std::string allFileFormats = ss.str();
  const boost::regex regex(prefix + ".*\\." + allFileFormats,
                           boost::regex_constants::icase);
  Poco::DirectoryIterator end_iter;
  DateAndTime d(date);
  DateAndTime refDate("1900-01-31 23:59:00"); // used to help determine the most
  // recently starting file, if none match
  DateAndTime refDateGoodFile(
      "1900-01-31 23:59:00"); // used to help determine the most recently

  // Two files could have the same `from` date so multimap is required.
  // Sort with newer dates placed at the beginning
  std::multimap<DateAndTime, std::string, std::greater<DateAndTime>>
      matchingFiles;
  std::string mostRecentFile; // path to the file with most recent "valid-from"
  for (const auto &directoryName : directoryNames) {
    // Iterate over the directories from user ->etc ->install, and find the
    // first beat file
    for (Poco::DirectoryIterator dir_itr(directoryName); dir_itr != end_iter;
         ++dir_itr) {

      const auto &filePath = dir_itr.path();
      if (!filePath.isFile())
        continue;

      const std::string &l_filenamePart = filePath.getFileName();
      if (regex_match(l_filenamePart, regex)) {
        const auto &pathName = filePath.toString();
        g_log.debug() << "Found file: '" << pathName << "'\n";
        std::string validFrom, validTo;
        getValidFromTo(pathName, validFrom, validTo);
        g_log.debug() << "File '" << pathName << " valid dates: from '"
                      << validFrom << "' to '" << validTo << "'\n";
        // Use default valid "from" and "to" dates if none were found.
        DateAndTime to, from;
        if (validFrom.length() > 0)
          from.setFromISO8601(validFrom);
        else
          from = refDate;
        if (validTo.length() > 0)
          to.setFromISO8601(validTo);
        else
          to.setFromISO8601("2100-01-01T00:00:00");

        if (from <= d && d <= to) {
          foundFile = true;
          matchingFiles.insert(
              std::pair<DateAndTime, std::string>(from, pathName));
        }
        // Consider the most recent file in the absence of matching files
        if (!foundFile && (from >= refDate)) {
          refDate = from;
          mostRecentFile = pathName;

  // Retrieve the file names only
  std::vector<std::string> pathNames;
  if (!matchingFiles.empty()) {
    pathNames.reserve(matchingFiles.size());
    for (auto &elem : matchingFiles)
      pathNames.emplace_back(std::move(elem.second));
    pathNames.emplace_back(std::move(mostRecentFile));
  return pathNames;
}

/** A given instrument may have multiple definition files associated with it.
 *This method returns a file name which identifies a given instrument definition
 *for a given instrument.
 *The instrument geometry can be loaded from either a ".xml" file (old-style
 *IDF) or a ".hdf5/.nxs" file (new-style nexus).
 *The filename is required to be of the form InstrumentName + _Definition +
 *Identifier + extension. The identifier then is the part of a filename that
 *identifies the instrument definition valid at a given date.
 *
 *  If several instrument files files are valid at the given date the file with
 *the most recent from date is selected. If no such files are found the file
 *with the latest from date is selected.
 *
 *  If no file is found for the given instrument, an empty string is returned.
 *
 *  @param instrumentName :: Instrument name e.g. GEM, TOPAS or BIOSANS
 *  @param date :: ISO 8601 date
 *  @return full path of instrument geometry file
 *
 * @throws Exception::NotFoundError If no valid instrument definition filename
 *is found
 */
std::string
ExperimentInfo::getInstrumentFilename(const std::string &instrumentName,
                                      const std::string &date) {
  const std::vector<std::string> validFormats = {"xml", "nxs", "hdf5"};
  g_log.debug() << "Looking for instrument file for " << instrumentName
                << " that is valid on '" << date << "'\n";
  // Lookup the instrument (long) name
  const std::string instrument(
      Kernel::ConfigService::Instance().getInstrument(instrumentName).name());

  // Get the instrument directories for instrument file search
  const std::vector<std::string> &directoryNames =
      Kernel::ConfigService::Instance().getInstrumentDirectories();

  // matching files sorted with newest files coming first
  const std::vector<std::string> matchingFiles = getResourceFilenames(
      instrument + "_Definition", validFormats, directoryNames, date);
  if (!matchingFiles.empty()) {
    instFile = matchingFiles[0];
    g_log.debug() << "Instrument file selected is " << instFile << '\n';
    g_log.debug() << "No instrument file found\n";