Skip to content

Commit a0043a8

Browse files
committed
PERF: Optimize Group C morphological filters for out-of-core
Optimizes all 5 Group C (Multi-Iteration Morphological / Neighbor Replacement) filters for out-of-core performance using Z-slice rolling buffers, bulk I/O via copyIntoBuffer/copyFromBuffer, and Z-slice buffered tuple transfer. Algorithm changes: * Z-slice rolling buffer for neighbor lookups in all 5 filters — all 6 face-neighbor reads come from RAM buffers instead of OOC stores * SliceBufferedTransfer utility for type-dispatched Z-slice buffered tuple copy — eliminates per-element copyTuple OOC overhead * Bulk I/O for all slice reads/writes using copyIntoBuffer/copyFromBuffer * Cache CrystalStructures ensemble array locally in NeighborOrientationCorrelation to avoid per-element virtual dispatch * Sequential per-array transfer ordering to prevent OOC cache thrashing * Deferred transfer in ErodeDilateCoordinationNumber for bulk I/O Bug fixes: * Honor XDirOn/YDirOn/ZDirOn direction flags in ErodeDilateBadData and ErodeDilateMask — parameters were declared but never applied Test changes: * Parameterized BuildTestData builders for small (20^3) and large (200^3) * All test helpers use Z-slice batched bulk reads for OOC efficiency * Tier 2 tests use programmatic input with ForceOocAlgorithmGuard * Tier 1 tests use single most-representative parameter combo * PreferencesSentinel updated from "Zarr" to "HDF5-OOC" * Retired legacy 6_6_ test archives in favor of programmatic data Signed-off-by: Joey Kleingers <joey.kleingers@bluequartz.net>
1 parent 2f82154 commit a0043a8

18 files changed

Lines changed: 1874 additions & 614 deletions

src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/NeighborOrientationCorrelation.cpp

Lines changed: 159 additions & 138 deletions
Large diffs are not rendered by default.

src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/NeighborOrientationCorrelation.hpp

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,76 @@
1010
namespace nx::core
1111
{
1212

13+
/**
14+
* @struct NeighborOrientationCorrelationInputValues
15+
* @brief Holds all user-supplied parameters for the NeighborOrientationCorrelation algorithm.
16+
*/
1317
struct ORIENTATIONANALYSIS_EXPORT NeighborOrientationCorrelationInputValues
1418
{
15-
DataPath ImageGeomPath;
16-
float32 MinConfidence;
17-
float32 MisorientationTolerance;
18-
int32 Level;
19-
DataPath ConfidenceIndexArrayPath;
20-
DataPath CellPhasesArrayPath;
21-
DataPath QuatsArrayPath;
22-
DataPath CrystalStructuresArrayPath;
23-
MultiArraySelectionParameter::ValueType IgnoredDataArrayPaths;
19+
DataPath ImageGeomPath; ///< Path to the ImageGeom that defines the voxel grid dimensions
20+
float32 MinConfidence; ///< Cells with confidence index below this value are candidates for replacement
21+
float32 MisorientationTolerance; ///< Angular tolerance (degrees) for comparing neighbor orientations
22+
int32 Level; ///< Minimum neighbor agreement count required to replace a cell (cleanup level)
23+
DataPath ConfidenceIndexArrayPath; ///< Path to the float32 confidence index array
24+
DataPath CellPhasesArrayPath; ///< Path to the int32 cell phases array
25+
DataPath QuatsArrayPath; ///< Path to the float32 quaternion array (4 components per tuple)
26+
DataPath CrystalStructuresArrayPath; ///< Path to the uint32 crystal structures ensemble array
27+
MultiArraySelectionParameter::ValueType IgnoredDataArrayPaths; ///< Data arrays excluded from the neighbor-copy transfer step
2428
};
2529

2630
/**
27-
* @class
31+
* @class NeighborOrientationCorrelation
32+
* @brief Corrects low-confidence EBSD voxels by replacing their cell data with
33+
* data from the most orientation-correlated face neighbor.
34+
*
35+
* The algorithm iterates through multiple "cleanup levels" (from 6 down to the
36+
* user-specified Level). At each level, every voxel whose confidence index is
37+
* below MinConfidence is examined. For that voxel, the 6 face neighbors are
38+
* compared pairwise: two neighbors "agree" if they share the same nonzero phase
39+
* and their misorientation is within MisorientationTolerance. Each neighbor
40+
* accumulates a similarity count (how many other neighbors agree with it). The
41+
* neighbor with the highest agreement is chosen as the replacement source.
42+
*
43+
* ## Z-Slice Buffering (Out-of-Core Optimization)
44+
*
45+
* To avoid random-access thrashing of out-of-core (OOC) compressed chunk stores,
46+
* the algorithm maintains a rolling window of 3 adjacent Z-slices for the
47+
* quaternion and phase arrays, plus 1 Z-slice for the confidence index. At each
48+
* Z-step, the window advances by swapping buffer slots and reading only the new
49+
* z+1 slice. All neighbor lookups then read from these local buffers instead of
50+
* the backing DataArray, eliminating repeated chunk decompressions.
51+
*
52+
* After identifying the best neighbor for every low-confidence voxel in a level,
53+
* all cell-level DataArrays (except ignored ones) are updated in parallel using
54+
* ParallelTaskAlgorithm, copying tuple data from each best neighbor.
2855
*/
2956
class ORIENTATIONANALYSIS_EXPORT NeighborOrientationCorrelation
3057
{
3158
public:
59+
/**
60+
* @brief Constructs the algorithm with all required references and parameters.
61+
* @param dataStructure The DataStructure containing all input/output arrays
62+
* @param mesgHandler Handler for sending progress messages to the UI
63+
* @param shouldCancel Atomic flag checked between iterations to support cancellation
64+
* @param inputValues User-supplied parameters controlling the algorithm behavior
65+
*/
3266
NeighborOrientationCorrelation(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel,
3367
NeighborOrientationCorrelationInputValues* inputValues);
68+
69+
/**
70+
* @brief Default destructor.
71+
*/
3472
~NeighborOrientationCorrelation() noexcept;
3573

3674
NeighborOrientationCorrelation(const NeighborOrientationCorrelation&) = delete;
3775
NeighborOrientationCorrelation(NeighborOrientationCorrelation&&) noexcept = delete;
3876
NeighborOrientationCorrelation& operator=(const NeighborOrientationCorrelation&) = delete;
3977
NeighborOrientationCorrelation& operator=(NeighborOrientationCorrelation&&) noexcept = delete;
4078

79+
/**
80+
* @brief Executes the neighbor orientation correlation algorithm.
81+
* @return Result<> indicating success or any errors encountered during execution
82+
*/
4183
Result<> operator()();
4284

4385
private:

src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/NeighborOrientationCorrelationFilter.hpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,21 @@ namespace nx::core
99
{
1010
/**
1111
* @class NeighborOrientationCorrelationFilter
12-
* @brief This filter will ....
12+
* @brief Cleans up EBSD data by replacing low-confidence voxels with data from
13+
* the most orientation-correlated face neighbor.
14+
*
15+
* This filter identifies voxels whose confidence index falls below a
16+
* user-specified threshold, then examines the 6 face neighbors of each such
17+
* voxel. Neighbor pairs are compared using crystallographic misorientation;
18+
* the neighbor that agrees most with the other neighbors (within a given
19+
* angular tolerance) is selected as the replacement source. All cell-level
20+
* DataArrays are updated to reflect the replacement. The process repeats
21+
* across multiple cleanup levels, progressively relaxing the required neighbor
22+
* agreement count.
23+
*
24+
* The underlying algorithm uses Z-slice buffering to maintain efficient
25+
* sequential access patterns, which is critical for out-of-core (OOC) data
26+
* where arrays are stored as compressed Zarr chunks on disk.
1327
*/
1428
class ORIENTATIONANALYSIS_EXPORT NeighborOrientationCorrelationFilter : public IFilter
1529
{

src/Plugins/OrientationAnalysis/test/NeighborOrientationCorrelationTest.cpp

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@
33
#include "OrientationAnalysisTestUtils.hpp"
44

55
#include "simplnx/Core/Application.hpp"
6+
#include "simplnx/DataStructure/AttributeMatrix.hpp"
7+
#include "simplnx/DataStructure/Geometry/ImageGeom.hpp"
68
#include "simplnx/Parameters/ArraySelectionParameter.hpp"
79
#include "simplnx/Parameters/BoolParameter.hpp"
810
#include "simplnx/Parameters/Dream3dImportParameter.hpp"
911
#include "simplnx/Parameters/GeometrySelectionParameter.hpp"
12+
#include "simplnx/Parameters/MultiArraySelectionParameter.hpp"
1013
#include "simplnx/UnitTest/UnitTestCommon.hpp"
14+
#include "simplnx/Utilities/AlgorithmDispatch.hpp"
15+
#include "simplnx/Utilities/DataStoreUtilities.hpp"
1116

1217
#include <catch2/catch.hpp>
1318

19+
#include <cmath>
1420
#include <filesystem>
1521

1622
namespace fs = std::filesystem;
@@ -38,6 +44,8 @@ using namespace nx::core::UnitTest;
3844
TEST_CASE("OrientationAnalysis::NeighborOrientationCorrelationFilter: Small IN100 Pipeline", "[OrientationAnalysis][NeighborOrientationCorrelationFilter]")
3945
{
4046
UnitTest::LoadPlugins();
47+
// 1 Z-slice of quats (largest array): 189*201*4*4 = 607824 bytes
48+
const UnitTest::PreferencesSentinel prefsSentinel("HDF5-OOC", 600000, true);
4149

4250
const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "neighbor_orientation_correlation.tar.gz", "neighbor_orientation_correlation.dream3d");
4351

@@ -194,3 +202,165 @@ TEST_CASE("OrientationAnalysis::NeighborOrientationCorrelationFilter: Small IN10
194202

195203
UnitTest::CheckArraysInheritTupleDims(dataStructure, SmallIn100::k_TupleCheckIgnoredPaths);
196204
}
205+
206+
namespace
207+
{
208+
const std::string k_GeomName("Image Geometry");
209+
const std::string k_CellDataName("Cell Data");
210+
211+
const DataPath k_GeomPath({k_GeomName});
212+
const DataPath k_CellDataPath = k_GeomPath.createChildPath(k_CellDataName);
213+
const DataPath k_CIPath = k_CellDataPath.createChildPath("Confidence Index");
214+
const DataPath k_QuatsPath = k_CellDataPath.createChildPath("Quats");
215+
const DataPath k_PhasesPath = k_CellDataPath.createChildPath("Phases");
216+
const DataPath k_CrystalStructuresPath = k_GeomPath.createChildPath("Ensemble Data").createChildPath("CrystalStructures");
217+
218+
void BuildTestData(DataStructure& dataStructure, usize dimX, usize dimY, usize dimZ, usize blockSize)
219+
{
220+
const ShapeType cellTupleShape = {dimZ, dimY, dimX};
221+
const usize sliceSize = dimX * dimY;
222+
223+
auto* imageGeom = ImageGeom::Create(dataStructure, k_GeomName);
224+
imageGeom->setDimensions({dimX, dimY, dimZ});
225+
imageGeom->setSpacing({1.0f, 1.0f, 1.0f});
226+
imageGeom->setOrigin({0.0f, 0.0f, 0.0f});
227+
228+
auto* cellAM = AttributeMatrix::Create(dataStructure, k_CellDataName, cellTupleShape, imageGeom->getId());
229+
imageGeom->setCellData(*cellAM);
230+
231+
auto quatsDataStore = DataStoreUtilities::CreateDataStore<float32>(cellTupleShape, {4}, IDataAction::Mode::Execute);
232+
auto* quatsArray = DataArray<float32>::Create(dataStructure, "Quats", quatsDataStore, cellAM->getId());
233+
auto& quatsStore = quatsArray->getDataStoreRef();
234+
235+
auto phasesDataStore = DataStoreUtilities::CreateDataStore<int32>(cellTupleShape, {1}, IDataAction::Mode::Execute);
236+
auto* phasesArray = DataArray<int32>::Create(dataStructure, "Phases", phasesDataStore, cellAM->getId());
237+
auto& phasesStore = phasesArray->getDataStoreRef();
238+
239+
auto ciDataStore = DataStoreUtilities::CreateDataStore<float32>(cellTupleShape, {1}, IDataAction::Mode::Execute);
240+
auto* ciArray = DataArray<float32>::Create(dataStructure, "Confidence Index", ciDataStore, cellAM->getId());
241+
auto& ciStore = ciArray->getDataStoreRef();
242+
243+
const usize blocksPerDimX = dimX / blockSize;
244+
const usize blocksPerDimY = dimY / blockSize;
245+
246+
std::vector<float32> quatsBuf(sliceSize * 4);
247+
std::vector<int32> phasesBuf(sliceSize);
248+
std::vector<float32> ciBuf(sliceSize);
249+
250+
for(usize z = 0; z < dimZ; z++)
251+
{
252+
for(usize y = 0; y < dimY; y++)
253+
{
254+
for(usize x = 0; x < dimX; x++)
255+
{
256+
const usize inSlice = y * dimX + x;
257+
phasesBuf[inSlice] = 1;
258+
259+
usize bx = x / blockSize;
260+
usize by = y / blockSize;
261+
usize bz = z / blockSize;
262+
float32 angle = static_cast<float32>(bz * blocksPerDimY * blocksPerDimX + by * blocksPerDimX + bx) * 0.1f;
263+
float32 sinHalf = std::sin(angle * 0.5f);
264+
float32 cosHalf = std::cos(angle * 0.5f);
265+
266+
const usize qIdx = inSlice * 4;
267+
quatsBuf[qIdx] = cosHalf;
268+
quatsBuf[qIdx + 1] = sinHalf * 0.577350269f; // 1/sqrt(3)
269+
quatsBuf[qIdx + 2] = sinHalf * 0.577350269f;
270+
quatsBuf[qIdx + 3] = sinHalf * 0.577350269f;
271+
272+
bool isBoundary = (x % blockSize == 0) || (y % blockSize == 0) || (z % blockSize == 0);
273+
bool isNoisy = ((x * 7 + y * 13 + z * 29) % 10 == 0);
274+
ciBuf[inSlice] = (isBoundary || isNoisy) ? 0.05f : 0.9f;
275+
}
276+
}
277+
const usize zOffset = z * sliceSize;
278+
quatsStore.copyFromBuffer(zOffset * 4, nonstd::span<const float32>(quatsBuf.data(), sliceSize * 4));
279+
phasesStore.copyFromBuffer(zOffset, nonstd::span<const int32>(phasesBuf.data(), sliceSize));
280+
ciStore.copyFromBuffer(zOffset, nonstd::span<const float32>(ciBuf.data(), sliceSize));
281+
}
282+
283+
// Ensemble data — small enough for per-element writes
284+
auto* ensembleAM = AttributeMatrix::Create(dataStructure, "Ensemble Data", {2}, imageGeom->getId());
285+
auto crystalStructuresDataStore = DataStoreUtilities::CreateDataStore<uint32>({2}, {1}, IDataAction::Mode::Execute);
286+
auto* crystalStructuresArray = DataArray<uint32>::Create(dataStructure, "CrystalStructures", crystalStructuresDataStore, ensembleAM->getId());
287+
std::array<uint32, 2> csData = {999, 1}; // Unknown, Cubic-High (m-3m)
288+
crystalStructuresArray->getDataStoreRef().copyFromBuffer(0, nonstd::span<const uint32>(csData.data(), 2));
289+
}
290+
} // namespace
291+
292+
TEST_CASE("OrientationAnalysis::NeighborOrientationCorrelationFilter: Generate Test Data", "[OrientationAnalysis][NeighborOrientationCorrelationFilter][.GenerateTestData]")
293+
{
294+
const auto outputDir = fs::path(unit_test::k_BinaryTestOutputDir.view()) / "generated_test_data" / "neighbor_orientation_correlation";
295+
fs::create_directories(outputDir);
296+
297+
// Large input data (200x200x200, blockSize=25)
298+
{
299+
DataStructure buildDS;
300+
BuildTestData(buildDS, 200, 200, 200, 25);
301+
UnitTest::WriteTestDataStructure(buildDS, outputDir / "large_input.dream3d");
302+
fmt::print("Generated large input: {}\n", (outputDir / "large_input.dream3d").string());
303+
}
304+
}
305+
306+
TEST_CASE("OrientationAnalysis::NeighborOrientationCorrelationFilter: 200x200x200 Large OOC", "[OrientationAnalysis][NeighborOrientationCorrelationFilter]")
307+
{
308+
UnitTest::LoadPlugins();
309+
bool forceOocAlgo = GENERATE(false, true);
310+
const nx::core::ForceOocAlgorithmGuard guard(forceOocAlgo);
311+
// 200x200x200, Quats (float32, 4-comp) => 200*200*4*4 = 640,000 bytes/slice
312+
const UnitTest::PreferencesSentinel prefsSentinel("HDF5-OOC", 640000, true);
313+
314+
DYNAMIC_SECTION("forceOoc: " << forceOocAlgo)
315+
{
316+
constexpr usize k_Dim = 200;
317+
constexpr usize k_Block = 25;
318+
319+
DataStructure dataStructure;
320+
BuildTestData(dataStructure, k_Dim, k_Dim, k_Dim, k_Block);
321+
322+
const NeighborOrientationCorrelationFilter filter;
323+
Arguments args;
324+
args.insertOrAssign(NeighborOrientationCorrelationFilter::k_ImageGeometryPath_Key, std::make_any<DataPath>(k_GeomPath));
325+
args.insertOrAssign(NeighborOrientationCorrelationFilter::k_MinConfidence_Key, std::make_any<float32>(0.2f));
326+
args.insertOrAssign(NeighborOrientationCorrelationFilter::k_MisorientationTolerance_Key, std::make_any<float32>(5.0f));
327+
args.insertOrAssign(NeighborOrientationCorrelationFilter::k_Level_Key, std::make_any<int32>(2));
328+
args.insertOrAssign(NeighborOrientationCorrelationFilter::k_CorrelationArrayPath_Key, std::make_any<DataPath>(k_CIPath));
329+
args.insertOrAssign(NeighborOrientationCorrelationFilter::k_CellPhasesArrayPath_Key, std::make_any<DataPath>(k_PhasesPath));
330+
args.insertOrAssign(NeighborOrientationCorrelationFilter::k_QuatsArrayPath_Key, std::make_any<DataPath>(k_QuatsPath));
331+
args.insertOrAssign(NeighborOrientationCorrelationFilter::k_CrystalStructuresArrayPath_Key, std::make_any<DataPath>(k_CrystalStructuresPath));
332+
args.insertOrAssign(NeighborOrientationCorrelationFilter::k_IgnoredDataArrayPaths_Key, std::make_any<MultiArraySelectionParameter::ValueType>(MultiArraySelectionParameter::ValueType{}));
333+
334+
auto preflightResult = filter.preflight(dataStructure, args);
335+
SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions);
336+
337+
auto executeResult = filter.execute(dataStructure, args);
338+
SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result);
339+
340+
// Some low-CI voxels should have been modified — use Z-slice batched reads for OOC efficiency
341+
const auto& ciAfter = dataStructure.getDataRefAs<Float32Array>(k_CIPath).getDataStoreRef();
342+
const usize sliceSize = k_Dim * k_Dim;
343+
std::vector<float32> ciBuf(sliceSize);
344+
usize modifiedCount = 0;
345+
for(usize z = 0; z < k_Dim; z++)
346+
{
347+
ciAfter.copyIntoBuffer(z * sliceSize, nonstd::span<float32>(ciBuf.data(), sliceSize));
348+
for(usize y = 0; y < k_Dim; y++)
349+
{
350+
for(usize x = 0; x < k_Dim; x++)
351+
{
352+
const usize inSlice = y * k_Dim + x;
353+
bool wasBoundary = (x % k_Block == 0) || (y % k_Block == 0) || (z % k_Block == 0);
354+
bool wasNoisy = ((x * 7 + y * 13 + z * 29) % 10 == 0);
355+
if((wasBoundary || wasNoisy) && ciBuf[inSlice] != 0.05f)
356+
{
357+
modifiedCount++;
358+
}
359+
}
360+
}
361+
}
362+
REQUIRE(modifiedCount > 0);
363+
364+
UnitTest::CheckArraysInheritTupleDims(dataStructure);
365+
}
366+
}

0 commit comments

Comments
 (0)