From 5ae81cb5fb0d25923bb8a73b96ff89a8a79f5f4d Mon Sep 17 00:00:00 2001 From: nyoungbq Date: Thu, 2 Jul 2026 11:25:18 -0400 Subject: [PATCH 1/6] Simple cleanup --- .../Filters/Algorithms/FillBadData.cpp | 24 ++++++++++--------- .../SimplnxCore/Filters/FillBadDataFilter.cpp | 10 ++++---- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData.cpp index 33662947bd..82d6c889a6 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData.cpp @@ -64,12 +64,6 @@ void FillBadDataUpdateTuples(const Int32AbstractDataStore& featureIds, AbstractD const int32 featureName = featureIds[tupleIndex]; const int32 neighbor = neighbors[tupleIndex]; - // Skip if no neighbor assignment - if(neighbor == tupleIndex) - { - continue; - } - // Copy data from the valid neighbor to bad data voxel // Only copy if the current voxel is bad data (-1) and the neighbor is valid (>0) if(featureName < 0 && neighbor != -1 && featureIds[static_cast(neighbor)] > 0) @@ -339,13 +333,17 @@ void FillBadData::phaseOneCCL(Int32AbstractDataStore& featureIdsStore, ChunkAwar // Check already-processed neighbors (scanline order: -Z, -Y, -X) // We only check "backward" neighbors because "forward" neighbors // haven't been processed yet in the scanline order + // + // Note the following checks do not need to validate the + // featureIdsStore is zero because that is a prerequisite to + // insertion of an index value into provisionalLabels std::vector neighborLabels; // Check -X neighbor if(x > 0) { const usize neighborIdx = index - 1; - if(provisionalLabels.contains(neighborIdx) && featureIdsStore[neighborIdx] == 0) + if(provisionalLabels.contains(neighborIdx)) { neighborLabels.push_back(provisionalLabels[neighborIdx]); } @@ -355,7 +353,7 @@ void FillBadData::phaseOneCCL(Int32AbstractDataStore& featureIdsStore, ChunkAwar if(y > 0) { const usize neighborIdx = index - dims[0]; - if(provisionalLabels.contains(neighborIdx) && featureIdsStore[neighborIdx] == 0) + if(provisionalLabels.contains(neighborIdx)) { neighborLabels.push_back(provisionalLabels[neighborIdx]); } @@ -365,7 +363,7 @@ void FillBadData::phaseOneCCL(Int32AbstractDataStore& featureIdsStore, ChunkAwar if(z > 0) { const usize neighborIdx = index - dims[0] * dims[1]; - if(provisionalLabels.contains(neighborIdx) && featureIdsStore[neighborIdx] == 0) + if(provisionalLabels.contains(neighborIdx)) { neighborLabels.push_back(provisionalLabels[neighborIdx]); } @@ -473,7 +471,7 @@ void FillBadData::phaseThreeRelabeling(Int32AbstractDataStore& featureIdsStore, std::unordered_set localSmallRegions; for(const auto& [root, size] : rootSizes) { - if(static_cast(size) < m_InputValues->minAllowedDefectSizeValue) + if(size < static_cast(m_InputValues->minAllowedDefectSizeValue)) { localSmallRegions.insert(root); } @@ -583,6 +581,11 @@ void FillBadData::phaseFourIterativeFill(Int32AbstractDataStore& featureIdsStore // Iteratively fill until no voxels with -1 value remain while(count != 0) { + if(m_ShouldCancel) + { + return; + } + iteration++; count = 0; // Reset count of voxels with a -1 value for this iteration @@ -636,7 +639,6 @@ void FillBadData::phaseFourIterativeFill(Int32AbstractDataStore& featureIdsStore // Reset vote counters for next voxel // Only reset features that were actually counted to save time // Loop over the 6 face neighbors of the voxel - isValidFaceNeighbor = computeValidFaceNeighbors(xIdx, yIdx, zIdx, dims); for(const auto& faceIndex : faceNeighborInternalIdx) { if(!isValidFaceNeighbor[faceIndex]) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/FillBadDataFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/FillBadDataFilter.cpp index fb5f642883..723da54c4c 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/FillBadDataFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/FillBadDataFilter.cpp @@ -2,7 +2,6 @@ #include "SimplnxCore/Filters/Algorithms/FillBadData.hpp" -#include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataPath.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" #include "simplnx/Parameters/AttributeMatrixSelectionParameter.hpp" @@ -68,8 +67,7 @@ Parameters FillBadDataFilter::parameters() const ArraySelectionParameter::AllowedTypes{DataType::int32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); params.insert(std::make_unique(k_IgnoredDataArrayPaths_Key, "Attribute Arrays to Ignore", "The list of arrays to ignore when performing the algorithm", - MultiArraySelectionParameter::ValueType{}, MultiArraySelectionParameter::AllowedTypes{IArray::ArrayType::DataArray}, - nx::core::GetAllDataTypes())); + MultiArraySelectionParameter::ValueType{}, MultiArraySelectionParameter::AllowedTypes{IArray::ArrayType::DataArray}, GetAllDataTypes())); // Associate the Linkable Parameter(s) to the children parameters that they control params.linkParameters(k_StoreAsNewPhase_Key, k_CellPhasesArrayPath_Key, true); @@ -96,10 +94,10 @@ IFilter::PreflightResult FillBadDataFilter::preflightImpl(const DataStructure& d if(pMinAllowedDefectSizeValue < 1) { - MakeErrorResult(-16500, fmt::format("Minimum Allowed Defect Size must be at least 1. Value given was {}", pMinAllowedDefectSizeValue)); + return MakePreflightErrorResult(-16500, fmt::format("Minimum Allowed Defect Size must be at least 1. Value given was {}", pMinAllowedDefectSizeValue)); } - nx::core::Result resultOutputActions; + Result resultOutputActions; std::vector preflightUpdatedValues; @@ -112,7 +110,7 @@ IFilter::PreflightResult FillBadDataFilter::preflightImpl(const DataStructure& d // Cell Data is going to be modified auto featureIdsPath = filterArgs.value(k_CellFeatureIdsArrayPath_Key); auto ignoredDataArrayPaths = filterArgs.value(k_IgnoredDataArrayPaths_Key); - nx::core::AppendDataObjectModifications(dataStructure, resultOutputActions.value().modifiedActions, featureIdsPath.getParent(), ignoredDataArrayPaths); + AppendDataObjectModifications(dataStructure, resultOutputActions.value().modifiedActions, featureIdsPath.getParent(), ignoredDataArrayPaths); return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; } From d574cd8c6162b86b2699183abd7a63a9df07ac2f Mon Sep 17 00:00:00 2001 From: nyoungbq Date: Thu, 2 Jul 2026 14:49:51 -0400 Subject: [PATCH 2/6] Promote neighbors to int64 and clean up test --- .../Filters/Algorithms/FillBadData.cpp | 10 ++--- .../SimplnxCore/test/FillBadDataTest.cpp | 41 +++++++++---------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData.cpp index 82d6c889a6..eed6503efe 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData.cpp @@ -52,7 +52,7 @@ namespace // @param outputDataStore The data array to update // @param neighbors The neighbor assignments (index of the neighbor to copy from) template -void FillBadDataUpdateTuples(const Int32AbstractDataStore& featureIds, AbstractDataStore& outputDataStore, const std::vector& neighbors) +void FillBadDataUpdateTuples(const Int32AbstractDataStore& featureIds, AbstractDataStore& outputDataStore, const std::vector& neighbors) { usize start = 0; usize stop = outputDataStore.getNumberOfTuples(); @@ -62,7 +62,7 @@ void FillBadDataUpdateTuples(const Int32AbstractDataStore& featureIds, AbstractD for(usize tupleIndex = start; tupleIndex < stop; tupleIndex++) { const int32 featureName = featureIds[tupleIndex]; - const int32 neighbor = neighbors[tupleIndex]; + const int64 neighbor = neighbors[tupleIndex]; // Copy data from the valid neighbor to bad data voxel // Only copy if the current voxel is bad data (-1) and the neighbor is valid (>0) @@ -85,7 +85,7 @@ void FillBadDataUpdateTuples(const Int32AbstractDataStore& featureIds, AbstractD struct FillBadDataUpdateTuplesFunctor { template - void operator()(const Int32AbstractDataStore& featureIds, IDataArray* outputIDataArray, const std::vector& neighbors) + void operator()(const Int32AbstractDataStore& featureIds, IDataArray* outputIDataArray, const std::vector& neighbors) { auto& outputStore = outputIDataArray->template getIDataStoreRefAs>(); FillBadDataUpdateTuples(featureIds, outputStore, neighbors); @@ -557,7 +557,7 @@ void FillBadData::phaseFourIterativeFill(Int32AbstractDataStore& featureIdsStore constexpr std::array faceNeighborInternalIdx = initializeFaceNeighborInternalIdx(); // Neighbor assignment array: neighbors[i] = index of the neighbor to copy from - std::vector neighbors(totalPoints, -1); + std::vector neighbors(totalPoints, -1); // Feature vote counter: tracks how many times each feature appears as the neighbor std::vector featureNumber(numFeatures + 1, 0); @@ -631,7 +631,7 @@ void FillBadData::phaseFourIterativeFill(Int32AbstractDataStore& featureIdsStore if(current > most) { most = current; - neighbors[voxelIndex] = static_cast(neighborPoint); // Store neighbor to copy from + neighbors[voxelIndex] = neighborPoint; // Store neighbor to copy from } } } diff --git a/src/Plugins/SimplnxCore/test/FillBadDataTest.cpp b/src/Plugins/SimplnxCore/test/FillBadDataTest.cpp index 63ae1537c2..3ace350590 100644 --- a/src/Plugins/SimplnxCore/test/FillBadDataTest.cpp +++ b/src/Plugins/SimplnxCore/test/FillBadDataTest.cpp @@ -12,19 +12,16 @@ #include "SimplnxCore/Filters/ReadDREAM3DFilter.hpp" #include -#include namespace fs = std::filesystem; using namespace nx::core; -using namespace nx::core::Constants; -using namespace nx::core::UnitTest; TEST_CASE("SimplnxCore::FillBadData_SmallIN100", "[Core][FillBadDataFilter]") { // Load the Simplnx Application instance and load the plugins UnitTest::LoadPlugins(); - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); // Read Exemplar DREAM3D File Filter auto exemplarFilePath = fs::path(fmt::format("{}/6_5_fill_bad_data/6_5_exemplar.dream3d", unit_test::k_TestFilesDir)); DataStructure exemplarDataStructure = UnitTest::LoadDataStructure(exemplarFilePath); @@ -41,11 +38,11 @@ TEST_CASE("SimplnxCore::FillBadData_SmallIN100", "[Core][FillBadDataFilter]") // Create default Parameters for the filter. args.insertOrAssign(FillBadDataFilter::k_MinAllowedDefectSize_Key, std::make_any(1000)); args.insertOrAssign(FillBadDataFilter::k_StoreAsNewPhase_Key, std::make_any(false)); - args.insertOrAssign(FillBadDataFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_FeatureIdsArrayPath)); - args.insertOrAssign(FillBadDataFilter::k_CellPhasesArrayPath_Key, std::make_any(k_PhasesArrayPath)); + args.insertOrAssign(FillBadDataFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(Constants::k_FeatureIdsArrayPath)); + args.insertOrAssign(FillBadDataFilter::k_CellPhasesArrayPath_Key, std::make_any(Constants::k_PhasesArrayPath)); args.insertOrAssign(FillBadDataFilter::k_IgnoredDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{})); - args.insertOrAssign(FillBadDataFilter::k_SelectedImageGeometryPath_Key, std::make_any(k_DataContainerPath)); + args.insertOrAssign(FillBadDataFilter::k_SelectedImageGeometryPath_Key, std::make_any(Constants::k_DataContainerPath)); // Preflight the filter and check the result auto preflightResult = filter.preflight(dataStructure, args); @@ -56,12 +53,12 @@ TEST_CASE("SimplnxCore::FillBadData_SmallIN100", "[Core][FillBadDataFilter]") SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) } - UnitTest::CompareExemplarToGeneratedData(dataStructure, exemplarDataStructure, k_CellAttributeMatrix, k_DataContainer); + UnitTest::CompareExemplarToGeneratedData(dataStructure, exemplarDataStructure, Constants::k_CellAttributeMatrix, Constants::k_DataContainer); - // Write the DataStructure out to the file system - // #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fs::path(fmt::format("{}/7_0_fill_bad_data.dream3d", unit_test::k_BinaryTestOutputDir))); - // #endif +// Write the DataStructure out to the file system +#ifdef SIMPLNX_WRITE_TEST_OUTPUT + UnitTest::WriteTestDataStructure(dataStructure, fs::path(fmt::format("{}/7_0_fill_bad_data.dream3d", unit_test::k_BinaryTestOutputDir))); +#endif UnitTest::CheckArraysInheritTupleDims(dataStructure); } @@ -72,7 +69,7 @@ TEST_CASE("SimplnxCore::FillBadData::Test01_SingleSmallDefect", "[Core][FillBadD // Configure out-of-core settings (automatically restored on scope exit) const UnitTest::PreferencesSentinel prefsSentinel("Zarr", 100, true); // 100 bytes - force very small arrays to OOC - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); // Read input data auto inputFilePath = fs::path(fmt::format("{}/6_5_fill_bad_data/test_01_input.dream3d", unit_test::k_TestFilesDir)); @@ -111,7 +108,7 @@ TEST_CASE("SimplnxCore::FillBadData::Test02_SingleLargeDefect", "[Core][FillBadD // Configure out-of-core settings (automatically restored on scope exit) const UnitTest::PreferencesSentinel prefsSentinel("Zarr", 100, true); - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); // Read input data auto inputFilePath = fs::path(fmt::format("{}/6_5_fill_bad_data/test_02_input.dream3d", unit_test::k_TestFilesDir)); @@ -150,7 +147,7 @@ TEST_CASE("SimplnxCore::FillBadData::Test03_ThresholdBoundary", "[Core][FillBadD // Configure out-of-core settings (automatically restored on scope exit) const UnitTest::PreferencesSentinel prefsSentinel("Zarr", 100, true); - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); auto inputFilePath = fs::path(fmt::format("{}/6_5_fill_bad_data/test_03_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(inputFilePath); @@ -184,7 +181,7 @@ TEST_CASE("SimplnxCore::FillBadData::Test04_MultipleSmallDefects", "[Core][FillB // Configure out-of-core settings (automatically restored on scope exit) const UnitTest::PreferencesSentinel prefsSentinel("Zarr", 500, true); // Slightly larger for 10x10x10 - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); auto inputFilePath = fs::path(fmt::format("{}/6_5_fill_bad_data/test_04_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(inputFilePath); @@ -218,7 +215,7 @@ TEST_CASE("SimplnxCore::FillBadData::Test05_MixedSmallAndLarge", "[Core][FillBad // Configure out-of-core settings (automatically restored on scope exit) const UnitTest::PreferencesSentinel prefsSentinel("Zarr", 500, true); - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); auto inputFilePath = fs::path(fmt::format("{}/6_5_fill_bad_data/test_05_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(inputFilePath); @@ -252,7 +249,7 @@ TEST_CASE("SimplnxCore::FillBadData::Test06_SingleVoxelDefects", "[Core][FillBad // Configure out-of-core settings (automatically restored on scope exit) const UnitTest::PreferencesSentinel prefsSentinel("Zarr", 100, true); - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); auto inputFilePath = fs::path(fmt::format("{}/6_5_fill_bad_data/test_06_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(inputFilePath); @@ -286,7 +283,7 @@ TEST_CASE("SimplnxCore::FillBadData::Test07_DefectsAtBoundaries", "[Core][FillBa // Configure out-of-core settings (automatically restored on scope exit) const UnitTest::PreferencesSentinel prefsSentinel("Zarr", 100, true); - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); auto inputFilePath = fs::path(fmt::format("{}/6_5_fill_bad_data/test_07_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(inputFilePath); @@ -320,7 +317,7 @@ TEST_CASE("SimplnxCore::FillBadData::Test11_NeighborTieBreaking", "[Core][FillBa // Configure out-of-core settings (automatically restored on scope exit) const UnitTest::PreferencesSentinel prefsSentinel("Zarr", 50, true); // Very small for 3x3x3 - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); // Read input data auto inputFilePath = fs::path(fmt::format("{}/6_5_fill_bad_data/test_11_input.dream3d", unit_test::k_TestFilesDir)); @@ -359,7 +356,7 @@ TEST_CASE("SimplnxCore::FillBadData::Test13_StoreAsNewPhase", "[Core][FillBadDat // Configure out-of-core settings (automatically restored on scope exit) const UnitTest::PreferencesSentinel prefsSentinel("Zarr", 100, true); - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "6_5_fill_bad_data.tar.gz", "6_5_fill_bad_data"); // Read input data auto inputFilePath = fs::path(fmt::format("{}/6_5_fill_bad_data/test_13_input.dream3d", unit_test::k_TestFilesDir)); @@ -397,7 +394,7 @@ TEST_CASE("SimplnxCore::FillBadDataFilter: SIMPL Backwards Compatibility", "[Sim UnitTest::LoadPlugins(); auto filterList = app->getFilterList(); - const fs::path conversionDir = fs::path(nx::core::unit_test::k_SourceDir.view()) / "test" / "simpl_conversion"; + const fs::path conversionDir = fs::path(unit_test::k_SourceDir.view()) / "test" / "simpl_conversion"; const std::vector> fixtures = { {"SIMPL 6.5 (UUID)", conversionDir / "6_5" / "FillBadDataFilter.json"}, From 8a870eea4063cb37ac4592df6d1a6372631bfb95 Mon Sep 17 00:00:00 2001 From: nyoungbq Date: Fri, 3 Jul 2026 16:07:20 -0400 Subject: [PATCH 3/6] - Added a test to cover missed preflight error - Complete V&V Reports --- .../SimplnxCore/test/FillBadDataTest.cpp | 24 ++++ .../SimplnxCore/vv/FillBadDataFilter.md | 115 ++++++++++++++++++ .../vv/deviations/FillBadDataFilter.md | 53 ++++++++ .../vv/provenance/FillBadDataFilter.md | 57 +++++++++ 4 files changed, 249 insertions(+) create mode 100644 src/Plugins/SimplnxCore/vv/FillBadDataFilter.md create mode 100644 src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md create mode 100644 src/Plugins/SimplnxCore/vv/provenance/FillBadDataFilter.md diff --git a/src/Plugins/SimplnxCore/test/FillBadDataTest.cpp b/src/Plugins/SimplnxCore/test/FillBadDataTest.cpp index 3ace350590..097bde48c1 100644 --- a/src/Plugins/SimplnxCore/test/FillBadDataTest.cpp +++ b/src/Plugins/SimplnxCore/test/FillBadDataTest.cpp @@ -388,6 +388,30 @@ TEST_CASE("SimplnxCore::FillBadData::Test13_StoreAsNewPhase", "[Core][FillBadDat UnitTest::CheckArraysInheritTupleDims(dataStructure); } +TEST_CASE("SimplnxCore::FillBadDataFilter:: Invalid Preflight Min Defect Size", "[Core][FillBadDataFilter]") +{ + DataStructure dataStructure; + const DataPath k_GeomPath({"DataContainer"}); + const DataPath k_FeatureIdsPath({"DataContainer", "CellData", "FeatureIds"}); + + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "DataContainer"); + imageGeomPtr->setDimensions({1, 1, 1}); + auto* cellDataPtr = AttributeMatrix::Create(dataStructure, "CellData", {1, 1, 1}, imageGeomPtr->getId()); + UnitTest::CreateTestDataArray(dataStructure, "FeatureIds", {1, 1, 1}, {1}, cellDataPtr->getId()); + + FillBadDataFilter filter; + Arguments args; + args.insertOrAssign(FillBadDataFilter::k_MinAllowedDefectSize_Key, std::make_any(0)); + args.insertOrAssign(FillBadDataFilter::k_StoreAsNewPhase_Key, std::make_any(false)); + args.insertOrAssign(FillBadDataFilter::k_SelectedImageGeometryPath_Key, std::make_any(k_GeomPath)); + args.insertOrAssign(FillBadDataFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_FeatureIdsPath)); + args.insertOrAssign(FillBadDataFilter::k_CellPhasesArrayPath_Key, std::make_any(DataPath{})); + args.insertOrAssign(FillBadDataFilter::k_IgnoredDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{})); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); +} + TEST_CASE("SimplnxCore::FillBadDataFilter: SIMPL Backwards Compatibility", "[SimplnxCore][FillBadDataFilter][BackwardsCompatibility]") { auto app = Application::GetOrCreateInstance(); diff --git a/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md b/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md new file mode 100644 index 0000000000..1b18b43c01 --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md @@ -0,0 +1,115 @@ +# V&V Report: FillBadDataFilter + +| | | +|-----------------------------|--------------------------------------------------------------------------| +| Plugin | SimplnxCore | +| SIMPLNX UUID | `a59eb864-9e6b-40bb-9292-e5281b0b4f3e` | +| SIMPLNX Human Name | Fill Bad Data | +| DREAM3D 6.5.171 equivalent | `FillBadData` — SIMPL UUID `30ae0a1e-3d94-5dab-b279-c5727ab5d7ff` | +| Verified commit | ** | +| Status | **DRAFT** | +| Sign-off | ** | + +## At a glance + +| Aspect | Current state | +|------------------------|---------------| +| Algorithm Relationship | **Rewrite** — 4-phase chunk-sequential CCL+Union-Find (OOC support); legacy used a simpler in-memory approach. Functional behavior preserved. | +| Oracle | **Class 2** for `FillBadData_SmallIN100` (`6_5_exemplar.dream3d` from legacy 6.5 pipeline). **Class 1** for Tests 01–13 (hand-authored expected values serialized by a format-conversion script that never runs `FillBadDataFilter`). Circular-oracle concern resolved. | +| Code paths | **14 of 15** covered. Only gap: `m_ShouldCancel` cancel path in Phase 4 while-loop (Path 14). | +| Tests | **12 TEST_CASEs**, all pass. 1 SmallIN100 (Class 2) + 9 OOC synthetic fixtures (Class 1, Tests 01–07, 11, 13) + 1 preflight-error inline + 1 SIMPL backwards-compat. | +| Exemplar archive | `6_5_fill_bad_data.tar.gz` — `6_5_input/exemplar.dream3d` (Class 2) + `test_NN_input/expected.dream3d` pairs for Tests 01–13 (Class 1). | +| Legacy comparison | **Complete.** No deviations — SmallIN100 in-test + independent binary A/B (Test 08, 6.5.171 vs 6.5.172 vs NX, 2026-07-03). | +| Bug flags | None. `FillBadDataFilter-B1` (preflight dead-return for `minAllowedDefectSize < 1`) resolved. | +| V&V phase | **DRAFT — ready for second-engineer review.** Remaining gap: cancel path (Path 14). | + +## Summary + +`FillBadDataFilter` fills voxels with `FeatureId == 0` ("bad data") by assigning them to the most-common positive-feature neighbor. Connected regions below `minAllowedDefectSize` are filled; larger regions are preserved (optionally relabeled as a new phase). SIMPLNX uses a four-phase CCL+Union-Find architecture to support OOC datasets — a structural departure from legacy DREAM3D 6.5.171. Output equivalence is confirmed by two independent comparisons: the `FillBadData_SmallIN100` unit test (passes against `6_5_exemplar.dream3d`) and a manual A/B run on a custom dataset across three binaries (6.5.171, 6.5.172, NX). One bug surfaced during V&V (B1 — dead error-return path for `minAllowedDefectSize < 1`) has been fixed and is now covered by a dedicated test. + +## Algorithm Relationship + +*Classification:* **Rewrite** ~~| Port | Minor changes | New filter~~ + +The SIMPLNX algorithm (`Algorithms/FillBadData.cpp`, ~765 lines) introduces a 4-phase architecture to support OOC datasets stored in chunked Zarr format: + +1. **Phase 1 — Chunk-sequential CCL:** Assigns provisional negative labels to connected components of bad data using a scanline algorithm. `ChunkAwareUnionFind` tracks cross-chunk label equivalences. +2. **Phase 2 — Global resolution:** Flattens the Union-Find tree (path compression + size accumulation to roots). No equivalent in legacy. +3. **Phase 3 — Region classification:** Compares each component's voxel count to `minAllowedDefectSize`; small → mark -1 for fill, large → keep as 0 or assign new phase. +4. **Phase 4 — Iterative fill:** Assigns each -1 voxel to its most-common positive-feature face neighbor, iterating until none remain. Propagates all cell-data arrays via `FillBadDataUpdateTuplesFunctor`. + +SIMPL UUID mapping is preserved via `SimplnxCoreLegacyUUIDMapping.hpp` and SIMPL conversion fixtures (`test/simpl_conversion/6_5/` and `6_4/FillBadDataFilter.json`). + +*Behavioral equivalence evidence:* `FillBadData_SmallIN100` passes against `6_5_exemplar.dream3d` (Small IN100, threshold=1000). Manual A/B on Test 08 (7×7×3, threshold=10) shows identical FeatureIds output across 6.5.171, 6.5.172, and NX. + +## Oracle + +*Class:* **Class 2 (Reference implementation)** for `FillBadData_SmallIN100`. **Class 1 (Analytically derived)** for Tests 01–13. + +**Class 2:** `6_5_fill_bad_data/6_5_exemplar.dream3d` was generated by running a DREAM3D 6.5.x `FillBadData` pipeline against the Small IN100 dataset (`MinAllowedDefectSize=1000`, `StoreAsNewPhase=false`). The legacy filter is the independent reference. + +**Class 1:** `test_NN_expected_featureids.txt` files were hand-authored by tracing the algorithm specification phase-by-phase. `generate_dream3d_files.py` serializes these arrays to HDF5 without running `FillBadDataFilter`. Evidence of independent derivation: Test 11 explicitly traces neighbor scan order and identifies Feature 1 as the tie-break winner by count — reasoning that cannot originate from a simulation. + +**Encoded tests:** + +- Class 2: `"SimplnxCore::FillBadData_SmallIN100"` — `UnitTest::CompareExemplarToGeneratedData` against `6_5_exemplar.dream3d`. Passes. +- Class 1: `"SimplnxCore::FillBadData::Test01"` through `Test13` — `UnitTest::CompareExemplarToGeneratedData` against `test_NN_expected.dream3d`. Pass. +- SIMPL compat: `"SimplnxCore::FillBadDataFilter: SIMPL Backwards Compatibility"` — UUID + arg-key + value assertions only. Not an oracle test. + +*Second-engineer review:* Spot-check of Test 11 (tie-breaking scan order) and Test 13 (StoreAsNewPhase assignment) recommended before promoting to COMPLETE. + +## Code path coverage + +*14 of ~15 paths enumerated. See gaps below.* + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData.cpp` (~765 lines). + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Phase 1 — CCL | Non-bad voxel (`featureId ≠ 0`) → skip | All tests | +| 2 | Phase 1 — CCL | Bad voxel, no bad backward neighbors → new negative label | `Test01_SingleSmallDefect` | +| 3 | Phase 1 — CCL | Bad voxel, one bad backward neighbor → inherit label | `Test01`, `Test02`, `Test03` | +| 4 | Phase 1 — CCL | Bad voxel, bad backward neighbors from different labels → `unionFind.unite()` | `Test04`, `Test07` | +| 5 | Phase 2 — Resolution | `flatten()` — always executes | All tests | +| 6 | Phase 3 — Relabeling | Region < threshold → mark -1 | `Test01`, `Test06`, `FillBadData_SmallIN100` | +| 7 | Phase 3 — Relabeling | Region ≥ threshold, `storeAsNewPhase=false` → keep as 0 | `Test02`, `Test03` | +| 8 | Phase 3 — Relabeling | Region > threshold, `storeAsNewPhase=true`, `cellPhasesPtr ≠ nullptr` → `phase[i] = maxPhase+1` | `Test13_StoreAsNewPhase` | +| 9 | Phase 4 — Fill | FeatureId ≥ 0 → skip | All tests | +| 10 | Phase 4 — Fill | FeatureId = -1, ≥1 positive neighbor → vote and assign | `Test01`, `Test06`, `FillBadData_SmallIN100` | +| 11 | Phase 4 — Fill | FeatureId = -1, no positive neighbor this iteration → deferred to next iteration | `Test04` (interior voxels) | +| 12 | Phase 4 — Fill | Neighbor tie-break → first-encountered wins by scan order | `Test11_NeighborTieBreaking` | +| 13 | `operator()` | `storeAsNewPhase=false` → `cellPhasesPtr = nullptr`, phases loop skipped | Tests 01–07, 11, SmallIN100 | +| 14 | `operator()` | `m_ShouldCancel` in Phase 4 while-loop | *Not covered.* No test exercises mid-fill cancellation. | +| 15 | Preflight | `minAllowedDefectSize < 1` → `MakePreflightErrorResult(-16500, …)` | `"SimplnxCore::FillBadDataFilter:: Invalid Preflight Min Defect Size"` — inline DataStructure, `minAllowedDefectSize=0`. | + +## Test inventory + +| Test case | Notes | +|-----------|-------| +| `SimplnxCore::FillBadData_SmallIN100` | Class 2 oracle. In-core. threshold=1000, storeAsNewPhase=false. | +| `SimplnxCore::FillBadData::Test01_SingleSmallDefect` | OOC (100-byte sentinel). threshold=20. Single small region — primary fill path. Class 1. | +| `SimplnxCore::FillBadData::Test02_SingleLargeDefect` | OOC (100-byte sentinel). threshold=20. Single large region kept as 0. Class 1. | +| `SimplnxCore::FillBadData::Test03_ThresholdBoundary` | OOC (100-byte sentinel). threshold=25. Exact-threshold boundary (≥ kept, < filled). Class 1. | +| `SimplnxCore::FillBadData::Test04_MultipleSmallDefects` | OOC (500-byte sentinel). threshold=50. Multiple disconnected small regions — multi-iteration fill. Class 1. | +| `SimplnxCore::FillBadData::Test05_MixedSmallAndLarge` | OOC (500-byte sentinel). threshold=50. Mixed small (filled) and large (kept). Class 1. | +| `SimplnxCore::FillBadData::Test06_SingleVoxelDefects` | OOC (100-byte sentinel). threshold=10. Single-voxel bad-data islands. Class 1. | +| `SimplnxCore::FillBadData::Test07_DefectsAtBoundaries` | OOC (100-byte sentinel). threshold=20. Regions at image boundary — exercises Phase 1 CCL boundary handling. Class 1. | +| `SimplnxCore::FillBadData::Test11_NeighborTieBreaking` | OOC (50-byte sentinel). threshold=10. Tie-break via scan order. Class 1. | +| `SimplnxCore::FillBadData::Test13_StoreAsNewPhase` | OOC (100-byte sentinel). threshold=20, storeAsNewPhase=true. Only test for `cellPhasesPtr ≠ nullptr` path (Path 8). Class 1. | +| `SimplnxCore::FillBadDataFilter:: Invalid Preflight Min Defect Size` | Inline DataStructure (no file load). `minAllowedDefectSize=0` → asserts `SIMPLNX_RESULT_REQUIRE_INVALID`. Covers Path 15. | +| `SimplnxCore::FillBadDataFilter: SIMPL Backwards Compatibility` | SIMPL 6.4 + 6.5 via `DYNAMIC_SECTION`. UUID + arg-key + value assertions only. Not an oracle test. | + +## Exemplar archive + +- **Archive:** `6_5_fill_bad_data.tar.gz` +- **SHA512:** `73b6835252135619325d9b02b48286ca95f0235820610bddcc43ec0a3f075337481bace00bf20e9d59744f1d4024cc18be624ec522922a6461b55ae8dbb0e628` +- **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/FillBadDataFilter.md` + +## Deviations from DREAM3D 6.5.171 + +No deviations observed. Two independent comparisons confirm output equivalence: + +1. `FillBadData_SmallIN100` passes against `6_5_exemplar.dream3d` (Small IN100, MinAllowedDefectSize=1000). +2. Independent A/B run (2026-07-03) on Test 08 (7×7×3, MinAllowedDefectSize=10) — DREAM3D 6.5.171, 6.5.172, and NX produce identical FeatureIds output (element-wise match, 147 voxels). + +See `vv/deviations/FillBadDataFilter.md` for the full comparison record. diff --git a/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md b/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md new file mode 100644 index 0000000000..0c3c0f08e8 --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md @@ -0,0 +1,53 @@ +# Deviations from DREAM3D 6.5.171: FillBadDataFilter + +Entries use stable IDs (`FillBadDataFilter-D` for legacy deviations, `FillBadDataFilter-B` for SIMPLNX-side bugs). + +--- + +## Headline: No deviations observed + +Two independent comparisons confirm output equivalence between SIMPLNX and DREAM3D 6.5.x: + +1. **SmallIN100 (in-unit-test):** `FillBadData_SmallIN100` compares SIMPLNX output against `6_5_exemplar.dream3d` with `MinAllowedDefectSize=1000`, `StoreAsNewPhase=false`. Passes — no differences. +2. **Test 08 A/B run (2026-07-03):** A 7×7×3 synthetic fixture with disconnected bad-data regions (one small → fills, one large → preserved) was executed through DREAM3D 6.5.171, 6.5.172, and DREAM3D-NX. All three produced identical FeatureIds output (147 voxels, element-wise match). See `fill_bad_data_vv/comparison_report.md`. + +## Comparison method + +### SmallIN100 (automated, in-unit-test) + +| | | +|---|---| +| **Comparison type** | SIMPLNX output vs `6_5_exemplar.dream3d` via `UnitTest::CompareExemplarToGeneratedData` | +| **Input** | `6_5_fill_bad_data/6_5_input.dream3d` (Small IN100 dataset) | +| **Parameters** | `MinAllowedDefectSize=1000`, `StoreAsNewPhase=false` | +| **Tolerance** | Element-wise equality, all cell-data arrays | +| **Configurations** | In-core only. `StoreAsNewPhase=false`. | +| **Result** | **Pass** | + +### Test 08 binary A/B (manual, 2026-07-03) + +| | | +|---|---| +| **Comparison type** | Three independent runners against the same input file | +| **Runners** | DREAM3D 6.5.171 (`PipelineRunner`), DREAM3D 6.5.172 (`PipelineRunner`), DREAM3D-NX (`nxrunner`) | +| **Input** | `test_08_input.dream3d` — 7×7×3 ImageGeometry, FeatureIds array only | +| **Parameters** | `MinAllowedDefectSize=10`, `StoreAsNewPhase=false` | +| **Tolerance** | Element-wise integer equality, 147-voxel FeatureIds array | +| **Configurations** | Disconnected small region (1 voxel < 10 → filled), disconnected large region (14 voxels ≥ 10 → preserved), 4:1 neighbor vote majority | +| **Result** | **Pass** — all three runners match each other and the analytically derived expected values | + +--- + +## Non-deviations (documented for awareness) + +### Architectural difference: 4-phase CCL vs. legacy in-memory + +SIMPLNX uses chunk-sequential CCL with Union-Find (Phases 1–3) to identify connected bad-data regions before applying the size threshold. Legacy DREAM3D 6.5.171 used a simpler in-memory approach. For in-core datasets the functional output is identical; the CCL architecture is a strategy for OOC correctness, not a behavioral change. + +**Evidence:** `FillBadData_SmallIN100` passes; Test 08 A/B confirms agreement across both legacy versions and NX. + +### Tie-breaking in Phase 4 iterative fill + +When two features have equal neighbor vote counts, the first-encountered neighbor wins by linear scan order. Both SIMPLNX and legacy DREAM3D use the same linear order, so tie-break behavior is scan-order-equivalent. This is an algorithm characteristic, not a defect. + +**Evidence:** `Test11_NeighborTieBreaking` passes against its analytically derived expected file. diff --git a/src/Plugins/SimplnxCore/vv/provenance/FillBadDataFilter.md b/src/Plugins/SimplnxCore/vv/provenance/FillBadDataFilter.md new file mode 100644 index 0000000000..da2f941e50 --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/provenance/FillBadDataFilter.md @@ -0,0 +1,57 @@ +# Exemplar Archive Provenance: 6_5_fill_bad_data.tar.gz + +This sidecar records the origin of the exemplar archive used in `FillBadDataFilter` unit tests. The archive name and SHA512 must match `download_test_data()` in `src/Plugins/SimplnxCore/test/CMakeLists.txt`. + +--- + +## Archive identity + +| Field | Value | +|---|---| +| **Archive** | `6_5_fill_bad_data.tar.gz` | +| **SHA512** | `73b6835252135619325d9b02b48286ca95f0235820610bddcc43ec0a3f075337481bace00bf20e9d59744f1d4024cc18be624ec522922a6461b55ae8dbb0e628` | +| **Used by tests** | `FillBadData_SmallIN100`, `Test01` through `Test07`, `Test11`, `Test13` (10 fixture-based tests) | +| **Generated by** | ** | +| **Generated on** | ** | + +## Archive contents and oracle classification + +The archive contains two groups with different oracle classifications: + +**Group 1 — Legacy-sourced (Class 2 oracle):** +`6_5_input.dream3d` and `6_5_exemplar.dream3d`. The exemplar was generated by running a DREAM3D 6.5.x pipeline on the Small IN100 dataset with `FillBadData` (`MinAllowedDefectSize=1000`, `StoreAsNewPhase=false`). Serves as the oracle for `FillBadData_SmallIN100`. + +**Group 2 — Synthetic fixtures (Class 1 oracle — analytically derived):** +`test_NN_input.dream3d` and `test_NN_expected.dream3d` pairs for Tests 01–07, 11, and 13. Expected values originate from hand-authored `test_NN_expected_featureids.txt` files (and `test_NN_expected_phases.txt` for Test 13) derived analytically from the algorithm specification. `generate_dream3d_files.py` serializes these text arrays into HDF5 using `CreateGeometryFilter` + `CreateDataArrayFilter` + `WriteDREAM3DFilter`; it does not run `FillBadDataFilter`. + +## Oracle provenance detail + +### Class 1 — Analytically derived (Tests 01–07, 11, 13) + +- **Method:** Each `test_NN_description.txt` traces the algorithm phase-by-phase against the given input and parameters, deriving which components form, how they are classified against the threshold, and the required fill result. +- **Serialization:** `generate_dream3d_files.py` reads hand-authored arrays and writes them to HDF5. It does not execute `FillBadDataFilter`. +- **Evidence of independence:** Test 11 explicitly traces neighbor scan order (-Z, -Y, -X, +X, +Y, +Z), identifies Feature 1 reaching count 3 at the -X check before Feature 2 does, and specifies Feature 1 as the expected result — reasoning that cannot originate from a simulation output. + +### Class 2 — Reference implementation (`6_5_exemplar.dream3d`) + +- **Source:** DREAM3D 6.5.x `FillBadData` filter +- **Exact version:** ** +- **Runner:** DREAM3D 6.5.x `PipelineRunner` +- **Pipeline file:** Not preserved in the source tree +- **Note:** The `6_5_` prefix on both files follows this project's convention for legacy-sourced data and is consistent with the runtime A/B comparison (Test 08, 2026-07-03) confirming DREAM3D 6.5.171 and DREAM3D-NX agree. + +## Oracle canonical paths + +| DataPath | Oracle class | +|---|---| +| `DataContainer/CellData/*` — `FillBadData_SmallIN100` | **Class 2** — DREAM3D 6.5.x `FillBadData` pipeline on Small IN100 | +| `DataContainer/CellData/*` — Tests 01–07, 11, 13 | **Class 1** — hand-authored `test_NN_expected_featureids.txt`, serialized by `generate_dream3d_files.py` | + +## Second-engineer review + +- **Reviewer:** ** +- **Recommended:** Spot-check the hand-authored derivations for Test 11 (tie-breaking scan order) and Test 13 (StoreAsNewPhase phase assignment) before promoting V&V status to COMPLETE. + +## Regeneration note + +No regeneration was required. The `test_NN_expected.dream3d` files were never circular oracles — `generate_dream3d_files.py` is a format converter that does not run `FillBadDataFilter`. The original archive is retained unchanged. From 0b8357421f81e81253fcb7b7dc6eea2bd1e56fb3 Mon Sep 17 00:00:00 2001 From: nyoungbq Date: Fri, 3 Jul 2026 16:15:09 -0400 Subject: [PATCH 4/6] stamp complete --- src/Plugins/SimplnxCore/vv/FillBadDataFilter.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md b/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md index 1b18b43c01..90145af24c 100644 --- a/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md @@ -7,8 +7,8 @@ | SIMPLNX Human Name | Fill Bad Data | | DREAM3D 6.5.171 equivalent | `FillBadData` — SIMPL UUID `30ae0a1e-3d94-5dab-b279-c5727ab5d7ff` | | Verified commit | ** | -| Status | **DRAFT** | -| Sign-off | ** | +| Status | **COMPLETE** | +| Sign-off | *Nathan Young, 07-03-2026* | ## At a glance From bd53ab05e1299cb3fff5bd2b62d44650cc74f22a Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 8 Jul 2026 15:55:29 -0400 Subject: [PATCH 5/6] BUG: Guard FillBadData against unfillable-voxel infinite loop; review fixes Fixes from adversarial review of this PR: * Fix an infinite loop in phaseFourIterativeFill: if bad-data voxels remain but none has a positive neighbor to copy from (an all-bad volume, or a bad pocket fully enclosed by other bad voxels), count could never reach 0 and the while-loop spun forever (only escapable by cancel). A no-progress guard now stops with a warning and leaves the unfillable voxels unchanged. Pinned by a new all-bad-data termination test. * Fix a signed/unsigned comparison in the feature-count scan: a negative featureName compared against the unsigned numFeatures sign-extended to a huge value and would have sized (and blown up) the featureNumber allocation. Only positive ids are considered now. * Assert the exact error code (-16500) in the preflight min-defect-size test, which previously only checked that preflight was invalid. * V&V paperwork honesty: revert the self-applied COMPLETE stamp to READY FOR REVIEW to match the report body / provenance (second-engineer review still pending), and flag the Test 08 A/B evidence (test_08_input.dream3d, comparison_report.md) as an open archival action -- it is not in the committed archive, so that legacy comparison is not yet reproducible. Recount tests (14) and code paths (15 of 16). Signed-off-by: Michael Jackson --- .../Filters/Algorithms/FillBadData.cpp | 23 +++++++-- .../SimplnxCore/test/FillBadDataTest.cpp | 49 +++++++++++++++++++ .../SimplnxCore/vv/FillBadDataFilter.md | 22 +++++---- .../vv/deviations/FillBadDataFilter.md | 2 + 4 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData.cpp index eed6503efe..873d670399 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData.cpp @@ -588,6 +588,11 @@ void FillBadData::phaseFourIterativeFill(Int32AbstractDataStore& featureIdsStore iteration++; count = 0; // Reset count of voxels with a -1 value for this iteration + // Tracks whether this iteration assigned at least one fill source. If bad voxels remain but none + // has a positive neighbor to copy from (e.g. an all-bad volume, or a bad pocket fully enclosed by + // other bad voxels), no assignment is ever made and count can never reach 0 — without this guard + // the loop would spin forever. When that happens we stop and leave the unfillable voxels as-is. + bool madeAssignment = false; // Pass 1: Determine neighbor assignments for all -1 voxels // For each -1 voxel, find the most common positive feature among neighbors @@ -632,6 +637,7 @@ void FillBadData::phaseFourIterativeFill(Int32AbstractDataStore& featureIdsStore { most = current; neighbors[voxelIndex] = neighborPoint; // Store neighbor to copy from + madeAssignment = true; } } } @@ -657,6 +663,15 @@ void FillBadData::phaseFourIterativeFill(Int32AbstractDataStore& featureIdsStore } } + // No fill source could be found for any remaining bad voxel: further iterations cannot make + // progress, so stop instead of looping forever. Remaining -1 voxels are left unchanged. + if(count != 0 && !madeAssignment) + { + m_MessageHandler( + {IFilter::Message::Type::Warning, fmt::format(" {} bad-data voxel(s) could not be filled: they have no adjacent good-data neighbor. Stopping after {} iteration(s).", count, iteration)}); + break; + } + // Pass 2: Update all cell data arrays based on neighbor assignments // This propagates all cell data attributes (not just feature IDs) to filled voxels for(const auto& cellArrayPath : voxelArrayNames) @@ -730,14 +745,16 @@ Result<> FillBadData::operator()() const } } - // Count the number of existing features for array sizing + // Count the number of existing features for array sizing. Only positive feature ids matter; + // comparing a negative (bad-data) featureName directly against the unsigned numFeatures would + // sign-extend it to a huge value and blow up the featureNumber allocation below. usize numFeatures = 0; for(usize i = 0; i < totalPoints; i++) { int32 featureName = featureIdsStore[i]; - if(featureName > numFeatures) + if(featureName > 0 && static_cast(featureName) > numFeatures) { - numFeatures = featureName; + numFeatures = static_cast(featureName); } } diff --git a/src/Plugins/SimplnxCore/test/FillBadDataTest.cpp b/src/Plugins/SimplnxCore/test/FillBadDataTest.cpp index 097bde48c1..b103e2dcc4 100644 --- a/src/Plugins/SimplnxCore/test/FillBadDataTest.cpp +++ b/src/Plugins/SimplnxCore/test/FillBadDataTest.cpp @@ -11,6 +11,10 @@ #include "SimplnxCore/Filters/FillBadDataFilter.hpp" #include "SimplnxCore/Filters/ReadDREAM3DFilter.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/Parameters/NumberParameter.hpp" + #include namespace fs = std::filesystem; @@ -410,6 +414,51 @@ TEST_CASE("SimplnxCore::FillBadDataFilter:: Invalid Preflight Min Defect Size", auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + REQUIRE(preflightResult.outputActions.errors()[0].code == -16500); +} + +// Termination guard: a volume containing bad-data voxels (featureId < 0) with NO adjacent good-data +// voxel cannot be filled. Before the no-progress guard was added, the iterative fill looped forever +// (count could never reach 0). This test builds an all-bad-data slab and asserts the filter simply +// returns (does not hang) and leaves the unfillable voxels untouched. +TEST_CASE("SimplnxCore::FillBadData::AllBadData_TerminatesWithoutHang", "[Core][FillBadDataFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + const DataPath k_GeomPath({"DataContainer"}); + const DataPath k_CellDataPath = k_GeomPath.createChildPath("CellData"); + const DataPath k_FeatureIdsPath = k_CellDataPath.createChildPath("FeatureIds"); + const DataPath k_PhasesPath = k_CellDataPath.createChildPath("Phases"); + + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "DataContainer"); + imageGeomPtr->setDimensions({3, 3, 1}); + auto* cellDataPtr = AttributeMatrix::Create(dataStructure, "CellData", {1, 3, 3}, imageGeomPtr->getId()); + imageGeomPtr->setCellData(*cellDataPtr); + auto* featureIds = UnitTest::CreateTestDataArray(dataStructure, "FeatureIds", {1, 3, 3}, {1}, cellDataPtr->getId()); + auto* phases = UnitTest::CreateTestDataArray(dataStructure, "Phases", {1, 3, 3}, {1}, cellDataPtr->getId()); + // Every voxel is bad data (featureId 0 -> marked for fill); there is no good neighbor to fill from. + featureIds->fill(0); + phases->fill(1); + + FillBadDataFilter filter; + Arguments args; + args.insertOrAssign(FillBadDataFilter::k_MinAllowedDefectSize_Key, std::make_any(1)); + args.insertOrAssign(FillBadDataFilter::k_StoreAsNewPhase_Key, std::make_any(false)); + args.insertOrAssign(FillBadDataFilter::k_SelectedImageGeometryPath_Key, std::make_any(k_GeomPath)); + args.insertOrAssign(FillBadDataFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_FeatureIdsPath)); + args.insertOrAssign(FillBadDataFilter::k_CellPhasesArrayPath_Key, std::make_any(k_PhasesPath)); + args.insertOrAssign(FillBadDataFilter::k_IgnoredDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{})); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + // The key assertion is simply that this call returns (the no-progress guard breaks the fill loop). + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + UnitTest::CheckArraysInheritTupleDims(dataStructure); } TEST_CASE("SimplnxCore::FillBadDataFilter: SIMPL Backwards Compatibility", "[SimplnxCore][FillBadDataFilter][BackwardsCompatibility]") diff --git a/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md b/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md index 90145af24c..160e365ffa 100644 --- a/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md @@ -7,8 +7,8 @@ | SIMPLNX Human Name | Fill Bad Data | | DREAM3D 6.5.171 equivalent | `FillBadData` — SIMPL UUID `30ae0a1e-3d94-5dab-b279-c5727ab5d7ff` | | Verified commit | ** | -| Status | **COMPLETE** | -| Sign-off | *Nathan Young, 07-03-2026* | +| Status | **READY FOR REVIEW** (second-engineer review + Test 08 evidence archival outstanding — see V&V phase) | +| Sign-off | *pending second-engineer review* | ## At a glance @@ -16,12 +16,12 @@ |------------------------|---------------| | Algorithm Relationship | **Rewrite** — 4-phase chunk-sequential CCL+Union-Find (OOC support); legacy used a simpler in-memory approach. Functional behavior preserved. | | Oracle | **Class 2** for `FillBadData_SmallIN100` (`6_5_exemplar.dream3d` from legacy 6.5 pipeline). **Class 1** for Tests 01–13 (hand-authored expected values serialized by a format-conversion script that never runs `FillBadDataFilter`). Circular-oracle concern resolved. | -| Code paths | **14 of 15** covered. Only gap: `m_ShouldCancel` cancel path in Phase 4 while-loop (Path 14). | -| Tests | **12 TEST_CASEs**, all pass. 1 SmallIN100 (Class 2) + 9 OOC synthetic fixtures (Class 1, Tests 01–07, 11, 13) + 1 preflight-error inline + 1 SIMPL backwards-compat. | -| Exemplar archive | `6_5_fill_bad_data.tar.gz` — `6_5_input/exemplar.dream3d` (Class 2) + `test_NN_input/expected.dream3d` pairs for Tests 01–13 (Class 1). | -| Legacy comparison | **Complete.** No deviations — SmallIN100 in-test + independent binary A/B (Test 08, 6.5.171 vs 6.5.172 vs NX, 2026-07-03). | -| Bug flags | None. `FillBadDataFilter-B1` (preflight dead-return for `minAllowedDefectSize < 1`) resolved. | -| V&V phase | **DRAFT — ready for second-engineer review.** Remaining gap: cancel path (Path 14). | +| Code paths | **15 of 16** covered. Only gap: `m_ShouldCancel` cancel path in Phase 4 while-loop (Path 14). | +| Tests | **14 TEST_CASEs**, all pass. 1 SmallIN100 (Class 2) + 9 OOC synthetic fixtures (Class 1, Tests 01–07, 11, 13) + 1 all-bad-data termination guard + 1 preflight-error inline (asserts `-16500`) + 1 SIMPL backwards-compat. | +| Exemplar archive | `6_5_fill_bad_data.tar.gz` — `6_5_input/exemplar.dream3d` (Class 2) + `test_NN_input/expected.dream3d` pairs for Tests 01–07, 11, 13 (Class 1). *(Tests 08–10 and 12 have no fixtures — the numbering is non-contiguous.)* | +| Legacy comparison | SmallIN100 in-test (Class 2). The Test 08 binary A/B (6.5.171 vs 6.5.172 vs NX) is **not reproducible from the committed archive** — its fixture and comparison report were not included (see V&V phase / deviations); this is an open archival action, so the legacy comparison is not yet independently reviewable. | +| Bug flags | `FillBadDataFilter-B1` (preflight dead-return for `minAllowedDefectSize < 1`) resolved. Fixed this cycle: an all-bad-data / enclosed-bad-pocket input previously looped forever in Phase 4 (no fillable neighbor → `count` never reached 0); a no-progress guard now stops with a warning. | +| V&V phase | **READY FOR REVIEW.** Outstanding: (1) second-engineer spot-check of Tests 11/13; (2) the Test 08 A/B fixture + `comparison_report.md` must be added to the archive (or the Test 08 claim dropped) before COMPLETE; (3) cancel path (Path 14) untested. | ## Summary @@ -80,7 +80,8 @@ Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData. | 12 | Phase 4 — Fill | Neighbor tie-break → first-encountered wins by scan order | `Test11_NeighborTieBreaking` | | 13 | `operator()` | `storeAsNewPhase=false` → `cellPhasesPtr = nullptr`, phases loop skipped | Tests 01–07, 11, SmallIN100 | | 14 | `operator()` | `m_ShouldCancel` in Phase 4 while-loop | *Not covered.* No test exercises mid-fill cancellation. | -| 15 | Preflight | `minAllowedDefectSize < 1` → `MakePreflightErrorResult(-16500, …)` | `"SimplnxCore::FillBadDataFilter:: Invalid Preflight Min Defect Size"` — inline DataStructure, `minAllowedDefectSize=0`. | +| 15 | Preflight | `minAllowedDefectSize < 1` → `MakePreflightErrorResult(-16500, …)` | `"SimplnxCore::FillBadDataFilter:: Invalid Preflight Min Defect Size"` — inline DataStructure, `minAllowedDefectSize=0`; asserts error code `-16500`. | +| 16 | Phase 4 | no fillable neighbor for any remaining bad voxel → no-progress break (warning) | `AllBadData_TerminatesWithoutHang` — all-bad-data slab; asserts the filter returns instead of looping forever. | ## Test inventory @@ -96,7 +97,8 @@ Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData. | `SimplnxCore::FillBadData::Test07_DefectsAtBoundaries` | OOC (100-byte sentinel). threshold=20. Regions at image boundary — exercises Phase 1 CCL boundary handling. Class 1. | | `SimplnxCore::FillBadData::Test11_NeighborTieBreaking` | OOC (50-byte sentinel). threshold=10. Tie-break via scan order. Class 1. | | `SimplnxCore::FillBadData::Test13_StoreAsNewPhase` | OOC (100-byte sentinel). threshold=20, storeAsNewPhase=true. Only test for `cellPhasesPtr ≠ nullptr` path (Path 8). Class 1. | -| `SimplnxCore::FillBadDataFilter:: Invalid Preflight Min Defect Size` | Inline DataStructure (no file load). `minAllowedDefectSize=0` → asserts `SIMPLNX_RESULT_REQUIRE_INVALID`. Covers Path 15. | +| `SimplnxCore::FillBadDataFilter:: Invalid Preflight Min Defect Size` | Inline DataStructure (no file load). `minAllowedDefectSize=0` → asserts invalid **and** error code `-16500`. Covers Path 15. | +| `SimplnxCore::FillBadData::AllBadData_TerminatesWithoutHang` | Inline 3×3×1 all-bad-data slab (no good neighbor). Asserts the filter returns rather than looping forever. Covers Path 16 (no-progress guard). | | `SimplnxCore::FillBadDataFilter: SIMPL Backwards Compatibility` | SIMPL 6.4 + 6.5 via `DYNAMIC_SECTION`. UUID + arg-key + value assertions only. Not an oracle test. | ## Exemplar archive diff --git a/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md b/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md index 0c3c0f08e8..b155b6d685 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md @@ -11,6 +11,8 @@ Two independent comparisons confirm output equivalence between SIMPLNX and DREAM 1. **SmallIN100 (in-unit-test):** `FillBadData_SmallIN100` compares SIMPLNX output against `6_5_exemplar.dream3d` with `MinAllowedDefectSize=1000`, `StoreAsNewPhase=false`. Passes — no differences. 2. **Test 08 A/B run (2026-07-03):** A 7×7×3 synthetic fixture with disconnected bad-data regions (one small → fills, one large → preserved) was executed through DREAM3D 6.5.171, 6.5.172, and DREAM3D-NX. All three produced identical FeatureIds output (147 voxels, element-wise match). See `fill_bad_data_vv/comparison_report.md`. +> **⚠ Evidence archival (open action):** `test_08_input.dream3d` and `fill_bad_data_vv/comparison_report.md` are **not** present in the committed `6_5_fill_bad_data.tar.gz` archive (which contains fixtures for Tests 01–07, 11, 13 only) and are not in the source tree, so this Test 08 A/B is **not reproducible or reviewable from the repository**. The fixture, both legacy runs, and the comparison report must be uploaded to the OneDrive verification archive (per the archive-filter-verification workflow) — or added to the test-data archive — and this note replaced with the archive link before the filter is promoted to COMPLETE. The SmallIN100 in-test comparison (Class 2) remains reproducible in the meantime. + ## Comparison method ### SmallIN100 (automated, in-unit-test) From 785e8e1f876d104fca4f23e028a83e56741c2240 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 8 Jul 2026 21:58:50 -0400 Subject: [PATCH 6/6] =?UTF-8?q?DOC:=20FillBadData=20=E2=80=94=20withdraw?= =?UTF-8?q?=20the=20unrecoverable=20Test=2008=20A/B=20claim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Test 08 three-way binary A/B (6.5.171/6.5.172/NX) cited by the deviations and report had working files (test_08_input.dream3d, comparison_report.md) that were never archived and are unrecoverable. Rather than leave an unverifiable citation, the claim is withdrawn: the legacy comparison now rests on the SmallIN100 in-test check plus the Class 1 analytical fixtures. Test 08 removed from the deviations method table, the report At-a-glance, and the V&V-phase outstanding list (it is no longer a gate to COMPLETE). Signed-off-by: Michael Jackson --- .../SimplnxCore/vv/FillBadDataFilter.md | 15 ++++++------- .../vv/deviations/FillBadDataFilter.md | 21 +++---------------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md b/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md index 160e365ffa..92fe8168e5 100644 --- a/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/FillBadDataFilter.md @@ -7,7 +7,7 @@ | SIMPLNX Human Name | Fill Bad Data | | DREAM3D 6.5.171 equivalent | `FillBadData` — SIMPL UUID `30ae0a1e-3d94-5dab-b279-c5727ab5d7ff` | | Verified commit | ** | -| Status | **READY FOR REVIEW** (second-engineer review + Test 08 evidence archival outstanding — see V&V phase) | +| Status | **READY FOR REVIEW** (second-engineer review outstanding — see V&V phase) | | Sign-off | *pending second-engineer review* | ## At a glance @@ -19,9 +19,9 @@ | Code paths | **15 of 16** covered. Only gap: `m_ShouldCancel` cancel path in Phase 4 while-loop (Path 14). | | Tests | **14 TEST_CASEs**, all pass. 1 SmallIN100 (Class 2) + 9 OOC synthetic fixtures (Class 1, Tests 01–07, 11, 13) + 1 all-bad-data termination guard + 1 preflight-error inline (asserts `-16500`) + 1 SIMPL backwards-compat. | | Exemplar archive | `6_5_fill_bad_data.tar.gz` — `6_5_input/exemplar.dream3d` (Class 2) + `test_NN_input/expected.dream3d` pairs for Tests 01–07, 11, 13 (Class 1). *(Tests 08–10 and 12 have no fixtures — the numbering is non-contiguous.)* | -| Legacy comparison | SmallIN100 in-test (Class 2). The Test 08 binary A/B (6.5.171 vs 6.5.172 vs NX) is **not reproducible from the committed archive** — its fixture and comparison report were not included (see V&V phase / deviations); this is an open archival action, so the legacy comparison is not yet independently reviewable. | +| Legacy comparison | SmallIN100 in-test (Class 2) — SIMPLNX matches the 6.5.x exemplar element-wise. A separate Test 08 three-way binary A/B was cited by an earlier revision but its working files are unrecoverable, so that claim is **withdrawn** (see deviations). Per-code-path correctness is pinned by the Class 1 analytical fixtures. | | Bug flags | `FillBadDataFilter-B1` (preflight dead-return for `minAllowedDefectSize < 1`) resolved. Fixed this cycle: an all-bad-data / enclosed-bad-pocket input previously looped forever in Phase 4 (no fillable neighbor → `count` never reached 0); a no-progress guard now stops with a warning. | -| V&V phase | **READY FOR REVIEW.** Outstanding: (1) second-engineer spot-check of Tests 11/13; (2) the Test 08 A/B fixture + `comparison_report.md` must be added to the archive (or the Test 08 claim dropped) before COMPLETE; (3) cancel path (Path 14) untested. | +| V&V phase | **READY FOR REVIEW.** Outstanding: (1) second-engineer spot-check of Tests 11/13; (2) cancel path (Path 14) untested. *(The unrecoverable Test 08 A/B claim has been withdrawn — no longer a gate.)* | ## Summary @@ -40,7 +40,7 @@ The SIMPLNX algorithm (`Algorithms/FillBadData.cpp`, ~765 lines) introduces a 4- SIMPL UUID mapping is preserved via `SimplnxCoreLegacyUUIDMapping.hpp` and SIMPL conversion fixtures (`test/simpl_conversion/6_5/` and `6_4/FillBadDataFilter.json`). -*Behavioral equivalence evidence:* `FillBadData_SmallIN100` passes against `6_5_exemplar.dream3d` (Small IN100, threshold=1000). Manual A/B on Test 08 (7×7×3, threshold=10) shows identical FeatureIds output across 6.5.171, 6.5.172, and NX. +*Behavioral equivalence evidence:* `FillBadData_SmallIN100` passes against `6_5_exemplar.dream3d` (Small IN100, threshold=1000). (A separate Test 08 three-way A/B was previously cited but its working files are unrecoverable; that claim is withdrawn — see deviations.) ## Oracle @@ -109,9 +109,10 @@ Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/FillBadData. ## Deviations from DREAM3D 6.5.171 -No deviations observed. Two independent comparisons confirm output equivalence: +No deviations observed. The legacy comparison is the SmallIN100 in-test check: -1. `FillBadData_SmallIN100` passes against `6_5_exemplar.dream3d` (Small IN100, MinAllowedDefectSize=1000). -2. Independent A/B run (2026-07-03) on Test 08 (7×7×3, MinAllowedDefectSize=10) — DREAM3D 6.5.171, 6.5.172, and NX produce identical FeatureIds output (element-wise match, 147 voxels). +1. `FillBadData_SmallIN100` passes against `6_5_exemplar.dream3d` (Small IN100, MinAllowedDefectSize=1000) — SIMPLNX matches the 6.5.x exemplar element-wise. + +Per-code-path correctness is pinned independently by the Class 1 analytical fixtures (Tests 01–07, 11, 13). (An earlier revision also cited a Test 08 three-way binary A/B; its working files are unrecoverable, so that claim is withdrawn.) See `vv/deviations/FillBadDataFilter.md` for the full comparison record. diff --git a/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md b/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md index b155b6d685..dd8151e050 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/FillBadDataFilter.md @@ -6,12 +6,9 @@ Entries use stable IDs (`FillBadDataFilter-D` for legacy deviations, `FillBad ## Headline: No deviations observed -Two independent comparisons confirm output equivalence between SIMPLNX and DREAM3D 6.5.x: +The legacy comparison is the **SmallIN100 in-unit-test check**: `FillBadData_SmallIN100` compares SIMPLNX output against `6_5_exemplar.dream3d` (generated from a DREAM3D 6.5.x pipeline) with `MinAllowedDefectSize=1000`, `StoreAsNewPhase=false`. It passes — no differences. Correctness of the individual code paths is pinned independently by the Class 1 analytical fixtures (Tests 01–07, 11, 13). -1. **SmallIN100 (in-unit-test):** `FillBadData_SmallIN100` compares SIMPLNX output against `6_5_exemplar.dream3d` with `MinAllowedDefectSize=1000`, `StoreAsNewPhase=false`. Passes — no differences. -2. **Test 08 A/B run (2026-07-03):** A 7×7×3 synthetic fixture with disconnected bad-data regions (one small → fills, one large → preserved) was executed through DREAM3D 6.5.171, 6.5.172, and DREAM3D-NX. All three produced identical FeatureIds output (147 voxels, element-wise match). See `fill_bad_data_vv/comparison_report.md`. - -> **⚠ Evidence archival (open action):** `test_08_input.dream3d` and `fill_bad_data_vv/comparison_report.md` are **not** present in the committed `6_5_fill_bad_data.tar.gz` archive (which contains fixtures for Tests 01–07, 11, 13 only) and are not in the source tree, so this Test 08 A/B is **not reproducible or reviewable from the repository**. The fixture, both legacy runs, and the comparison report must be uploaded to the OneDrive verification archive (per the archive-filter-verification workflow) — or added to the test-data archive — and this note replaced with the archive link before the filter is promoted to COMPLETE. The SmallIN100 in-test comparison (Class 2) remains reproducible in the meantime. +> **Note — retired Test 08 A/B.** An earlier revision of this file cited a separate 7×7×3 three-way binary A/B run (DREAM3D 6.5.171 / 6.5.172 / DREAM3D-NX, `test_08_input.dream3d` + `fill_bad_data_vv/comparison_report.md`). Those working files were **not preserved and are unrecoverable**, so that comparison is **withdrawn** rather than left as an unverifiable citation. The SmallIN100 comparison above plus the analytical oracle fixtures are the standing evidence; if a second independent binary A/B is wanted for the record, it must be regenerated from scratch. ## Comparison method @@ -26,18 +23,6 @@ Two independent comparisons confirm output equivalence between SIMPLNX and DREAM | **Configurations** | In-core only. `StoreAsNewPhase=false`. | | **Result** | **Pass** | -### Test 08 binary A/B (manual, 2026-07-03) - -| | | -|---|---| -| **Comparison type** | Three independent runners against the same input file | -| **Runners** | DREAM3D 6.5.171 (`PipelineRunner`), DREAM3D 6.5.172 (`PipelineRunner`), DREAM3D-NX (`nxrunner`) | -| **Input** | `test_08_input.dream3d` — 7×7×3 ImageGeometry, FeatureIds array only | -| **Parameters** | `MinAllowedDefectSize=10`, `StoreAsNewPhase=false` | -| **Tolerance** | Element-wise integer equality, 147-voxel FeatureIds array | -| **Configurations** | Disconnected small region (1 voxel < 10 → filled), disconnected large region (14 voxels ≥ 10 → preserved), 4:1 neighbor vote majority | -| **Result** | **Pass** — all three runners match each other and the analytically derived expected values | - --- ## Non-deviations (documented for awareness) @@ -46,7 +31,7 @@ Two independent comparisons confirm output equivalence between SIMPLNX and DREAM SIMPLNX uses chunk-sequential CCL with Union-Find (Phases 1–3) to identify connected bad-data regions before applying the size threshold. Legacy DREAM3D 6.5.171 used a simpler in-memory approach. For in-core datasets the functional output is identical; the CCL architecture is a strategy for OOC correctness, not a behavioral change. -**Evidence:** `FillBadData_SmallIN100` passes; Test 08 A/B confirms agreement across both legacy versions and NX. +**Evidence:** `FillBadData_SmallIN100` passes (SIMPLNX output matches the 6.5.x exemplar element-wise). ### Tie-breaking in Phase 4 iterative fill