diff --git a/Framework/Beamline/src/ComponentInfo.cpp b/Framework/Beamline/src/ComponentInfo.cpp index 93e47eca1823..17bef1276ba5 100644 --- a/Framework/Beamline/src/ComponentInfo.cpp +++ b/Framework/Beamline/src/ComponentInfo.cpp @@ -210,7 +210,7 @@ Eigen::Quaterniond ComponentInfo::rotation(const std::pair &inde /** * Extract the position of a component relative to it's parent * - * The parent rotatation is unwound prior to establishing the offset. This means + * The parent rotation is unwound prior to establishing the offset. This means *that * recorded relative positions are independent of changes in rotation. * diff --git a/Framework/DataHandling/inc/MantidDataHandling/H5Util.h b/Framework/DataHandling/inc/MantidDataHandling/H5Util.h index f4c82084b3da..2e8b491ef7cd 100644 --- a/Framework/DataHandling/inc/MantidDataHandling/H5Util.h +++ b/Framework/DataHandling/inc/MantidDataHandling/H5Util.h @@ -20,6 +20,7 @@ class DSetCreatPropList; class DataType; class Group; class H5File; +class H5Object; } // namespace H5 namespace Mantid { @@ -94,6 +95,22 @@ template std::vector readArray1DCoerce(H5::Group &group, c template std::vector readArray1DCoerce(H5::DataSet &dataset); +/// Test if a group already exists within an HDF5 file or parent group. +MANTID_DATAHANDLING_DLL bool groupExists(H5::H5Object &h5, const std::string &groupPath); + +/// Test if an attribute is present and has a specific string value for an HDF5 group or dataset. +MANTID_DATAHANDLING_DLL bool keyHasValue(H5::H5Object &h5, const std::string &key, const std::string &value); + +/// Copy a group and all of its contents, between the same or different HDF5 files or groups. +MANTID_DATAHANDLING_DLL void copyGroup(H5::H5Object &dest, const std::string &destGroupPath, H5::H5Object &src, + const std::string &srcGroupPath); + +/** + * Delete a target link for a group or dataset from a parent group. + * If this is the last link to the target in the HDF5 graph, then it will be removed from the file. + */ +MANTID_DATAHANDLING_DLL void deleteObjectLink(H5::H5Object &h5, const std::string &target); + } // namespace H5Util } // namespace DataHandling } // namespace Mantid diff --git a/Framework/DataHandling/inc/MantidDataHandling/LoadNexusProcessed.h b/Framework/DataHandling/inc/MantidDataHandling/LoadNexusProcessed.h index d76569e266e8..7b84c6ba39c7 100644 --- a/Framework/DataHandling/inc/MantidDataHandling/LoadNexusProcessed.h +++ b/Framework/DataHandling/inc/MantidDataHandling/LoadNexusProcessed.h @@ -93,10 +93,12 @@ class MANTID_DATAHANDLING_DLL LoadNexusProcessed : public API::NexusFileLoader { std::string loadWorkspaceName(Mantid::NeXus::NXRoot &root, const std::string &entry_name); /// Load nexus geometry and apply to workspace - virtual bool loadNexusGeometry(Mantid::API::Workspace &, const int, Kernel::Logger &, - const std::string &) { /*do nothing*/ - return false; + virtual bool loadNexusGeometry(Mantid::API::Workspace & /* ws */, size_t /* entryNumber */, + Kernel::Logger & /* logger */, + const std::string & /* filePath */) { /* args not used */ + return false; /*do nothing*/ } + /// Load a single entry API::Workspace_sptr loadEntry(Mantid::NeXus::NXRoot &root, const std::string &entry_name, const double &progressStart, const double &progressRange); diff --git a/Framework/DataHandling/inc/MantidDataHandling/LoadNexusProcessed2.h b/Framework/DataHandling/inc/MantidDataHandling/LoadNexusProcessed2.h index 8f3b59ede1f3..781b325717cf 100644 --- a/Framework/DataHandling/inc/MantidDataHandling/LoadNexusProcessed2.h +++ b/Framework/DataHandling/inc/MantidDataHandling/LoadNexusProcessed2.h @@ -13,6 +13,7 @@ #include "MantidIndexing/SpectrumNumber.h" #include "MantidKernel/NexusDescriptor.h" #include +#include namespace Mantid { namespace API { @@ -31,26 +32,37 @@ enum class InstrumentLayout { Mantid, NexusFormat, NotRecognised }; * Processed files. * * The majority of the implementation consists of function overrides for - * specific virtual hooks make in the base Algorithm LoadNexusProcessed + * specific virtual functions in the base Algorithm LoadNexusProcessed */ class MANTID_DATAHANDLING_DLL LoadNexusProcessed2 : public LoadNexusProcessed { public: - const std::string name() const override; + // algorithm "name" is still "LoadNexusProcessed" (not "LoadNexusProcessed2"): + // `cppcheck` has an issue with any "useless" override. + // const std::string name() const override; + int version() const override; int confidence(Kernel::NexusHDF5Descriptor &descriptor) const override; private: void readSpectraToDetectorMapping(Mantid::NeXus::NXEntry &mtd_entry, Mantid::API::MatrixWorkspace &ws) override; - bool loadNexusGeometry(Mantid::API::Workspace &ws, const int nWorkspaceEntries, Kernel::Logger &logger, - const std::string &filename) override; + + /// Load nexus geometry and apply to workspace + bool loadNexusGeometry(Mantid::API::Workspace &ws, size_t entryNumber, Kernel::Logger &logger, + const std::string &filePath) override; + /// Extract mapping information where it is build across NXDetectors void extractMappingInfoNew(const Mantid::NeXus::NXEntry &mtd_entry); - /// Load nexus geometry and apply to workspace - /// Local caches + InstrumentLayout m_instrumentLayout = InstrumentLayout::Mantid; - std::vector m_spectrumNumbers; - std::vector m_detectorIds; - std::vector m_detectorCounts; + + // Local cache vectors: + // spectral mapping information is accumulated before + // the instrument geometry has been completely loaded. + // + // The key is the NXentry-group name (in order to allow for group workspaces). + std::unordered_map> m_spectrumNumberss; + std::unordered_map> m_detectorIdss; + std::unordered_map> m_detectorCountss; }; } // namespace DataHandling diff --git a/Framework/DataHandling/inc/MantidDataHandling/SaveNexusESS.h b/Framework/DataHandling/inc/MantidDataHandling/SaveNexusESS.h index 9995f350f343..b41723495b89 100644 --- a/Framework/DataHandling/inc/MantidDataHandling/SaveNexusESS.h +++ b/Framework/DataHandling/inc/MantidDataHandling/SaveNexusESS.h @@ -31,9 +31,13 @@ class MANTID_DATAHANDLING_DLL SaveNexusESS : public Mantid::DataHandling::SaveNe bool processGroups() override; private: - void saveNexusGeometry(const Mantid::API::MatrixWorkspace &ws, const std::string &filename); + void saveNexusGeometry(const Mantid::API::MatrixWorkspace &ws, const std::string &filename, + std::optional entryNumber = std::optional()); + virtual bool saveLegacyInstrument() override; + void init() override; + void exec() override; }; diff --git a/Framework/DataHandling/src/H5Util.cpp b/Framework/DataHandling/src/H5Util.cpp index 973ed389214b..eae44a37d2c3 100644 --- a/Framework/DataHandling/src/H5Util.cpp +++ b/Framework/DataHandling/src/H5Util.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include using namespace H5; @@ -175,10 +177,10 @@ std::string readString(H5::H5File &file, const std::string &path) { try { auto data = file.openDataSet(path); return readString(data); - } catch (H5::FileIException &e) { + } catch (const H5::FileIException &e) { UNUSED_ARG(e); return ""; - } catch (H5::GroupIException &e) { + } catch (const H5::GroupIException &e) { UNUSED_ARG(e); return ""; } @@ -188,7 +190,7 @@ std::string readString(H5::Group &group, const std::string &name) { try { auto data = group.openDataSet(name); return readString(data); - } catch (H5::GroupIException &e) { + } catch (const H5::GroupIException &e) { UNUSED_ARG(e); return ""; } @@ -252,10 +254,10 @@ template std::vector readArray1DCoerce(H5::Group &group, c try { DataSet dataset = group.openDataSet(name); result = readArray1DCoerce(dataset); - } catch (H5::GroupIException &e) { + } catch (const H5::GroupIException &e) { UNUSED_ARG(e); g_log.information("Failed to open dataset \"" + name + "\"\n"); - } catch (H5::DataTypeIException &e) { + } catch (const H5::DataTypeIException &e) { UNUSED_ARG(e); g_log.information("DataSet \"" + name + "\" should be double" + "\n"); } @@ -395,6 +397,66 @@ template std::vector readArray1DCoerce(DataSet &dataset) { throw DataTypeIException(); } +/// Test if a group exists in an HDF5 file or parent group. +bool groupExists(H5::H5Object &h5, const std::string &groupPath) { + bool status = true; + // Unfortunately, this is actually the approach recommended by the HDF Group. + try { + h5.openGroup(groupPath); + } catch (const H5::Exception &e) { + UNUSED_ARG(e); + status = false; + } + return status; +} + +/// Test if an attribute is present on an HDF5 group or dataset and has a specific string value. +bool keyHasValue(H5::H5Object &h5, const std::string &key, const std::string &value) { + bool status = true; + try { + Attribute attr = h5.openAttribute(key); + std::string value_; + attr.read(attr.getDataType(), value_); + if (value_ != value) + status = false; + } catch (const H5::Exception &e) { + UNUSED_ARG(e); + status = false; + } + return status; +} + +void copyGroup(H5::H5Object &dest, const std::string &destGroupPath, H5::H5Object &src, + const std::string &srcGroupPath) { + // Source group must exist, and destination group must not exist. + if (!groupExists(src, srcGroupPath) || groupExists(dest, destGroupPath)) + throw std::invalid_argument(std::string("H5Util::copyGroup: source group '") + srcGroupPath + "' must exist and " + + "destination group '" + destGroupPath + "' must not exist."); + + // TODO: check that source file must have at least read access and destination file must have write access. + + // Note that in the HDF5 API: + // C++ API support for these HDF5 methods does not yet exist. + + // Create intermediate groups, if necessary + hid_t lcpl = H5Pcreate(H5P_LINK_CREATE); + if (H5Pset_create_intermediate_group(lcpl, 1) < 0) + throw std::runtime_error("H5Util::copyGroup: 'H5Pset_create_intermediate_group' error return."); + + if (H5Ocopy(src.getId(), srcGroupPath.c_str(), dest.getId(), destGroupPath.c_str(), H5P_DEFAULT, lcpl) < 0) + throw std::runtime_error("H5Util::copyGroup: 'H5Ocopy' error return."); + H5Pclose(lcpl); +} + +void deleteObjectLink(H5::H5Object &h5, const std::string &target) { + // Note that in the HDF5 API: + // C++ API support for this HDF5 method does not yet exist. + + // Target object must exist + if (H5Ldelete(h5.getId(), target.c_str(), H5P_DEFAULT) < 0) + throw std::runtime_error("H5Util::deleteObjectLink: 'H5Ldelete' error return."); +} + // ------------------------------------------------------------------- // instantiations for writeStrAttribute // ------------------------------------------------------------------- diff --git a/Framework/DataHandling/src/LoadNexusProcessed.cpp b/Framework/DataHandling/src/LoadNexusProcessed.cpp index 7b80e1bf24a2..66a19bf1afd6 100644 --- a/Framework/DataHandling/src/LoadNexusProcessed.cpp +++ b/Framework/DataHandling/src/LoadNexusProcessed.cpp @@ -375,6 +375,12 @@ void LoadNexusProcessed::execLoader() { API::Workspace_sptr tempWS; size_t nWorkspaceEntries = 0; + + // Check for an entry number property + int entryNumber = getProperty("EntryNumber"); + Property const *const entryNumberProperty = this->getProperty("EntryNumber"); + bool bDefaultEntryNumber = entryNumberProperty->isDefault(); + // Start scoped block { progress(0, "Opening file..."); @@ -390,13 +396,9 @@ void LoadNexusProcessed::execLoader() { nWorkspaceEntries = std::count_if(root.groups().cbegin(), root.groups().cend(), [](const auto &g) { return g.nxclass == "NXentry"; }); - // Check for an entry number property - int entrynumber = getProperty("EntryNumber"); - Property const *const entryNumberProperty = this->getProperty("EntryNumber"); - bool bDefaultEntryNumber = entryNumberProperty->isDefault(); - - if (!bDefaultEntryNumber && entrynumber > static_cast(nWorkspaceEntries)) { - g_log.error() << "Invalid entry number specified. File only contains " << nWorkspaceEntries << " entries.\n"; + if (!bDefaultEntryNumber && static_cast(entryNumber) > nWorkspaceEntries) { + g_log.error() << "Invalid entry number: " << entryNumber + << " specified. File only contains: " << nWorkspaceEntries << " entries.\n"; throw std::invalid_argument("Invalid entry number specified."); } @@ -405,9 +407,9 @@ void LoadNexusProcessed::execLoader() { std::ostringstream os; if (bDefaultEntryNumber) { // Set the entry number to 1 if not provided. - entrynumber = 1; + entryNumber = 1; } - os << basename << entrynumber; + os << basename << entryNumber; const std::string targetEntryName = os.str(); // Take the first real workspace obtainable. We need it even if loading @@ -502,12 +504,28 @@ void LoadNexusProcessed::execLoader() { } root.close(); - } // All file resources should be scoped to here. All previous file handles - // must be cleared to release locks - loadNexusGeometry(*tempWS, static_cast(nWorkspaceEntries), g_log, std::string(getProperty("Filename"))); + } + + // All file resources should be scoped to here. All previous file handles + // must be cleared to release locks. + + // NexusGeometry uses direct HDF5 access, and not the `NexusFileIO` methods. + // For this reason, a separate section is required to load the instrument[s] into the output workspace[s]. + + if (nWorkspaceEntries == 1 || !bDefaultEntryNumber) + loadNexusGeometry(*getValue("OutputWorkspace"), static_cast(entryNumber), g_log, + std::string(getProperty("Filename"))); + else { + for (size_t nEntry = 1; nEntry <= static_cast(nWorkspaceEntries); ++nEntry) { + std::ostringstream wsPropertyName; + wsPropertyName << "OutputWorkspace_" << nEntry; + loadNexusGeometry(*getValue(wsPropertyName.str()), nEntry, g_log, + std::string(getProperty("Filename"))); + } + } m_axis1vals.clear(); -} // namespace DataHandling +} /** * Decides what to call a child of a group workspace. @@ -1890,14 +1908,16 @@ API::Workspace_sptr LoadNexusProcessed::loadEntry(NXRoot &root, const std::strin progress(progressStart + 0.11 * progressRange, "Reading the parameter maps..."); local_workspace->readParameterMap(parameterStr); } catch (std::exception &e) { - // TODO. For workspaces saved via SaveNexusESS, these warnings are not - // relevant. Unfortunately we need to close all file handles before we can - // attempt loading the new way see loadNexusGeometry function . A better - // solution should be found - g_log.warning("Error loading Instrument section of nxs file"); - g_log.warning(e.what()); - g_log.warning("Try running LoadInstrument Algorithm on the Workspace to " - "update the geometry"); + // For workspaces saved via SaveNexusESS, these warnings are not + // relevant. Such workspaces will contain an `NXinstrument` entry + // with the name of the instrument. + const auto &entries = getFileInfo()->getAllEntries(); + if (version() < 2 || entries.find("NXinstrument") == entries.end()) { + g_log.warning("Error loading Instrument section of nxs file"); + g_log.warning(e.what()); + g_log.warning("Try running LoadInstrument Algorithm on the Workspace to " + "update the geometry"); + } } readSpectraToDetectorMapping(mtd_entry, *local_workspace); diff --git a/Framework/DataHandling/src/LoadNexusProcessed2.cpp b/Framework/DataHandling/src/LoadNexusProcessed2.cpp index 243e0c88edb5..2e778546884e 100644 --- a/Framework/DataHandling/src/LoadNexusProcessed2.cpp +++ b/Framework/DataHandling/src/LoadNexusProcessed2.cpp @@ -58,7 +58,7 @@ InstrumentLayout instrumentFormat(Mantid::NeXus::NXEntry &entry) { auto instr = entry.openNXInstrument("instrument"); if (instr.containsGroup("detector") || (instr.containsGroup("physical_detectors") && instr.containsGroup("physical_monitors"))) { - result = InstrumentLayout::Mantid; // 1 nxinstrument called instrument, + result = InstrumentLayout::Mantid; // 1 NXinstrument group called "instrument", } instr.close(); } @@ -69,9 +69,6 @@ InstrumentLayout instrumentFormat(Mantid::NeXus::NXEntry &entry) { } // namespace -/// Algorithms name for identification. @see Algorithm::name -const std::string LoadNexusProcessed2::name() const { return "LoadNexusProcessed"; } - /// Algorithm's version for identification. @see Algorithm::version int LoadNexusProcessed2::version() const { return 2; } @@ -88,17 +85,30 @@ void LoadNexusProcessed2::readSpectraToDetectorMapping(Mantid::NeXus::NXEntry &m g_log.information() << "Instrument layout not recognised. Spectra mappings not loaded."; } } + void LoadNexusProcessed2::extractMappingInfoNew(const Mantid::NeXus::NXEntry &mtd_entry) { using namespace Mantid::NeXus; + + const std::string &parent = mtd_entry.name(); auto result = findEntriesOfType(mtd_entry, "NXinstrument"); if (result.size() != 1) { - g_log.warning("We are expecting a single NXinstrument. No mappings loaded"); + g_log.warning("We are expecting a single NXinstrument. No mappings will be loaded"); } auto inst = mtd_entry.openNXInstrument(result[0].nxname); - auto &spectrumNumbers = m_spectrumNumbers; - auto &detectorIds = m_detectorIds; - auto &detectorCounts = m_detectorCounts; + // For workspace groups, the spectral mapping information is keyed by the name of the parent NXentry. + // Normally, we would not expect this key to have already been entered into these maps. + // However there is a known defect (EWM#7910): in that for a workspace group, the first NXentry is loaded twice. + // For this reason, at the moment, `std::unordered_map<..>::emplace` should not be used in the following. + + m_spectrumNumberss[parent] = std::vector(); + auto &spectrumNumbers = m_spectrumNumberss[parent]; + m_detectorIdss[parent] = std::vector(); + auto &detectorIds = m_detectorIdss[parent]; + m_detectorCountss[parent] = std::vector(); + auto &detectorCounts = m_detectorCountss[parent]; + + // Read and collate the spectrum-mapping information. for (const auto &group : inst.groups()) { if (group.nxclass == "NXdetector" || group.nxclass == "NXmonitor") { NXDetector detgroup = inst.openNXDetector(group.nxname); @@ -163,47 +173,57 @@ void LoadNexusProcessed2::extractMappingInfoNew(const Mantid::NeXus::NXEntry &mt * Attempt to load nexus geometry. Should fail without exception if not * possible. * - * Caveats are: - * 1. Only works for input files where there is a single NXEntry. Does nothing - * otherwise. - * 2. Is only applied after attempted instrument loading in the legacy fashion + * Caveat is: + * Is only applied after attempted instrument loading in the legacy fashion * that happens as part of loadEntry. So you will still get warning+error * messages from that even if this succeeds * * @param ws : Input workspace onto which instrument will get attached - * @param nWorkspaceEntries : number of entries + * @param entryNumber: number of the NXentry for the parent group: used to construct the group's name * @param logger : to write to - * @param filename : filename to load from. + * @param filePath : filename to load from. * @return true if successful */ -bool LoadNexusProcessed2::loadNexusGeometry(API::Workspace &ws, const int nWorkspaceEntries, Kernel::Logger &logger, - const std::string &filename) { - if (m_instrumentLayout == InstrumentLayout::NexusFormat && nWorkspaceEntries == 1) { +bool LoadNexusProcessed2::loadNexusGeometry(API::Workspace &ws, size_t entryNumber, Kernel::Logger &logger, + const std::string &filePath) { + if (m_instrumentLayout == InstrumentLayout::NexusFormat) { if (auto *matrixWs = dynamic_cast(&ws)) { try { using namespace Mantid::NexusGeometry; + + std::ostringstream parent_; + parent_ << "mantid_workspace_" << entryNumber; + const std::string &parent(parent_.str()); auto instrument = - NexusGeometry::NexusGeometryParser::createInstrument(filename, NexusGeometry::makeLogger(&logger)); + NexusGeometry::NexusGeometryParser::createInstrument(filePath, parent, NexusGeometry::makeLogger(&logger)); matrixWs->setInstrument(Geometry::Instrument_const_sptr(std::move(instrument))); - auto &detInfo = matrixWs->detectorInfo(); - Indexing::IndexInfo info(m_spectrumNumbers); + // Apply the previously-collated mapping information to the workspace. + const auto &detInfo = matrixWs->detectorInfo(); + auto &spectrumNumbers = m_spectrumNumberss[parent]; + Indexing::IndexInfo info(spectrumNumbers); + + const auto &detectorIds = m_detectorIdss[parent]; + const auto &detectorCounts = m_detectorCountss[parent]; + std::vector definitions; - definitions.reserve(m_spectrumNumbers.size()); + definitions.reserve(spectrumNumbers.size()); size_t detCounter = 0; - for (size_t i = 0; i < m_spectrumNumbers.size(); ++i) { + for (size_t i = 0; i < spectrumNumbers.size(); ++i) { // counts gives number of detectors per spectrum - size_t counts = m_detectorCounts[i]; + size_t counts = detectorCounts[i]; SpectrumDefinition def; + // Add the number of detectors known to be associated with this // spectrum for (size_t j = 0; j < counts; ++j, ++detCounter) { - def.add(detInfo.indexOf(m_detectorIds[detCounter])); + def.add(detInfo.indexOf(detectorIds[detCounter])); } definitions.emplace_back(def); } info.setSpectrumDefinitions(definitions); matrixWs->setIndexInfo(info); + return true; } catch (std::exception &e) { logger.warning(e.what()); diff --git a/Framework/DataHandling/src/SaveNexusESS.cpp b/Framework/DataHandling/src/SaveNexusESS.cpp index 275b057ffb6e..dabf77835f14 100644 --- a/Framework/DataHandling/src/SaveNexusESS.cpp +++ b/Framework/DataHandling/src/SaveNexusESS.cpp @@ -5,10 +5,12 @@ // Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS // SPDX - License - Identifier: GPL - 3.0 + #include "MantidDataHandling/SaveNexusESS.h" +#include "MantidAPI/Workspace_fwd.h" #include "MantidNexusGeometry/NexusGeometrySave.h" #include namespace Mantid::DataHandling { +using Mantid::API::Workspace_sptr; using Mantid::API::WorkspaceProperty; using Mantid::Kernel::Direction; @@ -37,20 +39,45 @@ const std::string SaveNexusESS::summary() const { * @return */ bool SaveNexusESS::processGroups() { - throw std::invalid_argument("SaveNexusESS does not currently support operations on groups"); + + // TODO: Due to the mixture of Nexus and HDF5 operations, and the original design of `SaveNexusESS`: this isn't as + // efficient as it could be. + + const std::string filename = getProperty("Filename"); + NexusGeometry::LogAdapter adapter(&g_log); + + SaveNexusProcessed::processGroups(); + + // Now loop over the workspace entries again and fill in their NXinstrument groups. + // (Also see comments at: `SaveNexusProcessed::processGroups`.) + const auto &workspaces = m_unrolledInputWorkspaces[0]; + if (!workspaces.empty()) { + for (size_t entry = 0; entry < workspaces.size(); entry++) { + const Workspace_sptr ws = workspaces[entry]; + auto matrixWs = std::dynamic_pointer_cast(ws); + if (!matrixWs) + throw std::runtime_error("SaveNexusESS::processGroups: workspace is not a MatrixWorkspace"); + + saveNexusGeometry(*matrixWs, filename, entry + 1); + g_log.information() << "Adding instrument to workspace at group index " << entry << "\n"; + } + } + + return true; } -void SaveNexusESS::saveNexusGeometry(const Mantid::API::MatrixWorkspace &ws, const std::string &filename) { +void SaveNexusESS::saveNexusGeometry(const Mantid::API::MatrixWorkspace &ws, const std::string &filename, + std::optional entryNumber) { try { NexusGeometry::LogAdapter adapter(&g_log); - NexusGeometry::NexusGeometrySave::saveInstrument(ws, filename, "mantid_workspace_1", adapter, true); + NexusGeometry::NexusGeometrySave::saveInstrument(ws, filename, "mantid_workspace_", entryNumber, adapter, true); } catch (std::exception &e) { - g_log.error(std::string(e.what()) + " Nexus Geometry may be absent or incomplete " - "from processed Nexus file"); + g_log.error(std::string(e.what()) + ":\n Nexus Geometry may be absent or incomplete " + "from the processed Nexus file"); } catch (H5::Exception &ex) { - g_log.error(ex.getDetailMsg() + " Nexus Geometry may be absent or incomplete " - "from processed Nexus file"); + g_log.error(ex.getDetailMsg() + ":\n Nexus Geometry may be absent or incomplete " + "from the processed Nexus file"); } } @@ -65,7 +92,7 @@ bool SaveNexusESS::saveLegacyInstrument() { /** Initialize the algorithm's properties. */ void SaveNexusESS::init() { - // Take same properties as base. + // Re-use the same properties as those of the base class. SaveNexusProcessed::init(); } @@ -73,19 +100,19 @@ void SaveNexusESS::init() { /** Execute the algorithm. */ void SaveNexusESS::exec() { - // Run the base algorithm. Template method approach used to call ESS - // specifics. API::Workspace_sptr ws = getProperty("InputWorkspace"); const std::string filename = getProperty("Filename"); auto matrixWs = std::dynamic_pointer_cast(ws); if (!matrixWs) throw std::runtime_error("SaveNexusESS expects a MatrixWorkspace as input"); + + // First: call the base-class `exec` method. SaveNexusProcessed::exec(); - // Now append nexus geometry + // Next: append the NeXus geometry saveNexusGeometry(*matrixWs, filename); - // Now write spectrum to detector maps; + return; } } // namespace Mantid::DataHandling diff --git a/Framework/DataHandling/src/SaveNexusGeometry.cpp b/Framework/DataHandling/src/SaveNexusGeometry.cpp index 5b69bbd570b3..45965ac7d927 100644 --- a/Framework/DataHandling/src/SaveNexusGeometry.cpp +++ b/Framework/DataHandling/src/SaveNexusGeometry.cpp @@ -78,14 +78,15 @@ void SaveNexusGeometry::init() { void SaveNexusGeometry::exec() { API::MatrixWorkspace_const_sptr workspace = getProperty("InputWorkspace"); std::string destinationFile = getPropertyValue("FileName"); - std::string rootFileName = getPropertyValue("EntryName"); + std::string parentEntryName = getPropertyValue("EntryName"); auto ws = workspace.get(); const auto &compInfo = ws->componentInfo(); const auto &detInfo = ws->detectorInfo(); NexusGeometry::LogAdapter adapter(&g_log); - Mantid::NexusGeometry::NexusGeometrySave::saveInstrument(compInfo, detInfo, destinationFile, rootFileName, adapter); + Mantid::NexusGeometry::NexusGeometrySave::saveInstrument(compInfo, detInfo, destinationFile, parentEntryName, + adapter); } } // namespace Mantid::DataHandling diff --git a/Framework/DataHandling/src/SaveNexusProcessed.cpp b/Framework/DataHandling/src/SaveNexusProcessed.cpp index 7e8e1511894d..af172ab15e1a 100644 --- a/Framework/DataHandling/src/SaveNexusProcessed.cpp +++ b/Framework/DataHandling/src/SaveNexusProcessed.cpp @@ -267,7 +267,7 @@ void SaveNexusProcessed::doExec(const Workspace_sptr &inputWorkspace, const bool append_to_file = getProperty("Append"); nexusFile->resetProgress(&prog_init); - nexusFile->openNexusWrite(filename, std::move(entryNumber), append_to_file || keepFile); + nexusFile->openNexusWrite(filename, entryNumber, append_to_file || keepFile); // Equivalent C++ API handle ::NeXus::File cppFile(nexusFile->fileID); @@ -526,7 +526,7 @@ void SaveNexusProcessed::setOtherProperties(IAlgorithm *alg, const std::string & } /** - Overriden process groups. + Overridden process groups. */ bool SaveNexusProcessed::processGroups() { // Then immediately open the file diff --git a/Framework/DataHandling/test/H5UtilTest.h b/Framework/DataHandling/test/H5UtilTest.h index a5d2b0752182..4d78c0829803 100644 --- a/Framework/DataHandling/test/H5UtilTest.h +++ b/Framework/DataHandling/test/H5UtilTest.h @@ -9,11 +9,12 @@ #include #include "MantidDataHandling/H5Util.h" +#include "MantidFrameworkTestHelpers/FileResource.h" #include "MantidKernel/System.h" #include -#include #include +#include #include using namespace H5; @@ -28,8 +29,8 @@ class H5UtilTest : public CxxTest::TestSuite { static void destroySuite(H5UtilTest *suite) { delete suite; } void removeFile(const std::string &filename) { - if (Poco::File(filename).exists()) - Poco::File(filename).remove(); + if (std::filesystem::exists(filename)) + std::filesystem::remove(filename); } void test_strings() { @@ -48,7 +49,7 @@ class H5UtilTest : public CxxTest::TestSuite { file.close(); } - TS_ASSERT(Poco::File(FILENAME).exists()); + TS_ASSERT(std::filesystem::exists(FILENAME)); do_assert_simple_string_data_set(FILENAME, GRP_NAME, DATA_NAME, DATA_VALUE); // cleanup @@ -99,7 +100,7 @@ class H5UtilTest : public CxxTest::TestSuite { } // Assert - TS_ASSERT(Poco::File(FILENAME).exists()); + TS_ASSERT(std::filesystem::exists(FILENAME)); std::map floatAttributesScalar{{ATTR_NAME_3, ATTR_VALUE_3}}; std::map intAttributesScalar{{ATTR_NAME_4, ATTR_VALUE_4}}; std::map> floatVectorAttributesScalar{{ATTR_NAME_5, ATTR_VALUE_5}}; @@ -133,7 +134,7 @@ class H5UtilTest : public CxxTest::TestSuite { file.close(); } - TS_ASSERT(Poco::File(FILENAME).exists()); + TS_ASSERT(std::filesystem::exists(FILENAME)); { // read tests H5File file(FILENAME, H5F_ACC_RDONLY); @@ -177,7 +178,7 @@ class H5UtilTest : public CxxTest::TestSuite { file.close(); // check it exists - TS_ASSERT(Poco::File(filename).exists()); + TS_ASSERT(std::filesystem::exists(filename)); // open and read the vector H5File file_read(filename, H5F_ACC_RDONLY); @@ -196,6 +197,198 @@ class H5UtilTest : public CxxTest::TestSuite { removeFile(filename); } + void test_groupExists() { + FileResource testInput("groupExists_test.h5"); + H5File h5(testInput.fullPath(), H5F_ACC_TRUNC); + h5.createGroup("/one"); + h5.createGroup("/two"); + Group g = h5.openGroup("/two"); + g.createGroup("three"); + g.close(); + h5.close(); + + TS_ASSERT(std::filesystem::exists(testInput.fullPath())); + H5File h5_ro(testInput.fullPath(), H5F_ACC_RDONLY); + + TS_ASSERT(H5Util::groupExists(h5_ro, "/one")); + TS_ASSERT(H5Util::groupExists(h5_ro, "/two/three")); + TS_ASSERT(!H5Util::groupExists(h5_ro, "/four")); + TS_ASSERT(!H5Util::groupExists(h5_ro, "/two/four")); + } + + void test_keyHasValue() { + constexpr auto NX_CLASS = "NX_class"; + constexpr auto NX_ENTRY = "NXentry"; + constexpr auto NX_INSTRUMENT = "NXinstrument"; + constexpr auto NX_DETECTOR = "NXdetector"; + + FileResource testInput("keyHasValue_test.h5"); + + { + H5File h5(testInput.fullPath(), H5F_ACC_TRUNC); + + // 1: Create groups with specific key: value attributes + Group g1 = h5.createGroup("/one"); + _writeStringAttribute(g1, NX_CLASS, NX_ENTRY); + Group g2 = g1.createGroup("two"); + _writeStringAttribute(g2, NX_CLASS, NX_INSTRUMENT); + + // 2: Create a group without any attributes + h5.createGroup("/three"); + } + + TS_ASSERT(std::filesystem::exists(testInput.fullPath())); + H5File h5(testInput.fullPath(), H5F_ACC_RDONLY); + + Group g1 = h5.openGroup("/one"); + Group g2 = g1.openGroup("two"); + Group g3 = h5.openGroup("/three"); + + // key: value + TS_ASSERT(H5Util::keyHasValue(g1, NX_CLASS, NX_ENTRY)); + + // not (key: wrong value) + TS_ASSERT(!H5Util::keyHasValue(g1, NX_CLASS, NX_DETECTOR)); + + // not (wrong key: value) + TS_ASSERT(!H5Util::keyHasValue(g1, "another_key", NX_ENTRY)); + + // nested group, key: value + TS_ASSERT(H5Util::keyHasValue(g2, NX_CLASS, NX_INSTRUMENT)); + + // nested group, not (key: wrong value) + TS_ASSERT(!H5Util::keyHasValue(g2, NX_CLASS, NX_ENTRY)); + + // no attributes present on group, not (key: value) + TS_ASSERT(!H5Util::keyHasValue(g3, NX_CLASS, NX_ENTRY)); + } + + void test_copyGroup_same_file() { + FileResource testInput("copy_group_same_file.h5"); + { + H5File input1(testInput.fullPath(), H5F_ACC_TRUNC); + input1.createGroup("/one"); + Group g2 = input1.createGroup("/two"); + g2.createGroup("three"); + } + { + // WARNING: `H5File::reopen` doesn't work for some reason. + H5File input1(testInput.fullPath(), H5F_ACC_RDONLY); + // verify the starting structure + _assert_group_structure(input1, + { + "/one", + "/two/three", + }, + "copyGroup: same file: starting structure"); + TS_ASSERT(!_groupExists(input1, "/one/two/three")); + } + { + H5File output1(testInput.fullPath(), H5F_ACC_RDWR); + H5Util::copyGroup(output1, "/one/two", output1, "/two"); + H5Util::copyGroup(output1, "/four", output1, "/two"); + } + { + H5File output1(testInput.fullPath(), H5F_ACC_RDONLY); + // verify the final structure + _assert_group_structure(output1, {"/one/two/three", "/two/three", "/four/three"}, + "copyGroup: same file: final structure"); + } + } + + void test_copyGroup_different_file() { + FileResource testInput1("copy_group_different_file1.h5"); + FileResource testInput2("copy_group_different_file2.h5"); + { + H5File input1(testInput1.fullPath(), H5F_ACC_TRUNC); + input1.createGroup("/one"); + Group g2 = input1.createGroup("/two"); + g2.createGroup("three"); + } + { + H5File input1(testInput1.fullPath(), H5F_ACC_RDONLY); + // verify the starting structure + _assert_group_structure(input1, + { + "/one", + "/two/three", + }, + "copyGroup: different file: starting structure"); + TS_ASSERT(!_groupExists(input1, "/one/two/three")); + input1.close(); + } + { + H5File input1(testInput1.fullPath(), H5F_ACC_RDONLY); + H5File output1(testInput2.fullPath(), H5F_ACC_TRUNC); + H5Util::copyGroup(output1, "/one", input1, "/one"); + H5Util::copyGroup(output1, "/two", input1, "/two"); + H5Util::copyGroup(output1, "/four", input1, "/two"); + } + { + H5File output1(testInput2.fullPath(), H5F_ACC_RDONLY); + // verify the final structure + _assert_group_structure(output1, {"/one", "/two/three", "/four/three"}, + "copyGroup: different file: final structure"); + } + } + + void test_deleteObjectLink_subgroup() { + FileResource testInput("delete_object_link_subgroup.h5"); + { + H5File input1(testInput.fullPath(), H5F_ACC_TRUNC); + input1.createGroup("/one"); + Group g2 = input1.createGroup("/two"); + g2.createGroup("three"); + } + { + H5File input1(testInput.fullPath(), H5F_ACC_RDWR); + // verify the starting structure + _assert_group_structure(input1, + { + "/one", + "/two/three", + }, + "deleteObjectLink: subgroup: starting structure"); + H5Util::deleteObjectLink(input1, "/two/three"); + } + { + H5File input1(testInput.fullPath(), H5F_ACC_RDONLY); + _assert_group_structure(input1, + { + "/one", + "/two", + }, + "deleteObjectLink: subgroup: final structure"); + TS_ASSERT(!_groupExists(input1, "/two/three")); + } + } + + void test_deleteObjectLink_rootgroup() { + FileResource testInput("delete_object_link_rootgroup.h5"); + { + H5File input1(testInput.fullPath(), H5F_ACC_TRUNC); + input1.createGroup("/one"); + Group g2 = input1.createGroup("/two"); + g2.createGroup("three"); + } + { + H5File input1(testInput.fullPath(), H5F_ACC_RDWR); + // verify the starting structure + _assert_group_structure(input1, + { + "/one", + "/two/three", + }, + "deleteObjectLink: rootgroup: starting structure"); + H5Util::deleteObjectLink(input1, "/two"); + } + { + H5File input1(testInput.fullPath(), H5F_ACC_RDONLY); + TS_ASSERT(_groupExists(input1, "/one")); + TS_ASSERT(!_groupExists(input1, "/two")); + } + } + private: void do_assert_simple_string_data_set( const std::string &filename, const std::string &groupName, const std::string &dataName, @@ -206,7 +399,7 @@ class H5UtilTest : public CxxTest::TestSuite { const std::map> &floatVectorAttributes = std::map>(), const std::map> &intVectorAttributes = std::map>()) { - TS_ASSERT(Poco::File(filename).exists()); + TS_ASSERT(std::filesystem::exists(filename)); // read tests H5File file(filename, H5F_ACC_RDONLY); @@ -275,4 +468,45 @@ class H5UtilTest : public CxxTest::TestSuite { TSM_ASSERT_EQUALS("Should retrieve the correct attribute value", expectedCoerced, value); } } + + // Attach a string key: value pair to a group or dataset. + static void _writeStringAttribute(H5::H5Object &h5, const std::string &key, const std::string &value) { + // for testing use: duplicates: `H5Util::writeStrAttribute` + + // Fixed-length string datatype + H5::StrType dt(0, value.size()); + Attribute attr = h5.createAttribute(key, dt, DataSpace(H5S_SCALAR)); + attr.write(dt, value); + } + + // Read the value of a string key: value pair attached to a group or dataset. + static std::string _readStringAttribute(H5::H5Object &h5, const std::string &key) { + // for testing use: duplicates: `H5Util::readAttributeAttributeAsString` + + std::string value; + Attribute attr = h5.openAttribute(key); + attr.read(attr.getDataType(), value); + return value; + } + + // test that a group exists in an HDF5 file + static bool _groupExists(H5::H5File &file, const std::string &groupPath) { + // for testing use: duplicates: `H5Util::groupExists` + + bool status = true; + try { + file.openGroup(groupPath); + } catch (const H5::Exception &x) { + UNUSED_ARG(x); + status = false; + } + return status; + } + + // test that multiple groups exist in an HDF5 file at the expected locations + void _assert_group_structure(H5::H5File &file, const std::vector &paths, const std::string &msg) { + for (const auto &path : paths) { + TSM_ASSERT((msg + ": '" + path + "'").c_str(), _groupExists(file, path)); + } + } }; diff --git a/Framework/DataHandling/test/SaveNexusESSTest.h b/Framework/DataHandling/test/SaveNexusESSTest.h index 391fdd68c8ee..f9629ddfc090 100644 --- a/Framework/DataHandling/test/SaveNexusESSTest.h +++ b/Framework/DataHandling/test/SaveNexusESSTest.h @@ -8,7 +8,11 @@ #include +#include "MantidAPI/AlgorithmManager.h" +#include "MantidAPI/AnalysisDataService.h" #include "MantidAPI/SpectrumInfo.h" +#include "MantidAPI/WorkspaceGroup.h" +#include "MantidDataHandling/H5Util.h" #include "MantidDataHandling/LoadEmptyInstrument.h" #include "MantidDataHandling/LoadNexusProcessed2.h" #include "MantidDataHandling/SaveNexusESS.h" @@ -32,7 +36,8 @@ using namespace Mantid::DataHandling; using namespace Mantid::API; namespace { -template void do_execute(const std::string &filename, T &ws) { + +template void do_execute(const std::string &filename, T &ws, bool append = false) { SaveNexusESS alg; alg.setChild(true); alg.setRethrows(true); @@ -40,11 +45,13 @@ template void do_execute(const std::string &filename, T &ws) { alg.isInitialized(); alg.setProperty("InputWorkspace", ws); alg.setProperty("Filename", filename); + alg.setProperty("Append", append); alg.execute(); alg.isExecuted(); } namespace test_utility { + Mantid::API::MatrixWorkspace_sptr reload(const std::string &filename) { LoadNexusProcessed2 loader; loader.setChild(true); @@ -68,6 +75,7 @@ Mantid::API::MatrixWorkspace_sptr from_instrument_file(const std::string &filena MatrixWorkspace_sptr ws = loader.getProperty("OutputWorkspace"); return ws; } + Mantid::API::MatrixWorkspace_sptr from_instrument_name(const std::string &name) { LoadEmptyInstrument loader; loader.setChild(true); @@ -78,9 +86,12 @@ Mantid::API::MatrixWorkspace_sptr from_instrument_name(const std::string &name) MatrixWorkspace_sptr ws = loader.getProperty("OutputWorkspace"); return ws; } + Mantid::API::MatrixWorkspace_sptr load(const std::string &name) { return reload(name); } + } // namespace test_utility } // namespace + class SaveNexusESSTest : public CxxTest::TestSuite { public: // This pair of boilerplate methods prevent the suite being created statically @@ -96,7 +107,9 @@ class SaveNexusESSTest : public CxxTest::TestSuite { void test_exec_rectangular_instrument_details() { using namespace Mantid::NexusGeometry; + FileResource fileInfo("test_rectangular_instrument.nxs"); + auto ws = WorkspaceCreationHelper::create2DWorkspaceWithRectangularInstrument(1 /*numBanks*/, 10 /*numPixels*/, 10 /*numBins*/); @@ -108,6 +121,7 @@ class SaveNexusESSTest : public CxxTest::TestSuite { // Load and check instrument geometry Mantid::Kernel::Logger logger("test_logger"); auto instr = NexusGeometryParser::createInstrument(fileInfo.fullPath(), makeLogger(&logger)); + Mantid::Geometry::ParameterMap pmap; auto beamline = instr->makeBeamline(pmap); const auto &outDetInfo = beamline.second; @@ -135,11 +149,9 @@ class SaveNexusESSTest : public CxxTest::TestSuite { } void test_with_ess_instrument() { - using namespace Mantid::HistogramData; FileResource fileInfo("test_ess_instrument.nxs"); - auto wsIn = test_utility::from_instrument_file("V20_4-tubes_90deg_Definition_v01.xml"); for (size_t i = 0; i < wsIn->getNumberHistograms(); ++i) { wsIn->setCounts(i, Counts{double(i)}); @@ -158,54 +170,216 @@ class SaveNexusESSTest : public CxxTest::TestSuite { } void test_demonstrate_spectra_detector_map_saved() { - - using namespace Mantid::Indexing; FileResource fileInfo("test_spectra_mapping.nxs"); - auto wsIn = WorkspaceCreationHelper::create2DWorkspaceWithRectangularInstrument(2 /*numBanks*/, 10 /*numPixels*/, - 12 /*numBins*/); + auto wsIn = createWorkspaceWithInstrumentAndSpectraMap("basic_rect"); - std::vector specDefinitions; - std::vector spectrumNumbers; - size_t i = wsIn->getNumberHistograms() - 1; - for (size_t j = 0; j < wsIn->getNumberHistograms(); --i, ++j) { - specDefinitions.emplace_back(SpectrumDefinition(i)); - spectrumNumbers.emplace_back(SpectrumNumber(static_cast(j))); - } - IndexInfo info(spectrumNumbers); - info.setSpectrumDefinitions(specDefinitions); - wsIn->setIndexInfo(info); do_execute(fileInfo.fullPath(), wsIn); - { - const auto rootName = wsIn->componentInfo().name(wsIn->componentInfo().root()); - // Check that mapping datasets are written - Mantid::NexusGeometry::NexusFileReader validator(fileInfo.fullPath()); - TS_ASSERT(validator.hasDataset("spectra", {"mantid_workspace_1", rootName, "bank1"})); - TS_ASSERT(validator.hasDataset("detector_list", {"mantid_workspace_1", rootName, "bank1"})); - TS_ASSERT(validator.hasDataset("detector_index", {"mantid_workspace_1", rootName, "bank1"})); - TS_ASSERT(validator.hasDataset("detector_count", {"mantid_workspace_1", rootName, "bank1"})); - TS_ASSERT(validator.hasDataset("spectra", {"mantid_workspace_1", rootName, "bank2"})); - TS_ASSERT(validator.hasDataset("detector_list", {"mantid_workspace_1", rootName, "bank2"})); - TS_ASSERT(validator.hasDataset("detector_index", {"mantid_workspace_1", rootName, "bank2"})); - TS_ASSERT(validator.hasDataset("detector_count", {"mantid_workspace_1", rootName, "bank2"})); - } + validate_spectra_detector_map_structure(fileInfo.fullPath(), "mantid_workspace_1", + wsIn->componentInfo().name(wsIn->componentInfo().root())); } - void test_base_function_with_workspace() { + void test_append_multiple_workspaces() { + FileResource fileInfo("test_multiple_workspaces.nxs"); + auto ws1 = createWorkspaceWithInstrumentAndSpectraMap("first_instrument"); + auto ws2 = createWorkspaceWithInstrumentAndSpectraMap("second_instrument"); + auto ws3 = createWorkspaceWithInstrumentAndSpectraMap("third_instrument"); + + // write NXentry: "mantid_workspace_1" + do_execute(fileInfo.fullPath(), ws1); + // write NXentry: "mantid_workspace_2" + do_execute(fileInfo.fullPath(), ws2, true); + // write NXentry: "mantid_workspace_3" + do_execute(fileInfo.fullPath(), ws3, true); + + validate_spectra_detector_map_structure(fileInfo.fullPath(), "mantid_workspace_1", "first_instrument"); + + validate_spectra_detector_map_structure(fileInfo.fullPath(), "mantid_workspace_2", "second_instrument"); + + validate_spectra_detector_map_structure(fileInfo.fullPath(), "mantid_workspace_3", "third_instrument"); + } + + void test_workspace_group() { + FileResource fileInfo("test_group_workspace.nxs"); + auto ws1 = createWorkspaceWithInstrumentAndSpectraMap("first_instrument"); + auto ws2 = createWorkspaceWithInstrumentAndSpectraMap("second_instrument"); + auto ws3 = createWorkspaceWithInstrumentAndSpectraMap("third_instrument"); + + auto &ADS = AnalysisDataService::Instance(); + ADS.add("ws1", ws1); + ADS.add("ws2", ws2); + ADS.add("ws3", ws3); + auto wss = groupWorkspaces("wss", {"ws1", "ws2", "ws3"}); + + // Write three NXentry, from the unrolled group workspace: + // "mantid_workspace_1", "mantid_workspace_2", and "mantid_workspace_3". + do_execute(fileInfo.fullPath(), wss); + + // File structure should be exactly the same as if the unrolled + // workspaces were appended separately. + validate_spectra_detector_map_structure(fileInfo.fullPath(), "mantid_workspace_1", "first_instrument"); + + validate_spectra_detector_map_structure(fileInfo.fullPath(), "mantid_workspace_2", "second_instrument"); + + validate_spectra_detector_map_structure(fileInfo.fullPath(), "mantid_workspace_3", "third_instrument"); + + ADS.remove("wss"); + ADS.remove("ws1"); + ADS.remove("ws2"); + ADS.remove("ws3"); + } + + void test_saveInstrument_with_workspace() { // This is testing the core routine, but we put it here and not in // NexusGeometrySave because we need access to WorkspaceCreationHelpers for // this. + FileResource fileResource("test_with_full_workspace.hdf5"); - std::string destinationFile = fileResource.fullPath(); + Mantid::Kernel::Logger logger("logger"); + Mantid::NexusGeometry::LogAdapter adapter(&logger); + auto ws = WorkspaceCreationHelper::create2DWorkspaceWithRectangularInstrument(2, 10, 20); + + Mantid::NexusGeometry::NexusGeometrySave::saveInstrument(*ws, fileResource.fullPath(), "entry", adapter); + } + + void test_saveInstrument_with_multiple_workspace_entries() { + + // Test that the correct instruments are appended to the NXentries, when entry numbers are specified: + // for this specific test, none of the NXentry parent groups will exist in advance. + + // As with the previous test, this test is placed here because it requires `WorkspaceCreationHelper`. + + using Mantid::NexusGeometry::NX_ENTRY; + using Mantid::NexusGeometry::NX_INSTRUMENT; + + const size_t N_workspace_entries = 3; + FileResource testInput("test_with_multiple_workspace_entries.hdf5"); + Mantid::Kernel::Logger logger("logger"); + Mantid::NexusGeometry::LogAdapter adapter(&logger); + + for (size_t n = 1; n <= N_workspace_entries; ++n) { + std::ostringstream name; + name << "instrument_" << n; + auto ws = WorkspaceCreationHelper::create2DWorkspaceWithRectangularInstrument(2, 10, 20, name.str()); + Mantid::NexusGeometry::NexusGeometrySave::saveInstrument(*ws, testInput.fullPath(), "mantid_workspace_", n, + adapter, n > 1); + } + + H5::H5File h5(testInput.fullPath(), H5F_ACC_RDONLY); + _assert_group_structure(h5, { + {"/mantid_workspace_1", NX_ENTRY}, + {"/mantid_workspace_1/instrument_1", NX_INSTRUMENT}, + {"/mantid_workspace_2", NX_ENTRY}, + {"/mantid_workspace_2/instrument_2", NX_INSTRUMENT}, + {"/mantid_workspace_3", NX_ENTRY}, + {"/mantid_workspace_3/instrument_3", NX_INSTRUMENT}, + }); + } + + void test_saveInstrument_with_multiple_existing_workspace_entries() { + + // Test that the instruments are appended to the correct NXentries, when entry numbers are specified: + // for this specific test, each of the NXentry parent groups will exist in advance. + + // As with the previous test, this test is placed here because it requires `WorkspaceCreationHelper`. + + using Mantid::NexusGeometry::NX_CLASS; + using Mantid::NexusGeometry::NX_ENTRY; + using Mantid::NexusGeometry::NX_INSTRUMENT; + + FileResource testInput("test_with_multiple_existing_workspace_entries.hdf5"); + Mantid::Kernel::Logger logger("logger"); Mantid::NexusGeometry::LogAdapter adapter(&logger); - Mantid::NexusGeometry::NexusGeometrySave::saveInstrument(*ws, destinationFile, "entry", adapter); + + const size_t N_workspace_entries = 3; + { + // Create several NXentry. + H5::H5File h5(testInput.fullPath(), H5F_ACC_TRUNC); + for (size_t n = 1; n < N_workspace_entries; ++n) { + std::ostringstream entryName; + entryName << "/mantid_workspace_" << n; + std::cout << "creating: " << entryName.str() << std::endl; + H5::Group g = h5.createGroup(entryName.str()); + H5Util::writeStrAttribute(g, NX_CLASS, NX_ENTRY); + } + h5.close(); + } + + // Write an instrument to each NXentry. + for (size_t n = 1; n <= N_workspace_entries; ++n) { + std::ostringstream name; + name << "instrument_" << n; + auto ws = WorkspaceCreationHelper::create2DWorkspaceWithRectangularInstrument(2, 10, 20, name.str()); + Mantid::NexusGeometry::NexusGeometrySave::saveInstrument(*ws, testInput.fullPath(), "mantid_workspace_", n, + adapter, true); + } + + H5::H5File h5(testInput.fullPath(), H5F_ACC_RDONLY); + _assert_group_structure(h5, { + {"/mantid_workspace_1", NX_ENTRY}, + {"/mantid_workspace_1/instrument_1", NX_INSTRUMENT}, + {"/mantid_workspace_2", NX_ENTRY}, + {"/mantid_workspace_2/instrument_2", NX_INSTRUMENT}, + {"/mantid_workspace_3", NX_ENTRY}, + {"/mantid_workspace_3/instrument_3", NX_INSTRUMENT}, + }); + } + + void test_saveInstrument_with_multiple_workspaces_append() { + + // Test that the instrument is appended to the correct NXentry, + // when an explicit entry number is not specified. + + // As with the previous test, this test is placed here because it requires `WorkspaceCreationHelper`. + + using Mantid::NexusGeometry::NX_CLASS; + using Mantid::NexusGeometry::NX_ENTRY; + using Mantid::NexusGeometry::NX_INSTRUMENT; + + FileResource testInput("test_with_multiple_workspaces_append.hdf5"); + Mantid::Kernel::Logger logger("logger"); + Mantid::NexusGeometry::LogAdapter adapter(&logger); + + // Create an HDF5 file with several NXentry. + const size_t N_workspace_entries = 3; + for (size_t n = 1; n <= N_workspace_entries; ++n) { + { + // Write the latest NXentry. + H5::H5File h5(testInput.fullPath(), n > 1 ? H5F_ACC_RDWR : H5F_ACC_TRUNC); + std::ostringstream entryName; + entryName << "/mantid_workspace_" << n; + H5::Group g = h5.createGroup(entryName.str()); + H5Util::writeStrAttribute(g, NX_CLASS, NX_ENTRY); + h5.close(); + } + { + // Write the corresponding NXinstrument. + std::ostringstream instrumentName; + instrumentName << "instrument_" << n; + auto ws = WorkspaceCreationHelper::create2DWorkspaceWithRectangularInstrument(2, 10, 20, instrumentName.str()); + Mantid::NexusGeometry::NexusGeometrySave::saveInstrument(*ws, testInput.fullPath(), "mantid_workspace_", + std::nullopt, adapter, true); + } + } + + // Verify the resulting structure. + H5::H5File h5(testInput.fullPath(), H5F_ACC_RDONLY); + _assert_group_structure(h5, { + {"/mantid_workspace_1", NX_ENTRY}, + {"/mantid_workspace_1/instrument_1", NX_INSTRUMENT}, + {"/mantid_workspace_2", NX_ENTRY}, + {"/mantid_workspace_2/instrument_2", NX_INSTRUMENT}, + {"/mantid_workspace_3", NX_ENTRY}, + {"/mantid_workspace_3/instrument_3", NX_INSTRUMENT}, + }); } void test_regression_iris() { FileResource handle("test_regression_iris.nxs"); // IRIS has single monitors + auto iris = test_utility::from_instrument_name("IRIS"); do_execute(handle.fullPath(), iris); auto iris_reloaded = test_utility::reload(handle.fullPath()); @@ -216,6 +390,7 @@ class SaveNexusESSTest : public CxxTest::TestSuite { TS_ASSERT_EQUALS(inDetInfo.size(), outDetInfo.size()); TS_ASSERT_EQUALS(indexInfo.size(), indexInfoReload.size()); } + void test_not_all_detectors_mapped_to_spectrum() { FileResource handle("test_regression_iris_with_mappings.nxs"); // IRIS does not include all // detectors in it's @@ -226,6 +401,7 @@ class SaveNexusESSTest : public CxxTest::TestSuite { TS_ASSERT_THROWS_NOTHING( Mantid::NexusGeometry::NexusGeometrySave::saveInstrument(*ws, handle.fullPath(), "entry", adapter)); } + void test_not_all_detectors_mapped_to_spectrum_and_reloaded() { FileResource handle("test_regression_iris_with_mappings.nxs"); // IRIS does not include all // detectors in it's @@ -234,4 +410,62 @@ class SaveNexusESSTest : public CxxTest::TestSuite { do_execute(handle.fullPath(), ws); auto ws_out = test_utility::reload(handle.fullPath()); } + +private: + // Validate the spectra and detector-map structure of a single workspace entry. + static void validate_spectra_detector_map_structure(const std::string &filePath, const std::string &parentEntryName, + const std::string &instrumentName) { + Mantid::NexusGeometry::NexusFileReader validator(filePath); + TS_ASSERT(validator.hasDataset("spectra", {parentEntryName, instrumentName, "bank1"})); + TS_ASSERT(validator.hasDataset("detector_list", {parentEntryName, instrumentName, "bank1"})); + TS_ASSERT(validator.hasDataset("detector_index", {parentEntryName, instrumentName, "bank1"})); + TS_ASSERT(validator.hasDataset("detector_count", {parentEntryName, instrumentName, "bank1"})); + TS_ASSERT(validator.hasDataset("spectra", {parentEntryName, instrumentName, "bank2"})); + TS_ASSERT(validator.hasDataset("detector_list", {parentEntryName, instrumentName, "bank2"})); + TS_ASSERT(validator.hasDataset("detector_index", {parentEntryName, instrumentName, "bank2"})); + TS_ASSERT(validator.hasDataset("detector_count", {parentEntryName, instrumentName, "bank2"})); + } + + // Create a 2D workspace with spectra and a simple, named instrument. + static Mantid::DataObjects::Workspace2D_sptr + createWorkspaceWithInstrumentAndSpectraMap(const std::string &instrumentName) { + using namespace Mantid::Indexing; + auto ws = WorkspaceCreationHelper::create2DWorkspaceWithRectangularInstrument(2 /*numBanks*/, 10 /*numPixels*/, + 12 /*numBins*/, instrumentName); + std::vector specDefinitions; + std::vector spectrumNumbers; + size_t i = ws->getNumberHistograms() - 1; + for (size_t j = 0; j < ws->getNumberHistograms(); --i, ++j) { + specDefinitions.emplace_back(SpectrumDefinition(i)); + spectrumNumbers.emplace_back(SpectrumNumber(static_cast(j))); + } + IndexInfo info(spectrumNumbers); + info.setSpectrumDefinitions(specDefinitions); + ws->setIndexInfo(info); + return ws; + } + + // Create a group workspace from several input workspaces. + static Mantid::API::Workspace_sptr groupWorkspaces(const std::string &outputWSName, + const std::vector &inputWSNames) { + const auto groupAlg = AlgorithmManager::Instance().create("GroupWorkspaces"); + groupAlg->setProperty("OutputWorkspace", outputWSName); + groupAlg->setProperty("InputWorkspaces", inputWSNames); + groupAlg->execute(); + return AnalysisDataService::Instance().retrieve(outputWSName); + } + + // Verify that multiple HDF5 groups, with a specified NX_class, exist in an HDF5 file at the expected locations + void _assert_group_structure(H5::H5File &file, + const std::vector> &pathsWithClasses) { + using Mantid::NexusGeometry::NX_CLASS; + + for (const auto &pathWithClass : pathsWithClasses) { + const std::string &groupPath = pathWithClass.first; + const std::string &className = pathWithClass.second; + TS_ASSERT(H5Util::groupExists(file, groupPath)); + H5::Group g = file.openGroup(groupPath); + TS_ASSERT(H5Util::keyHasValue(g, NX_CLASS, className)); + } + } }; diff --git a/Framework/DataHandling/test/SaveNexusGeometryTest.h b/Framework/DataHandling/test/SaveNexusGeometryTest.h index 0a2ed1299d39..12af5521e7ae 100644 --- a/Framework/DataHandling/test/SaveNexusGeometryTest.h +++ b/Framework/DataHandling/test/SaveNexusGeometryTest.h @@ -38,7 +38,7 @@ class SaveNexusGeometryTest : public CxxTest::TestSuite { FileResource fileResource("algorithm_test_file.hdf5"); auto destinationFile = fileResource.fullPath(); - // Create test input if necessary + // Create test input Mantid::API::IEventWorkspace_sptr inputWS = WorkspaceCreationHelper::createEventWorkspaceWithFullInstrument2(1, 5); TS_ASSERT_THROWS_NOTHING(Mantid::API::AnalysisDataService::Instance().addOrReplace("testWS", inputWS)); @@ -56,7 +56,7 @@ class SaveNexusGeometryTest : public CxxTest::TestSuite { TS_ASSERT_THROWS_NOTHING(Mantid::API::AnalysisDataService::Instance().remove("testWS")); } - void test_execution_succesful_when_no_h5_root_provided_and_default_root_is_used() { + void test_execution_successful_when_no_h5_root_provided_and_default_root_is_used() { FileResource fileResource("algorithm_no_h5_root_file.hdf5"); auto destinationFile = fileResource.fullPath(); @@ -80,7 +80,7 @@ class SaveNexusGeometryTest : public CxxTest::TestSuite { void test_invalid_workspace_throws() { /* - test runtime error is thrown when a workspae without an Instrument is passed + test that runtime error is thrown when a workspace without an Instrument is passed into the Input workspace property. */ diff --git a/Framework/Geometry/src/Instrument/InstrumentDefinitionParser.cpp b/Framework/Geometry/src/Instrument/InstrumentDefinitionParser.cpp index 080640e959ec..3abc2dce5976 100644 --- a/Framework/Geometry/src/Instrument/InstrumentDefinitionParser.cpp +++ b/Framework/Geometry/src/Instrument/InstrumentDefinitionParser.cpp @@ -1280,9 +1280,9 @@ void InstrumentDefinitionParser::createDetectorOrMonitor(Geometry::ICompAssembly m_neutronicPos[detector] = pLocElem->getChildElement("neutronic"); } - // mark-as is a depricated attribute used before is="monitor" was introduced + // mark-as is a deprecated attribute used before is="monitor" was introduced if (pCompElem->hasAttribute("mark-as") || pLocElem->hasAttribute("mark-as")) { - g_log.warning() << "Attribute 'mark-as' is a depricated attribute in " + g_log.warning() << "Attribute 'mark-as' is a deprecated attribute in " "Instrument Definition File." << " Please see the deprecated section of " "docs.mantidproject.org/concepts/InstrumentDefinitionFile for how to remove this " @@ -1293,7 +1293,7 @@ void InstrumentDefinitionParser::createDetectorOrMonitor(Geometry::ICompAssembly if (category == "Monitor" || category == "monitor") m_instrument->markAsMonitor(detector); else { - // for backwards compatebility look for mark-as="monitor" + // for backwards compatibility look for mark-as="monitor" if ((pCompElem->hasAttribute("mark-as") && pCompElem->getAttribute("mark-as") == "monitor") || (pLocElem->hasAttribute("mark-as") && pLocElem->getAttribute("mark-as") == "monitor")) { m_instrument->markAsMonitor(detector); diff --git a/Framework/Nexus/src/NexusClasses.cpp b/Framework/Nexus/src/NexusClasses.cpp index 588da94f7c97..99c0d86e4ecd 100644 --- a/Framework/Nexus/src/NexusClasses.cpp +++ b/Framework/Nexus/src/NexusClasses.cpp @@ -198,7 +198,7 @@ void NXClass::open() { } /** It is fast, but the parent of this class must be open at - * the time of calling. openNXClass uses open() (the slow one). To open calss + * the time of calling. openNXClass uses open() (the slow one). To open class * using openLocal() do: * NXTheClass class(parent,name); * class.openLocal(); diff --git a/Framework/NexusGeometry/inc/MantidNexusGeometry/AbstractLogger.h b/Framework/NexusGeometry/inc/MantidNexusGeometry/AbstractLogger.h index 958649a35097..7da51a7375c0 100644 --- a/Framework/NexusGeometry/inc/MantidNexusGeometry/AbstractLogger.h +++ b/Framework/NexusGeometry/inc/MantidNexusGeometry/AbstractLogger.h @@ -18,8 +18,9 @@ namespace NexusGeometry { */ class MANTID_NEXUSGEOMETRY_DLL AbstractLogger { public: - virtual void warning(const std::string &warning) = 0; - virtual void error(const std::string &error) = 0; + virtual void debug(const std::string &message) = 0; + virtual void warning(const std::string &message) = 0; + virtual void error(const std::string &message) = 0; virtual ~AbstractLogger() = default; }; @@ -29,6 +30,7 @@ template class LogAdapter : public AbstractLogger { public: LogAdapter(T *adaptee) : m_adaptee(adaptee) {} + virtual void debug(const std::string &message) override { m_adaptee->debug(message); } virtual void warning(const std::string &message) override { m_adaptee->warning(message); } virtual void error(const std::string &message) override { m_adaptee->error(message); } }; @@ -46,6 +48,7 @@ template std::unique_ptr makeLogger(T *adaptee) { public: Adapter(T *adaptee) : m_adaptee(adaptee) {} + virtual void debug(const std::string &message) override { m_adaptee->debug(message); } virtual void warning(const std::string &message) override { m_adaptee->warning(message); } virtual void error(const std::string &message) override { m_adaptee->error(message); } }; diff --git a/Framework/NexusGeometry/inc/MantidNexusGeometry/NexusGeometryParser.h b/Framework/NexusGeometry/inc/MantidNexusGeometry/NexusGeometryParser.h index 299fb11a4009..7b41041a64d7 100644 --- a/Framework/NexusGeometry/inc/MantidNexusGeometry/NexusGeometryParser.h +++ b/Framework/NexusGeometry/inc/MantidNexusGeometry/NexusGeometryParser.h @@ -13,17 +13,31 @@ namespace Mantid { namespace Geometry { + class Instrument; } + namespace NexusGeometry { namespace NexusGeometryParser { + /** createInstrument : Responsible for parsing a nexus geometry file and creating an in-memory Mantid instrument. + This variant extracts the instrument from the first (and assumed only) NXentry in the file. */ MANTID_NEXUSGEOMETRY_DLL std::unique_ptr createInstrument(const std::string &fileName, std::unique_ptr logger); + +/** createInstrument : Responsible for parsing a nexus geometry file and + creating an in-memory Mantid instrument. + This variant extracts the instrument from a specific NXentry in the file. +*/ +MANTID_NEXUSGEOMETRY_DLL std::unique_ptr +createInstrument(const std::string &fileName, const std::string &parentGroupName, + std::unique_ptr logger); + MANTID_NEXUSGEOMETRY_DLL std::string getMangledName(const std::string &fileName, const std::string &instName); + } // namespace NexusGeometryParser } // namespace NexusGeometry } // namespace Mantid diff --git a/Framework/NexusGeometry/inc/MantidNexusGeometry/NexusGeometrySave.h b/Framework/NexusGeometry/inc/MantidNexusGeometry/NexusGeometrySave.h index 9ddcdda5fbfa..fb234e0d5384 100644 --- a/Framework/NexusGeometry/inc/MantidNexusGeometry/NexusGeometrySave.h +++ b/Framework/NexusGeometry/inc/MantidNexusGeometry/NexusGeometrySave.h @@ -19,6 +19,7 @@ #include "MantidNexusGeometry/AbstractLogger.h" #include "MantidNexusGeometry/DllConfig.h" #include +#include #include #include @@ -45,17 +46,23 @@ namespace NexusGeometrySave { MANTID_NEXUSGEOMETRY_DLL void saveInstrument(const Geometry::ComponentInfo &compInfo, const Geometry::DetectorInfo &detInfo, const std::string &fullPath, - const std::string &rootName, AbstractLogger &logger, bool append = false, + const std::string &parentGroupName, AbstractLogger &logger, + bool append = false, Kernel::ProgressBase *reporter = nullptr); + +MANTID_NEXUSGEOMETRY_DLL void saveInstrument(const Mantid::API::MatrixWorkspace &ws, const std::string &filePath, + const std::string &entryNamePrefix, std::optional entryNumber, + AbstractLogger &logger, bool append = false, Kernel::ProgressBase *reporter = nullptr); MANTID_NEXUSGEOMETRY_DLL void saveInstrument(const Mantid::API::MatrixWorkspace &ws, const std::string &fullPath, - const std::string &rootName, AbstractLogger &logger, bool append = false, - Kernel::ProgressBase *reporter = nullptr); + const std::string &parentGroupName, AbstractLogger &logger, + bool append = false, Kernel::ProgressBase *reporter = nullptr); MANTID_NEXUSGEOMETRY_DLL void saveInstrument( const std::pair, std::unique_ptr> &instrPair, - const std::string &fullPath, const std::string &rootName, AbstractLogger &logger, bool append = false, + const std::string &fullPath, const std::string &parentGroupName, AbstractLogger &logger, bool append = false, Kernel::ProgressBase *reporter = nullptr); + } // namespace NexusGeometrySave } // namespace NexusGeometry } // namespace Mantid diff --git a/Framework/NexusGeometry/inc/MantidNexusGeometry/NexusGeometryUtilities.h b/Framework/NexusGeometry/inc/MantidNexusGeometry/NexusGeometryUtilities.h index a715d611a1c1..32ca31f18730 100644 --- a/Framework/NexusGeometry/inc/MantidNexusGeometry/NexusGeometryUtilities.h +++ b/Framework/NexusGeometry/inc/MantidNexusGeometry/NexusGeometryUtilities.h @@ -32,9 +32,13 @@ H5::Group findGroupOrThrow(const H5::Group &parentGroup, const H5std_string &cla std::vector findGroups(const H5::Group &parentGroup, const H5std_string &classType); -std::optional findGroupByName(const H5::Group &parentGroup, const H5std_string &name); -bool hasNXAttribute(const H5::Group &group, const std::string &attributeValue); +std::optional findGroupByName(const H5::Group &parentGroup, const H5std_string &name, + const std::optional classType = std::nullopt); + +bool hasNXClass(const H5::Group &group, const std::string &attributeValue); + bool isNamed(const H5::H5Object &obj, const std::string &name); + } // namespace utilities } // namespace NexusGeometry } // namespace Mantid diff --git a/Framework/NexusGeometry/src/NexusGeometryParser.cpp b/Framework/NexusGeometry/src/NexusGeometryParser.cpp index 536b20a19f14..24909b3ddd01 100644 --- a/Framework/NexusGeometry/src/NexusGeometryParser.cpp +++ b/Framework/NexusGeometry/src/NexusGeometryParser.cpp @@ -214,7 +214,7 @@ class Parser { } } - // Provided to support invalid or null-termination character strings + // Provided to support invalid or empty null-termination character strings std::string readOrSubstitute(const std::string &dataset, const Group &group, const std::string &substitute) { auto read = get1DStringDataset(dataset, group); if (read.empty()) @@ -257,32 +257,26 @@ class Parser { } // Get the instrument name - std::string instrumentName(const Group &root) { - Group entryGroup = *utilities::findGroup(root, NX_ENTRY); - Group instrumentGroup = *utilities::findGroup(entryGroup, NX_INSTRUMENT); + std::string instrumentName(const Group &parent) { + Group instrumentGroup = *utilities::findGroup(parent, NX_INSTRUMENT); return get1DStringDataset("name", instrumentGroup); } - // Open all detector groups into a vector - std::vector openDetectorGroups(const Group &root) { - std::vector rawDataGroupPaths = openSubGroups(root, NX_ENTRY); + // Open all detector subgroups into a vector + std::vector openDetectorGroups(const Group &parent) { + + // This method was originally written to open all detector groups for all instruments in the file. + // But then it was used only with files containing a single instrument. + // In order to work correctly for files containing multiple workspaces, + // this method needs to open _only_ the detector groups for the current instrument. + + Group instrumentGroup = *utilities::findGroup(parent, NX_INSTRUMENT); + + // Open all detector subgroups within the instrument + std::vector detectorGroups = openSubGroups(instrumentGroup, NX_DETECTOR); - // Open all instrument groups within rawDataGroups - std::vector instrumentGroupPaths; - for (auto const &rawDataGroupPath : rawDataGroupPaths) { - std::vector instrumentGroups = openSubGroups(rawDataGroupPath, NX_INSTRUMENT); - instrumentGroupPaths.insert(instrumentGroupPaths.end(), instrumentGroups.begin(), instrumentGroups.end()); - } - // Open all detector groups within instrumentGroups - std::vector detectorGroupPaths; - for (auto const &instrumentGroupPath : instrumentGroupPaths) { - // Open sub detector groups - std::vector detectorGroups = openSubGroups(instrumentGroupPath, NX_DETECTOR); - // Append to detectorGroups vector - detectorGroupPaths.insert(detectorGroupPaths.end(), detectorGroups.begin(), detectorGroups.end()); - } // Return the detector groups - return detectorGroupPaths; + return detectorGroups; } // Function to return the (x,y,z) offsets of pixels in the chosen @@ -344,10 +338,10 @@ class Parser { } /** - * Creates a Homogeneous transfomation for nexus groups + * Creates a Homogeneous transformation for NeXus groups * * Walks the chain of transformations described in the file where W1 is first - *transformation and Wn is last and assembles them as + * transformation and Wn is last and assembles them as * * W = Wn x ... W2 x W1 * @@ -645,9 +639,9 @@ class Parser { void parseAndAddBank(const Group &shapeGroup, InstrumentBuilder &builder, const std::vector &detectorIds, const std::string &bankName, const Group &detectorGroup) { - if (utilities::hasNXAttribute(shapeGroup, NX_OFF)) { + if (utilities::hasNXClass(shapeGroup, NX_OFF)) { parseMeshAndAddDetectors(builder, shapeGroup, detectorIds, bankName, detectorGroup); - } else if (utilities::hasNXAttribute(shapeGroup, NX_CYLINDER)) { + } else if (utilities::hasNXClass(shapeGroup, NX_CYLINDER)) { parseNexusCylinderDetector(shapeGroup, bankName, builder, detectorIds); } else { std::stringstream ss; @@ -689,9 +683,8 @@ class Parser { } // Parse source and add to instrument - void parseAndAddSource(const H5File &file, const Group &root, InstrumentBuilder &builder) { - Group entryGroup = utilities::findGroupOrThrow(root, NX_ENTRY); - Group instrumentGroup = utilities::findGroupOrThrow(entryGroup, NX_INSTRUMENT); + void parseAndAddSource(const H5File &file, const Group &parent, InstrumentBuilder &builder) { + Group instrumentGroup = utilities::findGroupOrThrow(parent, NX_INSTRUMENT); Group sourceGroup = utilities::findGroupOrThrow(instrumentGroup, NX_SOURCE); std::string sourceName = "Unspecified"; if (utilities::findDataset(sourceGroup, "name")) @@ -702,9 +695,8 @@ class Parser { } // Parse sample and add to instrument - void parseAndAddSample(const H5File &file, const Group &root, InstrumentBuilder &builder) { - Group entryGroup = utilities::findGroupOrThrow(root, NX_ENTRY); - Group sampleGroup = utilities::findGroupOrThrow(entryGroup, NX_SAMPLE); + void parseAndAddSample(const H5File &file, const Group &parent, InstrumentBuilder &builder) { + Group sampleGroup = utilities::findGroupOrThrow(parent, NX_SAMPLE); auto sampleTransforms = getTransformations(file, sampleGroup); Eigen::Vector3d samplePos = sampleTransforms * Eigen::Vector3d(0.0, 0.0, 0.0); std::string sampleName = "Unspecified"; @@ -713,35 +705,36 @@ class Parser { builder.addSample(sampleName, samplePos); } - void parseMonitors(const H5File &file, const H5::Group &root, InstrumentBuilder &builder) { - std::vector rawDataGroupPaths = openSubGroups(root, NX_ENTRY); - - // Open all instrument groups within rawDataGroups - for (auto const &rawDataGroupPath : rawDataGroupPaths) { - std::vector instrumentGroups = openSubGroups(rawDataGroupPath, NX_INSTRUMENT); - for (auto &inst : instrumentGroups) { - std::vector monitorGroups = openSubGroups(inst, NX_MONITOR); - for (auto &monitor : monitorGroups) { - if (!utilities::findDataset(monitor, DETECTOR_ID)) - throw std::invalid_argument("NXmonitors must have " + DETECTOR_ID); - auto detectorId = readNXInts(monitor, DETECTOR_ID)[0]; - bool proxy = false; - auto monitorShape = parseNexusShape(monitor, proxy); - auto monitorTransforms = getTransformations(file, monitor); - builder.addMonitor(std::to_string(detectorId), static_cast(detectorId), - monitorTransforms * Eigen::Vector3d{0, 0, 0}, monitorShape); - } - } + void parseMonitors(const H5File &file, const H5::Group &parent, InstrumentBuilder &builder) { + // As for `openDetectorGroups`: this method was previously written to parse the monitors from every instrument in + // the file. But then was only used with files containing a single instrument. + + // In order to be used with files containing multiple workspaces, the requirement is actually + // to parse _only_ the monitors from the current instrument. + + Group instrumentGroup = utilities::findGroupOrThrow(parent, NX_INSTRUMENT); + + std::vector monitorGroups = openSubGroups(instrumentGroup, NX_MONITOR); + for (const auto &monitor : monitorGroups) { + if (!utilities::findDataset(monitor, DETECTOR_ID)) + throw std::invalid_argument("NXmonitors must have " + DETECTOR_ID); + auto detectorId = readNXInts(monitor, DETECTOR_ID)[0]; + bool proxy = false; + auto monitorShape = parseNexusShape(monitor, proxy); + auto monitorTransforms = getTransformations(file, monitor); + builder.addMonitor(std::to_string(detectorId), static_cast(detectorId), + monitorTransforms * Eigen::Vector3d{0, 0, 0}, monitorShape); } } public: explicit Parser(std::unique_ptr &&logger) : m_logger(std::move(logger)) {} - std::unique_ptr extractInstrument(const H5File &file, const Group &root) { - InstrumentBuilder builder(instrumentName(root)); - // Get path to all detector groups - const std::vector detectorGroups = openDetectorGroups(root); + std::unique_ptr extractInstrument(const H5File &file, const Group &parent) { + InstrumentBuilder builder(instrumentName(parent)); + + // Open all detector subgroups + const std::vector detectorGroups = openDetectorGroups(parent); for (auto &detectorGroup : detectorGroups) { // Transform in homogenous coordinates. Offsets will be rotated then bank // translation applied. @@ -791,24 +784,40 @@ class Parser { builder.addDetectorToLastBank(name, detectorIds[index], relativePos, detShape); } } - // Sort the detectors - // Parse source and sample and add to instrument - parseAndAddSample(file, root, builder); - parseAndAddSource(file, root, builder); - parseMonitors(file, root, builder); + + // TODO? Sort the detectors + + // Parse the source and sample and add to instrument + parseAndAddSample(file, parent, builder); + parseAndAddSource(file, parent, builder); + + // Parse and add the monitors + parseMonitors(file, parent, builder); + return builder.createInstrument(); } }; } // namespace -std::unique_ptr +std::unique_ptr NexusGeometryParser::createInstrument(const std::string &fileName, std::unique_ptr logger) { - const H5File file(fileName, H5F_ACC_RDONLY); auto rootGroup = file.openGroup("/"); + auto parentGroup = utilities::findGroupOrThrow(rootGroup, NX_ENTRY); + + Parser parser(std::move(logger)); + return parser.extractInstrument(file, parentGroup); +} + +std::unique_ptr +NexusGeometryParser::createInstrument(const std::string &fileName, const std::string &parentGroupName, + std::unique_ptr logger) { + + const H5File file(fileName, H5F_ACC_RDONLY); + auto parentGroup = file.openGroup(std::string("/") + parentGroupName); Parser parser(std::move(logger)); - return parser.extractInstrument(file, rootGroup); + return parser.extractInstrument(file, parentGroup); } // Create a unique instrument name from Nexus file diff --git a/Framework/NexusGeometry/src/NexusGeometrySave.cpp b/Framework/NexusGeometry/src/NexusGeometrySave.cpp index 40c39f1d58a5..ea5cbc2798f4 100644 --- a/Framework/NexusGeometry/src/NexusGeometrySave.cpp +++ b/Framework/NexusGeometry/src/NexusGeometrySave.cpp @@ -26,13 +26,19 @@ #include "MantidNexusGeometry/NexusGeometryDefinitions.h" #include "MantidNexusGeometry/NexusGeometryUtilities.h" #include +#include #include #include #include #include #include +#include +#include #include +using Mantid::API::SpectrumInfo; +using Mantid::Indexing::IndexInfo; + namespace Mantid::NexusGeometry::NexusGeometrySave { using namespace Geometry::ComponentInfoBankHelpers; /* @@ -144,8 +150,8 @@ H5::StrType strTypeOfSize(const std::string &str) { * 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. + * @param dSetName : dataset name. + * @param dSetVal : string value to be stored in dataset. */ void writeStrDataset(H5::Group &grp, const std::string &dSetName, const std::string &dSetVal, const H5::DataSpace &dataSpace = SCALAR) { @@ -281,8 +287,8 @@ void writeNXDetectorNumber(H5::Group &grp, const Geometry::ComponentInfo &compIn H5::DataSet detectorNumber; - std::vector bankDetIDs; // IDs of detectors beloning to bank - std::vector bankDetectors = compInfo.detectorsInSubtree(idx); // Indexes of children detectors in bank + std::vector bankDetIDs; // IDs of detectors belonging to bank + std::vector bankDetectors = compInfo.detectorsInSubtree(idx); // Indices of child detectors in bank bankDetIDs.reserve(bankDetectors.size()); // write the ID for each child detector to std::vector to be written to @@ -638,11 +644,11 @@ class NexusGeometrySaveImpl { /* * Function: saveNXSample - * For NXentry parent (root group). Produces an NXsample group in the parent + * For NXentry parent 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 parent : parent NXentry group in which to write the NXsample group. * @param compInfo : componentInfo object. */ void sample(const H5::Group &parentGroup, const Geometry::ComponentInfo &compInfo) { @@ -657,11 +663,11 @@ class NexusGeometrySaveImpl { /* * Function: saveNXSource - * For NXentry (root group). Produces an NXsource group in the parent group, + * For NXinstrument parent 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 parent : parent NXinstrument group in which to write the NXsource group. * @param compInfo : componentInfo object. */ void source(const H5::Group &parentGroup, const Geometry::ComponentInfo &compInfo) { @@ -714,12 +720,12 @@ class NexusGeometrySaveImpl { /* * Function: monitor - * For NXinstrument parent (component info root). Produces an NXmonitor - * groups from Component info, and saves it in the parent + * For NXinstrument parent. Produces an NXmonitor + * group 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 + * @param parentGroup : NXinstrument parent group in which to write the NXmonitor * group. * @param compInfo : componentInfo object. * @param monitorID : ID of the specific monitor. @@ -778,14 +784,14 @@ class NexusGeometrySaveImpl { return childGroup; } - /* For NXinstrument parent (component info root). Produces an NXmonitor - * groups from Component info, and saves it in the parent + /* For NXinstrument parent. Produces an NXmonitor + * group 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 + * @param parentGroup : NXinstrument parent group in which to write the NXmonitor * group. * @param compInfo : componentInfo object. * @param monitorId : ID of the specific monitor. @@ -793,32 +799,35 @@ class NexusGeometrySaveImpl { * @param mappings : Spectra to detector mappings */ void monitor(const H5::Group &parentGroup, const Geometry::ComponentInfo &compInfo, const int monitorId, - const size_t index, const SpectraMappings &mappings) { + const size_t index, const std::optional &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); + + if (mappings) { + // 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 + * For NXinstrument parent. Save method which produces + * an NXdetector group from a Component info detector bank, 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 + * @param parentGroup : NXinstrument parent group in which to write the NXdetector * 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 + * @return childGroup for further additions */ H5::Group detector(const H5::Group &parentGroup, const Geometry::ComponentInfo &compInfo, const std::vector &detIds, const size_t index) { @@ -857,7 +866,7 @@ class NexusGeometrySaveImpl { 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 + // orientation dataset containing the rotation transformation will be // location. Else dependency for orientation is self. std::string rotationDependency = locationIsOrigin ? NO_DEPENDENCY : H5_OBJ_NAME(transformations) + "/" + LOCATION; @@ -875,32 +884,34 @@ class NexusGeometrySaveImpl { /* * Function: detectors - * For NXinstrument parent (component info root). Save method which produces - * a set of NXdetctor groups from Component info detector banks, and saves + * For NXinstrument parent. Save method which produces + * an NXdetector group from a Component info detector bank, 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 + * @param parentGroup : NXinstrument parent group in which to write the NXdetector * 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 + * @param mappings : (std::optional) Spectra to detector mappings * NXdetector will be extracted. */ void detector(const H5::Group &parentGroup, const Geometry::ComponentInfo &compInfo, const std::vector &detIds, - const size_t index, const SpectraMappings &mappings) { + const size_t index, const std::optional &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); + if (mappings) { + // 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: @@ -929,60 +940,65 @@ class NexusGeometrySaveImpl { } }; // class NexusGeometrySaveImpl -/* - * Function: saveInstrument +/* Internal-use only signature: + * 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 detId2IndexMap: (optional) detid2index_map + * @param parentGroup : open H5::Group in which to place the NXinstrument. * @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); +void _saveInstrument(const H5::Group &parentGroup, bool append, AbstractLogger &logger, + const Geometry::ComponentInfo &compInfo, const Geometry::DetectorInfo &detInfo, + const std::optional detIdToIndexMap = std::nullopt, + const Indexing::IndexInfo *indexInfo = nullptr, const SpectrumInfo *spectrumInfo = nullptr, + Kernel::ProgressBase *reporter = nullptr) { + + { + std::ostringstream msg; + msg << "NexusGeometrySave::_saveInstrument: " + << "including " << (detIdToIndexMap ? "full " : "no ") << "spectra-mapping information."; + logger.debug(msg.str()); } - 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); + // open or create NXinstrument group + H5::Group instrument = writer.instrument(parentGroup, compInfo); // save NXsource writer.source(instrument, compInfo); // save NXsample - writer.sample(rootGroup, compInfo); + writer.sample(parentGroup, compInfo); - const auto &detIds = detInfo.detectorIDs(); // save NXdetectors + // IDs of all detectors in Instrument + const auto &detIds = detInfo.detectorIDs(); + std::list 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, detInfo, index)) { + if (isDesiredNXDetector(index, saved_indices, compInfo)) { + // Make spectra detector mappings + std::optional mappings; + if (detIdToIndexMap) { + if (!indexInfo || !spectrumInfo) + throw std::runtime_error("NexusGeometrySave::_saveInstrument: spectrum-mapping args are incomplete"); + mappings = makeMappings(compInfo, *detIdToIndexMap, *indexInfo, *spectrumInfo, detIds, index); + } + if (reporter != nullptr) reporter->report(); - writer.detector(instrument, compInfo, detIds, index); + writer.detector(instrument, compInfo, detIds, index, mappings); saved_indices.emplace_back(index); // Now record the fact that children of // this are not needed as NXdetectors } @@ -992,14 +1008,54 @@ void saveInstrument(const Geometry::ComponentInfo &compInfo, const Geometry::Det // save NXmonitors for (size_t index = 0; index < detInfo.size(); ++index) { if (detInfo.isMonitor(index)) { + // Make spectra detector mappings that can be used + std::optional mappings; + if (detIdToIndexMap) { + mappings = makeMappings(compInfo, *detIdToIndexMap, *indexInfo, *spectrumInfo, detIds, index); + } + if (reporter != nullptr) reporter->report(); - writer.monitor(instrument, compInfo, detIds[index], index); + writer.monitor(instrument, compInfo, detIds[index], index, mappings); } } +} - file.close(); // close file +/* + * 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 parentGroupName : name of root NXentry group in which to place the instrument. + * @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 &parentGroupName, AbstractLogger &logger, + bool append, Kernel::ProgressBase *reporter) { + + validateInputs(logger, fullPath, compInfo); + + H5::Group parentGroup; + H5::H5File file; + if (append) { + file = H5::H5File(fullPath, H5F_ACC_RDWR); // open file + parentGroup = file.openGroup(parentGroupName); + } else { + file = H5::H5File(fullPath, H5F_ACC_TRUNC); // open file + parentGroup = file.createGroup(parentGroupName); + writeStrAttribute(parentGroup, NX_CLASS, NX_ENTRY); + } + + _saveInstrument(parentGroup, append, logger, compInfo, detInfo, std::nullopt, nullptr, nullptr, reporter); + file.close(); // close file } // saveInstrument /** @@ -1010,7 +1066,7 @@ void saveInstrument(const Geometry::ComponentInfo &compInfo, const Geometry::Det * * @param instrPair : instrument 2.0 object. * @param fullPath : save destination as full path. - * @param rootName : name of root entry + * @param parentGroupName : name of root NXentry group in which to place the instrument. * @param logger : logging object * @param append : append mode, means openting and appending to existing file. * If false, creates new file. @@ -1018,86 +1074,157 @@ void saveInstrument(const Geometry::ComponentInfo &compInfo, const Geometry::Det */ void saveInstrument( const std::pair, std::unique_ptr> &instrPair, - const std::string &fullPath, const std::string &rootName, AbstractLogger &logger, bool append, + const std::string &fullPath, const std::string &parentGroupName, 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); + saveInstrument(compInfo, detInfo, fullPath, parentGroupName, 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) { +/** + * 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 ws : name of a MatrixWorkspace. + * @param fullPath : save destination as full path. + * @param parentGroupName : name of root NXentry group in which to place the instrument. + * @param logger : logging object + * @param append : append to the file, if it exists + * @param reporter : (optional) report to progressBase. + */ +void saveInstrument(const Mantid::API::MatrixWorkspace &ws, const std::string &filePath, + const std::string &parentGroupName, AbstractLogger &logger, bool append, + Kernel::ProgressBase *reporter) { const auto &detInfo = ws.detectorInfo(); const auto &compInfo = ws.componentInfo(); + const auto indexInfo = &ws.indexInfo(); + const auto spectrumInfo = &ws.spectrumInfo(); + const std::optional detIdToIndexMap = + ws.getDetectorIDToWorkspaceIndexMap(false); // allow multiple detectors / spectrum - // Exception handling. - validateInputs(logger, fullPath, compInfo); - // IDs of all detectors in Instrument - const auto &detIds = detInfo.detectorIDs(); + validateInputs(logger, filePath, compInfo); - H5::Group rootGroup; H5::H5File file; - if (append) { - file = H5::H5File(fullPath, H5F_ACC_RDWR); // open file - rootGroup = file.openGroup(rootName); + H5::Group parentGroup; + + // Create or overwrite the NXentry parent group. + if (Poco::File(filePath).exists() && append) { + file = H5::H5File(filePath, H5F_ACC_RDWR); // open existing file + H5::Group rootGroup = file.openGroup("/"); + std::optional maybeParent = utilities::findGroupByName(rootGroup, parentGroupName, NX_ENTRY); + if (maybeParent) + parentGroup = *maybeParent; + else { + parentGroup = rootGroup.createGroup(parentGroupName); + writeStrAttribute(parentGroup, NX_CLASS, NX_ENTRY); + } } else { - file = H5::H5File(fullPath, H5F_ACC_TRUNC); // open file - rootGroup = file.createGroup(rootName); + file = H5::H5File(filePath, H5F_ACC_TRUNC); // create a new file (or overwrite an existing one) + parentGroup = file.createGroup(std::string("/") + parentGroupName); + writeStrAttribute(parentGroup, NX_CLASS, NX_ENTRY); } - 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); + _saveInstrument(parentGroup, append, logger, compInfo, detInfo, detIdToIndexMap, indexInfo, spectrumInfo, reporter); - // save NXdetectors - auto detToIndexMap = ws.getDetectorIDToWorkspaceIndexMap(false /*do not throw if multiples*/); - std::list 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, detInfo, index)) { + file.close(); // close file +} - 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); +/** + * 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 ws : name of a MatrixWorkspace. + * @param filePath : save destination as full path. + * @param entryNamePrefix : prefix for workspace NXentry name, + * @param entryNumber: (optional) entry number, if not specified, the instrument + * will be appended to the latest workspace entry in the file + * @param logger : logging object + * @param append : append to an existing file. + * @param reporter : (optional) report to progressBase. + */ +void saveInstrument(const Mantid::API::MatrixWorkspace &ws, const std::string &filePath, + const std::string &entryNamePrefix, std::optional entryNumber, AbstractLogger &logger, + bool append, Kernel::ProgressBase *reporter) { - if (reporter != nullptr) - reporter->report(); - writer.detector(instrument, compInfo, detIds, index, mappings); - saved_indices.emplace_back(index); // Now record the fact that children of - // this are not needed as NXdetectors + const auto &detInfo = ws.detectorInfo(); + const auto &compInfo = ws.componentInfo(); + const auto indexInfo = &ws.indexInfo(); + const auto spectrumInfo = &ws.spectrumInfo(); + const std::optional detIdToIndexMap = + ws.getDetectorIDToWorkspaceIndexMap(false); // allow multiple detectors / spectrum + + // Note that `entryNumber` itself is validated elsewhere. + validateInputs(logger, filePath, compInfo); + + // Here we use the following entry-number behavior: + // (This is compatible with, although not as restrictive as, the standard Mantid behavior.) + // + // If an entry number is provided, we use it: + // * in this case entries may be written in any order; + // * If we are appending, any specific NXentry group may or may not already exist; + // but at the same time, a specific NXinstrument group must NOT exist. + // Overwriting an existing instrument group is not supported. + // + // Otherwise: + // * if we are not appending, the entry number will be one; + // * if we are appending, we write the NXinstrument group to the _latest_ NXentry group in the file; + // This latest group is defined as that which has the highest number suffix. + // + // Notes: + // * Any non-NXentry groups, or NXentry groups with unexpected names are ignored. + // + + if (!Poco::File(filePath).exists() && append) + throw std::runtime_error(std::string("NexusGeometrySave::saveInstrument: append specified but file '") + filePath + + "' does not exist"); + + H5::H5File file = H5::H5File(filePath, append ? H5F_ACC_RDWR : H5F_ACC_TRUNC); + H5::Group rootGroup = file.openGroup("/"); + + // Construct the correct parent-group (i.e. NXentry) name. + std::string entryName = entryNamePrefix + "1"; + if (entryNumber) { + std::ostringstream name; + name << entryNamePrefix << *entryNumber; + entryName = name.str(); + } else { + size_t latestEntryNumber = 1; + const auto nxEntries = utilities::findGroups(rootGroup, "NXentry"); + for (const auto &group : nxEntries) { + std::string name = group.getObjName(); + std::smatch m; + std::regex_search(name, m, std::regex(entryNamePrefix + "([0-9])")); + + if (!m.empty()) { + std::size_t entryNumber_ = static_cast(std::stoi(m[1])); + if (entryNumber_ > latestEntryNumber) { + latestEntryNumber = entryNumber_; + entryName = m[0]; // strips off the "/" + } } } } - // 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); - } + // Open or create the parent group. + H5::Group parentGroup; + auto maybeParentGroup = utilities::findGroupByName(rootGroup, entryName, NX_ENTRY); + if (maybeParentGroup) { + parentGroup = rootGroup.openGroup(entryName); + } else { + parentGroup = rootGroup.createGroup(entryName); + writeStrAttribute(parentGroup, NX_CLASS, NX_ENTRY); } - file.close(); // close file -} + _saveInstrument(parentGroup, append, logger, compInfo, detInfo, detIdToIndexMap, indexInfo, spectrumInfo, reporter); -// saveInstrument + file.close(); // close file +} // saveInstrument } // namespace Mantid::NexusGeometry::NexusGeometrySave diff --git a/Framework/NexusGeometry/src/NexusGeometryUtilities.cpp b/Framework/NexusGeometry/src/NexusGeometryUtilities.cpp index 7b991ff2f56f..895ab36768ae 100644 --- a/Framework/NexusGeometry/src/NexusGeometryUtilities.cpp +++ b/Framework/NexusGeometry/src/NexusGeometryUtilities.cpp @@ -28,20 +28,23 @@ std::optional findDataset(const H5::Group &parentGroup, const H5std return std::optional{}; // Empty } -std::optional findGroupByName(const H5::Group &parentGroup, const H5std_string &name) { +std::optional findGroupByName(const H5::Group &parentGroup, const H5std_string &name, + const std::optional classType) { for (hsize_t i = 0; i < parentGroup.getNumObjs(); ++i) { if (parentGroup.getObjTypeByIdx(i) == GROUP_TYPE) { H5std_string childPath = parentGroup.getObjnameByIdx(i); if (childPath == name) { - return std::optional(parentGroup.openGroup(childPath)); + H5::Group childGroup = parentGroup.openGroup(childPath); + if (!classType || hasNXClass(childGroup, *classType)) + return std::optional(childGroup); } } } - return std::optional(); + return std::nullopt; } -bool hasNXAttribute(const H5::Group &group, const std::string &attributeValue) { +bool hasNXClass(const H5::Group &group, const std::string &attributeValue) { bool result = false; for (uint32_t attribute_index = 0; attribute_index < static_cast(group.getNumAttrs()); ++attribute_index) { // Test attribute at current index for NX_class @@ -78,13 +81,13 @@ std::optional findGroup(const H5::Group &parentGroup, const H5std_str // Open the sub group auto childGroup = parentGroup.openGroup(childPath); // Iterate through attributes to find NX_class - if (hasNXAttribute(childGroup, classType)) { + if (hasNXClass(childGroup, classType)) { return std::optional(childGroup); } } } return std::optional{}; // Empty -} // namespace utilities +} /// Find all groups at the same level matching same class type. Returns first /// item found. @@ -97,12 +100,13 @@ std::vector findGroups(const H5::Group &parentGroup, const H5std_stri // Open the sub group auto childGroup = parentGroup.openGroup(childPath); // Iterate through attributes to find NX_class - if (hasNXAttribute(childGroup, classType)) + if (hasNXClass(childGroup, classType)) groups.emplace_back(childGroup); } } - return groups; // Empty + return groups; } + H5::Group findGroupOrThrow(const H5::Group &parentGroup, const H5std_string &classType) { auto found = findGroup(parentGroup, classType); if (!found) { diff --git a/Framework/NexusGeometry/test/NexusGeometryParserTest.h b/Framework/NexusGeometry/test/NexusGeometryParserTest.h index a957b816d880..42d18d759b01 100644 --- a/Framework/NexusGeometry/test/NexusGeometryParserTest.h +++ b/Framework/NexusGeometry/test/NexusGeometryParserTest.h @@ -8,6 +8,8 @@ #include +#include "MantidDataHandling/H5Util.h" +#include "MantidFrameworkTestHelpers/FileResource.h" #include "MantidGeometry/Instrument.h" #include "MantidGeometry/Instrument/ComponentInfo.h" #include "MantidGeometry/Instrument/DetectorInfo.h" @@ -17,6 +19,7 @@ #include "MantidGeometry/Surfaces/Cylinder.h" #include "MantidKernel/ConfigService.h" #include "MantidKernel/EigenConversionHelpers.h" +#include "MantidNexusGeometry/NexusGeometryDefinitions.h" #include "MantidNexusGeometry/NexusGeometryParser.h" #include "mockobjects.h" @@ -28,6 +31,8 @@ using namespace Mantid; using namespace NexusGeometry; +using namespace DataHandling; + namespace { std::unique_ptr extractDetectorInfo(const Mantid::Geometry::Instrument &instrument) { Geometry::ParameterMap pmap; @@ -44,6 +49,7 @@ extractBeamline(const Mantid::Geometry::Instrument &instrument) { std::string instrument_path(const std::string &local_name) { return Kernel::ConfigService::Instance().getFullPath(local_name, true, Poco::Glob::GLOB_DEFAULT); } + } // namespace class NexusGeometryParserTest : public CxxTest::TestSuite { @@ -53,32 +59,52 @@ class NexusGeometryParserTest : public CxxTest::TestSuite { static NexusGeometryParserTest *createSuite() { return new NexusGeometryParserTest(); } static void destroySuite(NexusGeometryParserTest *suite) { delete suite; } - static std::unique_ptr makeTestInstrument() { - const auto fullpath = instrument_path("unit_testing/SMALLFAKE_example_geometry.hdf5"); + void test_parse_from_specific_entry() { + // Test that the parser works correctly when there are multiple NXentry + // in the source file. - return NexusGeometryParser::createInstrument(fullpath, std::make_unique()); + FileResource multipleEntryInput("test_geometry_parser_with_multiple_entries.hdf5"); + { + // Load the multiple NXentry test input. + // (See notes about `NexusGeometrySave` and `NexusGeometryParser` at `_verify_basic_instrument` below.) + H5::H5File input(instrument_path("unit_testing/SMALLFAKE_example_multiple_entries.hdf5"), H5F_ACC_RDONLY); + + // Copy all of the NXentry groups to a new file. + H5::H5File testInput(multipleEntryInput.fullPath(), H5F_ACC_TRUNC); + H5Util::copyGroup(testInput, "/mantid_workspace_1", input, "/mantid_workspace_1"); + H5Util::copyGroup(testInput, "/mantid_workspace_2", input, "/mantid_workspace_2"); + H5Util::copyGroup(testInput, "/mantid_workspace_3", input, "/mantid_workspace_3"); + + // Remove the instrument from the first NXentry group. + H5Util::deleteObjectLink(testInput, "/mantid_workspace_1/SmallFakeTubeInstrument"); + } + + // The default `createInstrument` signature should fail: it will try to load from the first NXentry, + // which no longer has an instrument. + TS_ASSERT_THROWS( + NexusGeometryParser::createInstrument(multipleEntryInput.fullPath(), std::make_unique()), + const H5::Exception &); + + // Loading explicitly from the first entry should also fail for the same reason. + TS_ASSERT_THROWS(NexusGeometryParser::createInstrument(multipleEntryInput.fullPath(), "mantid_workspace_1", + std::make_unique()), + const H5::Exception &); + + // Loading explicitly from the second entry should succeed. + auto instrument = NexusGeometryParser::createInstrument(multipleEntryInput.fullPath(), "mantid_workspace_2", + std::make_unique()); + + // Verify that the instrument has been parsed correctly. + _verify_basic_instrument(*instrument, true); } void test_basic_instrument_information() { - auto instrument = makeTestInstrument(); - auto beamline = extractBeamline(*instrument); - auto componentInfo = std::move(beamline.first); - auto detectorInfo = std::move(beamline.second); - - TSM_ASSERT_EQUALS("Detectors + 1 monitor", detectorInfo->size(), 128 * 2 + 1); - TSM_ASSERT_EQUALS("Detectors + 2 banks + 16 tubes + root + source + sample", componentInfo->size(), - detectorInfo->size() + 21); - // Check 128 detectors in first bank - TS_ASSERT_EQUALS(128, componentInfo->detectorsInSubtree(componentInfo->root() - 3).size()); - TS_ASSERT_EQUALS("rear-detector", componentInfo->name(componentInfo->root() - 3)); - TS_ASSERT(Mantid::Kernel::toVector3d(componentInfo->position(componentInfo->root() - 3)) - .isApprox(Eigen::Vector3d{0, 0, 4})); - // Check 128 detectors in second bank - TS_ASSERT_EQUALS(128, componentInfo->detectorsInSubtree(componentInfo->root() - 12).size()); + auto instrument = _makeTestInstrument(); + _verify_basic_instrument(*instrument); } void test_source_is_where_expected() { - auto instrument = makeTestInstrument(); + auto instrument = _makeTestInstrument(); auto beamline = extractBeamline(*instrument); auto componentInfo = std::move(beamline.first); @@ -88,7 +114,7 @@ class NexusGeometryParserTest : public CxxTest::TestSuite { } void test_simple_translation() { - auto instrument = makeTestInstrument(); + auto instrument = _makeTestInstrument(); auto detectorInfo = extractDetectorInfo(*instrument); // First pixel in bank 2 auto det0Position = Kernel::toVector3d(detectorInfo->position(detectorInfo->indexOf(1100000))); @@ -111,7 +137,7 @@ class NexusGeometryParserTest : public CxxTest::TestSuite { } void test_complex_translation() { - auto instrument = makeTestInstrument(); + auto instrument = _makeTestInstrument(); auto detectorInfo = extractDetectorInfo(*instrument); // First pixel in bank 1 @@ -141,7 +167,7 @@ class NexusGeometryParserTest : public CxxTest::TestSuite { } void test_shape_cylinder_shape() { - auto instrument = makeTestInstrument(); + auto instrument = _makeTestInstrument(); auto beamline = extractBeamline(*instrument); auto componentInfo = std::move(beamline.first); const auto &det1Shape = componentInfo->shape(1); @@ -160,7 +186,7 @@ class NexusGeometryParserTest : public CxxTest::TestSuite { } void test_mesh_shape() { - auto instrument = makeTestInstrument(); + auto instrument = _makeTestInstrument(); auto beamline = extractBeamline(*instrument); auto componentInfo = std::move(beamline.first); auto detectorInfo = std::move(beamline.second); @@ -341,6 +367,63 @@ class NexusGeometryParserTest : public CxxTest::TestSuite { TS_ASSERT_EQUALS(parsedShapeMesh->numberOfTriangles(), 8); } } + +private: + // Parse a basic instrument from "unit_testing/SMALLFAKE_example_geometry.hdf5". + static std::unique_ptr _makeTestInstrument() { + const auto fullpath = instrument_path("unit_testing/SMALLFAKE_example_geometry.hdf5"); + return NexusGeometryParser::createInstrument(fullpath, std::make_unique()); + } + + // Verify that the instrument from "unit_testing/SMALLFAKE_example_geometry.hdf5" has been parsed correctly. + // Notes: + // * The original HDF5 test-input file contains detector tubes. These will be parsed correctly by + // `NexusGeometryParser`, + // but unfortunately at present, these tubes are ignored by `NexusGeometrySave`; + // * The `saveAndReparse` argument flag, and the `detectorInfoSize`, and `componentInfoSize` + // constants are provided to allow the required adjustments, when reparsing an instrument saved by + // `NexusGeometrySave`. + // * For the original instrument: + // -- `componentInfo->size() == 128 * 2 + 1 + 2 + 16 + 1 + 1 + 1`; + // -- `detectorInfo->size() == 128 * 2 + 1; + // * For the saved and reparsed instrument, excluding the tubes, these become: + // -- `componentInfo->size() == 128 * 2 + 1 + 2 + 1 + 1 + 1`; + // -- `detectorInfo->size() == 128 * 2 + 1`; + // + static void _verify_basic_instrument(const Mantid::Geometry::Instrument &instrument, bool saveAndReparse = false) { + + const size_t expectedDetectorBankSize = 128; + const size_t numberOfDetectorBanks = 2; + const size_t numberOfMonitors = 1; + const size_t expectedDetectorInfoSize = numberOfDetectorBanks * expectedDetectorBankSize + numberOfMonitors; + const size_t numberOfTubes = 16; + + const size_t expectedComponentInfoSize = + saveAndReparse + ? numberOfDetectorBanks * expectedDetectorBankSize + numberOfMonitors + numberOfDetectorBanks + 1 + 1 + 1 + : numberOfDetectorBanks * expectedDetectorBankSize + numberOfMonitors + numberOfDetectorBanks + + numberOfTubes + 1 + 1 + 1; + const std::string componentInfoDescription = saveAndReparse + ? "Detectors + 2 banks + root + source + sample" + : "Detectors + 2 banks + 16 tubes + root + source + sample"; + + auto [componentInfo, detectorInfo] = extractBeamline(instrument); + + TSM_ASSERT_EQUALS("Detectors + 1 monitor", detectorInfo->size(), expectedDetectorInfoSize); + TSM_ASSERT_EQUALS(componentInfoDescription.c_str(), componentInfo->size(), expectedComponentInfoSize); + + // Check 128 detectors in first bank + size_t rearBankIndex(-1); + TS_ASSERT_THROWS_NOTHING(rearBankIndex = componentInfo->indexOfAny("rear-detector")); + TS_ASSERT_EQUALS(128, componentInfo->detectorsInSubtree(rearBankIndex).size()); + + TS_ASSERT(Mantid::Kernel::toVector3d(componentInfo->position(rearBankIndex)).isApprox(Eigen::Vector3d{0, 0, 4})); + + // Check 128 detectors in second bank + size_t frontBankIndex(-1); + TS_ASSERT_THROWS_NOTHING(frontBankIndex = componentInfo->indexOfAny("front-detector")); + TS_ASSERT_EQUALS(128, componentInfo->detectorsInSubtree(frontBankIndex).size()); + } }; class NexusGeometryParserTestPerformance : public CxxTest::TestSuite { diff --git a/Framework/NexusGeometry/test/NexusGeometrySaveTest.h b/Framework/NexusGeometry/test/NexusGeometrySaveTest.h index c06d6e49d438..355a23191360 100644 --- a/Framework/NexusGeometry/test/NexusGeometrySaveTest.h +++ b/Framework/NexusGeometry/test/NexusGeometrySaveTest.h @@ -235,8 +235,21 @@ used. } void test_NXInstrument_name_is_aways_instrument() { - // NXInstrument group name is always written as "instrument" for legacy - // compatibility reasons + // TODO: Deprecate and "clean up" (i.e. re-integrate) the + // `SaveNexusESS`, `NexusGeometrySave` and `NexusGeometryParser` + // code sections. + // What is described by the next comment is just one example of many, where + // an implementation seemed to just "stop in the middle". + + // THIS TEST DOES NOT VERIFY WHAT ITS NAME ACTUALLY SUGGESTS: + // in order to be backwards compatible, for the "legacy" instrument format; + // YES, the NXInstrument group does need to be named "instrument". + // However, what this test actually verifies is that the NXinstrument group is assigned the same name + // as the name of the `ComponentInfo` root. + + // Since the NXinstrument group actually has a separate "name" attribute, almost certainly, + // for these codes, it could just be given the group name "instrument". + // However, fixing this was beyond the scope of the current changes. // RAII file resource for test file destination FileResource fileResource("check_instrument_name_test_file.nxs"); @@ -874,7 +887,7 @@ Instrument cache. /* 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) + 'depends_on' has value "/absolute/path/to/location", and (dataset) 'location' has dAttribute (AKA attribute of dataset) 'depends_on' with value "." */ @@ -1035,7 +1048,7 @@ Instrument cache. bool sourceDependencyIsSelf = tester.dataSetHasStrValue(DEPENDS_ON, NO_DEPENDENCY, sourcePath); TS_ASSERT(sourceDependencyIsSelf); - // assert the group NXtransformations doesnt exist in file + // assert the group NXtransformations doesn't exist in the file TS_ASSERT_THROWS(tester.openfullH5Path(transformationsPath), H5::GroupIException &); } }; diff --git a/Framework/NexusGeometry/test/mockobjects.h b/Framework/NexusGeometry/test/mockobjects.h index 01ff9f620443..5ec0d005015d 100644 --- a/Framework/NexusGeometry/test/mockobjects.h +++ b/Framework/NexusGeometry/test/mockobjects.h @@ -20,6 +20,7 @@ class MockProgressBase : public Mantid::Kernel::ProgressBase { class MockLogger : public Mantid::NexusGeometry::AbstractLogger { public: GNU_DIAG_OFF_SUGGEST_OVERRIDE + MOCK_METHOD1(debug, void(const std::string &)); MOCK_METHOD1(warning, void(const std::string &)); MOCK_METHOD1(error, void(const std::string &)); GNU_DIAG_ON_SUGGEST_OVERRIDE diff --git a/Framework/PythonInterface/test/testhelpers/WorkspaceCreationHelper/WorkspaceCreationHelperModule.cpp b/Framework/PythonInterface/test/testhelpers/WorkspaceCreationHelper/WorkspaceCreationHelperModule.cpp index 2e25dedf07ec..1b1aebfdd83f 100644 --- a/Framework/PythonInterface/test/testhelpers/WorkspaceCreationHelper/WorkspaceCreationHelperModule.cpp +++ b/Framework/PythonInterface/test/testhelpers/WorkspaceCreationHelper/WorkspaceCreationHelperModule.cpp @@ -57,7 +57,7 @@ BOOST_PYTHON_MODULE(_WorkspaceCreationHelper) { // Function pointers to disambiguate the calls using Signature1_2D = Workspace2D_sptr (*)(int, int, bool, bool, bool, const std::string &, bool); - using Signature2_2D = Workspace2D_sptr (*)(int, int, int); + using Signature2_2D = Workspace2D_sptr (*)(int, int, int, const std::string &); using Signature3_2D = Workspace2D_sptr (*)(int, int, int, int); def("create2DWorkspaceWithFullInstrument", reinterpret_cast(&create2DWorkspaceWithFullInstrument), diff --git a/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/ComponentCreationHelper.h b/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/ComponentCreationHelper.h index 9d4f9b38aacc..c5089b335b31 100644 --- a/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/ComponentCreationHelper.h +++ b/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/ComponentCreationHelper.h @@ -189,7 +189,8 @@ void addRectangularBank(Mantid::Geometry::Instrument &testInstrument, int idStar Mantid::Geometry::Instrument_sptr createTestInstrumentRectangular(int num_banks, int pixels, double pixelSpacing = 0.008, double bankDistanceFromSample = 5.0, - bool addMonitor = false); + bool addMonitor = false, + const std::string &instrumentName = "basic_rect"); Mantid::Geometry::Instrument_sptr createTestInstrumentRectangular2(int num_banks, int pixels, double pixelSpacing = 0.008); diff --git a/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/NexusFileReader.h b/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/NexusFileReader.h index fb63d8474aa8..e5371d3da181 100644 --- a/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/NexusFileReader.h +++ b/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/NexusFileReader.h @@ -102,7 +102,7 @@ class NexusFileReader { // Get the NX_class type H5std_string classType; attribute.read(dataType, classType); - // If group of correct type, return the childGroup + // If group is of the correct type, include the group in the count if (classType == nxClass) { counter++; } @@ -113,7 +113,7 @@ class NexusFileReader { return counter; } - // read a multidimensional dataset and returns vector containing the data + // read a multidimensional dataset and return a vector containing the data template std::vector readDataSetMultidimensional(FullNXPath &pathToGroup, const std::string &dataSetName) { diff --git a/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/NexusGeometryTestHelpers.h b/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/NexusGeometryTestHelpers.h index 810a9b9b3de3..604b07386ae0 100644 --- a/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/NexusGeometryTestHelpers.h +++ b/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/NexusGeometryTestHelpers.h @@ -13,13 +13,20 @@ namespace Mantid { namespace Geometry { + class IObject; + } } // namespace Mantid namespace NexusGeometryTestHelpers { + std::shared_ptr createShape(); + Pixels generateCoLinearPixels(); + Pixels generateNonCoLinearPixels(); + std::vector getFakeDetIDs(); + } // namespace NexusGeometryTestHelpers diff --git a/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/WorkspaceCreationHelper.h b/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/WorkspaceCreationHelper.h index 208e04631f71..cae3b80a62bb 100644 --- a/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/WorkspaceCreationHelper.h +++ b/Framework/TestHelpers/inc/MantidFrameworkTestHelpers/WorkspaceCreationHelper.h @@ -250,8 +250,9 @@ Mantid::DataObjects::Workspace2D_sptr create2DWorkspaceWithGeographicalDetectors */ Mantid::DataObjects::Workspace2D_sptr create2DWorkspaceThetaVsTOF(int nHist, int nBins); -Mantid::DataObjects::Workspace2D_sptr create2DWorkspaceWithRectangularInstrument(int numBanks, int numPixels, - int numBins); +Mantid::DataObjects::Workspace2D_sptr +create2DWorkspaceWithRectangularInstrument(int numBanks, int numPixels, int numBins, + const std::string &instrumentName = "basic_rect"); Mantid::DataObjects::Workspace2D_sptr create2DWorkspace123WithMaskedBin(int numHist, int numBins, int maskedWorkspaceIndex, int maskedBinIndex); diff --git a/Framework/TestHelpers/src/ComponentCreationHelper.cpp b/Framework/TestHelpers/src/ComponentCreationHelper.cpp index 1786df0efcd0..fa35d9519f29 100644 --- a/Framework/TestHelpers/src/ComponentCreationHelper.cpp +++ b/Framework/TestHelpers/src/ComponentCreationHelper.cpp @@ -533,10 +533,12 @@ void addRectangularBank(Instrument &testInstrument, int idStart, int pixels, dou * @param pixelSpacing :: padding between pixels * @param bankDistanceFromSample :: How far the bank is from the sample * @param addMonitor :: whether to add a monitor detector to the instrument + * @param instrumentName :: the name of the new instrument */ Instrument_sptr createTestInstrumentRectangular(int num_banks, int pixels, double pixelSpacing, - double bankDistanceFromSample, bool addMonitor) { - auto testInst = std::make_shared("basic_rect"); + double bankDistanceFromSample, bool addMonitor, + const std::string &instrumentName) { + auto testInst = std::make_shared(instrumentName); for (int banknum = 1; banknum <= num_banks; banknum++) { // Make a new bank diff --git a/Framework/TestHelpers/src/WorkspaceCreationHelper.cpp b/Framework/TestHelpers/src/WorkspaceCreationHelper.cpp index e8835e107ad2..b956ae07ad3e 100644 --- a/Framework/TestHelpers/src/WorkspaceCreationHelper.cpp +++ b/Framework/TestHelpers/src/WorkspaceCreationHelper.cpp @@ -432,11 +432,14 @@ Workspace2D_sptr create2DWorkspaceWithGeographicalDetectors(const int nlat, cons * @param numBanks :: number of rectangular banks * @param numPixels :: each bank will be numPixels*numPixels * @param numBins :: each spectrum will have this # of bins + * @param instrumentName :: the name of the new instrument * @return The Workspace2D */ Mantid::DataObjects::Workspace2D_sptr create2DWorkspaceWithRectangularInstrument(int numBanks, int numPixels, - int numBins) { - Instrument_sptr inst = ComponentCreationHelper::createTestInstrumentRectangular(numBanks, numPixels); + int numBins, + const std::string &instrumentName) { + Instrument_sptr inst = + ComponentCreationHelper::createTestInstrumentRectangular(numBanks, numPixels, 0.008, 5.0, false, instrumentName); Workspace2D_sptr ws = create2DWorkspaceBinned(numBanks * numPixels * numPixels, numBins); ws->setInstrument(inst); ws->getAxis(0)->setUnit("dSpacing"); diff --git a/buildconfig/CMake/CppCheck_Suppressions.txt.in b/buildconfig/CMake/CppCheck_Suppressions.txt.in index 828c3749e37f..42b4420383b5 100644 --- a/buildconfig/CMake/CppCheck_Suppressions.txt.in +++ b/buildconfig/CMake/CppCheck_Suppressions.txt.in @@ -571,9 +571,6 @@ constVariablePointer:${CMAKE_SOURCE_DIR}/Framework/CurveFitting/src/LatticeDomai missingOverride:${CMAKE_SOURCE_DIR}/Framework/DataHandling/inc/MantidDataHandling/DownloadInstrument.h:26 constVariableReference:${CMAKE_SOURCE_DIR}/Framework/CurveFitting/src/MSVesuvioHelpers.cpp:369 constVariableReference:${CMAKE_SOURCE_DIR}/Framework/CurveFitting/src/MSVesuvioHelpers.cpp:370 -constVariableReference:${CMAKE_SOURCE_DIR}/Framework/DataHandling/src/H5Util.cpp:178 -constVariableReference:${CMAKE_SOURCE_DIR}/Framework/DataHandling/src/H5Util.cpp:181 -constVariableReference:${CMAKE_SOURCE_DIR}/Framework/DataHandling/src/H5Util.cpp:191 missingOverride:${CMAKE_SOURCE_DIR}/Framework/DataHandling/inc/MantidDataHandling/ISISJournal.h:35 returnByReference:${CMAKE_SOURCE_DIR}/Framework/Nexus/inc/MantidNexus/NexusClasses.h:107 constVariableReference:${CMAKE_SOURCE_DIR}/Framework/DataHandling/src/DataBlockComposite.cpp:383 @@ -936,8 +933,6 @@ syntaxError:${CMAKE_SOURCE_DIR}/Framework/PythonInterface/mantid/kernel/src/Expo syntaxError:${CMAKE_SOURCE_DIR}/Framework/PythonInterface/mantid/kernel/src/Exports/VMD.cpp:100 constVariablePointer:${CMAKE_SOURCE_DIR}/Framework/PythonInterface/mantid/kernel/src/Exports/IPropertyManager.cpp:46 constParameterCallback:${CMAKE_SOURCE_DIR}/Framework/PythonInterface/mantid/kernel/src/Exports/IPropertyManager.cpp:102 -constVariableReference:${CMAKE_SOURCE_DIR}/Framework/NexusGeometry/src/NexusGeometryParser.cpp:722 -constVariableReference:${CMAKE_SOURCE_DIR}/Framework/NexusGeometry/src/NexusGeometryParser.cpp:724 unknownMacro:${CMAKE_SOURCE_DIR}/Framework/PythonInterface/mantid/kernel/src/Exports/TimeSeriesProperty.cpp:134 iterateByValue:${CMAKE_SOURCE_DIR}/Framework/PythonInterface/mantid/kernel/src/Exports/ArrayProperty.cpp:52 constParameterPointer:${CMAKE_SOURCE_DIR}/Framework/PythonInterface/core/inc/MantidPythonInterface/core/IsNone.h:26 diff --git a/docs/source/algorithms/SaveNexusESS-v1.rst b/docs/source/algorithms/SaveNexusESS-v1.rst index 56efa4434b0c..4f8919d3f859 100644 --- a/docs/source/algorithms/SaveNexusESS-v1.rst +++ b/docs/source/algorithms/SaveNexusESS-v1.rst @@ -10,13 +10,13 @@ Description ----------- -Saves a processed nexus file similar to :ref:`algm-SaveNexusProcessed`, but provides nexus geometry which is an accurate snapshot of the calibrated/transformed instrument geometry in-memory. One current major difference between the two algorithms is that SaveNexusESS does not support the generation of a single processed file based on a :ref:`GroupWorkspace ` input. +Saves a processed nexus file similar to :ref:`algm-SaveNexusProcessed`, but provides nexus geometry which is an accurate snapshot of the calibrated/transformed instrument geometry in-memory. The algorithm writes out spectra-detector mappings and can handle detector groupings. This algorithm may be deprecated in future in favour of a master :ref:`algm-SaveNexusProcessed` algorithm. -This algorithm currently provides not shape information for component geometry. +This algorithm currently does not support shape information for component geometry. Usage ----- diff --git a/docs/source/algorithms/SaveNexusGeometry-v1.rst b/docs/source/algorithms/SaveNexusGeometry-v1.rst index cae442176e2f..c504dc7c2548 100644 --- a/docs/source/algorithms/SaveNexusGeometry-v1.rst +++ b/docs/source/algorithms/SaveNexusGeometry-v1.rst @@ -16,8 +16,8 @@ For more information on the Nexus format, see https://www.nexusformat.org/ the Instrument will be extracted from the specified workspace, and written to the specified location. -The (optional) H5 root group name is the parent group in which the Instrument and sample data are stored. -If no name is given, the root group will have a default nme of 'entry' +The (optional) H5 root group name is the parent NXentry group in which the Instrument and sample data are stored. +If no name is given, the root group will have a default name of 'entry' Usage diff --git a/docs/source/api/python/mantid/kernel/DateAndTime.rst b/docs/source/api/python/mantid/kernel/DateAndTime.rst index 33082742f62d..a93b1f8b702f 100644 --- a/docs/source/api/python/mantid/kernel/DateAndTime.rst +++ b/docs/source/api/python/mantid/kernel/DateAndTime.rst @@ -11,7 +11,7 @@ classes have a different EPOCH. Note that reason, there is an additional method :meth:`mantid.kernel.DateAndTime.to_datetime64`. -To convert an array of :class:`mantid.kernel.DateAndTime`, analgous to +To convert an array of :class:`mantid.kernel.DateAndTime`, analogous to what :meth:`mantid.kernel.FloatTimeSeriesProperty.times` does internally, use the code: diff --git a/docs/source/release/v6.12.0/Framework/Algorithms/New_features/38309.rst b/docs/source/release/v6.12.0/Framework/Algorithms/New_features/38309.rst new file mode 100644 index 000000000000..64cc00d3bd1c --- /dev/null +++ b/docs/source/release/v6.12.0/Framework/Algorithms/New_features/38309.rst @@ -0,0 +1 @@ +- Algorithm SaveNexusESS now supports append mode, allowing multiple workspaces to be written either one at a time, or as a group workspace, to a single NeXus HDF5 file. diff --git a/instrument/unit_testing/SMALLFAKE_example_multiple_entries.hdf5 b/instrument/unit_testing/SMALLFAKE_example_multiple_entries.hdf5 new file mode 100644 index 000000000000..6de4a55b952a Binary files /dev/null and b/instrument/unit_testing/SMALLFAKE_example_multiple_entries.hdf5 differ