diff --git a/Framework/API/inc/MantidAPI/GridDomain1D.h b/Framework/API/inc/MantidAPI/GridDomain1D.h index 02689f95e5263c01c2a01771a7731961ba94f8fb..ca98152e2df5f5c18b7cdafe22d895fbe5ee2d89 100644 --- a/Framework/API/inc/MantidAPI/GridDomain1D.h +++ b/Framework/API/inc/MantidAPI/GridDomain1D.h @@ -11,7 +11,7 @@ // Includes //---------------------------------------------------------------------- #include <stdexcept> - +#include <string> #include "MantidAPI/DllConfig.h" #include "MantidAPI/GridDomain.h" diff --git a/Framework/API/inc/MantidAPI/WorkspaceOpOverloads.h b/Framework/API/inc/MantidAPI/WorkspaceOpOverloads.h index d3a14845ea4340ae9a46fefcd39ec2f0fd9c2658..74d8afe4599dee033c83c4750d38eb2d8a62e878 100644 --- a/Framework/API/inc/MantidAPI/WorkspaceOpOverloads.h +++ b/Framework/API/inc/MantidAPI/WorkspaceOpOverloads.h @@ -9,6 +9,7 @@ #include "MantidAPI/DllConfig.h" #include "MantidAPI/MatrixWorkspace_fwd.h" +#include <string> namespace Mantid { namespace API { diff --git a/Framework/Kernel/inc/MantidKernel/Material.h b/Framework/Kernel/inc/MantidKernel/Material.h index fce89375ee03eab8545e200f357e8549ab13821a..c5088567ec8baa4155d4b74e8bd628bf1b9ae93e 100644 --- a/Framework/Kernel/inc/MantidKernel/Material.h +++ b/Framework/Kernel/inc/MantidKernel/Material.h @@ -14,6 +14,7 @@ #include "MantidKernel/PhysicalConstants.h" #include <boost/shared_ptr.hpp> #include <vector> +#include <string> // Forward Declares namespace NeXus { diff --git a/Framework/NexusGeometry/src/NexusGeometryParser.cpp b/Framework/NexusGeometry/src/NexusGeometryParser.cpp index 2b78be367c5506e489eb0dcb26fa4c61bd8b4d37..cba48969581d16dac88b4a10c1f5fc40ce3fa423 100644 --- a/Framework/NexusGeometry/src/NexusGeometryParser.cpp +++ b/Framework/NexusGeometry/src/NexusGeometryParser.cpp @@ -531,61 +531,35 @@ private: const std::vector<float> &vertices, const std::unordered_map<int, uint32_t> &detIdToIndex, const std::vector<uint32_t> &faceIndices, - const size_t vertsPerFace, std::vector<std::vector<Eigen::Vector3d>> &detFaceVerts, std::vector<std::vector<uint32_t>> &detFaceIndices, std::vector<std::vector<uint32_t>> &detWindingOrder, std::vector<int32_t> &detIds) { - const size_t vertStride = 3; - std::fill(detFaceIndices.begin(), detFaceIndices.end(), - std::vector<uint32_t>(1, 0)); for (size_t i = 0; i < detFaces.size(); i += 2) { - const auto faceIndex = faceIndices[detFaces[i]]; + const auto faceIndexOfDetector = detFaces[i]; + const auto faceIndex = faceIndices[faceIndexOfDetector]; + auto nextFaceIndex = windingOrder.size(); + if (faceIndexOfDetector + 1 < detFaces.size()) + nextFaceIndex = faceIndices[faceIndexOfDetector + 1]; + const auto nVertsInFace = nextFaceIndex - faceIndex; const auto detID = detFaces[i + 1]; const auto detIndex = detIdToIndex.at(detID); auto &vertsForDet = detFaceVerts[detIndex]; auto &detWinding = detWindingOrder[detIndex]; - auto &detIndices = detFaceIndices[detIndex]; - vertsForDet.reserve(vertsPerFace); - detWinding.reserve(vertsPerFace); - // Use face index to index into winding order. - for (size_t j = faceIndex; j < vertsPerFace + faceIndex; ++j) { - for (size_t v = 0; v < vertsPerFace; ++v) { - const auto vi = windingOrder[faceIndex + v] * vertStride; - vertsForDet.emplace_back(vertices[vi], vertices[vi + 1], - vertices[vi + 2]); - detWinding.push_back(static_cast<uint32_t>(detWinding.size())); - } + vertsForDet.reserve(nVertsInFace); + detWinding.reserve(nVertsInFace); + detFaceIndices[detIndex].push_back( + faceIndex); // Associate face with detector index + // Use face index to index into winding order. + for (size_t v = 0; v < nVertsInFace; ++v) { + const auto vi = windingOrder[faceIndex + v] * 3; + vertsForDet.emplace_back(vertices[vi], vertices[vi + 1], + vertices[vi + 2]); + detWinding.push_back(static_cast<uint32_t>(detWinding.size())); } - // Index -> Id detIds[detIndex] = detID; - detIndices.push_back(static_cast<uint32_t>(vertsForDet.size())); } - /* - std::fill(detFaceIndices.begin(), detFaceIndices.end(), - std::vector<uint32_t>(1, 0)); - for (size_t i = 0; i < windingOrder.size(); i += vertsPerFace) { - auto detFaceId = detFaces[detFaceIndex]; - // Id -> Index - auto detIndex = detIdToIndex.at(detFaceId); - auto &detVerts = detFaceVerts[detIndex]; - auto &detIndices = detFaceIndices[detIndex]; - auto &detWinding = detWindingOrder[detIndex]; - detVerts.reserve(vertsPerFace); - detWinding.reserve(vertsPerFace); - for (size_t v = 0; v < vertsPerFace; ++v) { - const auto vi = windingOrder[i + v] * vertStride; - detVerts.emplace_back(vertices[vi], vertices[vi + 1], vertices[vi + - 2]); detWinding.push_back(static_cast<uint32_t>(detWinding.size())); - } - // Index -> Id - detIds[detIndex] = detFaceId; - detIndices.push_back(static_cast<uint32_t>(detVerts.size())); - // Detector faces is 2N detectors - detFaceIndex += 2; - } - */ } void parseNexusMeshAndAddDetectors( @@ -595,30 +569,30 @@ private: const std::vector<float> &vertices, const size_t numDets, const std::unordered_map<int, uint32_t> &detIdToIndex, const std::string &name, InstrumentBuilder &builder) { - auto vertsPerFace = windingOrder.size() / faceIndices.size(); std::vector<std::vector<Eigen::Vector3d>> detFaceVerts(numDets); std::vector<std::vector<uint32_t>> detFaceIndices(numDets); std::vector<std::vector<uint32_t>> detWindingOrder(numDets); std::vector<int> detIds(numDets); extractFacesAndIDs(detFaces, windingOrder, vertices, detIdToIndex, - faceIndices, vertsPerFace, detFaceVerts, detFaceIndices, + faceIndices, detFaceVerts, detFaceIndices, detWindingOrder, detIds); for (size_t i = 0; i < numDets; ++i) { auto &detVerts = detFaceVerts[i]; - const auto &detIndices = detFaceIndices[i]; + const auto &faceIndices = detFaceIndices[i]; const auto &detWinding = detWindingOrder[i]; // Calculate polygon centre - auto centre = std::accumulate(detVerts.begin() + 1, detVerts.end(), - detVerts.front()) / - detVerts.size(); + Eigen::Vector3d centre = + std::accumulate(detVerts.begin() + 1, detVerts.end(), + detVerts.front()) / + detVerts.size(); // translate shape to origin for shape coordinates. std::for_each(detVerts.begin(), detVerts.end(), [¢re](Eigen::Vector3d &val) { val -= centre; }); - auto shape = NexusShapeFactory::createFromOFFMesh(detIndices, detWinding, + auto shape = NexusShapeFactory::createFromOFFMesh(faceIndices, detWinding, detVerts); builder.addDetectorToLastBank(name + "_" + std::to_string(i), detIds[i], centre, std::move(shape)); diff --git a/Framework/NexusGeometry/src/NexusGeometrySave.cpp.orig b/Framework/NexusGeometry/src/NexusGeometrySave.cpp.orig new file mode 100644 index 0000000000000000000000000000000000000000..544d2290a25741af45b721bff1a2514571b4d027 --- /dev/null +++ b/Framework/NexusGeometry/src/NexusGeometrySave.cpp.orig @@ -0,0 +1,1212 @@ +// Mantid Repository : https://github.com/mantidproject/mantid +// +// Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI, +// NScD Oak Ridge National Laboratory, European Spallation Source +// & Institut Laue - Langevin +// SPDX - License - Identifier: GPL - 3.0 + + +/* + * NexusGeometrySave::saveInstrument : + * Save methods to save geometry and metadata from memory + * to disk in Nexus file format for Instrument 2.0. + * + * @author Takudzwa Makoni, RAL (UKRI), ISIS + * @date 07/08/2019 + */ + +#include "MantidNexusGeometry/NexusGeometrySave.h" +#include "MantidAPI/SpectraDetectorTypes.h" +#include "MantidAPI/SpectrumInfo.h" +#include "MantidGeometry/Instrument/ComponentInfo.h" +#include "MantidGeometry/Instrument/ComponentInfoBankHelpers.h" +#include "MantidGeometry/Instrument/DetectorInfo.h" +#include "MantidIndexing/IndexInfo.h" +#include "MantidKernel/EigenConversionHelpers.h" +#include "MantidKernel/ProgressBase.h" +#include "MantidNexusGeometry/H5ForwardCompatibility.h" +#include "MantidNexusGeometry/NexusGeometryDefinitions.h" +#include "MantidNexusGeometry/NexusGeometryUtilities.h" +#include <H5Cpp.h> +#include <algorithm> +#include <boost/filesystem/operations.hpp> +#include <cmath> +#include <list> +#include <memory> +#include <regex> +#include <string> + +namespace Mantid { +namespace NexusGeometry { +namespace NexusGeometrySave { +using namespace Geometry::ComponentInfoBankHelpers; +/* + * Helper container for spectrum mapping information info + */ +struct SpectraMappings { + std::vector<int32_t> detector_index; + std::vector<int32_t> detector_count; + std::vector<int32_t> detector_list; + std::vector<int32_t> spectra_ids; + size_t number_spec = 0; + size_t number_dets = 0; +}; + +/** Function tryCreatGroup. will try to create a new child group with the given + * name inside the parent group. if a child group with that name already exists + * in the parent group, throws std::invalid_argument. H5 will not allow us to + * save duplicate groups with the same name, so this provides a utility for an + * eager check. + * + * @param parentGroup : H5 parent group. + * @param childGroupName : intended name of the child goup. + * @return : new H5 Group object with name <childGroupName> if did not throw. + */ +inline H5::Group tryCreateGroup(const H5::Group &parentGroup, + const std::string &childGroupName) { + H5std_string parentGroupName = H5_OBJ_NAME(parentGroup); + for (hsize_t i = 0; i < parentGroup.getNumObjs(); ++i) { + if (parentGroup.getObjTypeByIdx(i) == GROUP_TYPE) { + H5std_string child = parentGroup.getObjnameByIdx(i); + if (childGroupName == child) { + // TODO: runtime error instead? + throw std::invalid_argument( + "Cannot create group with name " + childGroupName + + " inside parent group " + parentGroupName + + " because a group with this name already exists."); + } + } + } + return parentGroup.createGroup(childGroupName); +} +/* + * Function toStdVector (Overloaded). Store data in Mantid::Kernel::V3D vector + * into std::vector<double> vector. Used by saveInstrument to write array-type + * datasets to file. + * + * @param data : Mantid::Kernel::V3D vector containing data values + * @return std::vector<double> vector containing data values in + * Mantid::Kernel::V3D format. + */ +std::vector<double> toStdVector(const V3D &data) { + std::vector<double> stdVector; + stdVector.reserve(3); + stdVector.push_back(data.X()); + stdVector.push_back(data.Y()); + stdVector.push_back(data.Z()); + return stdVector; +} + +/* + * Function toStdVector (Overloaded). Store data in Eigen::Vector3d vector + * into std::vector<double> vector. Used by saveInstrument to write array-type + * datasets to file. + * + * @param data : Eigen::Vector3d vector containing data values + * @return std::vector<double> vector containing data values in + * Eigen::Vector3d format + */ +std::vector<double> toStdVector(const Eigen::Vector3d &data) { + return toStdVector(Kernel::toV3D(data)); +} + +/* + * Function: isApproxZero. returns true if all values in an variable-sized + * std-vector container evaluate to zero with a given level of precision. Used + * by SaveInstrument methods to determine whether or not to write a dataset to + * file. + * + * @param data : std::vector<T> data + * @param precision : double precision specifier + * @return true if all elements are approx zero, else false. + */ +bool isApproxZero(const std::vector<double> &data, const double &precision) { + + return std::all_of(data.begin(), data.end(), + [&precision](const double &element) { + return std::abs(element) < precision; + }); +} + +// overload. return true if vector is approx to zero +bool isApproxZero(const Eigen::Vector3d &data, const double &precision) { + return data.isApprox(Eigen::Vector3d(0, 0, 0), precision); +} + +// overload. returns true is angle is approx to zero +bool isApproxZero(const Eigen::Quaterniond &data, const double &precision) { + return data.isApprox(Eigen::Quaterniond(1, 0, 0, 0), precision); +} + +/* + * Function: strTypeOfSize + * Produces the HDF StrType of size equal to that of the + * input string. + * + * @param str : std::string + * @return string datatype of size = length of input string + */ +H5::StrType strTypeOfSize(const std::string &str) { + H5::StrType stringType(H5::PredType::C_S1, str.size()); + return stringType; +} + +/* + * Function: writeStrDataset + * writes a StrType HDF dataset and dataset value to a HDF group. + * + * @param grp : HDF group object. + * @param attrname : attribute name. + * @param attrVal : string attribute value to be stored in attribute. + */ +void writeStrDataset(H5::Group &grp, const std::string &dSetName, + const std::string &dSetVal, + const H5::DataSpace &dataSpace = SCALAR) { + // TODO. may need to review if we shoud in fact replace. + if (!utilities::findDataset(grp, dSetName)) { + H5::StrType dataType = strTypeOfSize(dSetVal); + H5::DataSet dSet = grp.createDataSet(dSetName, dataType, dataSpace); + dSet.write(dSetVal, dataType); + } +} + +/* + * Function: writeStrAttribute + * writes a StrType HDF attribute and attribute value to a HDF group. + * + * @param grp : HDF group object. + * @param attrname : attribute name. + * @param attrVal : string attribute value to be stored in attribute. + */ +void writeStrAttribute(H5::Group &grp, const std::string &attrName, + const std::string &attrVal, + const H5::DataSpace &dataSpace = SCALAR) { + if (!grp.attrExists(attrName)) { + H5::StrType dataType = strTypeOfSize(attrVal); + H5::Attribute attribute = + grp.createAttribute(attrName, dataType, dataSpace); + attribute.write(dataType, attrVal); + } +} + +/* + * Function: writeStrAttribute + * Overload function which writes a StrType HDF attribute and attribute value + * to a HDF dataset. + * + * @param dSet : HDF dataset object. + * @param attrname : attribute name. + * @param attrVal : string attribute value to be stored in attribute. + */ +void writeStrAttribute(H5::DataSet &dSet, const std::string &attrName, + const std::string &attrVal, + const H5::DataSpace &dataSpace = SCALAR) { + if (!dSet.attrExists(attrName)) { + H5::StrType dataType = strTypeOfSize(attrVal); + auto attribute = dSet.createAttribute(attrName, dataType, dataSpace); + attribute.write(dataType, attrVal); + } +} + +/* + * Function: writeXYZPixeloffset + * write the x, y, and z offset of the pixels from the parent detector bank as + * HDF5 datasets to HDF5 group. If all of the pixel offsets in either x, y, or z + * are approximately zero, skips writing that dataset to file. + * @param grp : HDF5 parent group + * @param compInfo : Component Info Instrument cache + * @param idx : index of bank in cache. + */ +void writeXYZPixeloffset(H5::Group &grp, + const Geometry::ComponentInfo &compInfo, + const size_t idx) { + + H5::DataSet xPixelOffset, yPixelOffset, zPixelOffset; + auto childrenDetectors = compInfo.detectorsInSubtree(idx); + + std::vector<double> posx; + std::vector<double> posy; + std::vector<double> posz; + + posx.reserve(childrenDetectors.size()); + posy.reserve(childrenDetectors.size()); + posz.reserve(childrenDetectors.size()); + + for (const size_t &i : childrenDetectors) { + + auto offset = Geometry::ComponentInfoBankHelpers::offsetFromAncestor( + compInfo, idx, i); + + posx.push_back(offset[0]); + posy.push_back(offset[1]); + posz.push_back(offset[2]); + } + + bool xIsZero = isApproxZero(posx, PRECISION); + bool yIsZero = isApproxZero(posy, PRECISION); + bool zIsZero = isApproxZero(posz, PRECISION); + + auto bankName = compInfo.name(idx); + const auto nDetectorsInBank = static_cast<hsize_t>(posx.size()); + + int rank = 1; + hsize_t dims[static_cast<hsize_t>(1)]; + dims[0] = nDetectorsInBank; + + H5::DataSpace space = H5Screate_simple(rank, dims, nullptr); + + if (!xIsZero) { + xPixelOffset = + grp.createDataSet(X_PIXEL_OFFSET, H5::PredType::NATIVE_DOUBLE, space); + xPixelOffset.write(posx.data(), H5::PredType::NATIVE_DOUBLE, space); + writeStrAttribute(xPixelOffset, UNITS, METRES); + } + + if (!yIsZero) { + yPixelOffset = + grp.createDataSet(Y_PIXEL_OFFSET, H5::PredType::NATIVE_DOUBLE, space); + yPixelOffset.write(posy.data(), H5::PredType::NATIVE_DOUBLE); + writeStrAttribute(yPixelOffset, UNITS, METRES); + } + + if (!zIsZero) { + zPixelOffset = + grp.createDataSet(Z_PIXEL_OFFSET, H5::PredType::NATIVE_DOUBLE, space); + zPixelOffset.write(posz.data(), H5::PredType::NATIVE_DOUBLE); + writeStrAttribute(zPixelOffset, UNITS, METRES); + } +} + +template <typename T> +void write1DIntDataset(H5::Group &grp, const H5std_string &name, + const std::vector<T> &container) { + const int rank = 1; + hsize_t dims[1] = {static_cast<hsize_t>(container.size())}; + + H5::DataSpace space = H5Screate_simple(rank, dims, nullptr); + + auto dataset = grp.createDataSet(name, H5::PredType::NATIVE_INT, space); + if (!container.empty()) + dataset.write(container.data(), H5::PredType::NATIVE_INT, space); +} + +/* + * Function: writeNXDetectorNumber + * For use with NXdetector group. Writes the detector numbers for all detector + * pixels in compInfo to a new dataset in the group. + * + * @param detectorIDs : std::vector<int> container of all detectorIDs to be + * stored into dataset 'detector_number'. + * @param compInfo : instrument cache with component info. + * @idx : size_t index of bank in compInfo. + */ +void writeNXDetectorNumber(H5::Group &grp, + const Geometry::ComponentInfo &compInfo, + const std::vector<int> &detectorIDs, + const size_t idx) { + + H5::DataSet detectorNumber; + + std::vector<int> bankDetIDs; // IDs of detectors beloning to bank + std::vector<size_t> bankDetectors = + compInfo.detectorsInSubtree(idx); // Indexes of children detectors in bank + bankDetIDs.reserve(bankDetectors.size()); + + // write the ID for each child detector to std::vector to be written to + // dataset + std::for_each(bankDetectors.begin(), bankDetectors.end(), + [&bankDetIDs, &detectorIDs](const size_t index) { + bankDetIDs.push_back(detectorIDs[index]); + }); + + write1DIntDataset(grp, DETECTOR_IDS, bankDetIDs); +} + +// Write the count of how many detectors contribute to each spectra +void writeDetectorCount(H5::Group &grp, const SpectraMappings &mappings) { + write1DIntDataset(grp, SPECTRA_COUNTS, mappings.detector_count); +} + +// Write the detectors ids ordered by spectra index 0 - N for each NXDetector +void writeDetectorList(H5::Group &grp, const SpectraMappings &mappings) { + write1DIntDataset(grp, DETECTOR_LIST, mappings.detector_list); +} + +// Write the detector indexes. This provides offsets into the detector_list and +// is sized to the number of spectra +void writeDetectorIndex(H5::Group &grp, const SpectraMappings &mappings) { + write1DIntDataset(grp, DETECTOR_INDEX, mappings.detector_index); +} + +// Write the spectra numbers for each spectra +void writeSpectra(H5::Group &grp, const SpectraMappings &mappings) { + write1DIntDataset(grp, SPECTRA_NUMBERS, mappings.spectra_ids); +} + +/* + * Function: writeNXMonitorNumber + * For use with NXmonitor group. write 'detector_id's of an NXmonitor, which + * is a specific type of pixel, to its group. + * + * @param grp : NXmonitor group (HDF group) + * @param monitorID : monitor ID to be + * stored into dataset 'detector_id' (or 'detector_number'. naming convention + * inconsistency?). + */ +void writeNXMonitorNumber(H5::Group &grp, const int monitorID) { + + // these DataSets are duplicates of each other. written to the NXmonitor + // group to handle the naming inconsistency. probably temporary. + H5::DataSet detectorNumber, detector_id; + + int rank = 1; + hsize_t dims[static_cast<hsize_t>(1)]; + dims[0] = static_cast<hsize_t>(1); + + H5::DataSpace space = H5Screate_simple(rank, dims, nullptr); + + // these DataSets are duplicates of each other. written to the group to + // handle the naming inconsistency. probably temporary. + if (!utilities::findDataset(grp, DETECTOR_IDS)) { + detectorNumber = + grp.createDataSet(DETECTOR_IDS, H5::PredType::NATIVE_INT, space); + detectorNumber.write(&monitorID, H5::PredType::NATIVE_INT, space); + } + if (!utilities::findDataset(grp, DETECTOR_ID)) { + + detector_id = + grp.createDataSet(DETECTOR_ID, H5::PredType::NATIVE_INT, space); + detector_id.write(&monitorID, H5::PredType::NATIVE_INT, space); + } +} + +/* + * Function: writeLocation + * For use with NXdetector group. Writes absolute position of detector bank to + * dataset and metadata as attributes. + * + * @param grp : NXdetector group : (HDF group) + * @param position : Eigen::Vector3d position of component in instrument cache. + */ +inline void writeLocation(H5::Group &grp, const Eigen::Vector3d &position) { + + std::string dependency = NO_DEPENDENCY; // self dependent + + double norm; + + H5::DataSet location; + H5::DataSpace dspace; + H5::DataSpace aspace; + + H5::Attribute vector; + H5::Attribute units; + H5::Attribute transformationType; + H5::Attribute dependsOn; + + H5::StrType strSize; + + int drank = 1; // rank of dataset + hsize_t ddims[static_cast<hsize_t>(1)]; // dimensions of dataset + ddims[0] = static_cast<hsize_t>(1); // datapoints in dataset dimension 0 + + norm = position.norm(); // norm od the position vector + auto unitVec = position.normalized(); // unit vector of the position vector + std::vector<double> stdNormPos = + toStdVector(unitVec); // convert to std::vector + + dspace = H5Screate_simple(drank, ddims, nullptr); // dataspace for dataset + location = grp.createDataSet(LOCATION, H5::PredType::NATIVE_DOUBLE, + dspace); // dataset location + location.write(&norm, H5::PredType::NATIVE_DOUBLE, + dspace); // write norm to location + + int arank = 1; // rank of attribute + hsize_t adims[static_cast<hsize_t>(3)]; // dimensions of attribute + adims[0] = 3; // datapoints in attribute dimension 0 + + aspace = H5Screate_simple(arank, adims, nullptr); // dataspace for attribute + vector = location.createAttribute(VECTOR, H5::PredType::NATIVE_DOUBLE, + aspace); // attribute vector + vector.write(H5::PredType::NATIVE_DOUBLE, + stdNormPos.data()); // write unit vector to vector + + // units attribute + strSize = strTypeOfSize(METRES); + units = location.createAttribute(UNITS, strSize, SCALAR); + units.write(strSize, METRES); + + // transformation-type attribute + strSize = strTypeOfSize(TRANSLATION); + transformationType = + location.createAttribute(TRANSFORMATION_TYPE, strSize, SCALAR); + transformationType.write(strSize, TRANSLATION); + + // dependency attribute + strSize = strTypeOfSize(dependency); + dependsOn = location.createAttribute(DEPENDS_ON, strSize, SCALAR); + dependsOn.write(strSize, dependency); +} + +/* + * Function: writeOrientation + * For use with NXdetector group. Writes the absolute rotation of detector + * bank to dataset and metadata as attributes. + * + * @param grp : NXdetector group : (HDF group) + * @param rotation : Eigen::Quaterniond rotation of component in instrument + * cache. + * @param dependency : dependency of the orientation dataset: + * Compliant to the Mantid Instrument Definition file, if a translation + * exists, it precedes a rotation. + * https://docs.mantidproject.org/nightly/concepts/InstrumentDefinitionFile.html + */ +inline void writeOrientation(H5::Group &grp, const Eigen::Quaterniond &rotation, + const std::string &dependency) { + + // dependency for orientation defaults to self-dependent. If Location + // dataset exists, the orientation will depend on it instead. + + double angle; + + H5::DataSet orientation; + H5::DataSpace dspace; + H5::DataSpace aspace; + + H5::Attribute vector; + H5::Attribute units; + H5::Attribute transformationType; + H5::Attribute dependsOn; + + H5::StrType strSize; + + int drank = 1; // rank of dataset + hsize_t ddims[static_cast<hsize_t>(1)]; // dimensions of dataset + ddims[0] = static_cast<hsize_t>(1); // datapoints in dataset dimension 0 + + angle = std::acos(rotation.w()) * (360.0 / M_PI); // angle magnitude + Eigen::Vector3d axisOfRotation = rotation.vec().normalized(); // angle axis + std::vector<double> stdNormAxis = + toStdVector(axisOfRotation); // convert to std::vector + + dspace = H5Screate_simple(drank, ddims, nullptr); // dataspace for dataset + orientation = grp.createDataSet(ORIENTATION, H5::PredType::NATIVE_DOUBLE, + dspace); // dataset orientation + orientation.write(&angle, H5::PredType::NATIVE_DOUBLE, + dspace); // write angle magnitude to orientation + + int arank = 1; // rank of attribute + hsize_t adims[static_cast<hsize_t>(3)]; // dimensions of attribute + adims[0] = static_cast<hsize_t>(3); // datapoints in attibute dimension 0 + + aspace = H5Screate_simple(arank, adims, nullptr); // dataspace for attribute + vector = orientation.createAttribute(VECTOR, H5::PredType::NATIVE_DOUBLE, + aspace); // attribute vector + vector.write(H5::PredType::NATIVE_DOUBLE, + stdNormAxis.data()); // write angle axis to vector + + // units attribute + strSize = strTypeOfSize(DEGREES); + units = orientation.createAttribute(UNITS, strSize, SCALAR); + units.write(strSize, DEGREES); + + // transformation-type attribute + strSize = strTypeOfSize(ROTATION); + transformationType = + orientation.createAttribute(TRANSFORMATION_TYPE, strSize, SCALAR); + transformationType.write(strSize, ROTATION); + + // dependency attribute + strSize = strTypeOfSize(dependency); + dependsOn = orientation.createAttribute(DEPENDS_ON, strSize, SCALAR); + dependsOn.write(strSize, dependency); +} + +SpectraMappings makeMappings(const Geometry::ComponentInfo &compInfo, + const detid2index_map &detToIndexMap, + const Indexing::IndexInfo &indexInfo, + const API::SpectrumInfo &specInfo, + const std::vector<Mantid::detid_t> &detIds, + size_t index) { + auto childrenDetectors = compInfo.detectorsInSubtree(index); + size_t nChildDetectors = + childrenDetectors.size(); // Number of detectors actually considered in + // spectra-detector map for this NXdetector + // local to this nxdetector + std::map<size_t, int> detector_count_map; + // We start knowing only the detector index, we have to establish spectra from + // that. + for (const auto det_index : childrenDetectors) { + auto detector_id = detIds[det_index]; + + // A detector might not belong to any spectrum at all. + if (detToIndexMap.find(detector_id) != detToIndexMap.end()) { + auto spectrum_index = detToIndexMap.at(detector_id); + detector_count_map[spectrum_index]++; // Attribute detector to a give + // spectrum_index + } else { + --nChildDetectors; // Detector is not part of any spectra-detector + // mapping. So we have one less detector to consider + // recording + } + } + // Sized to spectra in bank + SpectraMappings mappings; + mappings.detector_list.resize(nChildDetectors); + mappings.detector_count.resize(detector_count_map.size(), 0); + mappings.detector_index.resize(detector_count_map.size() + 1, 0); + mappings.spectra_ids.resize(detector_count_map.size(), 0); + mappings.number_dets = nChildDetectors; + mappings.number_spec = detector_count_map.size(); + size_t specCounter = 0; + size_t detCounter = 0; + for (auto &pair : detector_count_map) { + // using sort order of map to ensure we are ordered by lowest to highest + // spectrum index + mappings.detector_count[specCounter] = (pair.second); // Counts + mappings.detector_index[specCounter + 1] = + mappings.detector_index[specCounter] + (pair.second); + mappings.spectra_ids[specCounter] = + int32_t(indexInfo.spectrumNumber(pair.first)); + + // We will list everything by spectrum index, so we need to add the detector + // ids in the same order. + const auto &specDefintion = specInfo.spectrumDefinition(pair.first); + for (const auto &def : specDefintion) { + mappings.detector_list[detCounter] = detIds[def.first]; + ++detCounter; + } + ++specCounter; + } + mappings.detector_index.resize( + detector_count_map.size()); // cut-off last item + + return mappings; +} + +void validateInputs(AbstractLogger &logger, const std::string &fullPath, + const Geometry::ComponentInfo &compInfo) { + boost::filesystem::path tmp(fullPath); + if (!boost::filesystem::is_directory(tmp.root_directory())) { + throw std::invalid_argument( + "The path provided for saving the file is invalid: " + fullPath + "\n"); + } + + // check the file extension matches any of the valid extensions defined in + // nexus_geometry_extensions + const auto ext = boost::filesystem::path(tmp).extension(); + bool isValidExt = std::any_of( + nexus_geometry_extensions.begin(), nexus_geometry_extensions.end(), + [&ext](const std::string &str) { return ext.generic_string() == str; }); + + // throw if the file extension is invalid + if (!isValidExt) { + // string of valid extensions to output in exception + std::string extensions; + std::for_each( + nexus_geometry_extensions.begin(), nexus_geometry_extensions.end(), + [&extensions](const std::string &str) { extensions += " " + str; }); + std::string message = "invalid extension for file: '" + + ext.generic_string() + + "'. Expected any of: " + extensions; + logger.error(message); + throw std::invalid_argument(message); + } + + if (!compInfo.hasDetectorInfo()) { + logger.error( + "No detector info was found in the Instrument. Instrument not saved."); + throw std::invalid_argument("No detector info was found in the Instrument"); + } + if (!compInfo.hasSample()) { + logger.error( + "No sample was found in the Instrument. Instrument not saved."); + throw std::invalid_argument("No sample was found in the Instrument"); + } + + if (Mantid::Kernel::V3D{0, 0, 0} != compInfo.samplePosition()) { + logger.error("The sample positon is required to be at the origin. " + "Instrument not saved."); + throw std::invalid_argument( + "The sample positon is required to be at the origin"); + } + + if (!compInfo.hasSource()) { + logger.error("No source was found in the Instrument. " + "Instrument not saved."); + throw std::invalid_argument("No source was found in the Instrument"); + } +} + +/* + * Function determines if a given index has an ancestor index in the + * saved_indices list. This allows us to prevent duplicate saving of things that + * could be considered NXDetectors + */ +template <typename T> +bool isDesiredNXDetector(size_t index, const T &saved_indices, + const Geometry::ComponentInfo &compInfo) { + return saved_indices.end() == + std::find_if(saved_indices.begin(), saved_indices.end(), + [&compInfo, &index](const size_t idx) { + return isAncestorOf(compInfo, idx, index); + }); +} + +/** + * Internal save implementation. We can either write a new file containing only + * the geometry, or we might also need to append/merge with an existing file. + * Knowing the logic for this is important so we build an object around the Mode + * state. + */ +class NexusGeometrySaveImpl { +public: + enum class Mode { Trunc, Append }; + + explicit NexusGeometrySaveImpl(Mode mode) : m_mode(mode) {} + NexusGeometrySaveImpl(const NexusGeometrySaveImpl &) = + delete; // No intention to suport copies + + /* + * Function: NXInstrument + * for NXentry parent (root group). Produces an NXinstrument group in the + * parent group, and writes Nexus compliant datasets and metadata stored in + * attributes to the new group. + * + * @param parent : parent group in which to write the NXinstrument group. + * @param compInfo : componentinfo + * @return NXinstrument group, to be passed into children save methods. + */ + H5::Group instrument(const H5::Group &parent, + const Geometry::ComponentInfo &compInfo) { + + std::string nameInCache = compInfo.name(compInfo.root()); + std::string instrName = + nameInCache.empty() ? "unspecified_instrument" : nameInCache; + + H5::Group childGroup = openOrCreateGroup(parent, instrName, NX_INSTRUMENT); + + writeStrDataset(childGroup, NAME, instrName); + writeStrAttribute(childGroup, NX_CLASS, NX_INSTRUMENT); + + std::string defaultShortName = instrName.substr(0, 3); + H5::DataSet name = childGroup.openDataSet(NAME); + writeStrAttribute(name, SHORT_NAME, defaultShortName); + return childGroup; + } + + /* + * Function: saveNXSample + * For NXentry parent (root group). Produces an NXsample group in the parent + * group, and writes the Nexus compliant datasets and metadata stored in + * attributes to the new group. + * + * @param parent : parent group in which to write the NXinstrument group. + * @param compInfo : componentInfo object. + */ + void sample(const H5::Group &parentGroup, + const Geometry::ComponentInfo &compInfo) { + + std::string nameInCache = compInfo.name(compInfo.sample()); + std::string sampleName = + nameInCache.empty() ? "unspecified_sample" : nameInCache; + + H5::Group childGroup = + openOrCreateGroup(parentGroup, sampleName, NX_SAMPLE); + writeStrAttribute(childGroup, NX_CLASS, NX_SAMPLE); + writeStrDataset(childGroup, NAME, sampleName); + } + + /* + * Function: saveNXSource + * For NXentry (root group). Produces an NXsource group in the parent group, + * and writes the Nexus compliant datasets and metadata stored in attributes + * to the new group. + * + * @param parent : parent group in which to write the NXinstrument group. + * @param compInfo : componentInfo object. + */ + void source(const H5::Group &parentGroup, + const Geometry::ComponentInfo &compInfo) { + + size_t index = compInfo.source(); + + std::string nameInCache = compInfo.name(index); + std::string sourceName = + nameInCache.empty() ? "unspecified_source" : nameInCache; + + std::string dependency = NO_DEPENDENCY; + + Eigen::Vector3d position = + Mantid::Kernel::toVector3d(compInfo.position(index)); + Eigen::Quaterniond rotation = + Mantid::Kernel::toQuaterniond(compInfo.rotation(index)); + + bool locationIsOrigin = isApproxZero(position, PRECISION); + bool orientationIsZero = isApproxZero(rotation, PRECISION); + + H5::Group childGroup = + openOrCreateGroup(parentGroup, sourceName, NX_SOURCE); + writeStrAttribute(childGroup, NX_CLASS, NX_SOURCE); + + // do not write NXtransformations if there is no translation or rotation + if (!(locationIsOrigin && orientationIsZero)) { + H5::Group transformations = + simpleNXSubGroup(childGroup, TRANSFORMATIONS, NX_TRANSFORMATIONS); + + // self, ".", is the default first NXsource dependency in the chain. + // first check translation in NXsource is non-zero, and set dependency + // to location if true and write location. Then check if orientation in + // NXsource is non-zero, replace dependency with orientation if true. If + // neither orientation nor location are non-zero, NXsource is self + // dependent. + if (!locationIsOrigin) { + dependency = H5_OBJ_NAME(transformations) + "/" + LOCATION; + writeLocation(transformations, position); + } + if (!orientationIsZero) { + dependency = H5_OBJ_NAME(transformations) + "/" + ORIENTATION; + + // If location dataset is written to group also, then dependency for + // orientation dataset containg the rotation transformation will be + // location. Else dependency for orientation is self. + std::string rotationDependency = + locationIsOrigin ? NO_DEPENDENCY + : H5_OBJ_NAME(transformations) + "/" + LOCATION; + writeOrientation(transformations, rotation, rotationDependency); + } + } + + writeStrDataset(childGroup, NAME, sourceName); + writeStrDataset(childGroup, DEPENDS_ON, dependency); + } + + /* + * Function: monitor + * For NXinstrument parent (component info root). Produces an NXmonitor + * groups from Component info, and saves it in the parent + * group, along with the Nexus compliant datasets, and metadata stored in + * attributes to the new group. + * + * @param parentGroup : parent group in which to write the NXinstrument + * group. + * @param compInfo : componentInfo object. + * @param monitorID : ID of the specific monitor. + * @param index : index of the specific monitor in the Instrument cache. + * @return child group for further additions + */ + H5::Group monitor(const H5::Group &parentGroup, + const Geometry::ComponentInfo &compInfo, + const int monitorId, const size_t index) { + + // if the component is unnamed sets the name as unspecified with the + // location of the component in the cache + std::string nameInCache = compInfo.name(index); + std::string monitorName = + nameInCache.empty() ? "unspecified_monitor_" + std::to_string(index) + : nameInCache; + + Eigen::Vector3d position = + Mantid::Kernel::toVector3d(compInfo.position(index)); + Eigen::Quaterniond rotation = + Mantid::Kernel::toQuaterniond(compInfo.rotation(index)); + + std::string dependency = NO_DEPENDENCY; // dependency initialiser + + bool locationIsOrigin = isApproxZero(position, PRECISION); + bool orientationIsZero = isApproxZero(rotation, PRECISION); + + H5::Group childGroup = + openOrCreateGroup(parentGroup, monitorName, NX_MONITOR); + writeStrAttribute(childGroup, NX_CLASS, NX_MONITOR); + + // do not write NXtransformations if there is no translation or rotation + if (!(locationIsOrigin && orientationIsZero)) { + H5::Group transformations = + simpleNXSubGroup(childGroup, TRANSFORMATIONS, NX_TRANSFORMATIONS); + + // self, ".", is the default first NXmonitor dependency in the chain. + // first check translation in NXmonitor is non-zero, and set dependency + // to location if true and write location. Then check if orientation in + // NXmonitor is non-zero, replace dependency with orientation if true. + // If neither orientation nor location are non-zero, NXmonitor is self + // dependent. + if (!locationIsOrigin) { + dependency = H5_OBJ_NAME(transformations) + "/" + LOCATION; + writeLocation(transformations, position); + } + if (!orientationIsZero) { + dependency = H5_OBJ_NAME(transformations) + "/" + ORIENTATION; + + // If location dataset is written to group also, then dependency for + // orientation dataset containg the rotation transformation will be + // location. Else dependency for orientation is self. + std::string rotationDependency = + locationIsOrigin ? NO_DEPENDENCY + : H5_OBJ_NAME(transformations) + "/" + LOCATION; + writeOrientation(transformations, rotation, rotationDependency); + } + } + + H5::StrType dependencyStrType = strTypeOfSize(dependency); + writeNXMonitorNumber(childGroup, monitorId); + + writeStrDataset(childGroup, BANK_NAME, monitorName); + writeStrDataset(childGroup, DEPENDS_ON, dependency); + return childGroup; + } + + /* For NXinstrument parent (component info root). Produces an NXmonitor + * groups from Component info, and saves it in the parent + * group, along with the Nexus compliant datasets, and metadata stored in + * attributes to the new group. + * + * Saves detector-spectra mappings too + * + * @param parentGroup : parent group in which to write the NXinstrument + * group. + * @param compInfo : componentInfo object. + * @param monitorId : ID of the specific monitor. + * @param index : index of the specific monitor in the Instrument cache. + * @param mappings : Spectra to detector mappings + */ + void monitor(const H5::Group &parentGroup, + const Geometry::ComponentInfo &compInfo, const int monitorId, + const size_t index, SpectraMappings &mappings) { + + auto childGroup = monitor(parentGroup, compInfo, monitorId, index); + // Additional mapping information written. + writeDetectorCount(childGroup, mappings); + // Note that the detector list is the same as detector_number, but it is + // ordered by spectrum index 0 - N, whereas detector_number is just written + // out in the order the detectors are encountered in the bank. + writeDetectorList(childGroup, mappings); + writeDetectorIndex(childGroup, mappings); + writeSpectra(childGroup, mappings); + } + + /* + * Function: detectors + * For NXinstrument parent (component info root). Save method which produces + * a set of NXdetctor groups from Component info detector banks, and saves + * it in the parent group, along with the Nexus compliant datasets, and + * metadata stored in attributes to the new group. + * + * @param parentGroup : parent group in which to write the NXinstrument + * group. + * @param compInfo : componentInfo object. + * @param detIDs : global detector IDs, from which those specific to the + * NXdetector will be extracted. + * @return childGroup for futher additions + */ + H5::Group detector(const H5::Group &parentGroup, + const Geometry::ComponentInfo &compInfo, + const std::vector<int> &detIds, const size_t index) { + + // if the component is unnamed sets the name as unspecified with the + // location of the component in the cache + std::string nameInCache = compInfo.name(index); + std::string detectorName = + nameInCache.empty() ? "unspecified_detector_at_" + std::to_string(index) + : nameInCache; + + Eigen::Vector3d position = + Mantid::Kernel::toVector3d(compInfo.position(index)); + Eigen::Quaterniond rotation = + Mantid::Kernel::toQuaterniond(compInfo.rotation(index)); + + std::string dependency = NO_DEPENDENCY; // dependency initialiser + + bool locationIsOrigin = isApproxZero(position, PRECISION); + bool orientationIsZero = isApproxZero(rotation, PRECISION); + + H5::Group childGroup = + openOrCreateGroup(parentGroup, detectorName, NX_DETECTOR); + writeStrAttribute(childGroup, NX_CLASS, NX_DETECTOR); + + // do not write NXtransformations if there is no translation or rotation + if (!(locationIsOrigin && orientationIsZero)) { + H5::Group transformations = + simpleNXSubGroup(childGroup, TRANSFORMATIONS, NX_TRANSFORMATIONS); + + // self, ".", is the default first NXdetector dependency in the chain. + // first check translation in NXdetector is non-zero, and set dependency + // to location if true and write location. Then check if orientation in + // NXdetector is non-zero, replace dependency with orientation if true. + // If neither orientation nor location are non-zero, NXdetector is self + // dependent. + if (!locationIsOrigin) { + dependency = H5_OBJ_NAME(transformations) + "/" + LOCATION; + writeLocation(transformations, position); + } + if (!orientationIsZero) { + dependency = H5_OBJ_NAME(transformations) + "/" + ORIENTATION; + + // If location dataset is written to group also, then dependency for + // orientation dataset containg the rotation transformation will be + // location. Else dependency for orientation is self. + std::string rotationDependency = + locationIsOrigin ? NO_DEPENDENCY + : H5_OBJ_NAME(transformations) + "/" + LOCATION; + writeOrientation(transformations, rotation, rotationDependency); + } + } + + H5::StrType dependencyStrType = strTypeOfSize(dependency); + writeXYZPixeloffset(childGroup, compInfo, index); + writeNXDetectorNumber(childGroup, compInfo, detIds, index); + + writeStrDataset(childGroup, BANK_NAME, detectorName); + writeStrDataset(childGroup, DEPENDS_ON, dependency); + return childGroup; + } + + /* + * Function: detectors + * For NXinstrument parent (component info root). Save method which produces + * a set of NXdetctor groups from Component info detector banks, and saves + * it in the parent group, along with the Nexus compliant datasets, and + * metadata stored in attributes to the new group. + * + * @param parentGroup : parent group in which to write the NXinstrument + * group. + * @param compInfo : componentInfo object. + * @param detIDs : global detector IDs, from which those specific to the + * @param index : current component index + * @param mappings : Spectra to detector mappings + * NXdetector will be extracted. + */ + void detector(const H5::Group &parentGroup, + const Geometry::ComponentInfo &compInfo, + const std::vector<int> &detIds, const size_t index, + SpectraMappings &mappings) { + + auto childGroup = detector(parentGroup, compInfo, detIds, index); + + // Additional mapping information written. + writeDetectorCount(childGroup, mappings); + // Note that the detector list is the same as detector_number, but it is + // ordered by spectrum index 0 - N, whereas detector_number is just written + // out in the order the detectors are encountered in the bank. + writeDetectorList(childGroup, mappings); + writeDetectorIndex(childGroup, mappings); + writeSpectra(childGroup, mappings); + } + +private: + const Mode m_mode; + + H5::Group openOrCreateGroup(const H5::Group &parent, const std::string &name, + const std::string &classType) { + + if (m_mode == Mode::Append) { + // Find by class and by name + auto results = utilities::findGroups(parent, classType); + for (auto &result : results) { + auto resultName = H5_OBJ_NAME(result); + // resultName gives full path. We match the last name on the path + if (std::regex_match(resultName, std::regex(".*/" + name + "$"))) { + return result; + } + } + } + // We can't find it, or we are writing from scratch anyway + return tryCreateGroup(parent, name); + } + + // function to create a simple sub-group that has a nexus class attribute, + // inside a parent group. + H5::Group simpleNXSubGroup(H5::Group &parent, const std::string &name, + const std::string &nexusAttribute) { + H5::Group subGroup = openOrCreateGroup(parent, name, nexusAttribute); + writeStrAttribute(subGroup, NX_CLASS, nexusAttribute); + return subGroup; + } +}; // class NexusGeometrySaveImpl + +/* + * Function: saveInstrument + * calls the save methods to write components to file after exception + * checking. Produces a Nexus format file containing the Instrument geometry + * and metadata. + * + * @param compInfo : componentInfo object. + * @param detInfo : detectorInfo object. + * @param fullPath : save destination as full path. + * @param rootName : name of root entry + * @param logger : logging object + * @param append : append mode, means openting and appending to existing file. + * If false, creates new file. + * @param reporter : (optional) report to progressBase. + */ +void saveInstrument(const Geometry::ComponentInfo &compInfo, + const Geometry::DetectorInfo &detInfo, + const std::string &fullPath, const std::string &rootName, + AbstractLogger &logger, bool append, + Kernel::ProgressBase *reporter) { + + validateInputs(logger, fullPath, compInfo); + // IDs of all detectors in Instrument + H5::Group rootGroup; + H5::H5File file; + if (append) { + file = H5::H5File(fullPath, H5F_ACC_RDWR); // open file + rootGroup = file.openGroup(rootName); + } else { + file = H5::H5File(fullPath, H5F_ACC_TRUNC); // open file + rootGroup = file.createGroup(rootName); + } + + writeStrAttribute(rootGroup, NX_CLASS, NX_ENTRY); + + using Mode = NexusGeometrySaveImpl::Mode; + NexusGeometrySaveImpl writer(append ? Mode::Append : Mode::Trunc); + // save and capture NXinstrument (component root) + H5::Group instrument = writer.instrument(rootGroup, compInfo); + + // save NXsource + writer.source(instrument, compInfo); + + // save NXsample + writer.sample(rootGroup, compInfo); + + const auto &detIds = detInfo.detectorIDs(); + // save NXdetectors + std::list<size_t> saved_indices; + // Looping from highest to lowest component index is critical + for (size_t index = compInfo.root() - 1; index >= detInfo.size(); --index) { + if (Geometry::ComponentInfoBankHelpers::isSaveableBank(compInfo, index)) { + if (isDesiredNXDetector(index, saved_indices, compInfo)) { + if (reporter != nullptr) + reporter->report(); + writer.detector(instrument, compInfo, detIds, index); + saved_indices.push_back(index); // Now record the fact that children of + // this are not needed as NXdetectors + } + } + } + + // save NXmonitors + for (size_t index = 0; index < detInfo.size(); ++index) { + if (detInfo.isMonitor(index)) { + if (reporter != nullptr) + reporter->report(); + writer.monitor(instrument, compInfo, detIds[index], index); + } + } + + file.close(); // close file + +} // saveInstrument + +/** + * Function: saveInstrument (overload) + * calls the save methods to write components to file after exception + * checking. Produces a Nexus format file containing the Instrument geometry + * and metadata. + * + * @param instrPair : instrument 2.0 object. + * @param fullPath : save destination as full path. + * @param rootName : name of root entry + * @param logger : logging object + * @param append : append mode, means openting and appending to existing file. + * If false, creates new file. + * @param reporter : (optional) report to progressBase. + */ +void saveInstrument( + const std::pair<std::unique_ptr<Geometry::ComponentInfo>, + std::unique_ptr<Geometry::DetectorInfo>> &instrPair, + const std::string &fullPath, const std::string &rootName, + AbstractLogger &logger, bool append, Kernel::ProgressBase *reporter) { + + const Geometry::ComponentInfo &compInfo = (*instrPair.first); + const Geometry::DetectorInfo &detInfo = (*instrPair.second); + + return saveInstrument(compInfo, detInfo, fullPath, rootName, logger, append, + reporter); +} + +void saveInstrument(const Mantid::API::MatrixWorkspace &ws, + const std::string &fullPath, const std::string &rootName, + AbstractLogger &logger, bool append, + Kernel::ProgressBase *reporter) { + + const auto &detInfo = ws.detectorInfo(); + const auto &compInfo = ws.componentInfo(); + + // Exception handling. + validateInputs(logger, fullPath, compInfo); + // IDs of all detectors in Instrument + const auto &detIds = detInfo.detectorIDs(); + + H5::Group rootGroup; + H5::H5File file; + if (append) { + file = H5::H5File(fullPath, H5F_ACC_RDWR); // open file + rootGroup = file.openGroup(rootName); + } else { + file = H5::H5File(fullPath, H5F_ACC_TRUNC); // open file + rootGroup = file.createGroup(rootName); + } + + writeStrAttribute(rootGroup, NX_CLASS, NX_ENTRY); + + using Mode = NexusGeometrySaveImpl::Mode; + NexusGeometrySaveImpl writer(append ? Mode::Append : Mode::Trunc); + // save and capture NXinstrument (component root) + H5::Group instrument = writer.instrument(rootGroup, compInfo); + + // save NXsource + writer.source(instrument, compInfo); + + // save NXsample + writer.sample(rootGroup, compInfo); + + // save NXdetectors + auto detToIndexMap = +<<<<<<< HEAD + ws.getDetectorIDToWorkspaceIndexMap(false /*do not throw if multiples*/); +======= + ws.getDetectorIDToWorkspaceIndexMap(true /*throw if multiples*/); + // save NXdetectors +>>>>>>> 0f9c9150d20a44750bfcea8438f981c177dd2cf7 + std::list<size_t> saved_indices; + // Looping from highest to lowest component index is critical + for (size_t index = compInfo.root() - 1; index >= detInfo.size(); --index) { + if (Geometry::ComponentInfoBankHelpers::isSaveableBank(compInfo, index)) { + + if (isDesiredNXDetector(index, saved_indices, compInfo)) { + // Make spectra detector mappings that can be used + SpectraMappings mappings = + makeMappings(compInfo, detToIndexMap, ws.indexInfo(), + ws.spectrumInfo(), detIds, index); + + if (reporter != nullptr) + reporter->report(); + writer.detector(instrument, compInfo, detIds, index, mappings); + saved_indices.push_back(index); // Now record the fact that children of + // this are not needed as NXdetectors + } + } + } + + // save NXmonitors + for (size_t index = 0; index < detInfo.size(); ++index) { + if (detInfo.isMonitor(index)) { + // Make spectra detector mappings that can be used + SpectraMappings mappings = + makeMappings(compInfo, detToIndexMap, ws.indexInfo(), + ws.spectrumInfo(), detIds, index); + + if (reporter != nullptr) + reporter->report(); + writer.monitor(instrument, compInfo, detIds[index], index, mappings); + } + } + + file.close(); // close file +} + +// saveInstrument + +} // namespace NexusGeometrySave +} // namespace NexusGeometry +} // namespace Mantid diff --git a/Framework/NexusGeometry/test/NexusGeometryParserTest.h b/Framework/NexusGeometry/test/NexusGeometryParserTest.h index fc23bd8eee4a2d8599ffe3af601b301728aa25da..aa0a1eed0a8303147fc82b4ae1e95dc74074a3a6 100644 --- a/Framework/NexusGeometry/test/NexusGeometryParserTest.h +++ b/Framework/NexusGeometry/test/NexusGeometryParserTest.h @@ -66,7 +66,7 @@ public: void test_pixel_shape_as_mesh() { auto instrument = NexusGeometryParser::createInstrument( - "/home/spu92482/Downloads/DETGEOM_example_1.nxs", + "/Users/spu92482/Downloads/DETGEOM_example_1.nxs", std::make_unique<testing::NiceMock<MockLogger>>()); auto beamline = extractBeamline(*instrument); auto &compInfo = *beamline.first; @@ -80,12 +80,14 @@ public: TS_ASSERT(shape1Mesh); TS_ASSERT(shape2Mesh); TS_ASSERT_EQUALS(shape1Mesh, shape2Mesh); // pixel shape - all identical. + TSM_ASSERT_EQUALS("Same objects, same address", &shape1, + &shape2); // Shapes are shared when identical TS_ASSERT_EQUALS(shape1Mesh->numberOfTriangles(), 2); TS_ASSERT_EQUALS(shape1Mesh->numberOfVertices(), 4); } void test_pixel_shape_as_cylinders() { auto instrument = NexusGeometryParser::createInstrument( - "/home/spu92482/Downloads/DETGEOM_example_2.nxs", + "/Users/spu92482/Downloads/DETGEOM_example_2.nxs", std::make_unique<testing::NiceMock<MockLogger>>()); auto beamline = extractBeamline(*instrument); auto &compInfo = *beamline.first; @@ -94,6 +96,8 @@ public: auto &shape1 = compInfo.shape(0); auto &shape2 = compInfo.shape(1); + TSM_ASSERT_EQUALS("Same objects, same address", &shape1, + &shape2); // Shapes are shared when identical auto *shape1Cylinder = dynamic_cast<const Geometry::CSGObject *>(&shape1); // Test detectors auto *shape2Cylinder = dynamic_cast<const Geometry::CSGObject *>(&shape2); @@ -109,7 +113,7 @@ public: } void test_detector_shape_as_mesh() { auto instrument = NexusGeometryParser::createInstrument( - "/home/spu92482/Downloads/DETGEOM_example_3.nxs", + "/Users/spu92482/Downloads/DETGEOM_example_3.nxs", std::make_unique<testing::NiceMock<MockLogger>>()); auto beamline = extractBeamline(*instrument); auto &compInfo = *beamline.first; @@ -117,19 +121,21 @@ public: TS_ASSERT_EQUALS(detInfo.size(), 4); auto &shape1 = compInfo.shape(0); auto &shape2 = compInfo.shape(1); + TSM_ASSERT_DIFFERS("Different objects, different addresses", &shape1, + &shape2); // Shapes are not shared auto *shape1Mesh = dynamic_cast<const Geometry::MeshObject2D *>(&shape1); // Test detectors auto *shape2Mesh = dynamic_cast<const Geometry::MeshObject2D *>(&shape2); TS_ASSERT(shape1Mesh); TS_ASSERT(shape2Mesh); - TS_ASSERT_EQUALS(shape1Mesh, shape2Mesh); // pixel shape - all identical. TS_ASSERT_EQUALS(shape1Mesh->numberOfTriangles(), 1); TS_ASSERT_EQUALS(shape1Mesh->numberOfVertices(), 3); - // auto componentInfo = *beamline.first; + TS_ASSERT_EQUALS(shape2Mesh->numberOfTriangles(), 1); + TS_ASSERT_EQUALS(shape2Mesh->numberOfVertices(), 3); } void test_detector_shape_as_cylinders() { auto instrument = NexusGeometryParser::createInstrument( - "/home/spu92482/Downloads/DETGEOM_example_4.nxs", + "/Users/spu92482/Downloads/DETGEOM_example_4.nxs", std::make_unique<testing::NiceMock<MockLogger>>()); auto beamline = extractBeamline(*instrument); diff --git a/Framework/NexusGeometry/test/NexusGeometrySaveTest.h.orig b/Framework/NexusGeometry/test/NexusGeometrySaveTest.h.orig new file mode 100644 index 0000000000000000000000000000000000000000..876b52172751aca7e535f6c66daf4ee6739edabb --- /dev/null +++ b/Framework/NexusGeometry/test/NexusGeometrySaveTest.h.orig @@ -0,0 +1,1170 @@ +// Mantid Repository : https://github.com/mantidproject/mantid +// +// Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI, +// NScD Oak Ridge National Laboratory, European Spallation Source +// & Institut Laue - Langevin +// SPDX - License - Identifier: GPL - 3.0 + +#ifndef MANTID_NEXUSGEOMETRY_NEXUSGEOMETRYSAVETEST_H_ +#define MANTID_NEXUSGEOMETRY_NEXUSGEOMETRYSAVETEST_H_ + +#include "MantidGeometry/Instrument/ComponentInfo.h" +#include "MantidGeometry/Instrument/ComponentInfoBankHelpers.h" +#include "MantidGeometry/Instrument/DetectorInfo.h" +#include "MantidGeometry/Instrument/InstrumentVisitor.h" +#include "MantidKernel/EigenConversionHelpers.h" +#include "MantidNexusGeometry/NexusGeometryDefinitions.h" +#include "MantidNexusGeometry/NexusGeometrySave.h" +#include "MantidTestHelpers/ComponentCreationHelper.h" +#include "MantidTestHelpers/FileResource.h" +#include "MantidTestHelpers/NexusFileReader.h" + +#include "mockobjects.h" +#include <cxxtest/TestSuite.h> +#include <gmock/gmock.h> + +using namespace Mantid::NexusGeometry; + +class NexusGeometrySaveTest : public CxxTest::TestSuite { +private: + testing::NiceMock<MockLogger> m_mockLogger; + +public: + // This pair of boilerplate methods prevent the suite being created statically + // This means the constructor isn't called when running other tests + static NexusGeometrySaveTest *createSuite() { + return new NexusGeometrySaveTest(); + } + static void destroySuite(NexusGeometrySaveTest *suite) { delete suite; } + + NexusGeometrySaveTest() {} + + /* +==================================================================== + +IO PRECONDITIONS TESTS + +DESCRIPTION: + +The following tests are written to document the behaviour of the SaveInstrument +method when a valid and invalid beamline Instrument is attempted to be saved +out from memory to file. Included also are tests that document the behaviour +when a valid (.nxs, .hdf5 ) or invalid output file extension is attempted to be +used. + +==================================================================== +*/ + + void test_providing_invalid_path_throws() { + + FileResource fileResource("invalid_path_to_file_test_file.hdf5"); + const std::string badDestinationPath = + "false_directory\\" + fileResource.fullPath(); + + auto instrument = ComponentCreationHelper::createMinimalInstrument( + V3D(0, 0, -10), V3D(0, 0, 0), V3D(0, 0, 10)); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + TS_ASSERT_THROWS( + NexusGeometrySave::saveInstrument( + instr, badDestinationPath, DEFAULT_ROOT_ENTRY_NAME, m_mockLogger), + std::invalid_argument &); + } + + void test_progress_reporting() { + + const int nbanks = 2; + MockProgressBase progressRep; + EXPECT_CALL(progressRep, doReport(testing::_)) + .Times(nbanks); // Progress report once for each bank + + FileResource fileResource("progress_report_test_file.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + auto instrument = ComponentCreationHelper::createTestInstrumentRectangular2( + nbanks /*number of banks*/, 2 /*number of pixels per bank*/); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger, + false /*strict*/, &progressRep); + TS_ASSERT(testing::Mock::VerifyAndClearExpectations(&progressRep)); + } + + void test_false_file_extension_throws() { + + FileResource fileResource("invalid_extension_test_file.abc"); + std::string destinationFile = fileResource.fullPath(); + + auto instrument = ComponentCreationHelper::createMinimalInstrument( + V3D(0, 0, -10), V3D(0, 0, 0), V3D(0, 0, 10)); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + TS_ASSERT_THROWS(NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, + m_mockLogger), + std::invalid_argument &); + // Same error but log rather than throw + MockLogger logger; + EXPECT_CALL(logger, error(testing::_)).Times(1); + TS_ASSERT_THROWS(NexusGeometrySave::saveInstrument( + instr, destinationFile, DEFAULT_ROOT_ENTRY_NAME, + logger, false /*append*/), + std::invalid_argument); + TS_ASSERT(testing::Mock::VerifyAndClearExpectations(&logger)); + } + + void test_instrument_without_sample_throws() { + + auto const &instrument = + ComponentCreationHelper::createInstrumentWithOptionalComponents( + true, false, true); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + FileResource fileResource("check_no_sample_throws_test_file.hdf5"); + auto destinationFile = fileResource.fullPath(); + + // instrument cache + auto const &compInfo = (*instr.first); + + TS_ASSERT(compInfo.hasDetectorInfo()); // rule out throw by no detector info + TS_ASSERT(compInfo.hasSource()); // rule out throw by no source + TS_ASSERT(!compInfo.hasSample()); // verify component has no sample + + TS_ASSERT_THROWS(NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, + m_mockLogger), + std::invalid_argument &); + + // Same error but log rather than throw + MockLogger logger; + EXPECT_CALL(logger, error(testing::_)).Times(1); + TS_ASSERT_THROWS(NexusGeometrySave::saveInstrument( + instr, destinationFile, DEFAULT_ROOT_ENTRY_NAME, + logger, false /*append*/), + std::invalid_argument &); + TS_ASSERT(testing::Mock::VerifyAndClearExpectations(&logger)); + } + + void test_instrument_without_source_throws() { + + auto const &instrument = + ComponentCreationHelper::createInstrumentWithOptionalComponents( + false, true, true); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // instrument cache + auto &compInfo = (*instr.first); + + FileResource fileResource("check_no_source_throws_test_file.hdf5"); + auto destinationFile = fileResource.fullPath(); + + TS_ASSERT(compInfo.hasDetectorInfo()); // rule out throw by no detector info + TS_ASSERT(compInfo.hasSample()); // rule out throw by no sample + TS_ASSERT(!compInfo.hasSource()); // verify component has no source + + TS_ASSERT_THROWS(NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, + m_mockLogger), + std::invalid_argument &); + // Same error but log rather than throw + MockLogger logger; + EXPECT_CALL(logger, error(testing::_)).Times(1); + TS_ASSERT_THROWS(NexusGeometrySave::saveInstrument( + instr, destinationFile, DEFAULT_ROOT_ENTRY_NAME, + logger, false /*append*/), + std::invalid_argument &); + TS_ASSERT(testing::Mock::VerifyAndClearExpectations(&logger)); + } + + void test_sample_not_at_origin_throws() { + + auto instrument = ComponentCreationHelper::createMinimalInstrument( + V3D(0, 0, -10), V3D(0, 0, 2), V3D(0, 0, 10)); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + FileResource fileResource("check_nxsource_group_test_file.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + TS_ASSERT_THROWS(NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, + m_mockLogger), + std::invalid_argument &); + // Same error but log rather than throw + MockLogger logger; + EXPECT_CALL(logger, error(testing::_)).Times(1); + TS_ASSERT_THROWS(NexusGeometrySave::saveInstrument( + instr, destinationFile, DEFAULT_ROOT_ENTRY_NAME, + logger, false /*append*/), + std::invalid_argument &); + TS_ASSERT(testing::Mock::VerifyAndClearExpectations(&logger)); + } + + /* + ==================================================================== + + NEXUS FILE FORMAT TESTS + + DESCRIPTION: + + The following tests document that the file format produced by saveInstrument is + compliant to the present Nexus standard as of the date corresponding to the + latest version of this document. + + ==================================================================== + */ + + void test_root_group_is_nxentry_class() { + // this test checks that the root group of the output file in saveInstrument + // has NXclass attribute of NXentry. as required by the Nexus file format. + + // RAII file resource for test file destination + FileResource fileResource("check_nxentry_group_test_file.nxs"); + std::string destinationFile = fileResource.fullPath(); + + // test instrument + auto instrument = ComponentCreationHelper::createMinimalInstrument( + V3D(0, 0, -10) /*source position*/, V3D(0, 0, 0) /*sample position*/, + V3D(0, 0, 10) /*bank position*/); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // test utility to check output file + NexusFileReader tester(destinationFile); + + // assert the group at the root H5 path is NXentry + TS_ASSERT(tester.groupHasNxClass(NX_ENTRY, DEFAULT_ROOT_ENTRY_NAME)); + } + + void test_nxinstrument_group_exists_in_root_group() { + // this test checks that inside of the NXentry root group, the instrument + // data is saved to a group of NXclass NXinstrument + + // RAII file resource for test file destination + FileResource fileResource("check_nxinstrument_group_test_file.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + // test instrument with some geometry + auto instrument = ComponentCreationHelper::createMinimalInstrument( + V3D(0, 0, -10) /*source position*/, V3D(0, 0, 0) /*sample position*/, + V3D(0, 0, 10) /*bank position*/); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // call saveinstrument taking test instrument as parameter + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // test utility to check the output file + NexusFileReader tester(destinationFile); + + // assert that inside a group with attribute NXentry, which as per the + // previous test we know to be the root group, there exists a group of + // NXclass NXinstrument. + TS_ASSERT(tester.parentNXgroupHasChildNXgroup(NX_ENTRY, NX_INSTRUMENT)); + } + + void test_NXInstrument_name_is_aways_instrument() { + // NXInstrument group name is always written as "instrument" for legacy + // compatibility reasons + + // RAII file resource for test file destination + FileResource fileResource("check_instrument_name_test_file.nxs"); + auto destinationFile = fileResource.fullPath(); + + // test instrument + auto instrument = ComponentCreationHelper::createMinimalInstrument( + V3D(0, 0, -10) /*source position*/, V3D(0, 0, 0) /*sample position*/, + V3D(0, 0, 10) /*bank position*/); + + // set name of instrument + instrument->setName("test_instrument_name"); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // call saveInstrument passing the test instrument as parameter. + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, + m_mockLogger); // saves instrument + + // test utility to check the output file. + NexusFileReader testUtility(destinationFile); + + const auto &compInfo = *instr.first; + + // full H5 path to the NXinstrument group + FullNXPath path = {DEFAULT_ROOT_ENTRY_NAME, compInfo.name(compInfo.root())}; + + // assert no exception thrown on open of instrument group in file with + // manually set name. + TS_ASSERT_THROWS_NOTHING(testUtility.openfullH5Path(path)); + + // assert group is indeed NXinstrument. + TS_ASSERT(testUtility.hasNXAttributeInGroup(NX_INSTRUMENT, path)); + + // assert the dataset containing the instrument name has been correctly + // stored also. + TS_ASSERT(testUtility.dataSetHasStrValue( + NAME, compInfo.name(compInfo.root()), path)); + } + +<<<<<<< HEAD + void + test_NXclass_without_name_is_assigned_unique_default_name_for_each_group() { + // this test will try to save and unnamed instrument with multiple unnamed + // detector banks, to verify that the unique group names which + // saveInstrument provides for each NXclass do not throw a H5 error due to + // duplication of group names. If any group in the same tree path share the + // same name, HDF5 will throw a group exception. In this test, we expect no + // such exception to throw. + + // RAII file resource for test file destination. + FileResource fileResource("default_group_names_test.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + // unnamed ("") instrument with multiple unnamed detector banks ("") + auto instrument = ComponentCreationHelper::createTestInstrumentRectangular2( + 2 /*number of banks*/, 2 /*number of pixels*/, 0.008, true); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + TS_ASSERT_THROWS_NOTHING(NexusGeometrySave::saveInstrument( + instr, destinationFile, DEFAULT_ROOT_ENTRY_NAME, m_mockLogger)); + } + +======= +>>>>>>> 76b68e5e679f2310789e1d43e5d8aecf093666fa + void test_nxsource_group_exists_and_is_in_nxinstrument_group() { + // this test checks that inside of the NXinstrument group, the the source + // data is saved to a group of NXclass NXsource + + // RAII file resource for test file destination. + FileResource fileResource("check_nxsource_group_test_file.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + // test instrument + auto instrument = ComponentCreationHelper::createMinimalInstrument( + V3D(0, 0, -10) /*source position*/, V3D(0, 0, 0) /*sample position*/, + V3D(0, 0, 10) /*bank position*/); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // call saveInstrument passing test instrument as parameter + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // test utility to check output file + NexusFileReader tester(destinationFile); + + // assert that inside a group with attribute NXinstrument, which as per the + // previous test we know to be the instrument group, there exists a group of + // NXclass NXsource. + TS_ASSERT(tester.parentNXgroupHasChildNXgroup(NX_INSTRUMENT, NX_SOURCE)); + } + + void test_nxsample_group_exists_and_is_in_nxentry_group() { + + FileResource fileResource("check_nxsource_group_test_file.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + auto instrument = ComponentCreationHelper::createMinimalInstrument( + V3D(0, 0, -10) /*source position*/, V3D(0, 0, 0) /*sample position*/, + V3D(0, 0, 10) /*bank position*/); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // instrument cache + auto &compInfo = (*instr.first); + + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + NexusFileReader tester(destinationFile); + + TS_ASSERT(compInfo.hasSample()); + TS_ASSERT(tester.parentNXgroupHasChildNXgroup(NX_ENTRY, NX_SAMPLE)); + } + + void test_correct_number_of_detectors_saved() { + + ScopedFileHandle fileResource("check_num_of_banks_test.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + int banksInInstrument = 3; + + auto instrument = ComponentCreationHelper::createTestInstrumentRectangular2( + banksInInstrument /*number of banks*/, 4 /*pixels (arbitrary)*/); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + NexusFileReader tester(destinationFile); + FullNXPath path = {DEFAULT_ROOT_ENTRY_NAME, + "basic_rect" /*instrument name*/}; + + int numOfNXDetectors = tester.countNXgroup(path, NX_DETECTOR); + + TS_ASSERT_EQUALS(numOfNXDetectors, banksInInstrument); + } + + /* +==================================================================== + +NEXUS TRANSFOMATIONS TESTS + +DESCRIPTION: + +The following tests document that saveInstrument will find and write detectors +and other Instrument components to file in Nexus format, and where there exists +transformations in ComponentInfo and DetectorInfo, SaveInstrument will generate +'NXtransformations' groups to contain the corresponding component +rotations/translations, and pixel offsets in any 'NXdetector' found in the +Instrument cache. + +==================================================================== +*/ + + void + test_rotation_of_NXdetector_written_to_file_is_same_as_in_component_info() { + + /* + test scenario: pass into saveInstrument an instrument with manually set + non-zero rotation in a detector bank. Expectation: test utilty will search + file for orientaion dataset, read the magnitude of the angle, and the axis + vector. The output quaternion from file will be compared to the input + quaternion manually set. Asserts that they are approximately equal, + indicating that saveinstrument has correctly written the orientation data. + */ + + // RAII file resource for test file destination + FileResource fileResource( + "check_rotation_written_to_nxdetector_test_file.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + // prepare rotation for instrument + const Quat bankRotation(15, V3D(0, 1, 0)); + const Quat detRotation(30, V3D(0, 1, 0)); + + // create test instrument and get cache + auto instrument = + ComponentCreationHelper::createSimpleInstrumentWithRotation( + Mantid::Kernel::V3D(0, 0, -10), Mantid::Kernel::V3D(0, 0, 0), + Mantid::Kernel::V3D(0, 0, 10), bankRotation, detRotation); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // call saveInstrument + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // instance of test utility to check saved file + NexusFileReader tester(destinationFile); + + // full path to group to be opened in test utility + FullNXPath path = { + DEFAULT_ROOT_ENTRY_NAME, + "test-instrument-with-detector-rotations" /*instrument name*/, + "detector-stage" /*bank name*/, TRANSFORMATIONS}; + + // get angle magnitude in dataset + double angleInFile = tester.readDoubleFromDataset(ORIENTATION, path); + + // get axis or rotation + std::string attributeName = "vector"; + std::vector<double> axisInFile = tester.readDoubleVectorFrom_d_Attribute( + attributeName, ORIENTATION, path); + V3D axisVectorInFile = {axisInFile[0], axisInFile[1], axisInFile[2]}; + + // Eigen copy of bankRotation for assertation + Eigen::Quaterniond bankRotationCopy = + Mantid::Kernel::toQuaterniond(bankRotation); + + // bank rotation in file as eigen Quaternion for assertation + Eigen::Quaterniond rotationInFile = + Mantid::Kernel::toQuaterniond(Quat(angleInFile, axisVectorInFile)); + + TS_ASSERT(rotationInFile.isApprox(bankRotationCopy)); + } + + void + test_rotation_of_NXmonitor_written_to_file_is_same_as_in_component_info() { + + /* + test scenario: pass into saveInstrument an instrument with manually set + non-zero rotation in a monitor. Expectation: test utilty will search + file for orientaion dataset, read the magnitude of the angle, and the + axis vector. The output quaternion from file will be compared to the + input quaternion manually set. Asserts that they are approximately equal, + indicating that saveinstrument has correctly written the orientation + data. + */ + + // RAII file resource for test file destination + FileResource fileResource( + "check_rotation_written_to_nx_monitor_test_file.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + // prepare rotation for instrument + const V3D monitorPosition(0, 1, 0); + const Quat monitorRotation(30, V3D(0, 1, 0)); + + // create test instrument and get cache + auto instrument = + ComponentCreationHelper::createMinimalInstrumentWithMonitor( + monitorPosition, monitorRotation); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // call saveInstrument + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // instance of test utility to check saved file + NexusFileReader tester(destinationFile); + + // full path to group to be opened in test utility + FullNXPath path = {DEFAULT_ROOT_ENTRY_NAME, "test-instrument-with-monitor", + "test-monitor", TRANSFORMATIONS}; + + // get angle magnitude in dataset + double angleInFile = tester.readDoubleFromDataset(ORIENTATION, path); + + // get axis or rotation + std::string attributeName = "vector"; + std::vector<double> axisInFile = tester.readDoubleVectorFrom_d_Attribute( + attributeName, ORIENTATION, path); + V3D axisVectorInFile = {axisInFile[0], axisInFile[1], axisInFile[2]}; + + // Eigen copy of monitorRotation for assertation + Eigen::Quaterniond monitorRotationCopy = + Mantid::Kernel::toQuaterniond(monitorRotation); + + // bank rotation in file as eigen Quaternion for assertation + Eigen::Quaterniond rotationInFile = + Mantid::Kernel::toQuaterniond(Quat(angleInFile, axisVectorInFile)); + + TS_ASSERT(rotationInFile.isApprox(monitorRotationCopy)); + } + + void test_location_written_to_file_is_same_as_in_component_info() { + + /* + test scenario: pass into saveInstrument an instrument with manually set + non-zero translation in the source. Expectation: test utilty will search + file for location dataset, read the norm of the vector, and the unit vector. + The output vector from file will be compared to the input vector manually + set. Asserts that they are approximately equal, indicating that + saveinstrument has correctly written the location data. + */ + + // RAII file resource for test file destination + FileResource fileResource( + "check_location_written_to_nxsource_test_file.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + // prepare location for instrument + const V3D sourceLocation(0, 0, 10); + + // create test instrument and get cache + auto instrument = + ComponentCreationHelper::createInstrumentWithSourceRotation( + sourceLocation, Mantid::Kernel::V3D(0, 0, 0), + Mantid::Kernel::V3D(0, 0, 10), Quat(90, V3D(0, 1, 0))); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // call saveInstrument + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // instance of test utility to check saved file + NexusFileReader tester(destinationFile); + + // full path to group to be opened in test utility + FullNXPath path = {DEFAULT_ROOT_ENTRY_NAME, + "test-instrument" /*instrument name*/, + "source" /*source name*/, TRANSFORMATIONS}; + + // get magnitude of vector in dataset + double normInFile = tester.readDoubleFromDataset(LOCATION, path); + + // get axis or rotation + std::string attributeName = "vector"; + std::vector<double> data = + tester.readDoubleVectorFrom_d_Attribute(attributeName, LOCATION, path); + Eigen::Vector3d unitVecInFile = {data[0], data[1], data[2]}; + + // Eigen copy of sourceRotation for assertation + Eigen::Vector3d sourceLocationCopy = + Mantid::Kernel::toVector3d(sourceLocation); + + auto positionInFile = normInFile * unitVecInFile; + + TS_ASSERT(positionInFile.isApprox(sourceLocationCopy)); + } + + void + test_rotation_of_nx_source_written_to_file_is_same_as_in_component_info() { + + /* + test scenario: pass into saveInstrument an instrument with manually set + non-zero rotation in the source. Expectation: test utilty will search file + for orientaion dataset, read the magnitude of the angle, and the axis + vector. The output quaternion from file will be compared to the input + quaternion manually set. Asserts that they are approximately equal, + indicating that saveinstrument has correctly written the orientation data. + */ + + // RAII file resource for test file destination + FileResource fileResource( + "check_rotation_written_to_nxsource_test_file.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + // prepare rotation for instrument + const Quat sourceRotation(90, V3D(0, 1, 0)); + + // create test instrument and get cache + auto instrument = + ComponentCreationHelper::createInstrumentWithSourceRotation( + Mantid::Kernel::V3D(0, 0, -10), Mantid::Kernel::V3D(0, 0, 0), + Mantid::Kernel::V3D(0, 0, 10), sourceRotation); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // call saveInstrument + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // instance of test utility to check saved file + NexusFileReader tester(destinationFile); + + // full path to group to be opened in test utility + FullNXPath path = {DEFAULT_ROOT_ENTRY_NAME, + "test-instrument" /*instrument name*/, + "source" /*source name*/, TRANSFORMATIONS}; + + // get angle magnitude in dataset + double angleInFile = tester.readDoubleFromDataset(ORIENTATION, path); + + // get axis or rotation + std::string attributeName = "vector"; + std::vector<double> axisInFile = tester.readDoubleVectorFrom_d_Attribute( + attributeName, ORIENTATION, path); + V3D axisVectorInFile = {axisInFile[0], axisInFile[1], axisInFile[2]}; + + // Eigen copy of sourceRotation for assertation + Eigen::Quaterniond sourceRotationCopy = + Mantid::Kernel::toQuaterniond(sourceRotation); + + // source rotation in file as eigen Quaternion for assertation + Eigen::Quaterniond rotationInFile = + Mantid::Kernel::toQuaterniond(Quat(angleInFile, axisVectorInFile)); + + TS_ASSERT(rotationInFile.isApprox(sourceRotationCopy)); + } + + void + test_an_nx_class_location_is_not_written_when_component_position_is_at_origin() { + + /* + test scenario: pass into saveInstrument an instrument with zero source + translation. Inspection: test utilty will search file for location + dataset and should return false, indicating that saveInstrument + identified the transformation as effectively zero, and did not write the + transformation to file + */ + + // RAII file resource for test file destination + FileResource fileResource("origin_nx_source_location_file_test.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + // prepare geometry for instrument + const V3D detectorLocation(0, 0, 10); + const V3D sourceLocation(0, 0, 0); // set to zero for test + const Quat sourceRotation(90, V3D(0, 1, 0)); + + // create test instrument and get cache + auto instrument = + ComponentCreationHelper::createInstrumentWithSourceRotation( + sourceLocation, Mantid::Kernel::V3D(0, 0, 0), detectorLocation, + sourceRotation); // source rotation + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // call saveInstrument + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // instance of test utility to check saved file + NexusFileReader tester(destinationFile); + + // full path to group to be opened in test utility + FullNXPath path = {DEFAULT_ROOT_ENTRY_NAME, + "test-instrument" /*instrument name*/, + "source" /*source name*/, TRANSFORMATIONS}; + + // assertations + bool hasLocation = tester.hasDataset(LOCATION, path); + TS_ASSERT(!hasLocation); + } + + void test_nx_detector_rotation_not_written_when_is_zero() { + + /* + test scenario: pass into saveInstrument an instrument with zero detector bank + rotation. Inspection: test utilty will search file for orientation + dataset and should return false, indicating that saveInstrument + identified the transformation as effectively zero, and did not write the + transformation to file + */ + + const V3D detectorLocation(0, 0, 10); // arbitrary non-zero + const V3D sourceLocation(0, 0, -10); // arbitrary + + const Quat someRotation(30, V3D(1, 0, 0)); // arbitrary + const Quat bankRotation(0, V3D(0, 0, 1)); // set (angle) to zero + + // RAII file resource for test file destination + FileResource fileResource("zero_nx_detector_rotation_file_test.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + // test instrument with zero source rotation + auto instrument = + ComponentCreationHelper::createSimpleInstrumentWithRotation( + sourceLocation, Mantid::Kernel::V3D(0, 0, 0), detectorLocation, + bankRotation, someRotation); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // full path to access NXtransformations group with test utility + FullNXPath path = { + DEFAULT_ROOT_ENTRY_NAME, + "test-instrument-with-detector-rotations" /*instrument name*/, + "detector-stage" /*bank name*/, TRANSFORMATIONS}; + + // call saveInstrument passing test instrument as parameter + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // test utility to check output file + NexusFileReader tester(destinationFile); + + // assert rotation not written to file + bool hasRotation = tester.hasDataset(ORIENTATION, path); + TS_ASSERT(!hasRotation); + } + + void test_nx_monitor_rotation_not_written_when_is_zero() { + + /* + test scenario: pass into saveInstrument an instrument with zero monitor + rotation. Inspection: test utilty will search file for orientation dataset + and should return false, indicating that saveInstrument identified the + transformation as effectively zero, and did not write the transformation to + file + */ + + // RAII file resource for test file destination + FileResource fileResource("zero_nx_monitor_rotation_file_test.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + V3D someLocation(0.0, 0.0, -5.0); // arbitrary monitor location + + // test instrument with zero monitor rotation + auto instrument = + ComponentCreationHelper::createMinimalInstrumentWithMonitor( + someLocation, Quat(0, V3D(0, 1, 0)) /*monitor rotation of zero*/); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // full path to group to be opened in test utility + FullNXPath path = {DEFAULT_ROOT_ENTRY_NAME, "test-instrument-with-monitor", + "test-monitor", TRANSFORMATIONS}; + + // call saveInstrument passing test instrument as parameter + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // test utility to check output file + NexusFileReader tester(destinationFile); + + // assert that no dataset named 'orientation' exists in output file + bool hasRotation = tester.hasDataset(ORIENTATION, path); + TS_ASSERT(!hasRotation); + } + + void test_source_rotation_not_written_when_is_zero() { + + /* + test scenario: pass into saveInstrument an instrument with zero source + rotation. Inspection: test utilty will search file for orientation dataset + and should return false, indicating that saveInstrument identified the + transformation as effectively zero, and did not write the transformation to + file + */ + + // geometry for test instrument + const V3D detectorLocation(0, 0, 10); + const V3D sourceLocation(-10, 0, 0); + const Quat sourceRotation(0, V3D(0, 0, 1)); // set (angle) to zero + + // RAII file resource for test file destination + FileResource inFileResource("zero_nx_source_rotation_file_test.hdf5"); + std::string destinationFile = inFileResource.fullPath(); + + // test instrument with zero rotation + auto instrument = + ComponentCreationHelper::createInstrumentWithSourceRotation( + sourceLocation, Mantid::Kernel::V3D(0, 0, 0), detectorLocation, + sourceRotation); // source rotation + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // full path to group to be opened in test utility + FullNXPath path = {DEFAULT_ROOT_ENTRY_NAME, "test-instrument", "source", + TRANSFORMATIONS}; + + // call saveinstrument passing test instrument as parameter + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // test utility to check output file + NexusFileReader tester(destinationFile); + + // assert dataset 'orientation' doesnt exist + bool hasRotation = tester.hasDataset(ORIENTATION, path); + TS_ASSERT(!hasRotation); + } + + void + test_xyz_pixel_offset_in_file_is_relative_position_from_bank_without_bank_transformations() { + + // this test will check that the pixel offsets are stored as their positions + // relative to the parent bank, ignoring any transformations + + /* + test scenario: instrument with manually set pixel offset passed into + saveInstrument. Inspection: xyz pixel offset written in file matches the + manually set offset. + */ + + // create RAII file resource for testing + FileResource fileResource("check_pixel_offset_format_test_file.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + // prepare geometry for instrument + const Quat relativeBankRotation(45.0, V3D(0.0, 1.0, 0.0)); + const Quat relativeDetRotation(45.0, V3D(0.0, 1.0, 0.0)); + const V3D absBankposition(0, 0, 10); + const V3D relativeDetposition(2.0, -2.0, 0.0); // i.e. pixel offset + + // create test instrument with one bank consisting of one detector (pixel) + auto instrument = + ComponentCreationHelper::createSimpleInstrumentWithRotation( + Mantid::Kernel::V3D(0, 0, -10), // source position + Mantid::Kernel::V3D(0, 0, 0), // sample position + absBankposition, // bank position + relativeBankRotation, // bank rotation + relativeDetRotation, // detector (pixel) rotation + relativeDetposition); // detector (pixel) position + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // call save insrument passing the test instrument as parameter + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // instance of test utility to check saved file + NexusFileReader tester(destinationFile); + FullNXPath path = { + DEFAULT_ROOT_ENTRY_NAME, + "test-instrument-with-detector-rotations" /*instrument name*/, + "detector-stage" /*bank name*/}; + + // initalise to zero for case when an offset is not written to file, + // thus its values are zero + + // read the xyz offset of the pixel from the output file + double pixelOffsetX = tester.readDoubleFromDataset(X_PIXEL_OFFSET, path); + double pixelOffsetY = tester.readDoubleFromDataset(Y_PIXEL_OFFSET, path); + + // implicitly assert that z offset is zero, and not written to file, as + // demonstrated in eairlier tests, where the same method is apled for the + // pixel offsets. + TS_ASSERT(!tester.hasDataset(Z_PIXEL_OFFSET, path)); + + // store offset in this bank to Eigen vector for testing + Eigen::Vector3d offsetInFile(pixelOffsetX, pixelOffsetY, 0); + + // assert the offset in the file is approximately the same as that specified + // manually. thus the offset written by saveInstrument has removed the + // transformations of the bank + TS_ASSERT( + offsetInFile.isApprox(Mantid::Kernel::toVector3d(relativeDetposition))); + } + + /* + ==================================================================== + + DEPENDENCY CHAIN TESTS + + DESCRIPTION: + The following tests document that saveInstrument will write the + NXtransformations dependencies as specified in the Mantid Instrument + Definition file, which says that if a translation and rotation exists, the + translation precedes the rotation, so that the NXclass depends on dataset + 'orientation', which depends on dataset 'location'. If only one + NXtransformation exists, the NXclass group will depend on it. Finally, if no + NXtransformations are present, the NXclass group will be self dependent. + + ==================================================================== + */ + + void + test_when_location_is_not_written_and_orientation_exists_dependency_is_orientation_path_and_orientation_is_self_dependent() { + + // USING SOURCE FOR DEMONSTRATION. + + /* + test scenario: saveInstrument called with zero translation, and some + non-zero rotation in source. Expected behaviour is: (dataset) 'depends_on' + has value /absoulute/path/to/orientation, and (dataset) 'orientation' has + dAttribute (AKA attribute of dataset) 'depends_on' with value "." + */ + + // geometry for test instrument + const V3D detectorLocation(0, 0, 10); // arbitrary + const Quat sourceRotation(90, V3D(0, 1, 0)); // arbitrary + const V3D sourceLocation(0, 0, 0); // set to zero + + // create RAII file resource for testing + FileResource fileResource("no_location_dependency_test.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + // test instrument with location of source at zero + auto instrument = + ComponentCreationHelper::createInstrumentWithSourceRotation( + sourceLocation, + Mantid::Kernel::V3D(0, 0, 0) /*sample position at zero*/, + detectorLocation, sourceRotation); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + FullNXPath transformationsPath = { + DEFAULT_ROOT_ENTRY_NAME, "test-instrument" /*instrument name*/, + "source" /*source name*/, TRANSFORMATIONS}; + + FullNXPath sourcePath = transformationsPath; + sourcePath.pop_back(); // source path is one level abve transformationsPath + + // call saveInstrument with test instrument as parameter + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // test utility to check output file + NexusFileReader tester(destinationFile); + + // assert what there is no 'location' dataset in NXtransformations, but + // there is the dataset 'orientation', confirming that saveInstrument has + // skipped writing a zero translation. + bool hasLocation = tester.hasDataset(LOCATION, transformationsPath); + bool hasOrientation = tester.hasDataset(ORIENTATION, transformationsPath); + TS_ASSERT(hasOrientation); // assert orientation dataset exists. + TS_ASSERT(!hasLocation); // assert location dataset doesn't exist. + + // assert that the NXsource depends on dataset 'orientation' in the + // transformationsPath, since the dataset exists. + bool sourceDependencyIsOrientation = tester.dataSetHasStrValue( + DEPENDS_ON, toNXPathString(transformationsPath) + "/" + ORIENTATION, + sourcePath); + TS_ASSERT(sourceDependencyIsOrientation); + + // assert that the orientation depends on itself, since not translation is + // present + bool orientationDependencyIsSelf = tester.hasAttributeInDataSet( + ORIENTATION, DEPENDS_ON, NO_DEPENDENCY, transformationsPath); + TS_ASSERT(orientationDependencyIsSelf); + } + + void + test_when_orientation_is_not_written_and_location_exists_dependency_is_location_path_and_location_is_self_dependent() { + + // USING SOURCE FOR DEMONSTRATION. + + /* + test scenario: saveInstrument called with zero rotation, and some + non-zero translation in source. Expected behaviour is: (dataset) + 'depends_on' has value "/absoulute/path/to/location", and (dataset) + 'location' has dAttribute (AKA attribute of dataset) 'depends_on' with + value "." + */ + + // Geometry for test instrument + const V3D detectorLocation(0.0, 0.0, 10.0); // arbitrary + const V3D sourceLocation(0.0, 0.0, -10.0); // arbitrary + const Quat sourceRotation(0.0, V3D(0.0, 1.0, 0.0)); // set to zero + + // create RAII file resource for testing + FileResource fileResource("no_orientation_dependency_test.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + // test instrument with rotation of source of zero + auto instrument = + ComponentCreationHelper::createInstrumentWithSourceRotation( + sourceLocation, + Mantid::Kernel::V3D(0.0, 0.0, 0.0) /*samle position*/, + detectorLocation, sourceRotation); + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + FullNXPath transformationsPath = { + DEFAULT_ROOT_ENTRY_NAME, "test-instrument" /*instrument name*/, + "source" /*source name*/, TRANSFORMATIONS}; + + FullNXPath sourcePath = transformationsPath; + sourcePath.pop_back(); // source path is one level abve transformationsPath + + // call saveInstrument with test instrument as parameter + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // test utility for checking file + NexusFileReader tester(destinationFile); + + // assert what there is no 'orientation' dataset in NXtransformations, but + // there is the dataset 'location', confirming that saveInstrument has + // skipped writing a zero reotation. + bool hasLocation = tester.hasDataset(LOCATION, transformationsPath); + bool hasOrientation = tester.hasDataset(ORIENTATION, transformationsPath); + TS_ASSERT(!hasOrientation); // assert orientation dataset doesn't exist. + TS_ASSERT(hasLocation); // assert location dataset exists. + + // assert that the NXsource depends on dataset 'location' in the + // transformationsPath, since the dataset exists. + bool sourceDependencyIsLocation = tester.dataSetHasStrValue( + DEPENDS_ON /*dataset name*/, + toNXPathString(transformationsPath) + "/" + LOCATION /*dataset value*/, + sourcePath /*where the dataset lives*/); + TS_ASSERT(sourceDependencyIsLocation); + + // assert that the location depends on itself. + bool locationDependencyIsSelf = tester.hasAttributeInDataSet( + LOCATION /*dataset name*/, DEPENDS_ON /*dAttribute name*/, + NO_DEPENDENCY /*attribute value*/, + transformationsPath /*where the dataset lives*/); + TS_ASSERT(locationDependencyIsSelf); + } + + void + test_when_both_orientation_and_Location_are_written_dependency_chain_is_orientation_location_self_dependent() { + + // USING SOURCE FOR DEMONSTRATION. + + /* + test scenario: saveInstrument called with non-zero rotation, and some + non-zero translation in source. Expected behaviour is: (dataset) + 'depends_on' has value "/absoulute/path/to/orientation", (dataset) + 'orientation' has dAttribute (AKA attribute of dataset) 'depends_on' with + value "/absoulute/path/to/location", and (dataset) 'location' has + dAttribute 'depends_on' with value "." + */ + + // Geometry for test instrument + const V3D detectorLocation(0, 0, 10); // arbitrary + const V3D sourceLocation(0, 0, -10); // arbitrary non-origin + const Quat sourceRotation(45, V3D(0, 1, 0)); // arbitrary non-zero + + // create RAII file resource for testing + FileResource fileResource("both_transformations_dependency_test.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + // test instrument with non zero rotation and translation + auto instrument = + ComponentCreationHelper::createInstrumentWithSourceRotation( + sourceLocation, Mantid::Kernel::V3D(0, 0, 0), detectorLocation, + sourceRotation); // source rotation + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // path to NXtransformations subgoup in NXsource + FullNXPath transformationsPath = { + DEFAULT_ROOT_ENTRY_NAME, "test-instrument" /*instrument name*/, + "source" /*source name*/, TRANSFORMATIONS}; + + // path to NXsource group + FullNXPath sourcePath = transformationsPath; + sourcePath.pop_back(); // source path is one level abve transformationsPath + + // call saveInstrument passing test instrument as parameter + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // test utility for checking output file + NexusFileReader tester(destinationFile); + + // assert both location and orientation exists + bool hasLocation = tester.hasDataset(LOCATION, transformationsPath); + bool hasOrientation = tester.hasDataset(ORIENTATION, transformationsPath); + TS_ASSERT(hasOrientation); // assert orientation dataset exists. + TS_ASSERT(hasLocation); // assert location dataset exists. + + bool sourceDependencyIsOrientation = + tester.dataSetHasStrValue(DEPENDS_ON /*dataset name*/, + toNXPathString(transformationsPath) + "/" + + ORIENTATION /*value in dataset*/, + sourcePath /*where the dataset lives*/); + TS_ASSERT(sourceDependencyIsOrientation); + auto x = toNXPathString(transformationsPath) + "/" + + LOCATION /*dAttribute value*/; + bool orientationDependencyIsLocation = tester.hasAttributeInDataSet( + ORIENTATION /*dataset name*/, DEPENDS_ON /*dAttribute name*/, + toNXPathString(transformationsPath) + "/" + + LOCATION /*dAttribute value*/, + transformationsPath + /*where the dataset lives*/); + TS_ASSERT(orientationDependencyIsLocation); + + bool locationDependencyIsSelf = tester.hasAttributeInDataSet( + LOCATION /*dataset name*/, DEPENDS_ON /*dAttribute name*/, + NO_DEPENDENCY /*dAttribute value*/, + transformationsPath /*where the dataset lives*/); + TS_ASSERT(locationDependencyIsSelf); + } + + void + test_when_neither_orientation_nor_Location_are_written_dependency_is_self_and_nx_transformations_group_is_not_written() { + + // USING SOURCE FOR DEMONSTRATION. + + /* + test scenario: saveInstrument called with zero rotation, and + zero translation in source. Expected behaviour is: (dataset) + 'depends_on' has value "." + */ + + const V3D detectorLocation(0, 0, 10); // arbitrary + const V3D sourceLocation(0, 0, 0); // set to zero + const Quat sourceRotation(0, V3D(0, 1, 0)); // set to zero + + // create RAII file resource for testing + FileResource fileResource("neither_transformations_dependency_test.hdf5"); + std::string destinationFile = fileResource.fullPath(); + + // test instrument with zero translation and rotation + auto instrument = + ComponentCreationHelper::createInstrumentWithSourceRotation( + sourceLocation, Mantid::Kernel::V3D(0, 0, 0), detectorLocation, + sourceRotation); // source rotation + auto instr = Mantid::Geometry::InstrumentVisitor::makeWrappers(*instrument); + + // path to NXtransformations subgoup in NXsource + FullNXPath transformationsPath = { + DEFAULT_ROOT_ENTRY_NAME, "test-instrument" /*instrument name*/, + "source" /*source name*/, TRANSFORMATIONS}; + + // path to NXsource group + FullNXPath sourcePath = transformationsPath; + sourcePath.pop_back(); // source path is one level abve transformationsPath + + // call saveInstrument passing test instrument as parameter + NexusGeometrySave::saveInstrument(instr, destinationFile, + DEFAULT_ROOT_ENTRY_NAME, m_mockLogger); + + // test utility to check output file + NexusFileReader tester(destinationFile); + + // assert source is self dependent + bool sourceDependencyIsSelf = + tester.dataSetHasStrValue(DEPENDS_ON, NO_DEPENDENCY, sourcePath); + TS_ASSERT(sourceDependencyIsSelf); + + // assert the group NXtransformations doesnt exist in file + TS_ASSERT_THROWS(tester.openfullH5Path(transformationsPath), + H5::GroupIException &); + } +}; + +#endif /* MANTID_NEXUSGEOMETRY_NEXUSGEOMETRYSAVETEST_H_ */