diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CreateFeatureArrayFromElementArray.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CreateFeatureArrayFromElementArray.cpp index 97c4a5bbed..7f936077c7 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CreateFeatureArrayFromElementArray.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CreateFeatureArrayFromElementArray.cpp @@ -2,7 +2,6 @@ #include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" -#include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/Utilities/FilterUtilities.hpp" using namespace nx::core; @@ -17,12 +16,12 @@ struct CopyCellDataFunctor const auto& selectedCellStore = selectedCellArray->template getIDataStoreRefAs>(); auto& createdDataStore = createdArray->template getIDataStoreRefAs>(); - usize totalCellArrayComponents = selectedCellStore.getNumberOfComponents(); + const usize totalCellArrayComponents = selectedCellStore.getNumberOfComponents(); std::map featureMap; Result<> result; - usize totalCellArrayTuples = selectedCellStore.getNumberOfTuples(); + const usize totalCellArrayTuples = selectedCellStore.getNumberOfTuples(); for(usize cellTupleIdx = 0; cellTupleIdx < totalCellArrayTuples; cellTupleIdx++) { if(shouldCancel) @@ -30,17 +29,17 @@ struct CopyCellDataFunctor return {}; } - // Get the feature identifier (or what ever the user has selected as their "Feature" identifier - int32 featureIdx = featureIds[cellTupleIdx]; + // Get the feature identifier (or whatever the user has selected as their "Feature" identifier + const int32 featureIdx = featureIds[cellTupleIdx]; // Store the index of the first tuple with this feature identifier in the map - if(featureMap.find(featureIdx) == featureMap.end()) + if(!featureMap.contains(featureIdx)) { featureMap[featureIdx] = totalCellArrayComponents * cellTupleIdx; } // Check that the values at the current index match the value at the first index - usize firstInstanceCellTupleIdx = featureMap[featureIdx]; + const usize firstInstanceCellTupleIdx = featureMap[featureIdx]; for(usize cellCompIdx = 0; cellCompIdx < totalCellArrayComponents; cellCompIdx++) { T firstInstanceCellVal = selectedCellStore[firstInstanceCellTupleIdx + cellCompIdx]; @@ -83,13 +82,32 @@ Result<> CreateFeatureArrayFromElementArray::operator()() auto* createdArray = m_DataStructure.getDataAs(createdArrayPath); // Resize the created array to the proper size - usize featureIdsMaxIdx = std::distance(featureIdsRef.begin(), std::max_element(featureIdsRef.cbegin(), featureIdsRef.cend())); - usize maxValue = featureIdsRef[featureIdsMaxIdx]; + const usize featureIdsMaxIdx = std::distance(featureIdsRef.begin(), std::max_element(featureIdsRef.cbegin(), featureIdsRef.cend())); + const int32 maxValue = featureIdsRef[featureIdsMaxIdx]; + + // Validate no underflow + if(maxValue < 0) + { + return MakeErrorResult(-81880, "Invalid Input, Feature Ids Array must contain a positive value"); + } + auto& cellFeatureAttrMat = m_DataStructure.getDataRefAs(m_InputValues->CellFeatureAttributeMatrixPath); - auto* createdArrayStore = createdArray->template getIDataStoreAs(); - createdArrayStore->resizeTuples(std::vector{maxValue + 1}); - cellFeatureAttrMat.resizeTuples(std::vector{maxValue + 1}); + // validate resize won't shrink child arrays + if(maxValue + 1 > cellFeatureAttrMat.getNumberOfTuples()) + { + for(const auto& childObject : cellFeatureAttrMat) + { + const auto* iArray = dynamic_cast(childObject.second.get()); + if(iArray != nullptr && iArray->getNumberOfTuples() > (maxValue + 1)) + { + return MakeErrorResult(-81881, fmt::format("Resizing would cause data loss in {}. Make sure all objects in {} have tuple counts equal to or less then the max Feature ID {}!", + iArray->getName(), m_InputValues->CellFeatureAttributeMatrixPath.toString(), maxValue + 1)); + } + } + + cellFeatureAttrMat.resizeTuples(std::vector{static_cast(maxValue) + 1}); + } return ExecuteDataFunction(CopyCellDataFunctor{}, selectedCellArray->getDataType(), selectedCellArray, featureIdsRef, createdArray, m_ShouldCancel); } diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CreateFeatureArrayFromElementArrayFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CreateFeatureArrayFromElementArrayFilter.cpp index 81e9b6d415..4dd6f12790 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CreateFeatureArrayFromElementArrayFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/CreateFeatureArrayFromElementArrayFilter.cpp @@ -4,7 +4,6 @@ #include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/DataStructure/DataPath.hpp" -#include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/Filter/Actions/CreateArrayAction.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" #include "simplnx/Parameters/AttributeMatrixSelectionParameter.hpp" @@ -51,8 +50,7 @@ Parameters CreateFeatureArrayFromElementArrayFilter::parameters() const // Create the parameter descriptors that are needed for this filter params.insertSeparator(Parameters::Separator{"Input Data"}); - params.insert( - std::make_unique(k_SelectedCellArrayPath_Key, "Data to Copy to Feature Data", "Element Data to Copy to Feature Data", DataPath{}, nx::core::GetAllDataTypes())); + params.insert(std::make_unique(k_SelectedCellArrayPath_Key, "Data to Copy to Feature Data", "Element Data to Copy to Feature Data", DataPath{}, GetAllDataTypes())); params.insert(std::make_unique(k_CellFeatureIdsArrayPath_Key, "Cell Feature Ids", "Specifies to which feature each cell belongs.", DataPath({"Cell Data", "FeatureIds"}), ArraySelectionParameter::AllowedTypes{DataType::int32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); params.insertSeparator(Parameters::Separator{"Input Feature Data"}); @@ -81,15 +79,15 @@ IFilter::UniquePointer CreateFeatureArrayFromElementArrayFilter::clone() const IFilter::PreflightResult CreateFeatureArrayFromElementArrayFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { - auto pSelectedCellArrayPathValue = filterArgs.value(k_SelectedCellArrayPath_Key); - auto pFeatureIdsArrayPathValue = filterArgs.value(k_CellFeatureIdsArrayPath_Key); - auto pCellFeatureAttributeMatrixPathValue = filterArgs.value(k_CellFeatureAttributeMatrixPath_Key); - auto pCreatedArrayNameValue = filterArgs.value(k_CreatedArrayName_Key); + const auto pSelectedCellArrayPathValue = filterArgs.value(k_SelectedCellArrayPath_Key); + const auto pFeatureIdsArrayPathValue = filterArgs.value(k_CellFeatureIdsArrayPath_Key); + const auto pCellFeatureAttributeMatrixPathValue = filterArgs.value(k_CellFeatureAttributeMatrixPath_Key); + const auto pCreatedArrayNameValue = filterArgs.value(k_CreatedArrayName_Key); const auto& selectedCellArray = dataStructure.getDataRefAs(pSelectedCellArrayPathValue); const IDataStore& selectedCellArrayStore = selectedCellArray.getIDataStoreRef(); - nx::core::Result resultOutputActions; + Result resultOutputActions; std::vector preflightUpdatedValues; // Get the target Attribute Matrix that the output array will be stored with diff --git a/src/Plugins/SimplnxCore/test/CreateFeatureArrayFromElementArrayTest.cpp b/src/Plugins/SimplnxCore/test/CreateFeatureArrayFromElementArrayTest.cpp index 52e9643be8..fd337402f4 100644 --- a/src/Plugins/SimplnxCore/test/CreateFeatureArrayFromElementArrayTest.cpp +++ b/src/Plugins/SimplnxCore/test/CreateFeatureArrayFromElementArrayTest.cpp @@ -2,14 +2,13 @@ #include "SimplnxCore/SimplnxCore_test_dirs.hpp" #include "simplnx/Core/Application.hpp" -#include "simplnx/Parameters/ArrayCreationParameter.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/Pipeline/Pipeline.hpp" #include "simplnx/Pipeline/PipelineFilter.hpp" #include "simplnx/UnitTest/UnitTestCommon.hpp" #include #include -#include namespace fs = std::filesystem; using namespace nx::core; @@ -22,20 +21,19 @@ const std::string k_Computed_CellData("Computed_CellData"); template void testElementArray(const DataPath& cellDataPath) { - UnitTest::LoadPlugins(); - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_5_test_data_1_v2.tar.gz", "6_5_test_data_1_v2"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "6_5_test_data_1_v2.tar.gz", "6_5_test_data_1_v2"); // Read the Small IN100 Data set - auto baseDataFilePath = fs::path(fmt::format("{}/6_5_test_data_1_v2/6_5_test_data_1_v2.dream3d", nx::core::unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/6_5_test_data_1_v2/6_5_test_data_1_v2.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); - DataPath smallIn100Group({nx::core::Constants::k_DataContainer}); + DataPath smallIn100Group({Constants::k_DataContainer}); // This section creates the needed AttributeMatrix of size 1. The filter should be resizing as needed. { AttributeMatrix::Create(dataStructure, k_Computed_CellData, std::vector{1}, dataStructure.getId(smallIn100Group)); } - DataPath featureIdsDataPath = smallIn100Group.createChildPath(nx::core::Constants::k_CellData).createChildPath(k_FeatureIDs); + DataPath featureIdsDataPath = smallIn100Group.createChildPath(Constants::k_CellData).createChildPath(k_FeatureIDs); DataPath computedFeatureGroupPath = smallIn100Group.createChildPath(k_Computed_CellData); DataPath computedFeatureArrayPath = computedFeatureGroupPath.createChildPath(cellDataPath.getTargetName()); @@ -79,31 +77,361 @@ void testElementArray(const DataPath& cellDataPath) } } // namespace -TEST_CASE("SimplnxCore::CreateFeatureArrayFromElementArrayFilter: Valid filter execution - 1 Component") +namespace AnalyticalFixtures { - UnitTest::LoadPlugins(); +constexpr StringLiteral k_ParentDGName = "ParentGroup"; +const DataPath k_ParentDGPath({k_ParentDGName}); + +const DataPath k_CellAMPath = k_ParentDGPath.createChildPath(Constants::k_CellData); +const DataPath k_FeatureIdsPath = k_CellAMPath.createChildPath(Constants::k_FeatureIds); +const DataPath k_InputCellPath = k_CellAMPath.createChildPath(Constants::k_FaceData); - DataPath smallIn100Group({nx::core::Constants::k_DataContainer}); - DataPath cellDataPath = smallIn100Group.createChildPath(nx::core::Constants::k_CellData).createChildPath(nx::core::Constants::k_ConfidenceIndex); +const DataPath k_FeatureAMPath = k_ParentDGPath.createChildPath(Constants::k_FeatureData); +const DataPath k_OutputFeaturePath = k_FeatureAMPath.createChildPath("OutputFeatureArray"); +} // namespace AnalyticalFixtures + +TEST_CASE("SimplnxCore::CreateFeatureArrayFromElementArrayFilter: Valid filter execution - 1 Component") +{ + DataPath smallIn100Group({Constants::k_DataContainer}); + DataPath cellDataPath = smallIn100Group.createChildPath(Constants::k_CellData).createChildPath(Constants::k_ConfidenceIndex); testElementArray(cellDataPath); } TEST_CASE("SimplnxCore::CreateFeatureArrayFromElementArrayFilter: Valid filter execution - 3 Component") { - UnitTest::LoadPlugins(); - - DataPath smallIn100Group({nx::core::Constants::k_DataContainer}); - DataPath cellDataPath = smallIn100Group.createChildPath(nx::core::Constants::k_CellData).createChildPath(nx::core::Constants::k_IPFColors); + DataPath smallIn100Group({Constants::k_DataContainer}); + DataPath cellDataPath = smallIn100Group.createChildPath(Constants::k_CellData).createChildPath(Constants::k_IPFColors); testElementArray(cellDataPath); } +TEST_CASE("SimplnxCore::CreateFeatureArrayFromElementArrayFilter: AnalyticalFixtures — AF-1 single-component consistent", "[SimplnxCore][CreateFeatureArrayFromElementArrayFilter][AnalyticalFixtures]") +{ + // Oracle class: Class 1 (Analytical) + Class 4 (Invariant) + // featureIds = [0, 1, 2, 1, 2] + // cellValues (float32, 1-comp) = [5.0, 10.0, 20.0, 10.0, 20.0] + // Hand derivation: + // feature 0: cell 0 only → output[0] = 5.0 + // feature 1: cell 1 (10.0) then cell 3 (10.0) — same, no warning → output[1] = 10.0 + // feature 2: cell 2 (20.0) then cell 4 (20.0) — same, no warning → output[2] = 20.0 + // Expected output: [5.0, 10.0, 20.0], 0 warnings + DataStructure ds; + // Creation + { + auto* topGroup = DataGroup::Create(ds, AnalyticalFixtures::k_ParentDGName); + auto* cellAM = AttributeMatrix::Create(ds, AnalyticalFixtures::k_CellAMPath.getTargetName(), ShapeType{5ULL}, topGroup->getId()); + AttributeMatrix::Create(ds, AnalyticalFixtures::k_FeatureAMPath.getTargetName(), ShapeType{1ULL}, topGroup->getId()); + + auto* fidsArray = Int32Array::CreateWithStore>(ds, AnalyticalFixtures::k_FeatureIdsPath.getTargetName(), cellAM->getShape(), ShapeType{1ULL}, cellAM->getId()); + (*fidsArray)[0] = 0; + (*fidsArray)[1] = 1; + (*fidsArray)[2] = 2; + (*fidsArray)[3] = 1; + (*fidsArray)[4] = 2; + + auto* cellFloatArray = Float32Array::CreateWithStore>(ds, AnalyticalFixtures::k_InputCellPath.getTargetName(), cellAM->getShape(), ShapeType{1ULL}, cellAM->getId()); + (*cellFloatArray)[0] = 5.0f; + (*cellFloatArray)[1] = 10.0f; + (*cellFloatArray)[2] = 20.0f; + (*cellFloatArray)[3] = 10.0f; + (*cellFloatArray)[4] = 20.0f; + } + + // Execution + { + CreateFeatureArrayFromElementArrayFilter filter; + Arguments args; + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_SelectedCellArrayPath_Key, std::make_any(AnalyticalFixtures::k_InputCellPath)); + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(AnalyticalFixtures::k_FeatureIdsPath)); + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_CellFeatureAttributeMatrixPath_Key, std::make_any(AnalyticalFixtures::k_FeatureAMPath)); + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_CreatedArrayName_Key, std::make_any(AnalyticalFixtures::k_OutputFeaturePath.getTargetName())); + + auto preflightResult = filter.preflight(ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + REQUIRE(executeResult.result.warnings().empty()); // Class 4: 0 warnings when all features are consistent + } + + // Validation + { + REQUIRE_NOTHROW(ds.getDataRefAs(AnalyticalFixtures::k_InputCellPath)); + const auto& inputArray = ds.getDataRefAs(AnalyticalFixtures::k_InputCellPath); + const auto& outArray = ds.getDataRefAs(AnalyticalFixtures::k_OutputFeaturePath); + + // Class 4 invariants: shape, type, and component count + REQUIRE(outArray.getNumberOfTuples() == 3); // max(featureIds)+1 = 2+1 = 3 + REQUIRE(outArray.getDataType() == inputArray.getDataType()); + REQUIRE(outArray.getNumberOfComponents() == inputArray.getNumberOfComponents()); + + // Class 1 expected values (hand-derived) + REQUIRE(outArray[0] == 5.0f); + REQUIRE(outArray[1] == 10.0f); + REQUIRE(outArray[2] == 20.0f); + + UnitTest::CheckArraysInheritTupleDims(ds); + } +} + +TEST_CASE("SimplnxCore::CreateFeatureArrayFromElementArrayFilter: AnalyticalFixtures — AF-2 single-component inconsistent", + "[SimplnxCore][CreateFeatureArrayFromElementArrayFilter][AnalyticalFixtures]") +{ + // Oracle class: Class 1 (Analytical) + Class 4 (Invariant) + // featureIds = [1, 2, 1, 2] + // cellValues (float32, 1-comp) = [10.0, 20.0, 15.0, 20.0] + // Hand derivation: + // feature 0: never written → 0.0 (CreateArrayAction fill="0") + // feature 1: cell 0 (10.0) then cell 2 (15.0) — differ → one warning; last-writer → output[1] = 15.0 + // feature 2: cell 1 (20.0) then cell 3 (20.0) — same, no additional warning → output[2] = 20.0 + // Expected output: [0.0, 15.0, 20.0], exactly 1 warning + DataStructure ds; + // Creation + { + auto* topGroup = DataGroup::Create(ds, AnalyticalFixtures::k_ParentDGName); + auto* cellAM = AttributeMatrix::Create(ds, AnalyticalFixtures::k_CellAMPath.getTargetName(), ShapeType{4ULL}, topGroup->getId()); + AttributeMatrix::Create(ds, AnalyticalFixtures::k_FeatureAMPath.getTargetName(), ShapeType{1ULL}, topGroup->getId()); + + auto* fidsArray = Int32Array::CreateWithStore>(ds, AnalyticalFixtures::k_FeatureIdsPath.getTargetName(), cellAM->getShape(), ShapeType{1ULL}, cellAM->getId()); + (*fidsArray)[0] = 1; + (*fidsArray)[1] = 2; + (*fidsArray)[2] = 1; + (*fidsArray)[3] = 2; + + auto* cellFloatArray = Float32Array::CreateWithStore>(ds, AnalyticalFixtures::k_InputCellPath.getTargetName(), cellAM->getShape(), ShapeType{1ULL}, cellAM->getId()); + (*cellFloatArray)[0] = 10.0f; + (*cellFloatArray)[1] = 20.0f; + (*cellFloatArray)[2] = 15.0f; + (*cellFloatArray)[3] = 20.0f; + } + + // Execution + { + CreateFeatureArrayFromElementArrayFilter filter; + Arguments args; + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_SelectedCellArrayPath_Key, std::make_any(AnalyticalFixtures::k_InputCellPath)); + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(AnalyticalFixtures::k_FeatureIdsPath)); + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_CellFeatureAttributeMatrixPath_Key, std::make_any(AnalyticalFixtures::k_FeatureAMPath)); + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_CreatedArrayName_Key, std::make_any(AnalyticalFixtures::k_OutputFeaturePath.getTargetName())); + + auto preflightResult = filter.preflight(ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + REQUIRE(executeResult.result.warnings().size() == 1); // Class 4: exactly 1 warning (guarded by result.warnings().empty()) + } + + // Validation + { + REQUIRE_NOTHROW(ds.getDataRefAs(AnalyticalFixtures::k_InputCellPath)); + const auto& inputArray = ds.getDataRefAs(AnalyticalFixtures::k_InputCellPath); + const auto& outArray = ds.getDataRefAs(AnalyticalFixtures::k_OutputFeaturePath); + + // Class 4 invariants: shape, type, and component count + REQUIRE(outArray.getNumberOfTuples() == 3); // max(featureIds)+1 = 2+1 = 3 + REQUIRE(outArray.getDataType() == inputArray.getDataType()); + REQUIRE(outArray.getNumberOfComponents() == inputArray.getNumberOfComponents()); + + // Class 1 expected values (hand-derived) + REQUIRE(outArray[0] == 0.0f); // feature 0: never written; fill value = "0" + REQUIRE(outArray[1] == 15.0f); // feature 1: last-writer cell 2 wins (15.0 over first-seen 10.0) + REQUIRE(outArray[2] == 20.0f); // feature 2: consistent + + UnitTest::CheckArraysInheritTupleDims(ds); + } +} + +TEST_CASE("SimplnxCore::CreateFeatureArrayFromElementArrayFilter: AnalyticalFixtures — AF-3 three-component consistent", "[SimplnxCore][CreateFeatureArrayFromElementArrayFilter][AnalyticalFixtures]") +{ + // Oracle class: Class 1 (Analytical) + Class 4 (Invariant) + // featureIds = [1, 2, 1, 2] + // cellValues (uint8, 3-comp): cell0=[10,20,30], cell1=[40,50,60], cell2=[10,20,30], cell3=[40,50,60] + // Hand derivation: + // feature 0: never written → [0, 0, 0] + // feature 1: cell 0 [10,20,30] then cell 2 [10,20,30] — same per component → no warning → output[1] = [10, 20, 30] + // feature 2: cell 1 [40,50,60] then cell 3 [40,50,60] — same per component → no warning → output[2] = [40, 50, 60] + // Expected output: [[0,0,0], [10,20,30], [40,50,60]], 0 warnings + DataStructure ds; + // Creation + { + auto* topGroup = DataGroup::Create(ds, AnalyticalFixtures::k_ParentDGName); + auto* cellAM = AttributeMatrix::Create(ds, AnalyticalFixtures::k_CellAMPath.getTargetName(), ShapeType{4ULL}, topGroup->getId()); + AttributeMatrix::Create(ds, AnalyticalFixtures::k_FeatureAMPath.getTargetName(), ShapeType{1ULL}, topGroup->getId()); + + auto* fidsArray = Int32Array::CreateWithStore>(ds, AnalyticalFixtures::k_FeatureIdsPath.getTargetName(), cellAM->getShape(), ShapeType{1ULL}, cellAM->getId()); + (*fidsArray)[0] = 1; + (*fidsArray)[1] = 2; + (*fidsArray)[2] = 1; + (*fidsArray)[3] = 2; + + auto* cellRGBArray = UInt8Array::CreateWithStore>(ds, AnalyticalFixtures::k_InputCellPath.getTargetName(), cellAM->getShape(), ShapeType{3ULL}, cellAM->getId()); + // cell 0 → [10, 20, 30] + (*cellRGBArray)[0 * 3 + 0] = 10; + (*cellRGBArray)[0 * 3 + 1] = 20; + (*cellRGBArray)[0 * 3 + 2] = 30; + // cell 1 → [40, 50, 60] + (*cellRGBArray)[1 * 3 + 0] = 40; + (*cellRGBArray)[1 * 3 + 1] = 50; + (*cellRGBArray)[1 * 3 + 2] = 60; + // cell 2 → [10, 20, 30] + (*cellRGBArray)[2 * 3 + 0] = 10; + (*cellRGBArray)[2 * 3 + 1] = 20; + (*cellRGBArray)[2 * 3 + 2] = 30; + // cell 3 → [40, 50, 60] + (*cellRGBArray)[3 * 3 + 0] = 40; + (*cellRGBArray)[3 * 3 + 1] = 50; + (*cellRGBArray)[3 * 3 + 2] = 60; + } + + // Execution + { + CreateFeatureArrayFromElementArrayFilter filter; + Arguments args; + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_SelectedCellArrayPath_Key, std::make_any(AnalyticalFixtures::k_InputCellPath)); + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(AnalyticalFixtures::k_FeatureIdsPath)); + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_CellFeatureAttributeMatrixPath_Key, std::make_any(AnalyticalFixtures::k_FeatureAMPath)); + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_CreatedArrayName_Key, std::make_any(AnalyticalFixtures::k_OutputFeaturePath.getTargetName())); + + auto preflightResult = filter.preflight(ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + REQUIRE(executeResult.result.warnings().empty()); // Class 4: 0 warnings when all features are consistent + } + + // Validation + { + REQUIRE_NOTHROW(ds.getDataRefAs(AnalyticalFixtures::k_InputCellPath)); + const auto& inputArray = ds.getDataRefAs(AnalyticalFixtures::k_InputCellPath); + const auto& outArray = ds.getDataRefAs(AnalyticalFixtures::k_OutputFeaturePath); + + // Class 4 invariants: shape, type, and component count + REQUIRE(outArray.getNumberOfTuples() == 3); // max(featureIds)+1 = 2+1 = 3 + REQUIRE(outArray.getDataType() == inputArray.getDataType()); + REQUIRE(outArray.getNumberOfComponents() == inputArray.getNumberOfComponents()); + + // Class 1 expected values (hand-derived) + // feature 0: never written → fill = [0, 0, 0] + REQUIRE(outArray[0 * 3 + 0] == 0); + REQUIRE(outArray[0 * 3 + 1] == 0); + REQUIRE(outArray[0 * 3 + 2] == 0); + // feature 1: cells 0 and 2 both [10, 20, 30] + REQUIRE(outArray[1 * 3 + 0] == 10); + REQUIRE(outArray[1 * 3 + 1] == 20); + REQUIRE(outArray[1 * 3 + 2] == 30); + // feature 2: cells 1 and 3 both [40, 50, 60] + REQUIRE(outArray[2 * 3 + 0] == 40); + REQUIRE(outArray[2 * 3 + 1] == 50); + REQUIRE(outArray[2 * 3 + 2] == 60); + + UnitTest::CheckArraysInheritTupleDims(ds); + } +} + +TEST_CASE("SimplnxCore::CreateFeatureArrayFromElementArrayFilter: AnalyticalFixtures — AF-4 error path all-negative featureIds", + "[SimplnxCore][CreateFeatureArrayFromElementArrayFilter][AnalyticalFixtures]") +{ + // Oracle class: Class 4 (Invariant) + // featureIds = [-1, -2, -1] — all negative; std::max_element returns -1 + // maxValue = -1 < 0 → MakeErrorResult(-81880, ...) + // Expected: preflight succeeds; execute fails with error code -81880 + DataStructure ds; + // Creation + { + auto* topGroup = DataGroup::Create(ds, AnalyticalFixtures::k_ParentDGName); + auto* cellAM = AttributeMatrix::Create(ds, AnalyticalFixtures::k_CellAMPath.getTargetName(), ShapeType{3ULL}, topGroup->getId()); + AttributeMatrix::Create(ds, AnalyticalFixtures::k_FeatureAMPath.getTargetName(), ShapeType{3ULL}, topGroup->getId()); + + auto* fidsArray = Int32Array::CreateWithStore>(ds, AnalyticalFixtures::k_FeatureIdsPath.getTargetName(), cellAM->getShape(), ShapeType{1ULL}, cellAM->getId()); + (*fidsArray)[0] = -1; + (*fidsArray)[1] = -2; + (*fidsArray)[2] = -1; + + auto* cellFloatArray = Float32Array::CreateWithStore>(ds, AnalyticalFixtures::k_InputCellPath.getTargetName(), cellAM->getShape(), ShapeType{1ULL}, cellAM->getId()); + (*cellFloatArray)[0] = 1.0f; + (*cellFloatArray)[1] = 2.0f; + (*cellFloatArray)[2] = 1.0f; + } + + // Execution + { + CreateFeatureArrayFromElementArrayFilter filter; + Arguments args; + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_SelectedCellArrayPath_Key, std::make_any(AnalyticalFixtures::k_InputCellPath)); + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(AnalyticalFixtures::k_FeatureIdsPath)); + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_CellFeatureAttributeMatrixPath_Key, std::make_any(AnalyticalFixtures::k_FeatureAMPath)); + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_CreatedArrayName_Key, std::make_any(AnalyticalFixtures::k_OutputFeaturePath.getTargetName())); + + auto preflightResult = filter.preflight(ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(ds, args); + REQUIRE_FALSE(executeResult.result.valid()); + REQUIRE(executeResult.result.errors()[0].code == -81880); + } +} + +TEST_CASE("SimplnxCore::CreateFeatureArrayFromElementArrayFilter: AnalyticalFixtures — AF-5 error path shrink-protection guard", + "[SimplnxCore][CreateFeatureArrayFromElementArrayFilter][AnalyticalFixtures]") +{ + // Oracle class: Class 4 (Invariant) + // Feature AM: 2 tuples; SiblingArray child created directly with 5 tuples (AM tuple count not cascaded) + // featureIds = [1, 2, 3, 2] → maxValue = 3, maxValue+1 = 4 + // 4 > AM.tupleCount=2 → outer grow condition fires + // SiblingArray.getNumberOfTuples()=5 > 4 → shrink-protection inner check fires → MakeErrorResult(-81881, ...) + // Expected: preflight succeeds; execute fails with error code -81881 + DataStructure ds; + const DataPath k_SiblingArrayPath = AnalyticalFixtures::k_FeatureAMPath.createChildPath("SiblingArray"); + + // Creation + { + auto* topGroup = DataGroup::Create(ds, AnalyticalFixtures::k_ParentDGName); + auto* cellAM = AttributeMatrix::Create(ds, AnalyticalFixtures::k_CellAMPath.getTargetName(), ShapeType{4ULL}, topGroup->getId()); + auto* featureAM = AttributeMatrix::Create(ds, AnalyticalFixtures::k_FeatureAMPath.getTargetName(), ShapeType{2ULL}, topGroup->getId()); + + // Child array created directly with 5 tuples — more than AM.tupleCount=2 and more than maxValue+1=4 + auto* siblingArray = Float32Array::CreateWithStore>(ds, k_SiblingArrayPath.getTargetName(), featureAM->getShape(), ShapeType{1ULL}, featureAM->getId()); + siblingArray->resizeTuples(ShapeType{5ULL}); + + auto* fidsArray = Int32Array::CreateWithStore>(ds, AnalyticalFixtures::k_FeatureIdsPath.getTargetName(), cellAM->getShape(), ShapeType{1ULL}, cellAM->getId()); + (*fidsArray)[0] = 1; + (*fidsArray)[1] = 2; + (*fidsArray)[2] = 3; + (*fidsArray)[3] = 2; + + auto* cellFloatArray = Float32Array::CreateWithStore>(ds, AnalyticalFixtures::k_InputCellPath.getTargetName(), cellAM->getShape(), ShapeType{1ULL}, cellAM->getId()); + (*cellFloatArray)[0] = 10.0f; + (*cellFloatArray)[1] = 20.0f; + (*cellFloatArray)[2] = 30.0f; + (*cellFloatArray)[3] = 20.0f; + } + + // Execution + { + CreateFeatureArrayFromElementArrayFilter filter; + Arguments args; + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_SelectedCellArrayPath_Key, std::make_any(AnalyticalFixtures::k_InputCellPath)); + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(AnalyticalFixtures::k_FeatureIdsPath)); + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_CellFeatureAttributeMatrixPath_Key, std::make_any(AnalyticalFixtures::k_FeatureAMPath)); + args.insertOrAssign(CreateFeatureArrayFromElementArrayFilter::k_CreatedArrayName_Key, std::make_any(AnalyticalFixtures::k_OutputFeaturePath.getTargetName())); + + auto preflightResult = filter.preflight(ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(ds, args); + REQUIRE_FALSE(executeResult.result.valid()); + REQUIRE(executeResult.result.errors()[0].code == -81881); + } +} + TEST_CASE("SimplnxCore::CreateFeatureArrayFromElementArrayFilter: SIMPL Backwards Compatibility", "[SimplnxCore][CreateFeatureArrayFromElementArrayFilter][BackwardsCompatibility]") { auto app = Application::GetOrCreateInstance(); 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" / "CreateFeatureArrayFromElementArrayFilter.json"}, diff --git a/src/Plugins/SimplnxCore/vv/CreateFeatureArrayFromElementArrayFilter.md b/src/Plugins/SimplnxCore/vv/CreateFeatureArrayFromElementArrayFilter.md new file mode 100644 index 0000000000..b450961671 --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/CreateFeatureArrayFromElementArrayFilter.md @@ -0,0 +1,155 @@ +# V&V Report: CreateFeatureArrayFromElementArrayFilter + +| | | +|---|---| +| Plugin | SimplnxCore | +| SIMPLNX Human Name | Create Feature Array from Element Array | +| SIMPLNX UUID | `50e1be47-b027-4f40-8f70-1283682ee3e7` | +| DREAM3D 6.5.171 equivalent | `CreateFeatureArrayFromElementArray` (SIMPL UUID `94438019-21bb-5b61-a7c3-66974b9a34dc`) | +| Verified commit | ** | +| Status | **COMPLETE** | +| Sign-off | *Nathan Young, 07-28-2026* | + +## At a glance + +| Aspect | Current state | +|---|---| +| Algorithm Relationship | **Port** — the per-cell copy loop is a line-by-line translation of SIMPL `CreateFeatureArrayFromElementArray`. UUID changed from SIMPL; legacy alias maintained via `FromSIMPLJson()` and SIMPL conversion fixtures. The sizing logic differs only in the AM under-sized case: SIMPL errors; SIMPLNX resizes and succeeds (see D1 in deviations). All pipelines that succeeded in SIMPL produce bit-identical output in SIMPLNX. | +| Oracle | **Class 1 (Analytical) primary + Class 4 (Invariant) companion** — expected outputs hand-derived for 5-fixture AnalyticalFixtures suite (AF-1: single-component consistent, AF-2: single-component inconsistent/warning, AF-3: 3-component consistent, AF-4: error -81880 all-negative featureIds, AF-5: error -81881 shrink-protection guard). Class 4 invariants: output array has `max(featureIds)+1` tuples; output data type and component shape match input; error codes for boundary inputs. Implemented as inline `REQUIRE` assertions in `test/CreateFeatureArrayFromElementArrayTest.cpp`. | +| Code paths enumerated | **8 of 9 paths exercised; 1 uncovered:** cancel check. Error paths -81880 and -81881 covered by AF-4 and AF-5. | +| Tests today | **8 test cases** (2 regression + 1 SIMPL backwards-compat + 5 AnalyticalFixtures). **Circular-oracle flag**: existing regression tests compare SIMPLNX output against legacy-generated `CellFeatureData` arrays pre-existing in `6_5_test_data_1_v2.dream3d`; not a valid correctness oracle per policy. AF-1 through AF-3 provide the independent Class 1 + Class 4 output oracle; AF-4 and AF-5 cover error-path Class 4 invariants. | +| Exemplar archive | `6_5_test_data_1_v2.tar.gz` (SHA512 `585b51b…`) — shared input archive. Pre-existing `CellFeatureData` arrays within the dream3d are legacy-generated; serve only as regression baselines. No oracle-specific archive needed for Class 1 (oracle is inline assertions). | +| Legacy comparison | **Complete (2026-07-23) — one deviation identified, no migration impact.** Empirical A/B comparison on synthetic 8×1×1 fixtures: bit-identical. Static source analysis identified D1 (AM under-sized: SIMPL errors -5555; SIMPLNX resizes and succeeds). D1 is unreachable from any SIMPL pipeline that ran successfully, so output is bit-identical for the entire valid SIMPL migration space. See `vv/deviations/CreateFeatureArrayFromElementArrayFilter.md`. | +| Bug flags | **None** | +| V&V phase | **Complete.** Oracle designed (Class 1 + Class 4) and implemented; code-path analysis complete; Algorithm Relationship classified (Port); AnalyticalFixtures tests implemented (AF-1 through AF-5); legacy A/B comparison run (2026-07-23, bit-identical on full valid SIMPL migration space); one deviation (D1, SIMPLNX improvement) identified and documented. | + +## Summary + +`CreateFeatureArrayFromElementArrayFilter` copies each element-level data array value to the feature-level entry identified by the corresponding FeatureId, using last-writer-wins semantics, and emits one warning if any cell's value for a feature differs from the first-seen value. The filter was verified analytically using a Class 1 hand-derived oracle on three small synthetic fixtures covering consistent, inconsistent, and multi-component cases (AF-1, AF-2, AF-3), plus Class 4 invariants on output shape and type, and two error-path fixtures exercising the all-negative-featureIds guard (AF-4, error -81880) and the shrink-protection guard (AF-5, error -81881). All oracle assertions pass. The per-cell copy loop is a direct port of SIMPL `CreateFeatureArrayFromElementArray`; empirical A/B comparison (2026-07-23) confirmed bit-identical output for all inputs that SIMPL can process. One deviation exists (D1): SIMPLNX handles the AM under-sized case by resizing, whereas SIMPL errors with -5555. This deviation has no migration impact — it is unreachable from any pipeline that ran successfully in SIMPL. + +## Algorithm Relationship + +*Classification:* **Port** + +*Evidence:* The SIMPLNX algorithm at `Algorithms/CreateFeatureArrayFromElementArray.cpp` (113 lines) is a line-for-line translation of the SIMPL `CreateFeatureArrayFromElementArray` filter. Identical control flow: iterate over cell tuples, look up `featureIds[cellIdx]`, record first-seen tuple offset in `featureMap`, compare current value against first-seen per component, emit one warning on first mismatch, write current value to `createdDataStore[featureId * C + comp]` (last-writer-wins). The SIMPL UUID (`94438019-21bb-5b61-a7c3-66974b9a34dc`) differs from the SIMPLNX UUID (`50e1be47-b027-4f40-8f70-1283682ee3e7`) because the filter was re-UUID-ed during the SIMPL→SIMPLNX port; the legacy alias is maintained through `FromSIMPLJson()` and `test/simpl_conversion/6_5/CreateFeatureArrayFromElementArrayFilter.json`. + +*Port-time deltas that do not change output data:* + +1. `QMap` → `std::map` for `featureMap` — both are O(log n) sorted associative containers; SIMPL maps feature ID to a raw source pointer, SIMPLNX maps it to a flat element index. Lookup semantics and iteration order over cells are unchanged. +2. SIMPL `EXECUTE_FUNCTION_TEMPLATE` macro → `ExecuteDataFunction(CopyCellDataFunctor{}, ...)` dispatch — API modernization; identical runtime dispatch on element data type. +3. Warning API: SIMPL used `filter->setWarningCondition(-1000, ss)` guarded by `bool warningThrown = false;`; SIMPLNX uses `result.warnings().push_back(Warning{-1000, ...})` guarded by `result.warnings().empty()`. Both guards produce the same behavior: exactly **one** warning per execution when any feature's cell values are inconsistent. Confirmed by empirical A/B comparison (2026-07-23) — no behavioral delta. +4. Algorithm class extracted from `executeImpl` into `Algorithms/CreateFeatureArrayFromElementArray.{hpp,cpp}` — code organization change only (PRs #1301 and #1544). +5. Added `shouldCancel` check inside the per-cell loop — no output change (cancel path was absent in SIMPL). + +*Material PRs:* + +- **#1301** — "ENH: Move Execution to Algorithm Classes": extracted algorithm class skeleton. +- **#1544** — "ENH: Move non-trivial Filter executeImpl() logic to Algorithm classes": finalized algorithm class with resize logic. +- **#1278** — "BUG: Ensure FeatureId arrays are range checked against the Feature Attribute Matrix": Updated help text and default value for parameter. +- **#1295** — "ENH: Add Fill Functionality to CreateArrayAction": output array initialized to `"0"` fill at creation; ensures feature-0 slot has a defined value when no cell maps to feature 0. + +## Oracle + +*Class:* **1 (Analytical)** primary, **4 (Invariant)** companion. + +*Applied (Class 1 — Analytical):* Expected outputs are hand-derived without reference to any DREAM3D implementation. The algorithm is a pure indirection: for each cell `i` in order, write `output[featureIds[i] * C + j] = cellInput[i * C + j]` for each component `j`. Last-writer-wins when multiple cells share a featureId. The output AM is resized to `max(featureIds[:]) + 1` tuples. + +**Fixture AF-1** — single-component, all values consistent: + +| | | +|---|---| +| `featureIds` | `[0, 1, 2, 1, 2]` | +| `cellValues` (float32, 1-comp) | `[5.0, 10.0, 20.0, 10.0, 20.0]` | + +Hand derivation: +- Feature 0: only cell 0 → `output[0] = 5.0` +- Feature 1: cell 1 (first) = `10.0`; cell 3 (second) = `10.0` (same → no warning); last-writer = `10.0` +- Feature 2: cell 2 (first) = `20.0`; cell 4 (second) = `20.0` (same → no warning); last-writer = `20.0` + +Expected output (float32, 3 tuples): `[5.0, 10.0, 20.0]`. Expected warnings: **0**. + +**Fixture AF-2** — single-component, inconsistent values (warning path): + +| | | +|---|---| +| `featureIds` | `[1, 2, 1, 2]` | +| `cellValues` (float32, 1-comp) | `[10.0, 20.0, 15.0, 20.0]` | + +Hand derivation: +- Feature 0: never written; default fill `"0"` → `output[0] = 0.0` +- Feature 1: cell 0 (first) = `10.0` written; cell 2 (second) = `15.0` ≠ `10.0` → **one warning emitted** (Warning -1000: "Elements from Feature 1 do not all have the same value…"); `output[1] = 15.0` (last-writer) +- Feature 2: cell 1 (first) = `20.0`; cell 3 (second) = `20.0` (same → no additional warning); `output[2] = 20.0` + +Expected output (float32, 3 tuples): `[0.0, 15.0, 20.0]`. Expected warnings: **1** (Warning -1000 for feature 1). + +**Fixture AF-3** — 3-component, consistent: + +| | | +|---|---| +| `featureIds` | `[1, 2, 1, 2]` | +| `cellValues` (uint8, 3-comp) | `cell0=[10,20,30]`, `cell1=[40,50,60]`, `cell2=[10,20,30]`, `cell3=[40,50,60]` | + +Hand derivation: +- Feature 0: never written → `output[0] = [0, 0, 0]` +- Feature 1: cell 0 `[10,20,30]`; cell 2 `[10,20,30]` (same → no warning); `output[1] = [10, 20, 30]` +- Feature 2: cell 1 `[40,50,60]`; cell 3 `[40,50,60]` (same → no warning); `output[2] = [40, 50, 60]` + +Expected output (uint8, 3 tuples): `[[0,0,0], [10,20,30], [40,50,60]]`. Expected warnings: **0**. + +*Applied (Class 4 — Invariant):* Derivable properties any valid output must satisfy, asserted inline: + +- `outputArray.getNumberOfTuples() == max(featureIds[:]) + 1` (output array resized consistently with AM) +- `outputArray.getDataType() == inputCellArray.getDataType()` +- `outputArray.getNumberOfComponents() == inputCellArray.getNumberOfComponents()` +- `result.warnings().size() == 0` when all cells for each feature are value-consistent; `result.warnings().size() == 1` when any mismatch exists (one-warning-only guard) +- execute returns error -81880 when all feature IDs are negative; error -81881 when an AM child array has more tuples than the resize target + +*Encoded:* Implemented — `AnalyticalFixtures` TEST_CASEs in `test/CreateFeatureArrayFromElementArrayTest.cpp` (AF-1 through AF-5). `getNumberOfTuples`, `getDataType`, `getNumberOfComponents` asserted in the Validation block of AF-1/AF-2/AF-3; `warnings().size()` asserted in the Execution block immediately after `filter.execute()`; error code invariants asserted in the Execution blocks of AF-4 and AF-5. + +*Second-engineer review:* Skipped — the Class 1 oracle (AF-1/AF-2/AF-3) is integer-indexed indirection arithmetic on 4–5 element fixtures. No mathematical ambiguity: the closed-form derivation is `output[featureId * C + comp] = input[cellIdx * C + comp]` (last-writer), traceable directly to lines 25–56 of `Algorithms/CreateFeatureArrayFromElementArray.cpp`. The Class 4 error-path invariants (AF-4/AF-5) assert single integer error codes against structurally constructed fixtures; no arithmetic review is required. Formal second-engineer review was not justified for either oracle type. + +## Code path coverage + +*8 of 9 paths exercised; 1 uncovered (cancel check).* + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/CreateFeatureArrayFromElementArray.cpp` (113 lines) + +The algorithm has two phases: **(a) `operator()()` sizing and guard logic** — scan `featureIds` for `maxValue`; error on all-negative values; when `maxValue + 1 > AM.tupleCount`, enter the grow block — inner shrink-protection loop iterates all AM children and returns error -81881 if any child's `getNumberOfTuples() > (maxValue + 1)`; if all pass, calls `cellFeatureAttrMat.resizeTuples({maxValue + 1})` which cascades to all AM children via `AttributeMatrix::resizeTuples`; otherwise skip the grow block entirely; **(b) `CopyCellDataFunctor` per-cell copy loop** — cancel check, `featureMap` first-encounter insertion, per-component value comparison with one-warning-only guard, last-writer-wins write. + +| # | Phase | Path | Test case | +|---|---|---|---| +| 1 | (a) Guard | `maxValue < 0` (all feature IDs are negative) → `MakeErrorResult(-81880, ...)` | AF-4: `featureIds = [-1, -2, -1]`; preflight succeeds; execute returns error -81880 | +| 2 | (a) Grow — shrink guard | `maxValue + 1 > AM.tupleCount` AND some AM child array has `getNumberOfTuples() > (maxValue + 1)` → `MakeErrorResult(-81881, ...)` — refuses to grow AM if doing so would shrink an independently oversized child | AF-5: Feature AM with 2 tuples; sibling child created directly with 5 tuples; `featureIds` max=3 → `maxValue+1=4`; outer fires (4>2), sibling check fires (5>4); execute returns error -81881 | +| 3 | (a) Grow | `maxValue + 1 > AM.tupleCount`, shrink check passes → `cellFeatureAttrMat.resizeTuples({maxValue + 1})` cascades to all AM children including `createdArray` | AF-1 (AM 1→3), AF-2 (AM 1→3), AF-3 (AM 1→3) — all fixtures start with a 1-tuple AM; output `getNumberOfTuples()` asserted via Class 4 invariant | +| 4 | (a) Skip | `maxValue + 1 <= AM.tupleCount` → grow block skipped | `Valid filter execution - 1 Component`, `Valid filter execution - 3 Component` — Small IN100 AM already correctly sized | +| 5 | (b) Cancel | `if(shouldCancel) return {}` at top of per-cell loop | *Not tested. Cancel-signal injection not implemented in any test fixture.* | +| 6 | (b) Per-cell | First encounter for a `featureId`: insert into `featureMap`; write value to output | AF-1, AF-2, AF-3 (all feature IDs present) | +| 7 | (b) Per-cell | Subsequent encounter, values match first-seen: no warning emitted; write value | AF-1 (features 1 and 2), AF-3 (features 1 and 2) | +| 8 | (b) Per-cell | Subsequent encounter, values differ: one warning total (guarded by `result.warnings().empty()`); write current value (last-writer-wins) | AF-2 (feature 1: `10.0` vs `15.0`) | +| 9 | (b) Per-cell | Multi-component inner loop: `totalCellArrayComponents > 1` — inner `for(cellCompIdx)` iterates C times per cell | AF-3 (C=3, uint8) | + +## Test inventory + +| Test case | Status | Notes | +|---|---|---| +| `Valid filter execution - 1 Component` | kept — regression | Loads Small IN100 via `6_5_test_data_1_v2.tar.gz`; runs filter on `CellData/ConfidenceIndex` (float32, 1-comp); compares output against `CellFeatureData/ConfidenceIndex` pre-existing in the dream3d file. Exercises path #4 (skip-grow: AM already correctly sized). **Circular-oracle flag**: `CellFeatureData/ConfidenceIndex` was generated by DREAM3D 6.5.x using the same-algorithm filter; comparison is legacy-output regression, not an independent oracle. Early-exit loop (breaks on first mismatch) instead of `CompareDataArrays` — all values compared only when all match, which is valid for the regression purpose. | +| `Valid filter execution - 3 Component` | kept — regression | Same as above on `CellData/IPFColors` (uint8, 3-comp). Same circular-oracle flag applies. Exercises paths #4 and #9 in the coverage table (skip-grow and multi-component). | +| `SIMPL Backwards Compatibility` | kept | Verifies `FromSIMPLJson()` mapping from both 6.5 UUID (`94438019-21bb-5b61-a7c3-66974b9a34dc`) and 6.4 filter-name (`CreateFeatureArrayFromElementArray`) formats. 4 parameter assertions each variant. Passes at HEAD. | +| `AnalyticalFixtures — AF-1` | implemented | Class 1 + Class 4 oracle; single-component float32, 5 cells, all consistent. Covers paths #3, #6, #7. Inline `REQUIRE` assertions per hand derivation in Oracle section. | +| `AnalyticalFixtures — AF-2` | implemented | Class 1 + Class 4 oracle; single-component float32, 4 cells, feature-1 inconsistent. Covers paths #3, #6, #8; verifies `warnings().size() == 1`. Inline `REQUIRE` assertions. | +| `AnalyticalFixtures — AF-3` | implemented | Class 1 + Class 4 oracle; 3-component uint8, 4 cells, all consistent. Covers paths #3, #6, #7, #9. Inline `REQUIRE` assertions per hand derivation. | +| `AnalyticalFixtures — AF-4` | implemented | Class 4 oracle (error invariant); `featureIds = [-1, -2, -1]`; preflight succeeds; execute returns error -81880. Covers path #1. | +| `AnalyticalFixtures — AF-5` | implemented | Class 4 oracle (error invariant); Feature AM 2 tuples, sibling child created with 5 tuples, `featureIds` max=3; preflight succeeds; execute returns error -81881. Covers path #2. | + +## Exemplar archive + +- **Archive:** `6_5_test_data_1_v2.tar.gz` (shared input archive — not an oracle-specific archive for this filter) +- **SHA512:** `585b51ba1da9784a204fe88073ca562b45afd7007cf451b0193079b885c4b4caff7cf21b13e016433b84155546ac0f73f003a8b8ebb1c58360b2c56de3027d6c` +- **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/CreateFeatureArrayFromElementArrayFilter.md` +- **Oracle note:** The Class 1 and Class 4 oracles are encoded entirely as inline `REQUIRE` assertions in the AnalyticalFixtures test cases. No oracle-specific exemplar archive is created or required; `6_5_test_data_1_v2.tar.gz` provides regression test input data only. + +## Deviations from DREAM3D 6.5.171 + +See `vv/deviations/CreateFeatureArrayFromElementArrayFilter.md`. + +**Legacy comparison complete (2026-07-23) — one deviation, no migration impact.** Empirical A/B comparison was bit-identical on the Exact Match fixture. Static source analysis identified one deviation (D1): SIMPLNX handles the AM under-sized case (`max(featureIds) + 1 > AM.tupleCount`) by resizing the AM to accommodate, while SIMPL errors with -5555. For the Exact Match and AM over-sized cases — the only cases reachable from a SIMPL pipeline that ran successfully — SIMPLNX output is bit-identical to SIMPL. See `vv/deviations/CreateFeatureArrayFromElementArrayFilter.md` for full analysis. diff --git a/src/Plugins/SimplnxCore/vv/deviations/CreateFeatureArrayFromElementArrayFilter.md b/src/Plugins/SimplnxCore/vv/deviations/CreateFeatureArrayFromElementArrayFilter.md new file mode 100644 index 0000000000..9969bb209a --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/deviations/CreateFeatureArrayFromElementArrayFilter.md @@ -0,0 +1,86 @@ +# Deviations from DREAM3D 6.5.171: CreateFeatureArrayFromElementArrayFilter + +This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. + +Entries are referenced by stable ID (`CreateFeatureArrayFromElementArray-D`) from the V&V report and from public migration guidance. The Filter UUID field is the permanent cross-reference anchor. + +--- + +## Headline + +**One behavioral deviation identified — SIMPLNX improvement over SIMPL.** The per-cell copy loop is a genuine port and produces bit-identical output for all inputs that SIMPL can process. The single deviation is that SIMPLNX handles the under-sized AM case (where `max(featureIds) + 1 > AM.tupleCount`) by resizing the AM to accommodate, whereas SIMPL errors in that case. All pipelines that succeeded in SIMPL will succeed identically in SIMPLNX. + +--- + +## Known deviations + +### CreateFeatureArrayFromElementArray-D1 — AM under-sized: SIMPL errors; SIMPLNX resizes and succeeds + +**Severity:** Low for migration — this deviation only occurs in configurations that SIMPL could not process at all (error -5555). No SIMPL pipeline that previously succeeded can produce this condition, so there is no output difference for any pipeline that ran to completion in SIMPL. + +**SIMPL behavior** (`execute()`, lines 226–252): +1. Reads `numFeatures = attributeMatrix.getNumberOfTuples()` — the pre-existing AM tuple count. +2. Scans all cells for `largestFeature = max(featureIds[:])`. +3. If `largestFeature >= numFeatures` → **error -5555** ("Attribute Matrix has N tuples but the input array has a Feature ID value of at least M"). No output is produced; the filter exits. +4. Otherwise → `copyCellData(..., numFeatures, ...)` creates a fresh `numFeatures`-tuple zero-initialized output array and fills it. AM tuple count is **never changed**. + +**SIMPLNX behavior** (`operator()()`, lines 84–112): +1. Computes `maxValue = max(featureIds[:])` via `std::max_element`. +2. If `maxValue < 0` → error -81880 (all-negative guard; SIMPL has undefined behavior in this case). +3. If `maxValue + 1 > cellFeatureAttrMat.getNumberOfTuples()`: + - Runs a shrink-protection loop over all AM children: if any child array has `getNumberOfTuples() > (maxValue + 1)` — meaning growing the AM to `maxValue + 1` would shrink that child — returns error -81881. Exercised by AF-5 (`test/CreateFeatureArrayFromElementArrayTest.cpp`). + - **Resizes the AM** via `cellFeatureAttrMat.resizeTuples({maxValue + 1})`. This cascades to all AM children (`AttributeMatrix::resizeTuples` iterates `findAllChildrenOfType()` and calls `array->resizeTuples(m_TupleShape)` on each), including the newly created output array. +4. Runs the copy loop. + +**Divergent outcomes by case:** + +| Case | Condition | SIMPL result | SIMPLNX result | +|---|---|---|---| +| **Exact match** | `max(featureIds) + 1 == AM.tupleCount` | SUCCESS — output has `AM.tupleCount` tuples | SUCCESS — resize block skipped; output has `AM.tupleCount` tuples (identical) | +| **AM over-sized** | `max(featureIds) + 1 < AM.tupleCount` | SUCCESS — output has `AM.tupleCount` tuples; trailing feature slots zero-filled | SUCCESS — resize block skipped; output has `AM.tupleCount` tuples; trailing slots retain `"0"` fill from `CreateArrayAction` (identical) | +| **AM under-sized** | `max(featureIds) + 1 > AM.tupleCount` | **ERROR -5555** — "Attribute Matrix has N tuples but Feature ID value is at least M" | **SUCCESS** — AM and all children (including output array) resized to `max(featureIds) + 1`; copy runs normally | + +**Why the A/B comparison missed this:** +The synthetic fixture (`FeatureIds=[1,2,1,2,1,2,1,2]`, `max=2`) was paired with an AM of exactly 3 tuples — the Exact Match case. The AM under-sized case requires a pipeline configuration that SIMPL would have rejected at runtime, so it is not reachable from any SIMPL-generated test data. + +**Migration impact:** None. Any pipeline that completed successfully in SIMPL had `max(featureIds) < numFeatures` by definition, placing it in the Exact Match or AM over-sized case — both of which produce identical output in SIMPLNX. The AM under-sized case is a new capability in SIMPLNX. + +--- + +### Note: All-negative FeatureIds — SIMPLNX improvement over SIMPL + +Documented for completeness; not a deviation that disadvantages SIMPLNX. + +If all feature IDs are negative, `maxValue < 0` → SIMPLNX returns clean error -81880. In SIMPL, `largestFeature` stays 0 (negative values never satisfy `m_FeatureIds[i] > largestFeature`); `mismatchedFeatures` stays false; `copyCellData()` is called; inside, `featureIdx` is negative and `fPtr + (numComp * featureIdx)` is a pointer before the start of the allocation — undefined behavior. SIMPLNX is strictly safer for this input. + +--- + +## Comparison method + +| | | +|---|---| +| **Comparison type** | Runtime A/B (both implementations executed on identical input) + static source analysis | +| **Fixture** | Synthetic 8×1×1 image geometry; `FeatureIds=[1,2,1,2,1,2,1,2]`; `CellFloat` (float32, 1-comp): `[10,20,30,20,10,20,30,20]`; `CellRGB` (uint8, 3-comp): `[[10,20,30],[40,50,60],[10,20,30],[40,50,60],[10,20,30],[40,50,60],[70,80,90],[40,50,60]]` | +| **Fixture coverage** | Exact Match case (`max(featureIds)+1 == AM.tupleCount`). The AM under-sized case cannot be generated from a SIMPL pipeline, so no A/B fixture for it exists. | +| **Tolerance** | Bit-identical (copy-only filter; no floating-point accumulation) | +| **Comparison driver** | `feature_from_element_vv/compare_outputs.py` | +| **Run date** | 2026-07-23 | +| **SIMPL runner** | `DREAM3D-6.5.171-Linux-x86_64/bin/PipelineRunner` | +| **NX runner** | `DREAM3DNX-Release-Linux-x64/Bin/nxrunner` | + +--- + +## Results + +| Test | SIMPL vs Oracle | NX vs Oracle | A/B | +|---|---|---|---| +| 1-component float32 (`CellFloat → FeatureFloat`) | PASS | PASS | MATCH | +| 3-component uint8 (`CellRGB → FeatureRGB`) | PASS | PASS | MATCH | + +**A/B result is complete for the relevant migration space.** All configurations that SIMPL can execute fall into the Exact Match or AM over-sized cases, where SIMPLNX output is bit-identical to SIMPL. D1 covers a configuration SIMPL could not produce output for, so no A/B comparison is possible or necessary for it. + +--- + +## Migration recommendation + +**No action required.** Any pipeline that ran successfully in SIMPL will produce bit-identical output in SIMPLNX. SIMPLNX additionally handles the AM under-sized case that SIMPL rejected with error -5555. diff --git a/src/Plugins/SimplnxCore/vv/provenance/CreateFeatureArrayFromElementArrayFilter.md b/src/Plugins/SimplnxCore/vv/provenance/CreateFeatureArrayFromElementArrayFilter.md new file mode 100644 index 0000000000..39ef9bb61e --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/provenance/CreateFeatureArrayFromElementArrayFilter.md @@ -0,0 +1,78 @@ +# Exemplar Archive Provenance: CreateFeatureArrayFromElementArrayFilter + +This sidecar documents the oracle and archive strategy for `CreateFeatureArrayFromElementArrayFilter`. It covers both the Class 1 analytical oracle (inline, no archive needed) and the shared input archive used by the existing regression tests. + +--- + +## Oracle identity + +| Field | Value | +|---|---| +| **Oracle class** | Class 1 (Analytical) primary + Class 4 (Invariant) companion | +| **Oracle archive** | *None — Class 1 and Class 4 oracles require no exemplar archive; expected values are encoded as inline `REQUIRE` assertions in test source* | +| **A/B comparison artifacts** | `OneDrive - feature_from_element_vv/` — `generate_input.py`, `pipeline_simpl_{1,3}comp.json`, `pipeline_nx_{1,3}comp.d3dpipeline`, `compare_outputs.py`, `run_all.sh` | +| **A/B run date** | 2026-07-23 | +| **A/B result** | PASS — bit-identical on 1-component float32 and 3-component uint8 fixtures | +| **Regression input archive** | `6_5_test_data_1_v2.tar.gz` (shared across multiple SimplnxCore tests; not an oracle artifact for this filter) | +| **Regression input SHA512** | `585b51ba1da9784a204fe88073ca562b45afd7007cf451b0193079b885c4b4caff7cf21b13e016433b84155546ac0f73f003a8b8ebb1c58360b2c56de3027d6c` | +| **Used by tests** | `Valid filter execution - 1 Component`, `Valid filter execution - 3 Component` (regression); `AnalyticalFixtures — AF-1`, `AF-2`, `AF-3`, `AF-4`, `AF-5` (V&V oracle, no archive) | +| **Generated by** | BlueQuartzSoftware (shared Small IN100 dataset; not generated for this filter specifically) | +| **Generated on** | Prior to simplnx V&V initiative (provenance of the `6_5_test_data_1_v2.tar.gz` archive is established by other filters' V&V records) | + +--- + +## How the oracle was established + +The `CreateFeatureArrayFromElementArrayFilter` implements a pure indirection copy: `output[featureId * C + comp] = input[cellIdx * C + comp]`, with last-writer-wins when multiple cells share a `featureId`. This operation has a trivial closed-form expected output for any hand-constructed input, making Class 1 (Analytical) the natural and lowest-drift oracle choice. + +Five analytical fixtures were designed and hand-derived without executing any DREAM3D implementation: + +- **AF-1** (single-component, consistent): 5 cells, `featureIds = [0,1,2,1,2]`, `values = [5.0, 10.0, 20.0, 10.0, 20.0]` → expected output `[5.0, 10.0, 20.0]`, 0 warnings. +- **AF-2** (single-component, inconsistent): 4 cells, `featureIds = [1,2,1,2]`, `values = [10.0, 20.0, 15.0, 20.0]` → expected output `[0.0, 15.0, 20.0]`, 1 warning (feature 1: first=10.0, last=15.0). +- **AF-3** (3-component, consistent): 4 cells, `featureIds = [1,2,1,2]`, uint8 3-comp values `[[10,20,30],[40,50,60],[10,20,30],[40,50,60]]` → expected output `[[0,0,0],[10,20,30],[40,50,60]]`, 0 warnings. + +The AF-1/AF-2/AF-3 derivations follow directly from the per-cell copy loop at `Algorithms/CreateFeatureArrayFromElementArray.cpp` lines 25–56 and are embedded as comments in the test cases. AF-4 and AF-5 are Class 4 error-path fixtures: AF-4 exercises the `maxValue < 0` guard at line 89 (error -81880); AF-5 exercises the shrink-protection loop at lines 99–107 (error -81881) by creating an AM child array with more tuples than the resize target. + +--- + +## Canonical oracle output + +The oracle output is encoded entirely as inline `REQUIRE` assertions — there is no oracle archive and no external script. + +| Oracle content | Source of expected values | +|---|---| +| `output[0] = 5.0` (AF-1, feature 0) | Class 1 hand derivation: only cell 0 maps to feature 0 | +| `output[1] = 10.0` (AF-1, feature 1) | Class 1: cells 1 and 3 both write 10.0 (consistent; last = first) | +| `output[2] = 20.0` (AF-1, feature 2) | Class 1: cells 2 and 4 both write 20.0 (consistent) | +| `output[0] = 0.0` (AF-2, feature 0) | Class 4 invariant: never written; CreateArrayAction fill="0" | +| `output[1] = 15.0` (AF-2, feature 1) | Class 1: last-writer cell 2 writes 15.0 (overrides 10.0) | +| `output[2] = 20.0` (AF-2, feature 2) | Class 1: cells 1 and 3 write 20.0 (consistent) | +| warning count == 1 (AF-2) | Class 4 invariant: `result.warnings().empty()` guard → exactly one warning | +| `output[1] = [10,20,30]` (AF-3) | Class 1: both cells mapping to feature 1 have value [10,20,30] | +| `output[2] = [40,50,60]` (AF-3) | Class 1: both cells mapping to feature 2 have value [40,50,60] | +| Output AM shape = `{maxFeatureId + 1}` (all fixtures) | Class 4 invariant: `cellFeatureAttrMat.resizeTuples({maxValue + 1})` at line 109 | + +--- + +## Oracle provenance + +Classes 1 and 4 require no external provenance block. The expected values are derivable by arithmetic from the input definition. The derivations are embedded as inline comments in the `AnalyticalFixtures` test functions. + +*No Class 2, 3, or 5 oracle was used for this filter.* + +--- + +## Circular-oracle note on existing regression tests + +The existing `Valid filter execution - 1 Component` and `Valid filter execution - 3 Component` tests compare SIMPLNX output against `CellFeatureData/ConfidenceIndex` and `CellFeatureData/IPFColors` arrays pre-existing in `6_5_test_data_1_v2.dream3d`. These feature-level arrays were generated by DREAM3D 6.5.x running the equivalent `CreateFeatureArrayFromElementArray` filter on the same cell data — making them **legacy output, not an independent oracle**. Per policy, "legacy DREAM3D 6.5.171 produced this output" is not a valid correctness oracle. + +The circular nature of these tests is mitigated for this specific filter because the algorithm is a pure copy with deterministic, analytically verifiable output. The AnalyticalFixtures tests (AF-1, AF-2, AF-3) provide the independent oracle; the existing regression tests are retained as integration coverage to catch regressions against known baseline behavior. + +*No archive regeneration is required.* The existing regression tests use the shared input archive as input data only; the oracle output lives in the test source as inline assertions. + +--- + +## Second-engineer oracle review + +- **Reviewer:** Skipped +- **Skip reason:** The Class 1 oracle (AF-1/AF-2/AF-3) is integer-indexed indirection arithmetic on 4–5 element fixtures — traceable by inspection to lines 25–56 of `Algorithms/CreateFeatureArrayFromElementArray.cpp`; the closed-form is `output[featureId * C + comp] = input[lastCellIdxForFeature * C + comp]`. The Class 4 error-path oracles (AF-4/AF-5) assert single integer error codes against structurally constructed fixtures with no arithmetic to review. A wrong Class 1 oracle would produce a test failure inconsistent with the SIMPL regression baseline, providing external cross-validation.