diff --git a/src/Plugins/OrientationAnalysis/docs/ReadH5OinaDataFilter.md b/src/Plugins/OrientationAnalysis/docs/ReadH5OinaDataFilter.md
index 191ca58f05..ccecac72e3 100644
--- a/src/Plugins/OrientationAnalysis/docs/ReadH5OinaDataFilter.md
+++ b/src/Plugins/OrientationAnalysis/docs/ReadH5OinaDataFilter.md
@@ -17,9 +17,36 @@ The file is EBSD (Electron Backscatter Diffraction) scan data. The most importan
- **Pattern-quality metrics** — values such as Band Contrast, Band Slope, Bands, and Mean Angular Deviation describe how clear and reliable each measurement is. These are commonly used to flag unreliable pixels (see *Reference Frames* below).
- **Per-phase (Ensemble) data** — the crystal structure, lattice constants, and material name for each phase. An **Ensemble** here means one distinct material/crystal type.
+### Reading More Than One Scan
+
+An H5OINA file can hold several scans. Selecting more than one stacks them into a single
+3D **Image Geometry**: the X and Y extents and step sizes come from the first selected
+scan, the Z extent is the number of selected scans, and the Z spacing is the **Z Spacing**
+parameter. The selected scans become the slices of one 3D microstructure. Therefore,
+every scan must describe the same grid and use the same phase definitions. The phase-group
+names, material names, Laue groups, space groups, and lattice constants must match. All
+scans share one **Ensemble Attribute Matrix**, so different phase definitions cannot be
+represented correctly. Import scans with different phase definitions separately.
+
+The **Stacking Order** setting carried by the scan selection chooses which end of the
+list lands at Z = 0. *Low To High* stacks the scans in the order they are listed, so the
+first selected scan is at Z = 0. *High To Low* stacks them in the reverse of that order,
+so the last selected scan is at Z = 0.
+
### Limitations of the Filter
-The current implementation only understands **FORMAT VERSION 2.0** of the H5OINA file. A user can still read a newer H5OINA file, but the filter will only extract the VERSION 2.0 headers and data. If additional data is needed from the file, the [Read HDF5 Dataset](../SimplnxCore/ReadHDF5DatasetFilter.md) filter can be used to augment this filter.
+The filter reads a fixed set of header keys and nine data columns, the set Oxford documents
+for **FORMAT VERSION 2.0**. The file's `Format Version` value is not used to select what is
+read, and a file without that value is read the same way, so a file imports exactly when it
+carries that fixed set — the Format Version 5.0 export bundled with this filter's tests
+still does. Any column outside the set — for example `Pattern Quality`, `Beam Position
+X`/`Y` or the `Electron Image` tree — is ignored, and can be brought in with the
+[Read HDF5 Dataset](../SimplnxCore/ReadHDF5DatasetFilter.md) filter.
+
+**Importing diffraction patterns is not yet supported for H5OINA files.** Turning on
+*Import Pattern Data* stops the filter with an error rather than producing a partial result.
+A file's `Processed Patterns` or `Unprocessed Patterns` dataset can be read with the
+[Read HDF5 Dataset](../SimplnxCore/ReadHDF5DatasetFilter.md) filter.

@@ -35,11 +62,26 @@ Historical reference frame operations for Oxford data are the following:
+ Sample Reference Frame: 180o about the <010> Axis
+ Crystal Reference Frame: None
-The user also may want to assign un-indexed pixels to be ignored by flagging them as "bad". The [Multi-Threshold Objects](../SimplnxCore/MultiThresholdObjectsFilter.md) filter can be used to define this *mask* by thresholding on values such as *Error* = 0.
+The user also may want to assign un-indexed pixels to be ignored by flagging them as "bad". The [Multi-Threshold Objects](../SimplnxCore/MultiThresholdObjectsFilter.md) filter can be used to define this *mask*. For H5OINA data, threshold on `Phase` > 0: an un-indexed point carries phase 0, which is the reserved invalid-phase slot.
+
+Do not assume `Error` = 0 marks a good point in an H5OINA file. AZtec writes an enumerated status code there whose values are not the same as the `.ctf` convention: in the AZtec export bundled with this filter's tests, every one of the 587 indexed points carries `Error` = 1 and every one of the 38 un-indexed points carries `Error` = 2, and no point carries 0. A mask built from `Error` = 0 would select nothing on that file.
### Radians and Degrees
-All orientation data in the H5OINA file are in radians.
+All orientation data in the H5OINA file are in radians, and the imported `Euler` array is in
+radians as well — no conversion is applied.
+
+The per-phase `LatticeConstants` array is the one place where a unit does change on import.
+An H5OINA file stores its three lattice angles in radians; they are imported as **degrees**,
+so that the array means the same thing no matter which EBSD format the phase came from. A
+cubic phase therefore reports `90, 90, 90` rather than `1.5707964, 1.5707964, 1.5707964`.
+The three lattice dimensions are imported unchanged.
+
+**This is a breaking change to a published output, and it is not in any released
+version.** DREAM3D-NX 7.0.0 through 7.4.1 reported those three slots in radians for
+H5OINA imports. See the migration notes below before comparing an H5OINA import against
+a `.dream3d` file, a regression baseline, or a pipeline result produced by one of those
+releases.
### The Axis Alignment Issue for Hexagonal Symmetry [1]
@@ -50,7 +92,7 @@ All orientation data in the H5OINA file are in radians.
+ Caution: it appears that the axis alignment is a choice that must be made when installing TSL software so determination of which convention is in use must be made on a case-by-case basis. It is fixed to the y-convention in the HKL software.
+ The main clue that something is wrong in a conversion is that either the 2110 & 1010 pole figures are transposed, or that a peak in the inverse pole figure that should be present at 2110 has shifted over to 1010.
+ DREAM3D-NX uses the TSL/EDAX convention.
-+ __The result of this is that the filter will by default add 30 degrees to the second Euler Angle (phi2) when reading Oxford `.h5oina` files. This can be disabled by the user if necessary.__
++ __The result of this is that the filter will by default add 30 degrees to the third Euler angle (phi2) of every point whose phase is Hexagonal-High when reading Oxford `.h5oina` files. Because the file's Euler angles are in radians, the value actually added is 30 degrees expressed in radians (pi/6, about 0.5235988). Points of any other symmetry, and un-indexed points, are never adjusted. This can be disabled by the user if necessary.__
| Figure 1 |
|--------|
@@ -65,13 +107,61 @@ Once the reference frames are correct, the imported Euler angles are typically c
None — this filter reads directly from a `.h5oina` file on disk.
+## Created Outputs
+
+The array names are the H5OINA dataset names, so several contain spaces.
+
+### Cell Attribute Matrix
+
+| Name | Type | Components | Notes |
+|------|------|------------|-------|
+| `Band Contrast` | uint8 | 1 | |
+| `Band Slope` | uint8 | 1 | |
+| `Bands` | uint8 | 1 | |
+| `Error` | uint8 | 1 | AZtec status code; see the note on masking below — do not assume 0 means "indexed" |
+| `Euler` | float32 | 3 | Radians; phi2 optionally shifted for Hexagonal-High points |
+| `Mean Angular Deviation` | float32 | 1 | |
+| `Phase` | int32 or uint8 | 1 | int32 by default; uint8 when *Convert Phase Data to Int32* is off. 0 marks an un-indexed point |
+| `X` | float32 | 1 | |
+| `Y` | float32 | 1 | |
+
+### Ensemble Attribute Matrix
+
+One tuple per phase in the file, plus tuple 0, which is reserved for the invalid phase that
+un-indexed points refer to.
+
+| Name | Type | Components | Notes |
+|------|------|------------|-------|
+| `CrystalStructures` | uint32 | 1 | Mapped from the file's Laue group; 999 in tuple 0 |
+| `LatticeConstants` | float32 | 6 | a, b, c then alpha, beta, gamma in **degrees**; all zero in tuple 0 |
+| `MaterialName` | string | 1 | `"Invalid Phase"` in tuple 0 |
+
% Auto generated parameter table will be inserted here
+## Migration Notes
+
+Documented behavioral differences from DREAM3D-NX 7.0.0 through 7.4.1 are maintained as
+Deviation entries in the source tree at
+`src/Plugins/OrientationAnalysis/vv/deviations/ReadH5OinaDataFilter.md`. Two of them
+change values that an existing pipeline may compare against.
+
+- **`LatticeConstants` angle slots are degrees, not radians** (`ReadH5OinaDataFilter-D6`).
+ Every `.dream3d` file written by 7.0.0 through 7.4.1 carries radians in components 3, 4
+ and 5 of that array, so any saved exemplar, regression baseline or pipeline comparison
+ that reads them changes value. Multiply a stored radian value by 180/pi to compare it
+ against a new import, and remove any downstream conversion that was compensating for
+ the radian values. Nothing else in the import changes unit: the `Euler` array is still
+ radians and the three lattice dimensions are still unconverted.
+- **The hexagonal x-axis alignment adds 30 degrees, not 30 radians**
+ (`ReadH5OinaDataFilter-D1`). Orientations imported from a file with a hexagonal phase
+ by 7.0.0 through 7.4.1 with *Convert Hexagonal X-Axis to EDAX Standard* left on — its
+ default — are wrong and cannot be corrected after the fact; re-import the file.
+
## Example Pipelines
## References
-[1] Rollett, A.D. Lecture Slides located at [http://pajarito.materials.cmu.edu/rollett/27750/L17-EBSD-analysis-31Mar16.pdf](http://pajarito.materials.cmu.edu/rollett/27750/L17-EBSD-analysis-31Mar16.pdf)
+[1] Wright, S. I. and De Graef, M., "Electron backscatter diffraction," *International Tables for Crystallography*. [EBSD reference-frame conventions](https://onlinelibrary.wiley.com/iucr/itc/Cc/wf5160/).
## DREAM3D-NX Help
diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadH5OinaData.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadH5OinaData.cpp
index 9f934961f1..c19d7a95d7 100644
--- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadH5OinaData.cpp
+++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadH5OinaData.cpp
@@ -3,44 +3,131 @@
#include "simplnx/DataStructure/DataArray.hpp"
#include "simplnx/DataStructure/Geometry/ImageGeom.hpp"
+#include "H5Support/H5Lite.h"
+#include "H5Support/H5ScopedSentinel.h"
+#include "H5Support/H5Utilities.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+
using namespace nx::core;
namespace
{
+// The EDAX/TSL convention aligns the hexagonal crystal x-axis with [2-1-10] while
+// Oxford Instruments aligns it with [10-10]; converting between them is a 30 degree
+// rotation about [0001] applied to phi2. An H5OINA file stores its Euler angles in
+// RADIANS (unlike a .ctf file, which stores degrees), so the value added here is 30
+// degrees expressed in radians -- 30 * (pi/180), i.e. pi/6 -- and not the literal 30.
+// The addition runs on a double intermediate so the stored float32 is the correctly
+// rounded result, matching how the .ctf importer applies the same correction.
+constexpr float64 k_HexagonalAlignmentRadians = 30.0 * ebsdlib::constants::k_PiOver180D;
+
+// The nine datasets H5OINAReader reads out of a scan's Data group, paired with the
+// number of components each one carries per scan point.
+const std::vector> k_RequiredDataSets = {
+ {ebsdlib::H5OINA::BandContrast, 1}, {ebsdlib::H5OINA::BandSlope, 1}, {ebsdlib::H5OINA::Bands, 1}, {ebsdlib::H5OINA::Error, 1}, {ebsdlib::H5OINA::Euler, 3},
+ {ebsdlib::H5OINA::MeanAngularDeviation, 1}, {ebsdlib::H5OINA::Phase, 1}, {ebsdlib::H5OINA::X, 1}, {ebsdlib::H5OINA::Y, 1},
+};
+
+/**
+ * @brief Confirms that every Data dataset of a scan holds exactly as many elements as
+ * the Image Geometry expects.
+ *
+ * H5OINAReader sizes its buffers to whatever extent each dataset actually has, while
+ * the geometry and the destination arrays are sized from the header's X Cells and
+ * Y Cells. If a file's datasets are shorter than the header claims, copying
+ * totalPoints elements out of those buffers reads past their end.
+ *
+ * No reader API exposes the sizes the reader actually allocated, so the extents are
+ * re-probed from the file with H5Lite. That is the file's extents as of this call
+ * rather than the buffer sizes themselves; the two agree unless the file changed
+ * between H5OINAReader::readFile() and this call. A dataset LONGER than the header
+ * describes is rejected as well as a shorter one: the reader would size its buffers
+ * to the longer extent and the extra rows would be silently dropped, which is not a
+ * result a caller can distinguish from a correct import.
+ */
+Result<> validateDataSetExtents(const std::filesystem::path& filePath, const std::string& scanName, usize totalPoints)
+{
+ hid_t fileId = H5Support::H5Utilities::openFile(filePath.string(), true);
+ if(fileId < 0)
+ {
+ return MakeErrorResult(
+ -34971, fmt::format("The file '{}' could not be reopened to verify the extents of scan '{}'. The file may have been moved or changed since preflight.", filePath.string(), scanName));
+ }
+ H5Support::H5ScopedFileSentinel sentinel(fileId, false);
+
+ const std::string dataGroupPath = fmt::format("/{}/{}/{}", scanName, ebsdlib::H5OINA::EBSD, ebsdlib::H5OINA::Data);
+ for(const auto& [dataSetName, componentCount] : k_RequiredDataSets)
+ {
+ const std::string dataSetPath = dataGroupPath + "/" + dataSetName;
+ std::vector dims;
+ H5T_class_t classType = H5T_NO_CLASS;
+ usize typeSize = 0;
+ if(H5Support::H5Lite::getDatasetInfo(fileId, dataSetPath, dims, classType, typeSize) < 0)
+ {
+ // A missing dataset is fatal inside H5OINAReader, which has already run by the
+ // time this is called, so there is nothing to add here.
+ continue;
+ }
+ usize elementCount = 1;
+ for(const hsize_t dim : dims)
+ {
+ elementCount *= static_cast(dim);
+ }
+ const usize expectedCount = totalPoints * componentCount;
+ if(elementCount != expectedCount)
+ {
+ return MakeErrorResult(-34971, fmt::format("The dataset '{}' of scan '{}' in '{}' holds {} element(s), but the {} scan point(s) described by the scan's header require {}. The file is "
+ "malformed or was changed after preflight.",
+ dataSetPath, scanName, filePath.string(), elementCount, totalPoints, expectedCount));
+ }
+ }
+ return {};
+}
template
-void copyRawData(const ReadH5DataInputValues* m_InputValues, size_t totalPoints, DataStructure& m_DataStructure, ebsdlib::H5OINAReader& m_Reader, const std::string& name, usize offset)
+void copyRawData(const ReadH5DataInputValues* inputValues, usize count, DataStructure& dataStructure, ebsdlib::H5OINAReader& reader, const std::string& name, usize offset)
{
using ArrayType = DataArray;
- auto& dataRef = m_DataStructure.getDataRefAs(m_InputValues->CellAttributeMatrixPath.createChildPath(name));
+ auto& dataRef = dataStructure.getDataRefAs(inputValues->CellAttributeMatrixPath.createChildPath(name));
auto* dataStorePtr = dataRef.getDataStore();
- const nonstd::span rawDataPtr(reinterpret_cast(m_Reader.getPointerByName(name)), totalPoints);
- std::copy(rawDataPtr.begin(), rawDataPtr.end(), dataStorePtr->begin() + offset);
+ const nonstd::span rawDataSpan(reinterpret_cast(reader.getPointerByName(name)), count);
+ std::copy(rawDataSpan.begin(), rawDataSpan.end(), dataStorePtr->begin() + offset);
}
+/**
+ * @brief Applies the EDAX hexagonal x-axis alignment to the phi2 of every hexagonal
+ * point of one scan's tuple slab.
+ */
template
-void convertHexEulerAngle(const ReadH5DataInputValues* m_InputValues, size_t totalPoints, DataStructure& m_DataStructure)
+void convertHexEulerAngle(const ReadH5DataInputValues* inputValues, usize totalPoints, usize tupleOffset, DataStructure& dataStructure)
{
using ArrayType = DataArray;
- if(m_InputValues->EdaxHexagonalAlignment)
- {
- auto& crystalStructuresRef = m_DataStructure.getDataRefAs(m_InputValues->CellEnsembleAttributeMatrixPath.createChildPath(ebsdlib::AngFile::CrystalStructures));
- auto& crystalStructuresDSRef = crystalStructuresRef.getDataStoreRef();
+ const auto& crystalStructuresRef = dataStructure.getDataRefAs(inputValues->CellEnsembleAttributeMatrixPath.createChildPath(ebsdlib::AngFile::CrystalStructures));
+ const auto& crystalStructuresDSRef = crystalStructuresRef.getDataStoreRef();
- auto& cellPhasesRef = m_DataStructure.getDataRefAs(m_InputValues->CellAttributeMatrixPath.createChildPath(ebsdlib::H5OINA::Phase));
- auto& cellPhasesDSRef = cellPhasesRef.getDataStoreRef();
+ const auto& cellPhasesRef = dataStructure.getDataRefAs(inputValues->CellAttributeMatrixPath.createChildPath(ebsdlib::H5OINA::Phase));
+ const auto& cellPhasesDSRef = cellPhasesRef.getDataStoreRef();
- auto& eulerRef = m_DataStructure.getDataRefAs(m_InputValues->CellAttributeMatrixPath.createChildPath(ebsdlib::H5OINA::Euler));
- auto& eulerDataStoreRef = eulerRef.getDataStoreRef();
+ auto& eulerRef = dataStructure.getDataRefAs(inputValues->CellAttributeMatrixPath.createChildPath(ebsdlib::H5OINA::Euler));
+ auto& eulerDataStoreRef = eulerRef.getDataStoreRef();
- for(size_t i = 0; i < totalPoints; i++)
+ // Only this scan's slab is visited. Looping from 0 every time would shift the first
+ // scan's points once per scan and never reach the later scans' points.
+ for(usize tupleIdx = tupleOffset; tupleIdx < tupleOffset + totalPoints; tupleIdx++)
+ {
+ if(crystalStructuresDSRef[cellPhasesDSRef[tupleIdx]] == ebsdlib::CrystalStructure::Hexagonal_High)
{
- if(crystalStructuresDSRef[cellPhasesDSRef[i]] == ebsdlib::CrystalStructure::Hexagonal_High)
- {
- eulerDataStoreRef[3 * i + 2] = eulerDataStoreRef[3 * i + 2] + 30.0F; // See the documentation for this correction factor
- }
+ const auto phi2 = static_cast(eulerDataStoreRef[3 * tupleIdx + 2]);
+ eulerDataStoreRef[3 * tupleIdx + 2] = static_cast(phi2 + k_HexagonalAlignmentRadians);
}
}
}
@@ -48,8 +135,8 @@ void convertHexEulerAngle(const ReadH5DataInputValues* m_InputValues, size_t tot
} // namespace
// -----------------------------------------------------------------------------
-ReadH5OinaData::ReadH5OinaData(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, ReadH5DataInputValues* inputValues)
-: IEbsdOemReader(dataStructure, mesgHandler, shouldCancel, inputValues)
+ReadH5OinaData::ReadH5OinaData(DataStructure& dataStructure, const IFilter::MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, ReadH5DataInputValues* inputValues)
+: IEbsdOemReader(dataStructure, messageHandler, shouldCancel, inputValues)
{
}
@@ -59,75 +146,125 @@ ReadH5OinaData::~ReadH5OinaData() noexcept = default;
// -----------------------------------------------------------------------------
Result<> ReadH5OinaData::operator()()
{
- return execute();
+ auto& imageGeom = m_DataStructure.getDataRefAs(m_InputValues->ImageGeometryPath);
+ imageGeom.setUnits(IGeometry::LengthUnit::Micrometer);
+
+ // The scan loop is kept here rather than in IEbsdOemReader::execute() so that the
+ // cancel checks, the progress messages and the stacking order below apply to this
+ // filter only.
+ //
+ // The stacking order chooses which end of the selection list lands in tuple slab 0:
+ // Low-to-High reads the scans in the order they are listed, High-to-Low reads them in
+ // the reverse of that order, so the last selected scan occupies slab 0.
+ std::vector orderedScanNames(m_InputValues->SelectedScanNames.scanNames.cbegin(), m_InputValues->SelectedScanNames.scanNames.cend());
+ if(m_InputValues->SelectedScanNames.stackingOrder == RefFrameZDir::k_HightoLow)
+ {
+ std::reverse(orderedScanNames.begin(), orderedScanNames.end());
+ }
+
+ const usize scanCount = orderedScanNames.size();
+ for(usize scanIdx = 0; scanIdx < scanCount; scanIdx++)
+ {
+ if(m_ShouldCancel)
+ {
+ return {};
+ }
+
+ m_CurrentScanName = orderedScanNames[scanIdx];
+ m_MessageHandler({IFilter::Message::Type::Info, fmt::format("Reading scan '{}' ({} of {})", m_CurrentScanName, scanIdx + 1, scanCount)});
+ Result<> readResults = readData(m_CurrentScanName);
+ if(readResults.invalid())
+ {
+ return readResults;
+ }
+
+ if(m_ShouldCancel)
+ {
+ return {};
+ }
+
+ m_MessageHandler({IFilter::Message::Type::Info, fmt::format("Copying the cell data of scan '{}' ({} of {})", m_CurrentScanName, scanIdx + 1, scanCount)});
+ Result<> copyDataResults = copyRawEbsdData(static_cast(scanIdx));
+ if(copyDataResults.invalid())
+ {
+ return copyDataResults;
+ }
+ }
+ return {};
}
// -----------------------------------------------------------------------------
-Result<> ReadH5OinaData::copyRawEbsdData(int index)
+Result<> ReadH5OinaData::copyRawEbsdData(int scanIndex)
{
const auto& imageGeom = m_DataStructure.getDataRefAs(m_InputValues->ImageGeometryPath);
const usize totalPoints = imageGeom.getNumXCells() * imageGeom.getNumYCells();
- const usize offset = index * totalPoints;
-
- copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::BandContrast, offset);
- copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::BandSlope, offset);
- copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::Bands, offset);
- copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::Error, offset);
- copyRawData(m_InputValues, totalPoints * 3, m_DataStructure, *m_Reader, ebsdlib::H5OINA::Euler, offset);
- copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::MeanAngularDeviation, offset);
+ // Scan `scanIndex` occupies the tuple slab [scanIndex * totalPoints, (scanIndex + 1) * totalPoints).
+ const usize tupleOffset = static_cast(scanIndex) * totalPoints;
+
+ const std::string& scanName = m_CurrentScanName;
+
+ if(Result<> extentResults = validateDataSetExtents(m_InputValues->SelectedScanNames.inputFilePath, scanName, totalPoints); extentResults.invalid())
+ {
+ return extentResults;
+ }
+
+ copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::BandContrast, tupleOffset);
+ copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::BandSlope, tupleOffset);
+ copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::Bands, tupleOffset);
+ copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::Error, tupleOffset);
+ // Euler carries three components per scan point, so both the element count and the
+ // destination offset are three times the tuple counts.
+ copyRawData(m_InputValues, totalPoints * 3, m_DataStructure, *m_Reader, ebsdlib::H5OINA::Euler, tupleOffset * 3);
+ copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::MeanAngularDeviation, tupleOffset);
+
+ // The phase value of every point indexes the ensemble arrays, both in the alignment
+ // loop below and in every downstream filter, so it is range checked before it is
+ // stored. The valid range is [0, phase count]: 0 is the reserved Invalid Phase slot.
+ {
+ const auto& crystalStructures = m_DataStructure.getDataRefAs(m_InputValues->CellEnsembleAttributeMatrixPath.createChildPath(ebsdlib::AngFile::CrystalStructures));
+ const usize ensembleTupleCount = crystalStructures.getNumberOfTuples();
+ const nonstd::span rawPhaseSpan(reinterpret_cast(m_Reader->getPointerByName(ebsdlib::H5OINA::Phase)), totalPoints);
+ for(usize tupleIdx = 0; tupleIdx < totalPoints; tupleIdx++)
+ {
+ if(static_cast(rawPhaseSpan[tupleIdx]) >= ensembleTupleCount)
+ {
+ return MakeErrorResult(-34972, fmt::format("Scan point {} of scan '{}' in '{}' carries phase value {}, which is outside the valid range [0, {}] established by the file's phase definitions.",
+ tupleIdx, scanName, m_InputValues->SelectedScanNames.inputFilePath.string(), rawPhaseSpan[tupleIdx], ensembleTupleCount - 1));
+ }
+ }
+ }
+
if(m_InputValues->ConvertPhaseToInt32)
{
- const nonstd::span rawDataPtr(reinterpret_cast(m_Reader->getPointerByName(ebsdlib::H5OINA::Phase)), totalPoints);
- using ArrayType = DataArray;
- auto& dataRef = m_DataStructure.getDataRefAs(m_InputValues->CellAttributeMatrixPath.createChildPath(ebsdlib::H5OINA::Phase));
+ const nonstd::span rawDataSpan(reinterpret_cast(m_Reader->getPointerByName(ebsdlib::H5OINA::Phase)), totalPoints);
+ auto& dataRef = m_DataStructure.getDataRefAs(m_InputValues->CellAttributeMatrixPath.createChildPath(ebsdlib::H5OINA::Phase));
auto* dataStorePtr = dataRef.getDataStore();
- for(size_t i = 0; i < totalPoints; i++)
+ for(usize tupleIdx = 0; tupleIdx < totalPoints; tupleIdx++)
{
- dataStorePtr->setValue(i + offset, static_cast(rawDataPtr[i]));
+ dataStorePtr->setValue(tupleIdx + tupleOffset, static_cast(rawDataSpan[tupleIdx]));
}
}
else
{
- copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::Phase, offset);
+ copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::Phase, tupleOffset);
}
- copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::X, offset);
- copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::Y, offset);
+ copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::X, tupleOffset);
+ copyRawData(m_InputValues, totalPoints, m_DataStructure, *m_Reader, ebsdlib::H5OINA::Y, tupleOffset);
- if(m_InputValues->EdaxHexagonalAlignment)
+ if(m_ShouldCancel)
{
- if(m_InputValues->ConvertPhaseToInt32)
- {
- convertHexEulerAngle(m_InputValues, totalPoints, m_DataStructure);
- }
- else
- {
- convertHexEulerAngle(m_InputValues, totalPoints, m_DataStructure);
- }
+ return {};
}
- if(m_InputValues->ReadPatternData)
+ if(m_InputValues->EdaxHexagonalAlignment)
{
- const uint16* patternDataPtr = m_Reader->getPatternData();
- if(patternDataPtr == nullptr)
+ if(m_InputValues->ConvertPhaseToInt32)
{
- return MakeErrorResult(-34970, "Pattern data was requested but no pattern data was found in the data file");
+ convertHexEulerAngle(m_InputValues, totalPoints, tupleOffset, m_DataStructure);
}
- std::array pDims = {{0, 0}};
- m_Reader->getPatternDims(pDims);
- if(pDims[0] != 0 && pDims[1] != 0)
+ else
{
- std::vector pDimsV(2);
- pDimsV[0] = pDims[0];
- pDimsV[1] = pDims[1];
- auto& patternData = m_DataStructure.getDataRefAs(m_InputValues->CellAttributeMatrixPath.createChildPath(ebsdlib::H5OINA::UnprocessedPatterns));
- const usize numComponents = patternData.getNumberOfComponents();
- for(usize i = 0; i < totalPoints; i++)
- {
- for(usize j = 0; j < numComponents; ++j)
- {
- patternData[offset + numComponents * i + j] = patternDataPtr[numComponents * i + j];
- }
- }
+ convertHexEulerAngle(m_InputValues, totalPoints, tupleOffset, m_DataStructure);
}
}
diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadH5OinaData.hpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadH5OinaData.hpp
index f0dbb9cf5d..efeb9d46cb 100644
--- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadH5OinaData.hpp
+++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ReadH5OinaData.hpp
@@ -3,19 +3,20 @@
#include "OrientationAnalysis/OrientationAnalysis_export.hpp"
#include "OrientationAnalysis/utilities/IEbsdOemReader.hpp"
+#include
+
namespace nx::core
{
/**
* @class ReadH5OinaData
- * @brief This filter will read a single .h5 file into a new Image Geometry, allowing the immediate use of Filters on the data instead of having to generate the
- * intermediate .h5ebsd file.
+ * @brief Reads one or more scans from an H5OINA file into one Image Geometry.
*/
class ORIENTATIONANALYSIS_EXPORT ReadH5OinaData : public IEbsdOemReader
{
public:
- ReadH5OinaData(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, ReadH5DataInputValues* inputValues);
+ ReadH5OinaData(DataStructure& dataStructure, const IFilter::MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, ReadH5DataInputValues* inputValues);
~ReadH5OinaData() noexcept override;
ReadH5OinaData(const ReadH5OinaData&) = delete;
@@ -25,7 +26,16 @@ class ORIENTATIONANALYSIS_EXPORT ReadH5OinaData : public IEbsdOemReader operator()();
- Result<> copyRawEbsdData(int index) override;
+ Result<> copyRawEbsdData(int scanIndex) override;
+
+private:
+ /**
+ * @brief The scan name whose data is currently in the reader's buffers. The shared
+ * copyRawEbsdData(int) signature carries only the destination slab index, and under a
+ * High-to-Low stacking order that index no longer matches the scan's position in the
+ * selection list, so the name is carried here instead of being re-derived from it.
+ */
+ std::string m_CurrentScanName;
};
} // namespace nx::core
diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ReadH5OinaDataFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ReadH5OinaDataFilter.cpp
index d204390fe4..30394bf172 100644
--- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ReadH5OinaDataFilter.cpp
+++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ReadH5OinaDataFilter.cpp
@@ -22,8 +22,12 @@
#include
#include
-#include
-namespace fs = std::filesystem;
+#include
+#include
+#include
+#include
+
+#include
using namespace nx::core;
@@ -74,7 +78,10 @@ Parameters ReadH5OinaDataFilter::parameters() const
params.insert(std::make_unique(k_ConvertPhaseToInt32_Key, "Convert Phase Data to Int32", "Native Phases data value is uint8. Convert to Int32 for better filter compatibility", true));
params.insert(std::make_unique(k_Origin_Key, "Origin", "The origin of the volume", std::vector{0.0F, 0.0F, 0.0F}, std::vector{"x", "y", "z"}));
params.insert(std::make_unique(k_ZSpacing_Key, "Z Spacing (Microns)", "The spacing in microns between each layer.", 1.0f));
- params.insert(std::make_unique(k_ReadPatternData_Key, "Import Pattern Data", "Whether or not to import the pattern data", false));
+ params.insert(std::make_unique(k_ReadPatternData_Key, "Import Pattern Data",
+ "Whether or not to import the diffraction pattern data. Pattern import is not yet supported for H5OINA files, so turning this on stops the filter "
+ "with an error.",
+ false));
params.insertSeparator(Parameters::Separator{"Output Image Geometry"});
params.insert(std::make_unique(k_CreatedImageGeometryPath_Key, "Image Geometry", "The path to the created Image Geometry", DataPath({ImageGeom::k_TypeName})));
params.insertSeparator(Parameters::Separator{"Output Cell Attribute Matrix"});
@@ -90,7 +97,9 @@ Parameters ReadH5OinaDataFilter::parameters() const
//------------------------------------------------------------------------------
IFilter::VersionType ReadH5OinaDataFilter::parametersVersion() const
{
- return 1;
+ // Version 2: Pattern import is explicitly unsupported, and multi-scan inputs
+ // must have compatible geometry and identical phase definitions.
+ return 2;
}
//------------------------------------------------------------------------------
@@ -115,30 +124,200 @@ IFilter::PreflightResult ReadH5OinaDataFilter::preflightImpl(const DataStructure
DataPath cellEnsembleAMPath = pImageGeometryNameValue.createChildPath(pCellEnsembleAttributeMatrixNameValue);
DataPath cellAMPath = pImageGeometryNameValue.createChildPath(pCellAttributeMatrixNameValue);
- PreflightResult preflightResult;
nx::core::Result resultOutputActions;
std::vector preflightUpdatedValues;
- if(pZSpacingValue <= 0)
+ const std::string inputFilePath = pSelectedScanNamesValue.inputFilePath.string();
+
+ if(!std::isfinite(pZSpacingValue) || pZSpacingValue <= 0)
{
- return MakePreflightErrorResult(-9580, fmt::format("The Z Spacing field contains a value ({}) that is non-positive. The Z Spacing field must be set to a positive value.", pZSpacingValue));
+ return MakePreflightErrorResult(-9580, fmt::format("The Z Spacing value ({}) must be finite and positive.", pZSpacingValue));
}
if(pSelectedScanNamesValue.scanNames.empty())
{
- return MakePreflightErrorResult(-9581, "At least one scan must be chosen. Please select a scan from the list.");
+ return MakePreflightErrorResult(-9581, "At least one scan must be chosen. Please select a scan from the list.");
+ }
+ if(pReadPatternDataValue)
+ {
+ return MakePreflightErrorResult(-9583, fmt::format("Pattern import is not yet supported for H5OINA files, so 'Import Pattern Data' must be turned off to read '{}'. The diffraction "
+ "patterns a file does contain can be read with the 'Read HDF5 Dataset' filter.",
+ inputFilePath));
}
// read in the necessary info from the input h5 file
ebsdlib::H5OINAReader reader;
- reader.setFileName(pSelectedScanNamesValue.inputFilePath.string());
- reader.setReadPatternData(pReadPatternDataValue);
- reader.setHDF5Path(pSelectedScanNamesValue.scanNames.front());
+ reader.setFileName(inputFilePath);
+ reader.setReadPatternData(false);
+
+ // Every selected scan must be present in the file. Checking only the first one
+ // leaves a bad later name to fail part way through execute, with the scans that
+ // were already imported left in the output arrays.
+ std::list availableScanNames;
+ if(const int err = reader.readScanNames(availableScanNames); err < 0)
+ {
+ return MakePreflightErrorResult(-9582, fmt::format("An error occurred while listing the scans in '{}'.\n Error Code: {}\n Message: {}", inputFilePath, err, reader.getErrorMessage()));
+ }
+ for(const std::string& scanName : pSelectedScanNamesValue.scanNames)
+ {
+ if(std::find(availableScanNames.cbegin(), availableScanNames.cend(), scanName) == availableScanNames.cend())
+ {
+ std::string availableList;
+ for(const std::string& availableScanName : availableScanNames)
+ {
+ availableList += (availableList.empty() ? "" : ", ") + availableScanName;
+ }
+ return MakePreflightErrorResult(-9586, fmt::format("The selected scan '{}' is not present in '{}'. The scans available in this file are: {}", scanName, inputFilePath,
+ availableList.empty() ? std::string("") : availableList));
+ }
+ }
+
+ const std::string& firstScanName = pSelectedScanNamesValue.scanNames.front();
+ reader.setHDF5Path(firstScanName);
if(const int err = reader.readHeaderOnly(); err < 0)
{
- return MakePreflightErrorResult(-9582, fmt::format("An error occurred while reading the header data\n{} : {}", err, reader.getErrorMessage()));
+ return MakePreflightErrorResult(
+ -9582, fmt::format("An error occurred while reading the header of scan '{}' in '{}'.\n Error Code: {}\n Message: {}", firstScanName, inputFilePath, err, reader.getErrorMessage()));
+ }
+
+ // The geometry is sized from these two values, so a count below 1 has to be
+ // rejected here rather than producing an empty or absurdly large geometry.
+ if(reader.getXDimension() < 1 || reader.getYDimension() < 1)
+ {
+ return MakePreflightErrorResult(-9584, fmt::format("The header of scan '{}' in '{}' reports X Cells = {} and Y Cells = {}. Both must be at least 1. The file may be malformed or may not be an "
+ "H5OINA file.",
+ firstScanName, inputFilePath, reader.getXDimension(), reader.getYDimension()));
+ }
+
+ const auto validateSpacing = [&](const std::string& scanName, ebsdlib::H5OINAReader& scanReader) -> Result<> {
+ const float32 xStep = scanReader.getXStep();
+ const float32 yStep = scanReader.getYStep();
+ if(!std::isfinite(xStep) || !std::isfinite(yStep) || xStep <= 0.0F || yStep <= 0.0F)
+ {
+ return MakeErrorResult(-9591, fmt::format("Scan '{}' in '{}' reports X Step = {} and Y Step = {}. Both values must be finite and positive.", scanName, inputFilePath, xStep, yStep));
+ }
+ return {};
+ };
+
+ if(Result<> spacingCheck = validateSpacing(firstScanName, reader); spacingCheck.invalid())
+ {
+ return MakePreflightErrorResult(spacingCheck.errors().front().code, spacingCheck.errors().front().message);
}
- // create the Image Geometry and it's attribute matrices
+ // The Ensemble Attribute Matrix is sized from the number of phase groups in the
+ // FIRST selected scan, but the shared ensemble fill in IEbsdOemReader::readData runs
+ // once per selected scan and places each phase at the index carried by its HDF5 group
+ // name. Both properties below therefore have to hold for EVERY selected scan, not just
+ // the first: a later scan with a group named outside 1..N, or with more phase groups
+ // than the first scan, writes past the end of the ensemble arrays at execute.
+ const auto phases = reader.getPhaseVector();
+ const usize ensemblePhaseCount = phases.size();
+ const auto validatePhaseGroups = [&](const std::string& scanName, const auto& scanPhases, bool comparePhaseDefinitions) -> Result<> {
+ if(scanPhases.size() != ensemblePhaseCount)
+ {
+ return MakeErrorResult(-9589, fmt::format("Scan '{}' in '{}' declares {} phase group(s), but scan '{}' declares {}. Every selected scan must declare the same phase groups, because the single "
+ "Ensemble Attribute Matrix that all of the stacked scans share is sized and filled from those groups. Import scans with differing phase lists "
+ "separately.",
+ scanName, inputFilePath, scanPhases.size(), firstScanName, ensemblePhaseCount));
+ }
+ for(const auto& phase : scanPhases)
+ {
+ const int32 phaseIndex = phase->getPhaseIndex();
+ if(phaseIndex < 1 || static_cast(phaseIndex) > ensemblePhaseCount)
+ {
+ return MakeErrorResult(-9587, fmt::format("Scan '{}' in '{}' declares {} phase(s), but one of them carries index {}. The phase groups of an H5OINA file must be named 1 through {}.", scanName,
+ inputFilePath, ensemblePhaseCount, phaseIndex, ensemblePhaseCount));
+ }
+
+ if(comparePhaseDefinitions)
+ {
+ const auto referencePhaseIter = std::find_if(phases.cbegin(), phases.cend(), [phaseIndex](const auto& referencePhase) { return referencePhase->getPhaseIndex() == phaseIndex; });
+ if(referencePhaseIter == phases.cend())
+ {
+ return MakeErrorResult(-9587, fmt::format("Scan '{}' in '{}' declares phase index {}, but scan '{}' does not. Every selected scan must use the same phase group names.", scanName,
+ inputFilePath, phaseIndex, firstScanName));
+ }
+
+ const auto& referencePhase = *referencePhaseIter;
+ if(phase->getPhaseName() != referencePhase->getPhaseName())
+ {
+ return MakeErrorResult(-9590,
+ fmt::format("Phase group {} of scan '{}' in '{}' has material name '{}', but the same group of scan '{}' has material name '{}'. The selected scans form one 3D "
+ "microstructure and must use identical phase definitions.",
+ phaseIndex, scanName, inputFilePath, phase->getPhaseName(), firstScanName, referencePhase->getPhaseName()));
+ }
+ if(phase->getLaueGroup() != referencePhase->getLaueGroup())
+ {
+ return MakeErrorResult(-9590, fmt::format("Phase group {} of scan '{}' in '{}' has Laue group {}, but the same group of scan '{}' has Laue group {}. The selected scans form one 3D "
+ "microstructure and must use identical phase definitions.",
+ phaseIndex, scanName, inputFilePath, static_cast(phase->getLaueGroup()), firstScanName, static_cast(referencePhase->getLaueGroup())));
+ }
+ if(phase->getSpaceGroup() != referencePhase->getSpaceGroup())
+ {
+ return MakeErrorResult(-9590, fmt::format("Phase group {} of scan '{}' in '{}' has space group {}, but the same group of scan '{}' has space group {}. The selected scans form one 3D "
+ "microstructure and must use identical phase definitions.",
+ phaseIndex, scanName, inputFilePath, phase->getSpaceGroup(), firstScanName, referencePhase->getSpaceGroup()));
+ }
+
+ const std::vector scanLatticeConstants = phase->getLatticeConstants();
+ const std::vector referenceLatticeConstants = referencePhase->getLatticeConstants();
+ if(scanLatticeConstants != referenceLatticeConstants)
+ {
+ return MakeErrorResult(-9590,
+ fmt::format("Phase group {} of scan '{}' in '{}' has lattice constants [{}], but the same group of scan '{}' has lattice constants [{}]. The selected scans form "
+ "one 3D microstructure and must use identical phase definitions.",
+ phaseIndex, scanName, inputFilePath, fmt::join(scanLatticeConstants, ", "), firstScanName, fmt::join(referenceLatticeConstants, ", ")));
+ }
+ }
+ }
+ return {};
+ };
+
+ if(Result<> phaseCheck = validatePhaseGroups(firstScanName, phases, false); phaseCheck.invalid())
+ {
+ return MakePreflightErrorResult(phaseCheck.errors().front().code, phaseCheck.errors().front().message);
+ }
+
+ // Every other selected scan has to describe the same grid, because the geometry
+ // and every cell array are sized from the first scan's header alone. A second
+ // reader is used so the checks below do not disturb the header state that the
+ // preflight-updated values and the output actions are built from.
+ {
+ ebsdlib::H5OINAReader scanCheckReader;
+ scanCheckReader.setFileName(inputFilePath);
+ scanCheckReader.setReadPatternData(false);
+ for(const std::string& scanName : pSelectedScanNamesValue.scanNames)
+ {
+ if(scanName == firstScanName)
+ {
+ continue;
+ }
+ scanCheckReader.setHDF5Path(scanName);
+ if(const int err = scanCheckReader.readHeaderOnly(); err < 0)
+ {
+ return MakePreflightErrorResult(
+ -9582, fmt::format("An error occurred while reading the header of scan '{}' in '{}'.\n Error Code: {}\n Message: {}", scanName, inputFilePath, err, scanCheckReader.getErrorMessage()));
+ }
+ if(Result<> spacingCheck = validateSpacing(scanName, scanCheckReader); spacingCheck.invalid())
+ {
+ return MakePreflightErrorResult(spacingCheck.errors().front().code, spacingCheck.errors().front().message);
+ }
+ if(scanCheckReader.getXDimension() != reader.getXDimension() || scanCheckReader.getYDimension() != reader.getYDimension() || scanCheckReader.getXStep() != reader.getXStep() ||
+ scanCheckReader.getYStep() != reader.getYStep())
+ {
+ return MakePreflightErrorResult(
+ -9585, fmt::format("Scan '{}' in '{}' describes a {} x {} grid with steps ({}, {}), but scan '{}' describes a {} x {} grid with steps ({}, {}). Every selected scan must describe the same "
+ "grid, because they are stacked into a single Image Geometry.",
+ scanName, inputFilePath, scanCheckReader.getXDimension(), scanCheckReader.getYDimension(), scanCheckReader.getXStep(), scanCheckReader.getYStep(), firstScanName,
+ reader.getXDimension(), reader.getYDimension(), reader.getXStep(), reader.getYStep()));
+ }
+ if(Result<> phaseCheck = validatePhaseGroups(scanName, scanCheckReader.getPhaseVector(), true); phaseCheck.invalid())
+ {
+ return MakePreflightErrorResult(phaseCheck.errors().front().code, phaseCheck.errors().front().message);
+ }
+ }
+ }
+
+ // Create the Image Geometry and its attribute matrices.
const CreateImageGeometryAction::DimensionType dims = {static_cast(reader.getXDimension()), static_cast(reader.getYDimension()), pSelectedScanNamesValue.scanNames.size()};
const ShapeType tupleDims = {dims[2], dims[1], dims[0]};
{
@@ -151,7 +330,6 @@ IFilter::PreflightResult ReadH5OinaDataFilter::preflightImpl(const DataStructure
EbsdReaderUtilities::GeneratePreflightScanInformation(reader, preflightUpdatedValues);
EbsdReaderUtilities::GeneratePreflightPhaseInformation(reader, preflightUpdatedValues);
- const auto phases = reader.getPhaseVector();
std::vector ensembleTupleDims{phases.size() + 1};
{
auto createAttributeMatrixAction = std::make_unique(cellEnsembleAMPath, ensembleTupleDims);
@@ -190,19 +368,6 @@ IFilter::PreflightResult ReadH5OinaDataFilter::preflightImpl(const DataStructure
resultOutputActions.value().appendAction(std::make_unique(DataType::float32, tupleDims, std::vector{1}, cellAMPath.createChildPath(ebsdlib::H5OINA::X)));
resultOutputActions.value().appendAction(std::make_unique(DataType::float32, tupleDims, std::vector{1}, cellAMPath.createChildPath(ebsdlib::H5OINA::Y)));
- if(pReadPatternDataValue)
- {
- std::array patternDims = {{0, 0}};
- reader.getPatternDims(patternDims);
- if(patternDims[0] == 0 || patternDims[1] == 0)
- {
- return MakePreflightErrorResult(-9583, fmt::format("The parameter 'Read Pattern Data' has been enabled but there does not seem to be any pattern data in the file for the scan name selected"));
- }
- auto createArrayAction = std::make_unique(DataType::uint16, tupleDims, std::vector{static_cast(patternDims[0]), static_cast(patternDims[1])},
- cellAMPath.createChildPath(ebsdlib::H5OINA::UnprocessedPatterns));
- resultOutputActions.value().appendAction(std::move(createArrayAction));
- }
-
return {std::move(resultOutputActions), std::move(preflightUpdatedValues)};
}
diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ReadH5OinaDataFilter.hpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ReadH5OinaDataFilter.hpp
index 103700b0f0..690dc5a2b5 100644
--- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ReadH5OinaDataFilter.hpp
+++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ReadH5OinaDataFilter.hpp
@@ -116,4 +116,3 @@ class ORIENTATIONANALYSIS_EXPORT ReadH5OinaDataFilter : public IFilter
} // namespace nx::core
SIMPLNX_DEF_FILTER_TRAITS(nx::core, ReadH5OinaDataFilter, "fad3d47f-f1e1-4429-bc65-5e021be62ba0");
-/* LEGACY UUID FOR THIS FILTER 3ff4701b-3a0c-52e3-910a-fa927aa6584c */
diff --git a/src/Plugins/OrientationAnalysis/test/ReadH5OinaDataTest.cpp b/src/Plugins/OrientationAnalysis/test/ReadH5OinaDataTest.cpp
index 1ccd5c77da..7c6aa4503d 100644
--- a/src/Plugins/OrientationAnalysis/test/ReadH5OinaDataTest.cpp
+++ b/src/Plugins/OrientationAnalysis/test/ReadH5OinaDataTest.cpp
@@ -1,134 +1,1373 @@
-#include
+/* ============================================================================
+ * ReadH5OinaData V&V test suite.
+ *
+ * Verification is established INDEPENDENTLY of any DREAM3D output, per the V&V
+ * policy (src/Plugins/OrientationAnalysis/vv/ReadH5OinaDataFilter.md). There is
+ * no DREAM3D 6.5.171 H5OINA importer to compare against, so the oracle carries
+ * the whole burden:
+ *
+ * - .h5oina parsing : Part of the system under test. EbsdLib's H5OINAReader
+ * owns HDF5 traversal, header and phase parsing, required-
+ * dataset enforcement, and its own error codes. The tests
+ * verify its user-visible results against analytical fixture
+ * values and the independent h5py readback.
+ * - SIMPLNX value-add : Class 1 (analytical) + Class 4 (invariant). The filter's
+ * value-add is deterministic plumbing on top of the reader:
+ * geometry construction from the first scan's header, array
+ * creation and typing, per-scan slab offsets, the verbatim
+ * column copies, the uint8 -> int32 Phase widening, the
+ * EDAX hexagonal alignment applied to phi2, ensemble slot-0
+ * defaults and per-phase fill, and the malformed-input
+ * guards. The toy .h5oina fixtures below are written by
+ * this file with H5Lite and every expected value is derived
+ * from the fixture spec, never from observed output.
+ * - Real AZtec file : Class 2 independent readback. The archived production
+ * H5Oina_Test_Data.h5oina is compared against a readback of
+ * its own Data group datasets performed with H5Lite, i.e. the
+ * raw file bytes, bypassing H5OINAReader entirely. An
+ * equivalent h5py readback in a second language is recorded
+ * in the V&V working folder, described in
+ * vv/provenance/ReadH5OinaDataFilter.md.
+ *
+ * Precision pinning (the ReadCtfData lesson): every floating-point value written
+ * into a toy fixture below is a float32 literal stored as float32, so the file
+ * round-trips it bit-for-bit and the verbatim copies are asserted with exact
+ * equality. (The cell counts and the Laue and space group numbers are int32
+ * scalars and five of the nine data columns are uint8, all of which round-trip
+ * exactly as well. The production file's values are whatever AZtec wrote; that
+ * test asserts exact equality too, but against an independent readback of the
+ * file rather than against literals.)
+ *
+ * The hexagonal alignment adds 30 degrees expressed in radians, computed with a
+ * double-precision intermediate and stored back as float32. Five fixture phi2
+ * values -- 0.1F and 0.34F, used in Fixture A and in Fixture C's first scan, and
+ * 0.09F, 0.22F and 0.35F in Fixture C's second scan -- are chosen because their
+ * correctly-rounded float32 results DIFFER between a double-precision
+ * intermediate and a float32 intermediate, so the expectations pin the shape of
+ * the arithmetic and not merely its magnitude. Note that none of those five is an
+ * exactly representable decimal, and that is deliberate: a dyadic value has
+ * trailing zero mantissa bits, so adding the constant rounds to the same float32
+ * either way and cannot separate the two paths. 0.25F, 0.5F and 0.75F are exactly
+ * representable and are carried alongside on hexagonal points as controls whose
+ * results are identical under either intermediate. The hexagonal phase's third
+ * lattice angle, 2.0943952F -> 120.0F, is a sixth such discriminator: a float32
+ * intermediate in the radians-to-degrees conversion would give 120.00000762939453F.
+ * Every such literal was derived with IEEE-754 float32/float64 semantics in NumPy;
+ * the derivation script and its recorded output are described in
+ * vv/provenance/ReadH5OinaDataFilter.md.
+ *
+ * The archive's H5Oina_Test_Data.dream3d exemplar is no longer consulted: it was
+ * generated by this very filter, so it is a self-oracle that pins "the filter
+ * keeps doing what it did" rather than "the filter is right".
+ * ========================================================================== */
+#include "OrientationAnalysis/Filters/ReadH5OinaDataFilter.hpp"
+#include "OrientationAnalysis/OrientationAnalysis_test_dirs.hpp"
+#include "OrientationAnalysis/Parameters/OEMEbsdScanSelectionParameter.h"
+
+#include "simplnx/DataStructure/DataArray.hpp"
#include "simplnx/DataStructure/Geometry/ImageGeom.hpp"
+#include "simplnx/DataStructure/StringArray.hpp"
#include "simplnx/Parameters/BoolParameter.hpp"
#include "simplnx/Parameters/DataGroupCreationParameter.hpp"
#include "simplnx/Parameters/VectorParameter.hpp"
#include "simplnx/UnitTest/UnitTestCommon.hpp"
-#include "OrientationAnalysis/Filters/ReadH5OinaDataFilter.hpp"
-#include "OrientationAnalysis/OrientationAnalysis_test_dirs.hpp"
-#include "OrientationAnalysis/Parameters/OEMEbsdScanSelectionParameter.h"
+#include "H5Support/H5Lite.h"
+#include "H5Support/H5Utilities.h"
+
+#include
+#include
+#include
+
+#include
-#include
+#include
+#include
#include
+#include
+#include
+#include
+
namespace fs = std::filesystem;
using namespace nx::core;
using namespace nx::core::Constants;
+using namespace H5Support;
namespace
{
-const std::string k_ScanName = "1";
+const std::string k_CellAttributeMatrixName = "Cell Data";
+const std::string k_CellEnsembleAttributeMatrixName = "Cell Ensemble Data";
+const DataPath k_ImageGeomPath({ImageGeom::k_TypeName});
+const DataPath k_CellAMPath = k_ImageGeomPath.createChildPath(k_CellAttributeMatrixName);
+const DataPath k_EnsembleAMPath = k_ImageGeomPath.createChildPath(k_CellEnsembleAttributeMatrixName);
+
+// EbsdLib/IO/HKL/CtfConstants.h LaueGroupTable: 9 = LG_Hexagonal_High, 11 = LG_Cubic_High.
+// EbsdLib/Core/EbsdLibConstants.h: Hexagonal_High = 0, Cubic_High = 1, UnknownCrystalStructure = 999.
+constexpr int32 k_LaueHexagonalHigh = 9;
+constexpr int32 k_LaueCubicHigh = 11;
+
+//------------------------------------------------------------------------------
+// Toy .h5oina fixture description. Written out in full, the dataset set below is
+// exactly the minimum H5OINAReader requires: the four Header scalars, one or more
+// Phases/ groups carrying Phase Name / Lattice Dimensions / Lattice Angles /
+// Laue Group / Space Group, and the nine Data datasets it reads unconditionally.
+// The root Manufacturer / Software Version / Index datasets are written for
+// realism only -- the reader never reads them.
+//
+// The guard fixtures deliberately depart from that minimum: `omitBandsDataset`
+// and `omitLatticeAngles` drop a required dataset outright, and `groupName`
+// overrides the phase index that the group name carries. The remaining guard
+// fixtures keep the full dataset set but give it values a well formed file would
+// not have.
+//------------------------------------------------------------------------------
+struct PhaseSpec
+{
+ std::string name;
+ int32 laueGroup = k_LaueCubicHigh;
+ int32 spaceGroup = 225;
+ std::array latticeDimensions = {1.0F, 1.0F, 1.0F};
+ // Lattice angles as an H5OINA file stores them: RADIANS.
+ std::array latticeAngles = {1.5707964F, 1.5707964F, 1.5707964F};
+ // Group name override; empty means "use the phase's 1-based position".
+ std::string groupName;
+ bool omitLatticeAngles = false;
+};
+
+struct ScanSpec
+{
+ std::string name = "1";
+ int32 xCells = 1;
+ int32 yCells = 1;
+ float32 xStep = 1.0F;
+ float32 yStep = 1.0F;
+ std::vector phases;
+ std::vector phase;
+ std::vector bandContrast;
+ std::vector bandSlope;
+ std::vector bands;
+ std::vector error;
+ std::vector euler; // 3 components per point, interleaved
+ std::vector mad;
+ std::vector x;
+ std::vector y;
+ // Guard-fixture switches.
+ bool omitBandsDataset = false;
+};
+
+//------------------------------------------------------------------------------
+template
+void WriteVector(hid_t gid, const std::string& name, const std::vector& dims, const std::vector& data)
+{
+ const herr_t err = H5Lite::writeVectorDataset(gid, name, dims, data);
+ REQUIRE(err >= 0);
}
-TEST_CASE("OrientationAnalysis::ReadH5OinaDataFilter: Valid Filter Execution", "[OrientationAnalysis][ReadH5OinaDataFilter]")
+//------------------------------------------------------------------------------
+hid_t OpenOrCreateGroup(hid_t fileId, const std::string& path)
+{
+ const hid_t err = H5Utilities::createGroupsFromPath(path, fileId);
+ REQUIRE(err >= 0);
+ const hid_t gid = H5Gopen(fileId, path.c_str(), H5P_DEFAULT);
+ REQUIRE(gid >= 0);
+ return gid;
+}
+
+//------------------------------------------------------------------------------
+// Writes the fixture to / and returns the path.
+//------------------------------------------------------------------------------
+fs::path WriteH5OinaFixture(const std::string& fileName, const std::vector& scans, bool writeFormatVersion = true, const std::string& formatVersion = "5.0")
+{
+ const fs::path filePath = fs::path(unit_test::k_BinaryTestOutputDir.view()) / fileName;
+ const hid_t fileId = H5Fcreate(filePath.string().c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
+ REQUIRE(fileId >= 0);
+
+ if(writeFormatVersion)
+ {
+ REQUIRE(H5Lite::writeStringDataset(fileId, ebsdlib::H5OINA::FormatVersion, formatVersion) >= 0);
+ }
+ // Inert realism -- the reader never reads these three.
+ REQUIRE(H5Lite::writeStringDataset(fileId, ebsdlib::H5OINA::Manufacturer, "Oxford Instruments") >= 0);
+ REQUIRE(H5Lite::writeStringDataset(fileId, ebsdlib::H5OINA::SoftwareVersion, "V&V toy fixture") >= 0);
+ REQUIRE(H5Lite::writeStringDataset(fileId, ebsdlib::H5OINA::Index, "1") >= 0);
+
+ for(const ScanSpec& scan : scans)
+ {
+ const std::string headerPath = fmt::format("{}/{}/{}", scan.name, ebsdlib::H5OINA::EBSD, ebsdlib::H5OINA::Header);
+ const hid_t headerGid = OpenOrCreateGroup(fileId, headerPath);
+ REQUIRE(H5Lite::writeScalarDataset(headerGid, ebsdlib::H5OINA::XCells, scan.xCells) >= 0);
+ REQUIRE(H5Lite::writeScalarDataset(headerGid, ebsdlib::H5OINA::YCells, scan.yCells) >= 0);
+ REQUIRE(H5Lite::writeScalarDataset(headerGid, ebsdlib::H5OINA::XStep, scan.xStep) >= 0);
+ REQUIRE(H5Lite::writeScalarDataset(headerGid, ebsdlib::H5OINA::YStep, scan.yStep) >= 0);
+
+ for(usize i = 0; i < scan.phases.size(); i++)
+ {
+ const PhaseSpec& phaseSpec = scan.phases[i];
+ const std::string groupName = phaseSpec.groupName.empty() ? std::to_string(i + 1) : phaseSpec.groupName;
+ const hid_t phaseGid = OpenOrCreateGroup(fileId, fmt::format("{}/{}/{}", headerPath, ebsdlib::H5OINA::Phases, groupName));
+ REQUIRE(H5Lite::writeStringDataset(phaseGid, ebsdlib::H5OINA::PhaseName, phaseSpec.name) >= 0);
+ WriteVector(phaseGid, ebsdlib::H5OINA::LatticeDimensions, {1, 3}, {phaseSpec.latticeDimensions[0], phaseSpec.latticeDimensions[1], phaseSpec.latticeDimensions[2]});
+ if(!phaseSpec.omitLatticeAngles)
+ {
+ WriteVector(phaseGid, ebsdlib::H5OINA::LatticeAngles, {1, 3}, {phaseSpec.latticeAngles[0], phaseSpec.latticeAngles[1], phaseSpec.latticeAngles[2]});
+ }
+ REQUIRE(H5Lite::writeScalarDataset(phaseGid, ebsdlib::H5OINA::LaueGroup, phaseSpec.laueGroup) >= 0);
+ REQUIRE(H5Lite::writeScalarDataset(phaseGid, ebsdlib::H5OINA::SpaceGroup, phaseSpec.spaceGroup) >= 0);
+ REQUIRE(H5Gclose(phaseGid) >= 0);
+ }
+ REQUIRE(H5Gclose(headerGid) >= 0);
+
+ const hid_t dataGid = OpenOrCreateGroup(fileId, fmt::format("{}/{}/{}", scan.name, ebsdlib::H5OINA::EBSD, ebsdlib::H5OINA::Data));
+ const auto pointCount = static_cast(scan.phase.size());
+ WriteVector(dataGid, ebsdlib::H5OINA::BandContrast, {pointCount}, scan.bandContrast);
+ WriteVector(dataGid, ebsdlib::H5OINA::BandSlope, {pointCount}, scan.bandSlope);
+ if(!scan.omitBandsDataset)
+ {
+ WriteVector(dataGid, ebsdlib::H5OINA::Bands, {pointCount}, scan.bands);
+ }
+ WriteVector(dataGid, ebsdlib::H5OINA::Error, {pointCount}, scan.error);
+ WriteVector(dataGid, ebsdlib::H5OINA::Euler, {pointCount, 3}, scan.euler);
+ WriteVector(dataGid, ebsdlib::H5OINA::MeanAngularDeviation, {pointCount}, scan.mad);
+ WriteVector(dataGid, ebsdlib::H5OINA::Phase, {pointCount}, scan.phase);
+ WriteVector(dataGid, ebsdlib::H5OINA::X, {pointCount}, scan.x);
+ WriteVector(dataGid, ebsdlib::H5OINA::Y, {pointCount}, scan.y);
+ REQUIRE(H5Gclose(dataGid) >= 0);
+ }
+
+ REQUIRE(H5Fclose(fileId) >= 0);
+ return filePath;
+}
+
+//------------------------------------------------------------------------------
+// Fixture phases.
+//------------------------------------------------------------------------------
+// Hexagonal: Laue group 9 -> CrystalStructure Hexagonal_High (0). Its lattice
+// angles are 90/90/120 degrees written as radians, which is what an H5OINA file
+// carries; the third angle differs from the second so a gamma slot that echoed
+// beta would be visible.
+const PhaseSpec k_HexPhase{"Hex Phase A", k_LaueHexagonalHigh, 194, {2.5F, 2.5F, 4.0F}, {1.5707964F, 1.5707964F, 2.0943952F}};
+// Cubic: Laue group 11 -> CrystalStructure Cubic_High (1).
+const PhaseSpec k_CubicPhase{"Cubic Phase B", k_LaueCubicHigh, 225, {3.5F, 3.5F, 3.5F}, {1.5707964F, 1.5707964F, 1.5707964F}};
+
+//------------------------------------------------------------------------------
+// Fixture A: one scan "1", 3 x 2 cells, XStep 0.25 / YStep 0.5, two phases.
+//
+// Points 0, 1 and 2 all carry phi2 = 0.1F on a hexagonal, a cubic and an
+// unindexed (phase 0) point respectively, so the three expectations differ only
+// through the alignment branch.
+//------------------------------------------------------------------------------
+ScanSpec MakeFixtureAScan()
+{
+ ScanSpec scan;
+ scan.name = "1";
+ scan.xCells = 3;
+ scan.yCells = 2;
+ scan.xStep = 0.25F;
+ scan.yStep = 0.5F;
+ scan.phases = {k_HexPhase, k_CubicPhase};
+ scan.phase = {1, 2, 0, 2, 1, 1};
+ scan.bandContrast = {10, 20, 30, 40, 50, 60};
+ scan.bandSlope = {11, 21, 31, 41, 51, 61};
+ scan.bands = {1, 2, 3, 4, 5, 6};
+ scan.error = {0, 1, 0, 2, 0, 3};
+ scan.euler = {
+ 0.25F, 0.125F, 0.1F, // pt 0 hexagonal -> phi2 shifted
+ 0.5F, 0.375F, 0.1F, // pt 1 cubic -> phi2 untouched
+ 0.75F, 0.625F, 0.1F, // pt 2 unindexed -> phi2 untouched
+ 1.0F, 0.875F, 0.75F, // pt 3 cubic -> phi2 untouched
+ 1.25F, 1.125F, 0.34F, // pt 4 hexagonal -> phi2 shifted
+ 1.5F, 1.375F, 0.25F, // pt 5 hexagonal -> phi2 shifted (dyadic control)
+ };
+ scan.mad = {0.125F, 0.25F, 0.375F, 0.5F, 0.625F, 0.75F};
+ scan.x = {0.0F, 0.25F, 0.5F, 0.0F, 0.25F, 0.5F};
+ scan.y = {0.0F, 0.0F, 0.0F, 0.5F, 0.5F, 0.5F};
+ return scan;
+}
+
+// Fixture A expected Euler with the alignment ON. The three shifted values are the
+// correctly-rounded float32 results of double(phi2) + 30*(pi/180):
+// 0.1F -> 0.6235987544059753F (a float32 intermediate would give 0.6235988140106201F)
+// 0.34F -> 0.8635987639427185F (a float32 intermediate would give 0.8635988235473633F)
+// 0.25F -> 0.7735987901687622F (identical under either intermediate -- the control)
+// Adding the literal 30.0F, as a degree-valued importer would, gives 30.1F / 30.34F / 30.25F.
+const std::vector k_FixtureAEulerAligned = {
+ 0.25F, 0.125F, 0.6235987544059753F, 0.5F, 0.375F, 0.1F, 0.75F, 0.625F, 0.1F, 1.0F, 0.875F, 0.75F, 1.25F, 1.125F, 0.8635987639427185F, 1.5F, 1.375F, 0.7735987901687622F,
+};
+// The same fixture with the alignment OFF: every value verbatim from the file.
+const std::vector k_FixtureAEulerVerbatim = {
+ 0.25F, 0.125F, 0.1F, 0.5F, 0.375F, 0.1F, 0.75F, 0.625F, 0.1F, 1.0F, 0.875F, 0.75F, 1.25F, 1.125F, 0.34F, 1.5F, 1.375F, 0.25F,
+};
+
+//------------------------------------------------------------------------------
+// Fixture B: two scans, 2 x 2 each, CUBIC only. With no hexagonal points the
+// Euler array must be a pure verbatim copy, so any slab-placement error is
+// visible directly. Scan "2"'s values are disjoint from scan "1"'s.
+//------------------------------------------------------------------------------
+ScanSpec MakeFixtureBScan1()
+{
+ ScanSpec scan;
+ scan.name = "1";
+ scan.xCells = 2;
+ scan.yCells = 2;
+ scan.xStep = 0.25F;
+ scan.yStep = 0.5F;
+ scan.phases = {k_CubicPhase};
+ scan.phase = {1, 1, 1, 1};
+ scan.bandContrast = {10, 11, 12, 13};
+ scan.bandSlope = {20, 21, 22, 23};
+ scan.bands = {1, 2, 3, 4};
+ scan.error = {0, 1, 2, 3};
+ scan.euler = {0.125F, 0.25F, 0.375F, 0.5F, 0.625F, 0.75F, 0.875F, 1.0F, 1.125F, 1.25F, 1.375F, 1.5F};
+ scan.mad = {0.125F, 0.25F, 0.375F, 0.5F};
+ scan.x = {0.0F, 0.25F, 0.0F, 0.25F};
+ scan.y = {0.0F, 0.0F, 0.5F, 0.5F};
+ return scan;
+}
+
+ScanSpec MakeFixtureBScan2()
+{
+ ScanSpec scan = MakeFixtureBScan1();
+ scan.name = "2";
+ scan.bandContrast = {110, 111, 112, 113};
+ scan.bandSlope = {120, 121, 122, 123};
+ scan.bands = {5, 6, 7, 8};
+ scan.error = {4, 5, 6, 7};
+ scan.euler = {2.125F, 2.25F, 2.375F, 2.5F, 2.625F, 2.75F, 2.875F, 3.0F, 3.125F, 3.25F, 3.375F, 3.5F};
+ scan.mad = {1.125F, 1.25F, 1.375F, 1.5F};
+ // Every column has to differ from scan 1's, or a slab-placement error in that
+ // column cannot be detected across the two slabs. The phase group set is kept
+ // identical because the ensemble arrays are shared by every stacked scan; only
+ // the per-point phase VALUES differ, using the reserved unindexed value 0.
+ scan.phase = {1, 0, 1, 0};
+ scan.x = {1.0F, 1.25F, 1.0F, 1.25F};
+ scan.y = {1.0F, 1.0F, 1.5F, 1.5F};
+ return scan;
+}
+
+//------------------------------------------------------------------------------
+// Fixture C: two scans, 2 x 2 each, every point HEXAGONAL. A shift applied to
+// the wrong slab, or applied more than once to slab 0, changes values that are
+// pinned exactly here.
+//------------------------------------------------------------------------------
+ScanSpec MakeFixtureCScan1()
+{
+ ScanSpec scan;
+ scan.name = "1";
+ scan.xCells = 2;
+ scan.yCells = 2;
+ scan.xStep = 0.5F;
+ scan.yStep = 0.25F;
+ scan.phases = {k_HexPhase};
+ scan.phase = {1, 1, 1, 1};
+ scan.bandContrast = {30, 31, 32, 33};
+ scan.bandSlope = {40, 41, 42, 43};
+ scan.bands = {1, 2, 3, 4};
+ scan.error = {0, 0, 0, 0};
+ scan.euler = {0.25F, 0.5F, 0.1F, 0.75F, 1.0F, 0.34F, 1.25F, 1.5F, 0.25F, 1.75F, 2.0F, 0.5F};
+ scan.mad = {0.125F, 0.25F, 0.375F, 0.5F};
+ scan.x = {0.0F, 0.5F, 0.0F, 0.5F};
+ scan.y = {0.0F, 0.0F, 0.25F, 0.25F};
+ return scan;
+}
+
+ScanSpec MakeFixtureCScan2()
+{
+ ScanSpec scan = MakeFixtureCScan1();
+ scan.name = "2";
+ scan.bandContrast = {130, 131, 132, 133};
+ scan.bandSlope = {140, 141, 142, 143};
+ scan.bands = {5, 6, 7, 8};
+ scan.error = {1, 1, 1, 1};
+ scan.euler = {2.25F, 2.5F, 0.09F, 2.75F, 3.0F, 0.22F, 3.25F, 3.5F, 0.75F, 3.75F, 4.0F, 0.35F};
+ scan.mad = {1.125F, 1.25F, 1.375F, 1.5F};
+ return scan;
+}
+
+// Fixture C expected Euler across both slabs with the alignment ON. Every phi2 is
+// float32(double(phi2) + 30*(pi/180)); 0.1F/0.34F in slab 0 and 0.09F/0.22F/0.35F
+// in slab 1 separate a double intermediate from a float32 one, while 0.25F, 0.5F
+// and 0.75F round identically under either.
+const std::vector k_FixtureCEulerAligned = {
+ // slab 0 (scan "1")
+ 0.25F,
+ 0.5F,
+ 0.6235987544059753F,
+ 0.75F,
+ 1.0F,
+ 0.8635987639427185F,
+ 1.25F,
+ 1.5F,
+ 0.7735987901687622F,
+ 1.75F,
+ 2.0F,
+ 1.0235987901687622F,
+ // slab 1 (scan "2")
+ 2.25F,
+ 2.5F,
+ 0.6135987639427185F,
+ 2.75F,
+ 3.0F,
+ 0.7435987591743469F,
+ 3.25F,
+ 3.5F,
+ 1.2735987901687622F,
+ 3.75F,
+ 4.0F,
+ 0.8735987544059753F,
+};
+
+//------------------------------------------------------------------------------
+Arguments MakeArgs(const fs::path& inputFile, const std::list& scanNames, float32 zSpacing = 1.0F, bool hexAlignment = true, bool convertPhaseToInt32 = true, bool readPatternData = false,
+ uint32 stackingOrder = RefFrameZDir::k_LowtoHigh)
+{
+ Arguments args;
+ const OEMEbsdScanSelectionParameter::ValueType scanSelections = {inputFile, stackingOrder, scanNames};
+ args.insertOrAssign(ReadH5OinaDataFilter::k_SelectedScanNames_Key, std::make_any(scanSelections));
+ args.insertOrAssign(ReadH5OinaDataFilter::k_ZSpacing_Key, std::make_any(zSpacing));
+ args.insertOrAssign(ReadH5OinaDataFilter::k_Origin_Key, std::make_any(std::vector(3, 0.0F)));
+ args.insertOrAssign(ReadH5OinaDataFilter::k_ReadPatternData_Key, std::make_any(readPatternData));
+ args.insertOrAssign(ReadH5OinaDataFilter::k_EdaxHexagonalAlignment_Key, std::make_any(hexAlignment));
+ args.insertOrAssign(ReadH5OinaDataFilter::k_ConvertPhaseToInt32_Key, std::make_any(convertPhaseToInt32));
+ args.insertOrAssign(ReadH5OinaDataFilter::k_CreatedImageGeometryPath_Key, std::make_any(k_ImageGeomPath));
+ args.insertOrAssign(ReadH5OinaDataFilter::k_CellAttributeMatrixName_Key, std::make_any(k_CellAttributeMatrixName));
+ args.insertOrAssign(ReadH5OinaDataFilter::k_CellEnsembleAttributeMatrixName_Key, std::make_any(k_CellEnsembleAttributeMatrixName));
+ return args;
+}
+
+//------------------------------------------------------------------------------
+template
+void CompareArrayValues(const DataStructure& dataStructure, const DataPath& arrayPath, const std::vector& expected)
+{
+ REQUIRE_NOTHROW(dataStructure.getDataRefAs>(arrayPath));
+ const auto& dataArrayRef = dataStructure.getDataRefAs>(arrayPath);
+ REQUIRE(dataArrayRef.getSize() == expected.size());
+ for(usize i = 0; i < expected.size(); i++)
+ {
+ INFO(fmt::format("Array '{}' index {}", arrayPath.toString(), i));
+ REQUIRE(dataArrayRef[i] == expected[i]);
+ }
+}
+
+//------------------------------------------------------------------------------
+void CompareStringArrayValues(const DataStructure& dataStructure, const DataPath& arrayPath, const std::vector& expected)
+{
+ REQUIRE_NOTHROW(dataStructure.getDataRefAs(arrayPath));
+ const auto& stringArrayRef = dataStructure.getDataRefAs(arrayPath);
+ REQUIRE(stringArrayRef.getNumberOfTuples() == expected.size());
+ for(usize i = 0; i < expected.size(); i++)
+ {
+ INFO(fmt::format("Array '{}' index {}", arrayPath.toString(), i));
+ REQUIRE(stringArrayRef[i] == expected[i]);
+ }
+}
+
+//------------------------------------------------------------------------------
+// Reads a dataset straight out of a .h5oina with H5Lite, i.e. from the file
+// bytes, without involving H5OINAReader. Used by the real-file Class 2 readback.
+//------------------------------------------------------------------------------
+template
+std::vector ReadRawDataset(const fs::path& filePath, const std::string& datasetPath)
+{
+ // H5Utilities::closeFile() takes its argument by non-const reference and assigns -1 to
+ // it, so this identifier cannot be const.
+ hid_t fileId = H5Utilities::openFile(filePath.string(), true);
+ REQUIRE(fileId >= 0);
+ std::vector data;
+ const herr_t err = H5Lite::readVectorDataset(fileId, datasetPath, data);
+ REQUIRE(err >= 0);
+ REQUIRE(H5Utilities::closeFile(fileId) >= 0);
+ return data;
+}
+} // namespace
+
+//------------------------------------------------------------------------------
+// Class 1 (analytical) + Class 4 (invariant) oracle over Fixture A with the
+// shipped parameter defaults (hexagonal alignment ON, Phase widened to int32).
+// Every expected value below is derived from the fixture spec above.
+//------------------------------------------------------------------------------
+TEST_CASE("OrientationAnalysis::ReadH5OinaDataFilter: Class 1 Analytical Oracle", "[OrientationAnalysis][ReadH5OinaDataFilter]")
{
UnitTest::LoadPlugins();
- const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "H5Oina_Test_Data.tar.gz", "H5Oina_Test_Data");
+ const fs::path inputFile = WriteH5OinaFixture("read_h5oina_vv_oracle.h5oina", {MakeFixtureAScan()});
+
+ ReadH5OinaDataFilter filter;
+ DataStructure dataStructure;
+ const Arguments args = MakeArgs(inputFile, {"1"}, 0.75F);
+
+ auto preflightResult = filter.preflight(dataStructure, args);
+ SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions);
+
+ auto executeResult = filter.execute(dataStructure, args);
+ SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result);
+
+ // --- Geometry: dims and spacing come from the header; the z extent is the
+ // --- number of selected scans, the z spacing is the parameter, the origin is
+ // --- the parameter, and the units are the filter's hard-coded Micrometer. ---
+ REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_ImageGeomPath));
+ const auto& imageGeom = dataStructure.getDataRefAs(k_ImageGeomPath);
+ REQUIRE(imageGeom.getDimensions() == SizeVec3(3, 2, 1));
+ REQUIRE(imageGeom.getSpacing() == FloatVec3(0.25F, 0.5F, 0.75F));
+ REQUIRE(imageGeom.getOrigin() == FloatVec3(0.0F, 0.0F, 0.0F));
+ REQUIRE(imageGeom.getUnits() == IGeometry::LengthUnit::Micrometer);
+
+ // --- Cell arrays: eight verbatim copies plus the aligned Euler array. -------
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::BandContrast), {10, 20, 30, 40, 50, 60});
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::BandSlope), {11, 21, 31, 41, 51, 61});
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Bands), {1, 2, 3, 4, 5, 6});
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Error), {0, 1, 0, 2, 0, 3});
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::MeanAngularDeviation), {0.125F, 0.25F, 0.375F, 0.5F, 0.625F, 0.75F});
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::X), {0.0F, 0.25F, 0.5F, 0.0F, 0.25F, 0.5F});
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Y), {0.0F, 0.0F, 0.0F, 0.5F, 0.5F, 0.5F});
+ // Phase is widened from the file's uint8 to int32 by default, values unchanged
+ // (including the unindexed 0 at point 2).
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Phase), {1, 2, 0, 2, 1, 1});
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Euler), k_FixtureAEulerAligned);
+
+ // --- Ensemble arrays: slot 0 is the reserved Invalid Phase, then one slot per
+ // --- phase group in the file. Laue 9 -> Hexagonal_High (0), Laue 11 ->
+ // --- Cubic_High (1). The file's radian lattice angles are stored as degrees,
+ // --- matching the .ang and .ctf importers: 90, 90, 120 for the hexagonal
+ // --- phase and 90, 90, 90 for the cubic one. -------------------------------
+ CompareArrayValues(dataStructure, k_EnsembleAMPath.createChildPath(ebsdlib::AngFile::CrystalStructures),
+ {ebsdlib::CrystalStructure::UnknownCrystalStructure, ebsdlib::CrystalStructure::Hexagonal_High, ebsdlib::CrystalStructure::Cubic_High});
+ CompareStringArrayValues(dataStructure, k_EnsembleAMPath.createChildPath(ebsdlib::AngFile::MaterialName), {"Invalid Phase", "Hex Phase A", "Cubic Phase B"});
+ CompareArrayValues(dataStructure, k_EnsembleAMPath.createChildPath(ebsdlib::AngFile::LatticeConstants),
+ {
+ 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, // slot 0: Invalid Phase
+ 2.5F, 2.5F, 4.0F, 90.0F, 90.0F, 120.0F, // slot 1: Hex Phase A
+ 3.5F, 3.5F, 3.5F, 90.0F, 90.0F, 90.0F, // slot 2: Cubic Phase B
+ });
+
+ // Class 4 invariant: the ensemble matrix always carries exactly one more tuple
+ // than the file has phase groups.
+ REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_EnsembleAMPath.createChildPath(ebsdlib::AngFile::CrystalStructures)));
+ REQUIRE(dataStructure.getDataRefAs(k_EnsembleAMPath.createChildPath(ebsdlib::AngFile::CrystalStructures)).getNumberOfTuples() == 3);
+
+ UnitTest::CheckArraysInheritTupleDims(dataStructure);
+}
+
+//------------------------------------------------------------------------------
+// The two value-transform options across their full 2 x 2 grid. Class 4
+// invariants: the Phase values never change with either option, and the
+// hexagonal shift never reaches a cubic or unindexed point.
+//------------------------------------------------------------------------------
+TEST_CASE("OrientationAnalysis::ReadH5OinaDataFilter: Conversion Option Combinations", "[OrientationAnalysis][ReadH5OinaDataFilter]")
+{
+ UnitTest::LoadPlugins();
+
+ const fs::path inputFile = WriteH5OinaFixture("read_h5oina_vv_options.h5oina", {MakeFixtureAScan()});
+
+ const auto hexAlignment = GENERATE(true, false);
+ const auto convertPhaseToInt32 = GENERATE(true, false);
- // Read Exemplar DREAM3D File
- auto exemplarFilePath = fs::path(fmt::format("{}/H5Oina_Test_Data/H5Oina_Test_Data.dream3d", unit_test::k_TestFilesDir));
- DataStructure exemplarDataStructure = UnitTest::LoadDataStructure(exemplarFilePath);
+ DYNAMIC_SECTION("hexAlignment=" << hexAlignment << " convertPhaseToInt32=" << convertPhaseToInt32)
+ {
+ ReadH5OinaDataFilter filter;
+ DataStructure dataStructure;
+ const Arguments args = MakeArgs(inputFile, {"1"}, 1.0F, hexAlignment, convertPhaseToInt32);
+
+ auto preflightResult = filter.preflight(dataStructure, args);
+ SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions);
+ auto executeResult = filter.execute(dataStructure, args);
+ SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result);
+
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Euler), hexAlignment ? k_FixtureAEulerAligned : k_FixtureAEulerVerbatim);
+
+ if(convertPhaseToInt32)
+ {
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Phase), {1, 2, 0, 2, 1, 1});
+ }
+ else
+ {
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Phase), {1, 2, 0, 2, 1, 1});
+ }
+
+ // The remaining columns are verbatim regardless of either option.
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Bands), {1, 2, 3, 4, 5, 6});
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::MeanAngularDeviation), {0.125F, 0.25F, 0.375F, 0.5F, 0.625F, 0.75F});
+
+ UnitTest::CheckArraysInheritTupleDims(dataStructure);
+ }
+}
+
+//------------------------------------------------------------------------------
+// Two scans stacked into one geometry. Fixture B is cubic-only, so every cell
+// array -- Euler included -- must be a verbatim copy landing in its own tuple
+// slab: scan "1" in tuples [0, 4) and scan "2" in tuples [4, 8).
+//------------------------------------------------------------------------------
+TEST_CASE("OrientationAnalysis::ReadH5OinaDataFilter: Multi-Scan Slab Placement", "[OrientationAnalysis][ReadH5OinaDataFilter]")
+{
+ UnitTest::LoadPlugins();
+
+ const fs::path inputFile = WriteH5OinaFixture("read_h5oina_vv_multiscan.h5oina", {MakeFixtureBScan1(), MakeFixtureBScan2()});
- // Instantiate the filter, a DataStructure object and an Arguments Object
ReadH5OinaDataFilter filter;
DataStructure dataStructure;
- Arguments args;
+ const Arguments args = MakeArgs(inputFile, {"1", "2"}, 0.5F);
+
+ auto preflightResult = filter.preflight(dataStructure, args);
+ SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions);
+ auto executeResult = filter.execute(dataStructure, args);
+ SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result);
- auto h5TestFile = fs::path(fmt::format("{}/H5Oina_Test_Data/H5Oina_Test_Data.h5oina", unit_test::k_TestFilesDir));
- OEMEbsdScanSelectionParameter::ValueType scanSelections = {h5TestFile, ebsdlib::RefFrameZDir::LowtoHigh, {k_ScanName}};
+ REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_ImageGeomPath));
+ const auto& imageGeom = dataStructure.getDataRefAs(k_ImageGeomPath);
+ REQUIRE(imageGeom.getDimensions() == SizeVec3(2, 2, 2));
+ REQUIRE(imageGeom.getSpacing() == FloatVec3(0.25F, 0.5F, 0.5F));
- // Create default Parameters for the filter.
- args.insertOrAssign(ReadH5OinaDataFilter::k_SelectedScanNames_Key, std::make_any(scanSelections));
- args.insertOrAssign(ReadH5OinaDataFilter::k_ZSpacing_Key, std::make_any(1.0f));
- args.insertOrAssign(ReadH5OinaDataFilter::k_Origin_Key, std::make_any(std::vector(3, 0.0f)));
- args.insertOrAssign(ReadH5OinaDataFilter::k_ReadPatternData_Key, std::make_any(false));
- args.insertOrAssign(ReadH5OinaDataFilter::k_CreatedImageGeometryPath_Key, std::make_any(DataPath({ImageGeom::k_TypeName})));
- args.insertOrAssign(ReadH5OinaDataFilter::k_CellAttributeMatrixName_Key, std::make_any(k_CellData));
- args.insertOrAssign(ReadH5OinaDataFilter::k_CellEnsembleAttributeMatrixName_Key, std::make_any(k_CellEnsembleData));
+ // 24 Euler values: scan "1"'s 12 then scan "2"'s 12, each verbatim.
+ CompareArrayValues(
+ dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Euler),
+ {
+ 0.125F, 0.25F, 0.375F, 0.5F, 0.625F, 0.75F, 0.875F, 1.0F, 1.125F, 1.25F, 1.375F, 1.5F, 2.125F, 2.25F, 2.375F, 2.5F, 2.625F, 2.75F, 2.875F, 3.0F, 3.125F, 3.25F, 3.375F, 3.5F,
+ });
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::BandContrast), {10, 11, 12, 13, 110, 111, 112, 113});
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::BandSlope), {20, 21, 22, 23, 120, 121, 122, 123});
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Bands), {1, 2, 3, 4, 5, 6, 7, 8});
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Error), {0, 1, 2, 3, 4, 5, 6, 7});
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::MeanAngularDeviation), {0.125F, 0.25F, 0.375F, 0.5F, 1.125F, 1.25F, 1.375F, 1.5F});
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Phase), {1, 1, 1, 1, 1, 0, 1, 0});
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::X), {0.0F, 0.25F, 0.0F, 0.25F, 1.0F, 1.25F, 1.0F, 1.25F});
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Y), {0.0F, 0.0F, 0.5F, 0.5F, 1.0F, 1.0F, 1.5F, 1.5F});
+
+ UnitTest::CheckArraysInheritTupleDims(dataStructure);
+}
+
+//------------------------------------------------------------------------------
+// Two hexagonal scans. Every point takes the alignment, so the pinned values
+// establish that the shift reaches scan "2"'s slab and is applied to scan "1"'s
+// slab exactly once.
+//------------------------------------------------------------------------------
+TEST_CASE("OrientationAnalysis::ReadH5OinaDataFilter: Multi-Scan Hexagonal Alignment", "[OrientationAnalysis][ReadH5OinaDataFilter]")
+{
+ UnitTest::LoadPlugins();
+
+ const fs::path inputFile = WriteH5OinaFixture("read_h5oina_vv_multiscan_hex.h5oina", {MakeFixtureCScan1(), MakeFixtureCScan2()});
+
+ ReadH5OinaDataFilter filter;
+ DataStructure dataStructure;
+ const Arguments args = MakeArgs(inputFile, {"1", "2"}, 2.0F);
+
+ auto preflightResult = filter.preflight(dataStructure, args);
+ SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions);
+ auto executeResult = filter.execute(dataStructure, args);
+ SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result);
+
+ REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_ImageGeomPath));
+ REQUIRE(dataStructure.getDataRefAs(k_ImageGeomPath).getDimensions() == SizeVec3(2, 2, 2));
+ REQUIRE(dataStructure.getDataRefAs(k_ImageGeomPath).getSpacing() == FloatVec3(0.5F, 0.25F, 2.0F));
+
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Euler), k_FixtureCEulerAligned);
+
+ UnitTest::CheckArraysInheritTupleDims(dataStructure);
+}
+
+//------------------------------------------------------------------------------
+// The pinned reader ignores the Format Version entirely -- its four version
+// branches are empty and the dataset itself is optional. All three variants must
+// produce the same output.
+//------------------------------------------------------------------------------
+TEST_CASE("OrientationAnalysis::ReadH5OinaDataFilter: Format Version Variants", "[OrientationAnalysis][ReadH5OinaDataFilter]")
+{
+ UnitTest::LoadPlugins();
+
+ const auto variant = GENERATE(as{}, "5.0", "2.0", "absent");
+
+ DYNAMIC_SECTION("Format Version " << variant)
+ {
+ const fs::path inputFile = WriteH5OinaFixture(fmt::format("read_h5oina_vv_version_{}.h5oina", variant), {MakeFixtureAScan()}, variant != "absent", variant);
+
+ ReadH5OinaDataFilter filter;
+ DataStructure dataStructure;
+ const Arguments args = MakeArgs(inputFile, {"1"});
+
+ auto preflightResult = filter.preflight(dataStructure, args);
+ SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions);
+ auto executeResult = filter.execute(dataStructure, args);
+ SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result);
+
+ REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_ImageGeomPath));
+ REQUIRE(dataStructure.getDataRefAs(k_ImageGeomPath).getDimensions() == SizeVec3(3, 2, 1));
+ CompareArrayValues(dataStructure, k_CellAMPath.createChildPath(ebsdlib::H5OINA::Euler), k_FixtureAEulerAligned);
+
+ UnitTest::CheckArraysInheritTupleDims(dataStructure);
+ }
+}
+
+//------------------------------------------------------------------------------
+// Value-add preflight rejections that need no file at all.
+//------------------------------------------------------------------------------
+TEST_CASE("OrientationAnalysis::ReadH5OinaDataFilter: Parameter Rejections", "[OrientationAnalysis][ReadH5OinaDataFilter]")
+{
+ UnitTest::LoadPlugins();
+
+ const fs::path inputFile = WriteH5OinaFixture("read_h5oina_vv_params.h5oina", {MakeFixtureAScan()});
+
+ ReadH5OinaDataFilter filter;
+ DataStructure dataStructure;
+ Arguments args = MakeArgs(inputFile, {"1"});
+ int32 expectedCode = 0;
+
+ SECTION("Non-positive Z Spacing (-9580)")
+ {
+ args = MakeArgs(inputFile, {"1"}, 0.0F);
+ expectedCode = -9580;
+ }
+ SECTION("No Scan Names Selected (-9581)")
+ {
+ args = MakeArgs(inputFile, {});
+ expectedCode = -9581;
+ }
+ SECTION("Pattern Import Not Supported (-9583)")
+ {
+ args = MakeArgs(inputFile, {"1"}, 1.0F, true, true, true);
+ expectedCode = -9583;
+ }
+ SECTION("Non-finite Z Spacing (-9580)")
+ {
+ args = MakeArgs(inputFile, {"1"}, std::numeric_limits::quiet_NaN());
+ expectedCode = -9580;
+ }
- // Preflight the filter and check result
auto preflightResult = filter.preflight(dataStructure, args);
- SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions)
+ SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions);
+ REQUIRE(preflightResult.outputActions.errors()[0].code == expectedCode);
- // Execute the filter and check the result
auto executeResult = filter.execute(dataStructure, args);
- SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result)
+ SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result);
- const auto& imageGeom = dataStructure.getDataRefAs(DataPath({ImageGeom::k_TypeName}));
- const auto& exemplarImageGeom = exemplarDataStructure.getDataRefAs(DataPath({k_ExemplarDataContainer}));
- REQUIRE(imageGeom.getDimensions() == exemplarImageGeom.getDimensions());
- REQUIRE(imageGeom.getSpacing() == exemplarImageGeom.getSpacing());
- REQUIRE(imageGeom.getOrigin() == exemplarImageGeom.getOrigin());
- REQUIRE(imageGeom.getUnits() == IGeometry::LengthUnit::Micrometer);
+ UnitTest::CheckArraysInheritTupleDims(dataStructure);
+}
+
+//------------------------------------------------------------------------------
+// Value-add guard: a header whose cell counts are not usable is rejected before
+// a zero-sized (or absurdly-sized) geometry is created.
+//------------------------------------------------------------------------------
+TEST_CASE("OrientationAnalysis::ReadH5OinaDataFilter: Invalid Cell Counts rejected (-9584)", "[OrientationAnalysis][ReadH5OinaDataFilter]")
+{
+ UnitTest::LoadPlugins();
+
+ ScanSpec scan = MakeFixtureAScan();
+ std::string fileName;
+
+ SECTION("Zero X Cells")
+ {
+ scan.xCells = 0;
+ fileName = "read_h5oina_vv_zero_xcells.h5oina";
+ }
+ SECTION("Zero Y Cells")
+ {
+ scan.yCells = 0;
+ fileName = "read_h5oina_vv_zero_ycells.h5oina";
+ }
+ SECTION("Negative X Cells")
+ {
+ scan.xCells = -3;
+ fileName = "read_h5oina_vv_negative_xcells.h5oina";
+ }
+
+ const fs::path inputFile = WriteH5OinaFixture(fileName, {scan});
- UnitTest::CompareExemplarToGeneratedData(dataStructure, exemplarDataStructure, DataPath({ImageGeom::k_TypeName, k_CellData}), k_ExemplarDataContainer);
+ ReadH5OinaDataFilter filter;
+ DataStructure dataStructure;
+ const Arguments args = MakeArgs(inputFile, {"1"});
- const DataPath cellEnsemblePath({ImageGeom::k_TypeName, k_CellEnsembleData});
- const DataPath exemplarCellEnsemblePath({k_ExemplarDataContainer, k_CellEnsembleData});
- const auto& crystalStructures = dataStructure.getDataRefAs(cellEnsemblePath.createChildPath(ebsdlib::AngFile::CrystalStructures));
- const auto& crystalStructuresExemplar = exemplarDataStructure.getDataRefAs(exemplarCellEnsemblePath.createChildPath(ebsdlib::AngFile::CrystalStructures));
- UnitTest::CompareDataArrays(crystalStructures, crystalStructuresExemplar);
- const auto& latticeConstants = dataStructure.getDataRefAs(cellEnsemblePath.createChildPath(ebsdlib::AngFile::LatticeConstants));
- const auto& latticeConstantsExemplar = exemplarDataStructure.getDataRefAs(exemplarCellEnsemblePath.createChildPath(ebsdlib::AngFile::LatticeConstants));
- UnitTest::CompareDataArrays(latticeConstants, latticeConstantsExemplar);
- const auto& materialName = dataStructure.getDataRefAs(cellEnsemblePath.createChildPath(ebsdlib::AngFile::MaterialName));
- const auto& materialNameExemplar = exemplarDataStructure.getDataRefAs(exemplarCellEnsemblePath.createChildPath(ebsdlib::AngFile::MaterialName));
- UnitTest::CompareStringArrays(materialNameExemplar, materialName);
+ auto preflightResult = filter.preflight(dataStructure, args);
+ SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions);
+ REQUIRE(preflightResult.outputActions.errors()[0].code == -9584);
UnitTest::CheckArraysInheritTupleDims(dataStructure);
}
-TEST_CASE("OrientationAnalysis::ReadH5OinaDataFilter: InValid Filter Execution", "[OrientationAnalysis][ReadH5OinaDataFilter]")
+//------------------------------------------------------------------------------
+// Value-add guard: the geometry is built from the FIRST selected scan's header,
+// so every other selected scan has to agree with it. A second scan with a
+// different grid is rejected at preflight rather than driving an out-of-bounds
+// read of the reader's buffers at execute.
+//------------------------------------------------------------------------------
+TEST_CASE("OrientationAnalysis::ReadH5OinaDataFilter: Scan Header Mismatch rejected (-9585)", "[OrientationAnalysis][ReadH5OinaDataFilter]")
{
UnitTest::LoadPlugins();
- const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_6_ImportH5Data.tar.gz", "6_6_ImportH5Data");
+ ScanSpec scan2 = MakeFixtureBScan2();
+
+ SECTION("Differing cell counts")
+ {
+ scan2.xCells = 3;
+ scan2.yCells = 3;
+ scan2.phase.assign(9, 1);
+ scan2.bandContrast.assign(9, 7);
+ scan2.bandSlope.assign(9, 7);
+ scan2.bands.assign(9, 7);
+ scan2.error.assign(9, 0);
+ scan2.euler.assign(27, 0.5F);
+ scan2.mad.assign(9, 0.25F);
+ scan2.x.assign(9, 0.0F);
+ scan2.y.assign(9, 0.0F);
+ }
+ SECTION("Differing step size")
+ {
+ scan2.xStep = 0.75F;
+ }
+
+ const fs::path inputFile = WriteH5OinaFixture("read_h5oina_vv_scan_mismatch.h5oina", {MakeFixtureBScan1(), scan2});
- // Instantiate the filter, a DataStructure object and an Arguments Object
ReadH5OinaDataFilter filter;
DataStructure dataStructure;
- Arguments args;
- args.insertOrAssign(ReadH5OinaDataFilter::k_Origin_Key, std::make_any(std::vector(3, 0.0f)));
- args.insertOrAssign(ReadH5OinaDataFilter::k_CreatedImageGeometryPath_Key, std::make_any(DataPath({ImageGeom::k_TypeName})));
- args.insertOrAssign(ReadH5OinaDataFilter::k_CellAttributeMatrixName_Key, std::make_any(k_CellData));
- args.insertOrAssign(ReadH5OinaDataFilter::k_CellEnsembleAttributeMatrixName_Key, std::make_any(k_CellEnsembleData));
+ const Arguments args = MakeArgs(inputFile, {"1", "2"});
+
+ auto preflightResult = filter.preflight(dataStructure, args);
+ SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions);
+ REQUIRE(preflightResult.outputActions.errors()[0].code == -9585);
+
+ UnitTest::CheckArraysInheritTupleDims(dataStructure);
+}
+
+//------------------------------------------------------------------------------
+// Value-add guard: every selected scan name is checked against the file at
+// preflight, not just the first one. Without this a bad second name fails
+// part-way through execute with half the arrays already populated.
+//------------------------------------------------------------------------------
+TEST_CASE("OrientationAnalysis::ReadH5OinaDataFilter: Missing Scan Name rejected (-9586)", "[OrientationAnalysis][ReadH5OinaDataFilter]")
+{
+ UnitTest::LoadPlugins();
+
+ const fs::path inputFile = WriteH5OinaFixture("read_h5oina_vv_missing_scan.h5oina", {MakeFixtureBScan1(), MakeFixtureBScan2()});
+
+ std::list scanNames;
+ SECTION("Missing first scan")
+ {
+ scanNames = {"9"};
+ }
+ SECTION("Missing second scan")
+ {
+ scanNames = {"1", "9"};
+ }
- auto h5TestFile = fs::path(fmt::format("{}/H5Oina_Test_Data/FirstLook AB Site 1 Map Data 4.h5oina", unit_test::k_TestFilesDir));
- OEMEbsdScanSelectionParameter::ValueType scanSelections = {h5TestFile, ebsdlib::RefFrameZDir::LowtoHigh, {k_ScanName}};
+ ReadH5OinaDataFilter filter;
+ DataStructure dataStructure;
+ const Arguments args = MakeArgs(inputFile, scanNames);
+
+ auto preflightResult = filter.preflight(dataStructure, args);
+ SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions);
+ REQUIRE(preflightResult.outputActions.errors()[0].code == -9586);
+
+ UnitTest::CheckArraysInheritTupleDims(dataStructure);
+}
- SECTION("Invalid Z Spacing")
+//------------------------------------------------------------------------------
+// Value-add guard: phase indices come from the HDF5 phase GROUP NAMES, while the
+// ensemble matrix is sized from the phase COUNT. A file whose groups are not
+// numbered 1..N is rejected instead of writing past the end of the ensemble
+// arrays.
+//
+// The ensemble fill in the shared IEbsdOemReader::readData runs once per SELECTED
+// scan, so the check has to run for every selected scan and not only the first.
+// The "Later selected scan" section is the same defect shape moved into scan "2":
+// with the check applied to the first scan only, that file passes preflight and
+// crashes the process at execute.
+//------------------------------------------------------------------------------
+TEST_CASE("OrientationAnalysis::ReadH5OinaDataFilter: Phase Index Out Of Range rejected (-9587)", "[OrientationAnalysis][ReadH5OinaDataFilter]")
+{
+ UnitTest::LoadPlugins();
+
+ std::vector