diff --git a/src/Plugins/SimplnxCore/docs/ComputeFeaturePhasesFilter.md b/src/Plugins/SimplnxCore/docs/ComputeFeaturePhasesFilter.md index 10d0166308..01fc15c074 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeFeaturePhasesFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeFeaturePhasesFilter.md @@ -6,7 +6,11 @@ Generic (Misc) ## Description -This **Filter** determines the **Ensemble** of each **Feature** by querying the **Ensemble** of the **Elements** that belong to the **Feature**. Note that it is assumed that all **Elements** belonging to a **Feature** are of the same **Feature**, and thus any **Element** can be used to determine the **Ensemble** of the **Feature** that owns that **Element**. +This **Filter** determines the **Ensemble** (phase) of each **Feature** by iterating over all **Elements** and writing `featurePhases[featureId] = cellPhase` for each cell. When all cells of a **Feature** share the same phase the result is unambiguous. When they differ, the last cell encountered (by ascending index order) wins and a warning is emitted listing up to 15 affected **Feature** IDs. + +**Background feature:** Cells with `featureId == 0` are skipped. `featurePhases[0]` is never written and will always be `0`; downstream filters should not rely on its value. + +**Errors:** The filter halts with an error if any cell phase value is negative, or if the **Cell Phases** and **Feature Ids** arrays have different tuple counts. % Auto generated parameter table will be inserted here diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeaturePhases.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeaturePhases.cpp index eb6ac06fae..4380012a4c 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeaturePhases.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeaturePhases.cpp @@ -1,12 +1,8 @@ #include "ComputeFeaturePhases.hpp" -#include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/Utilities/DataArrayUtilities.hpp" -#include -#include - using namespace nx::core; // ----------------------------------------------------------------------------- @@ -24,23 +20,29 @@ ComputeFeaturePhases::~ComputeFeaturePhases() noexcept = default; // ----------------------------------------------------------------------------- Result<> ComputeFeaturePhases::operator()() { - auto featurePhasesArrayPath = m_InputValues->CellFeaturesAttributeMatrixPath.createChildPath(m_InputValues->FeaturePhasesArrayName); + const auto featurePhasesArrayPath = m_InputValues->CellFeaturesAttributeMatrixPath.createChildPath(m_InputValues->FeaturePhasesArrayName); - const auto& cellPhases = m_DataStructure.getDataAs(m_InputValues->CellPhasesArrayPath)->getDataStoreRef(); + const auto& cellPhasesStore = m_DataStructure.getDataRefAs(m_InputValues->CellPhasesArrayPath).getDataStoreRef(); const auto& featureIdsArray = m_DataStructure.getDataRefAs(m_InputValues->FeatureIdsPath); - const auto& featureIds = featureIdsArray.getDataStoreRef(); - auto& featurePhases = m_DataStructure.getDataAs(featurePhasesArrayPath)->getDataStoreRef(); + const auto& featureIdsStore = featureIdsArray.getDataStoreRef(); + auto& featurePhasesStore = m_DataStructure.getDataRefAs(featurePhasesArrayPath).getDataStoreRef(); - // Validate the featurePhases array is the proper size + // Validate the featurePhases array sizing matches feature index values in featureIds (bounds check) and feature ids are all positive auto validateNumFeatResult = ValidateFeatureIdsToFeatureAttributeMatrixIndexing(m_DataStructure, m_InputValues->CellFeaturesAttributeMatrixPath, featureIdsArray, false, m_MessageHandler); if(validateNumFeatResult.invalid()) { return validateNumFeatResult; } - usize totalPoints = featureIds.getNumberOfTuples(); - std::map featureMap; - std::set warnFeatures; + const usize totalPoints = featureIdsStore.getNumberOfTuples(); + if(totalPoints != cellPhasesStore.getNumberOfTuples()) + { + return MakeErrorResult(-61860, "Size mismatch between cell feature indices and cell phases arrays."); + } + + const usize numFeatures = featurePhasesStore.getNumberOfTuples(); + std::vector initialFeaturePhase(numFeatures, -1); + std::vector warnFeature(numFeatures, false); for(usize i = 0; i < totalPoints; i++) { @@ -49,29 +51,60 @@ Result<> ComputeFeaturePhases::operator()() return {}; } - int32 gnum = featureIds[i]; - featureMap.insert({gnum, cellPhases[i]}); + const int32 featureId = featureIdsStore.getValue(i); + // Ignore 0, invalid feature + if(featureId == 0) + { + continue; + } + + const int32 currentPhaseId = cellPhasesStore.getValue(i); + + if(currentPhaseId < 0) + { + return MakeErrorResult(-61861, fmt::format("Cell phases contains a negative value. Index: {} | Value: {}", i, currentPhaseId)); + } - int32 curPhaseVal = featureMap[gnum]; - if(curPhaseVal != cellPhases[i]) + int32 storedPhaseId = initialFeaturePhase[featureId]; + if(storedPhaseId == -1) { - warnFeatures.insert(gnum); + initialFeaturePhase[featureId] = currentPhaseId; } - featurePhases[gnum] = cellPhases[i]; + else if(storedPhaseId != currentPhaseId) + { + warnFeature[featureId] = true; + } + + featurePhasesStore.setValue(static_cast(featureId), currentPhaseId); } Result<> result; - if(!warnFeatures.empty()) + + std::string warnStr = "Elements from some features did not all have the same phase ID. The last phase ID copied into each feature will be used. Affected Phase Features: "; + usize count = 0; + // Ignore 0 it is not a valid feature + for(usize i = 1; i < numFeatures; i++) { - std::string warnStr = "Elements from some features did not all have the same phase ID. The last phase ID copied into each feature will be used. Effected Phase Features: "; - usize position = 0; - for(auto value : warnFeatures) + if(warnFeature[i]) { - warnStr.append(std::to_string(value)); - if(++position != warnFeatures.size()) + if(count < 15) { - warnStr.append(", "); + if(count > 0) + { + warnStr.append(", "); + } + warnStr.append(std::to_string(i)); } + count++; + } + } + + if(count != 0) + { + if(count > 15) + { + usize remainder = count - 15; + warnStr.append(fmt::format(", and {} more {}", remainder, remainder == 1 ? "occurrence" : "occurrences")); } result.warnings().push_back(Warning{-500, std::move(warnStr)}); } diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeaturePhases.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeaturePhases.hpp index 806ba846a0..fe18d0b300 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeaturePhases.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeaturePhases.hpp @@ -9,17 +9,6 @@ #include "simplnx/Parameters/AttributeMatrixSelectionParameter.hpp" #include "simplnx/Parameters/DataObjectNameParameter.hpp" -/** -* This is example code to put in the Execute Method of the filter. - ComputeFeaturePhasesInputValues inputValues; - inputValues.CellFeaturesAttributeMatrixPath = filterArgs.value(cell_features_attribute_matrix_path); - inputValues.CellPhasesArrayPath = filterArgs.value(cell_phases_array_path); - inputValues.FeatureIdsPath = filterArgs.value(feature_ids_path); - inputValues.FeaturePhasesArrayName = filterArgs.value(feature_phases_array_name); - return ComputeFeaturePhases(dataStructure, messageHandler, shouldCancel, &inputValues)(); - -*/ - namespace nx::core { diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeaturePhasesFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeaturePhasesFilter.cpp index d06063041e..3143211642 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeaturePhasesFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeaturePhasesFilter.cpp @@ -86,7 +86,7 @@ IFilter::PreflightResult ComputeFeaturePhasesFilter::preflightImpl(const DataStr auto pCellFeatureAMPathValue = filterArgs.value(k_CellFeaturesAttributeMatrixPath_Key); auto pFeaturePhasesArrayPathValue = pCellFeatureAMPathValue.createChildPath(filterArgs.value(k_FeaturePhasesArrayName_Key)); - nx::core::Result resultOutputActions; + Result resultOutputActions; const auto& cellFeatData = dataStructure.getDataRefAs(pCellFeatureAMPathValue); diff --git a/src/Plugins/SimplnxCore/test/ComputeFeaturePhasesFilterTest.cpp b/src/Plugins/SimplnxCore/test/ComputeFeaturePhasesFilterTest.cpp index 78639ba148..4751242c58 100644 --- a/src/Plugins/SimplnxCore/test/ComputeFeaturePhasesFilterTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeFeaturePhasesFilterTest.cpp @@ -8,72 +8,412 @@ #include #include -#include namespace fs = std::filesystem; using namespace nx::core; -using namespace nx::core::Constants; -using namespace nx::core::UnitTest; -TEST_CASE("SimplnxCore::ComputeFeaturePhasesFilter(Valid Parameters)", "[SimplnxCore][ComputeFeaturePhasesFilter]") +namespace { - UnitTest::LoadPlugins(); +// Cell +const std::string k_CellAMName = "CellData"; +const DataPath k_CellAMPath({k_CellAMName}); +const std::string k_FeatureIdsName = "FeatureIds"; +const DataPath k_FeatureIdsPath = k_CellAMPath.createChildPath(k_FeatureIdsName); +const std::string k_CellPhasesName = "CellPhases"; +const DataPath k_CellPhasesPath = k_CellAMPath.createChildPath(k_CellPhasesName); + +// Feature +const std::string k_FeatureAMName = "FeatureData"; +const DataPath k_FeatureAMPath({k_FeatureAMName}); +const std::string k_PhasesName = "Phases"; +const DataPath k_FeaturePhasesPath = k_FeatureAMPath.createChildPath(k_PhasesName); +} // namespace + +// Case 1: 7 cells, 3 features, uniform phases throughout +TEST_CASE("SimplnxCore::ComputeFeaturePhasesFilter: Valid: Uniform Phases", "[SimplnxCore][ComputeFeaturePhasesFilter]") +{ + // featureIds = [1, 1, 2, 2, 2, 3, 3] + // cellPhases = [1, 1, 2, 2, 2, 1, 1] + // Expected: featurePhases = [0, 1, 2, 1]; 0 warnings. + + DataStructure dataStructure; + // Construction + { + auto* cellAM = AttributeMatrix::Create(dataStructure, k_CellAMName, ShapeType{7}); + AttributeMatrix::Create(dataStructure, k_FeatureAMName, ShapeType{4}); + + auto* featureIds = Int32Array::CreateWithStore>(dataStructure, k_FeatureIdsName, cellAM->getShape(), ShapeType{1}, cellAM->getId()); + auto* cellPhases = Int32Array::CreateWithStore>(dataStructure, k_CellPhasesName, cellAM->getShape(), ShapeType{1}, cellAM->getId()); + + const std::array fids = {1, 1, 2, 2, 2, 3, 3}; + const std::array cphases = {1, 1, 2, 2, 2, 1, 1}; + for(usize i = 0; i < 7; i++) + { + (*featureIds)[i] = fids[i]; + (*cellPhases)[i] = cphases[i]; + } + } + + // Execution + { + ComputeFeaturePhasesFilter filter; + Arguments args; + args.insert(ComputeFeaturePhasesFilter::k_CellPhasesArrayPath_Key, std::make_any(k_CellPhasesPath)); + args.insert(ComputeFeaturePhasesFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_FeatureIdsPath)); + args.insert(ComputeFeaturePhasesFilter::k_CellFeaturesAttributeMatrixPath_Key, std::make_any(k_FeatureAMPath)); + args.insert(ComputeFeaturePhasesFilter::k_FeaturePhasesArrayName_Key, std::make_any(k_PhasesName)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + REQUIRE(executeResult.result.warnings().empty()); + } + + // Validation + const auto& featurePhases = dataStructure.getDataRefAs(k_FeaturePhasesPath); + REQUIRE(featurePhases.getNumberOfTuples() == 4); + + const std::array expected = {0, 1, 2, 1}; + for(usize i = 0; i < 4; i++) + { + REQUIRE(featurePhases[i] == expected[i]); + } + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +// Case 2: empty feature in cells - ignore feature 0 +TEST_CASE("SimplnxCore::ComputeFeaturePhasesFilter: Valid: Feature 0 Skip", "[SimplnxCore][ComputeFeaturePhasesFilter]") +{ + // featureIds = [0, 0, 1, 1, 2, 2] + // cellPhases = [2, 2, 1, 1, 2, 2] + // Expected featurePhases = [0, 1, 2]; 0 warnings. + + DataStructure dataStructure; + // Construction + { + auto* cellAM = AttributeMatrix::Create(dataStructure, k_CellAMName, ShapeType{6}); + AttributeMatrix::Create(dataStructure, k_FeatureAMName, ShapeType{3}); - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_6_stats_test_v2.tar.gz", "6_6_stats_test_v2.dream3d"); - // Read the Small IN100 Data set - auto baseDataFilePath = fs::path(fmt::format("{}/6_6_stats_test_v2.dream3d", unit_test::k_TestFilesDir)); - DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); - DataPath smallIn100Group({nx::core::Constants::k_DataContainer}); - DataPath cellDataPath = smallIn100Group.createChildPath(nx::core::Constants::k_CellData); - DataPath cellFeatureDataPath = smallIn100Group.createChildPath(Constants::k_CellFeatureData); - DataPath cellPhasesPath = cellDataPath.createChildPath(nx::core::Constants::k_Phases); - DataPath featureIdsPath = cellDataPath.createChildPath(nx::core::Constants::k_FeatureIds); - std::string computedPrefix = "Computed_"; - std::string featurePhasesName = computedPrefix + nx::core::Constants::k_Phases.str(); - DataPath featurePhasesPath = cellFeatureDataPath.createChildPath(featurePhasesName); + auto* featureIds = Int32Array::CreateWithStore>(dataStructure, k_FeatureIdsName, cellAM->getShape(), ShapeType{1}, cellAM->getId()); + auto* cellPhases = Int32Array::CreateWithStore>(dataStructure, k_CellPhasesName, cellAM->getShape(), ShapeType{1}, cellAM->getId()); + const std::array fids = {0, 0, 1, 1, 2, 2}; + const std::array cphases = {2, 2, 1, 1, 2, 2}; + for(usize i = 0; i < 6; i++) + { + (*featureIds)[i] = fids[i]; + (*cellPhases)[i] = cphases[i]; + } + } + + // Execution { - ComputeFeaturePhasesFilter ffpFilter; + ComputeFeaturePhasesFilter filter; Arguments args; - args.insert(ComputeFeaturePhasesFilter::k_CellPhasesArrayPath_Key, std::make_any(cellPhasesPath)); - args.insert(ComputeFeaturePhasesFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(featureIdsPath)); - args.insert(ComputeFeaturePhasesFilter::k_CellFeaturesAttributeMatrixPath_Key, std::make_any(cellFeatureDataPath)); - args.insert(ComputeFeaturePhasesFilter::k_FeaturePhasesArrayName_Key, std::make_any(featurePhasesName)); + args.insert(ComputeFeaturePhasesFilter::k_CellPhasesArrayPath_Key, std::make_any(k_CellPhasesPath)); + args.insert(ComputeFeaturePhasesFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_FeatureIdsPath)); + args.insert(ComputeFeaturePhasesFilter::k_CellFeaturesAttributeMatrixPath_Key, std::make_any(k_FeatureAMPath)); + args.insert(ComputeFeaturePhasesFilter::k_FeaturePhasesArrayName_Key, std::make_any(k_PhasesName)); - auto preflightResult = ffpFilter.preflight(dataStructure, args); + auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); - auto result = ffpFilter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(result.result); + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + REQUIRE(executeResult.result.warnings().empty()); + } + + // Validation + const auto& featurePhases = dataStructure.getDataRefAs(k_FeaturePhasesPath); + REQUIRE(featurePhases.getNumberOfTuples() == 3); + + REQUIRE(featurePhases[0] == 0); // Must be zero since it is ignored + REQUIRE(featurePhases[1] == 1); + REQUIRE(featurePhases[2] == 2); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +// Case 3: 2 cells, mixed phase warning +TEST_CASE("SimplnxCore::ComputeFeaturePhasesFilter: Valid: Mixed Phase Warning", "[SimplnxCore][ComputeFeaturePhasesFilter]") +{ + // featureIds = [1, 1] + // cellPhases = [1, 2] — last-cell-wins: featurePhases[1] == 2 + // Expected: 1 warning (-500), feature 1 + + DataStructure dataStructure; + // Construction + { + auto* cellAM = AttributeMatrix::Create(dataStructure, k_CellAMName, ShapeType{2}); + AttributeMatrix::Create(dataStructure, k_FeatureAMName, ShapeType{2}); + + auto* featureIds = Int32Array::CreateWithStore>(dataStructure, k_FeatureIdsName, cellAM->getShape(), ShapeType{1}, cellAM->getId()); + auto* cellPhases = Int32Array::CreateWithStore>(dataStructure, k_CellPhasesName, cellAM->getShape(), ShapeType{1}, cellAM->getId()); + + (*featureIds)[0] = 1; + (*featureIds)[1] = 1; + (*cellPhases)[0] = 1; + (*cellPhases)[1] = 2; + } + + // Execution + ComputeFeaturePhasesFilter filter; + Arguments args; + args.insert(ComputeFeaturePhasesFilter::k_CellPhasesArrayPath_Key, std::make_any(k_CellPhasesPath)); + args.insert(ComputeFeaturePhasesFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_FeatureIdsPath)); + args.insert(ComputeFeaturePhasesFilter::k_CellFeaturesAttributeMatrixPath_Key, std::make_any(k_FeatureAMPath)); + args.insert(ComputeFeaturePhasesFilter::k_FeaturePhasesArrayName_Key, std::make_any(k_PhasesName)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + // Validation + REQUIRE(executeResult.result.warnings().size() == 1); + REQUIRE(executeResult.result.warnings().front().code == -500); + + // Warning message contains feature 1 + const std::string& warnMsg = executeResult.result.warnings().front().message; + REQUIRE(warnMsg.find("Features: 1") != std::string::npos); + + // Tie-break - Last in. featurePhases[1] == 2 + const auto& featurePhases = dataStructure.getDataRefAs(k_FeaturePhasesPath); + REQUIRE(featurePhases[1] == 2); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +// Case 4: Exceed Max Printable Mixed Features +TEST_CASE("SimplnxCore::ComputeFeaturePhasesFilter: Valid: Truncated Mixed Feature Warning", "[SimplnxCore][ComputeFeaturePhasesFilter]") +{ + // 32 cells; features 1–16 each have 2 cells with different phases (phase 1 then phase 2). + // Expected: warning with 15 feature ids listed + "and 1 more occurrence". + + static constexpr usize k_NumFeatures = 16; + static constexpr usize k_NumCells = k_NumFeatures * 2; - DataPath exemplaryDataPath = cellFeatureDataPath.createChildPath(nx::core::Constants::k_Phases); - const Int32Array& featureArrayExemplary = dataStructure.getDataRefAs(exemplaryDataPath); + DataStructure dataStructure; + // Construction + { + auto* cellAM = AttributeMatrix::Create(dataStructure, k_CellAMName, ShapeType{k_NumCells}); + AttributeMatrix::Create(dataStructure, k_FeatureAMName, ShapeType{k_NumFeatures + 1}); - const Int32Array& createdFeatureArray = dataStructure.getDataRefAs(featurePhasesPath); - REQUIRE(createdFeatureArray.getNumberOfTuples() == featureArrayExemplary.getNumberOfTuples()); + auto* featureIds = Int32Array::CreateWithStore>(dataStructure, k_FeatureIdsName, cellAM->getShape(), ShapeType{1}, cellAM->getId()); + auto* cellPhases = Int32Array::CreateWithStore>(dataStructure, k_CellPhasesName, cellAM->getShape(), ShapeType{1}, cellAM->getId()); - for(usize i = 0; i < featureArrayExemplary.getSize(); i++) + // Each feature f (1..16) gets cells at indices (f-1)*2 and (f-1)*2+1 with phases 1 and 2. + for(usize feature = 1; feature <= k_NumFeatures; feature++) { - REQUIRE(featureArrayExemplary[i] == createdFeatureArray[i]); + const usize base = (feature - 1) * 2; + (*featureIds)[base] = static_cast(feature); + (*featureIds)[base + 1] = static_cast(feature); + (*cellPhases)[base] = 1; + (*cellPhases)[base + 1] = 2; } } -// Write the DataStructure out to the file system -#ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fs::path(fmt::format("{}/find_feature_phases_filter.dream3d", unit_test::k_BinaryTestOutputDir))); -#endif + // Execution + ComputeFeaturePhasesFilter filter; + Arguments args; + args.insert(ComputeFeaturePhasesFilter::k_CellPhasesArrayPath_Key, std::make_any(k_CellPhasesPath)); + args.insert(ComputeFeaturePhasesFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_FeatureIdsPath)); + args.insert(ComputeFeaturePhasesFilter::k_CellFeaturesAttributeMatrixPath_Key, std::make_any(k_FeatureAMPath)); + args.insert(ComputeFeaturePhasesFilter::k_FeaturePhasesArrayName_Key, std::make_any(k_PhasesName)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + // Validation + REQUIRE(executeResult.result.warnings().size() == 1); + REQUIRE(executeResult.result.warnings().front().code == -500); + + // Truncation: 16 conflicts, 15 listed + "and 1 more occurrence" + const std::string& warnMsg = executeResult.result.warnings().front().message; + REQUIRE(warnMsg.find("and 1 more occurrence") != std::string::npos); UnitTest::CheckArraysInheritTupleDims(dataStructure); } +// Case 5: featureId out of bounds +TEST_CASE("SimplnxCore::ComputeFeaturePhasesFilter: Invalid: FeatureId Out Of Bounds", "[SimplnxCore][ComputeFeaturePhasesFilter]") +{ + // featureIds = [1, 5] — max=5 >= numFeatures=3 → out-of-bounds + // cellPhases = [1, 1] + // Expected: error code -5351 + + DataStructure dataStructure; + // Construction + { + auto* cellAM = AttributeMatrix::Create(dataStructure, k_CellAMName, ShapeType{2}); + AttributeMatrix::Create(dataStructure, k_FeatureAMName, ShapeType{3}); + + auto* featureIds = Int32Array::CreateWithStore>(dataStructure, k_FeatureIdsName, cellAM->getShape(), ShapeType{1}, cellAM->getId()); + auto* cellPhases = Int32Array::CreateWithStore>(dataStructure, k_CellPhasesName, cellAM->getShape(), ShapeType{1}, cellAM->getId()); + + (*featureIds)[0] = 1; + (*featureIds)[1] = 5; + (*cellPhases)[0] = 1; + (*cellPhases)[1] = 1; + } + + // Execution + ComputeFeaturePhasesFilter filter; + Arguments args; + args.insert(ComputeFeaturePhasesFilter::k_CellPhasesArrayPath_Key, std::make_any(k_CellPhasesPath)); + args.insert(ComputeFeaturePhasesFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_FeatureIdsPath)); + args.insert(ComputeFeaturePhasesFilter::k_CellFeaturesAttributeMatrixPath_Key, std::make_any(k_FeatureAMPath)); + args.insert(ComputeFeaturePhasesFilter::k_FeaturePhasesArrayName_Key, std::make_any(k_PhasesName)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(dataStructure, args); + + // Validation + SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); + REQUIRE(executeResult.result.errors().front().code == -5351); +} + +// Case 6: Different Input Array Sizes +TEST_CASE("SimplnxCore::ComputeFeaturePhasesFilter: Invalid: Cell Array Size Mismatch", "[SimplnxCore][ComputeFeaturePhasesFilter]") +{ + // Expected: error code -61860 + + const std::string k_CellFeatIdsAMName = "FeatureIdsAM"; + const DataPath k_CellFeatIdsAMPath({k_CellFeatIdsAMName}); + const std::string k_CellPhasesAMName = "CellPhasesAM"; + const DataPath k_CellPhasesAMPath({k_CellPhasesAMName}); + + DataStructure dataStructure; + // Construction + { + auto* cellFeatIdsAM = AttributeMatrix::Create(dataStructure, k_CellFeatIdsAMName, ShapeType{4}); + auto* cellPhasesAM = AttributeMatrix::Create(dataStructure, k_CellPhasesAMName, ShapeType{5}); + AttributeMatrix::Create(dataStructure, k_FeatureAMName, ShapeType{3}); + + auto* featureIds = Int32Array::CreateWithStore>(dataStructure, k_FeatureIdsName, cellFeatIdsAM->getShape(), ShapeType{1}, cellFeatIdsAM->getId()); + Int32Array::CreateWithStore>(dataStructure, k_CellPhasesName, cellPhasesAM->getShape(), ShapeType{1}, cellPhasesAM->getId()); + + (*featureIds)[0] = 1; + (*featureIds)[1] = 1; + (*featureIds)[2] = 1; + (*featureIds)[3] = 1; + } + + // Execution + ComputeFeaturePhasesFilter filter; + Arguments args; + args.insert(ComputeFeaturePhasesFilter::k_CellPhasesArrayPath_Key, std::make_any(k_CellPhasesAMPath.createChildPath(k_CellPhasesName))); + args.insert(ComputeFeaturePhasesFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_CellFeatIdsAMPath.createChildPath(k_FeatureIdsName))); + args.insert(ComputeFeaturePhasesFilter::k_CellFeaturesAttributeMatrixPath_Key, std::make_any(k_FeatureAMPath)); + args.insert(ComputeFeaturePhasesFilter::k_FeaturePhasesArrayName_Key, std::make_any(k_PhasesName)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(dataStructure, args); + + // Validation + SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); + REQUIRE(executeResult.result.errors().front().code == -61860); +} + +// Case 7: Negative Cell Phase +TEST_CASE("SimplnxCore::ComputeFeaturePhasesFilter: Invalid: Negative Cell Phase", "[SimplnxCore][ComputeFeaturePhasesFilter]") +{ + // featureIds = [1, 1] + // cellPhases = [1, -1] + // Expected: error code -61861 + + DataStructure dataStructure; + // Construction + { + auto* cellAM = AttributeMatrix::Create(dataStructure, k_CellAMName, ShapeType{2}); + AttributeMatrix::Create(dataStructure, k_FeatureAMName, ShapeType{2}); + + auto* featureIds = Int32Array::CreateWithStore>(dataStructure, k_FeatureIdsName, cellAM->getShape(), ShapeType{1}, cellAM->getId()); + auto* cellPhases = Int32Array::CreateWithStore>(dataStructure, k_CellPhasesName, cellAM->getShape(), ShapeType{1}, cellAM->getId()); + + (*featureIds)[0] = 1; + (*featureIds)[1] = 1; + (*cellPhases)[0] = 1; + (*cellPhases)[1] = -1; + } + + // Execution + ComputeFeaturePhasesFilter filter; + Arguments args; + args.insert(ComputeFeaturePhasesFilter::k_CellPhasesArrayPath_Key, std::make_any(k_CellPhasesPath)); + args.insert(ComputeFeaturePhasesFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_FeatureIdsPath)); + args.insert(ComputeFeaturePhasesFilter::k_CellFeaturesAttributeMatrixPath_Key, std::make_any(k_FeatureAMPath)); + args.insert(ComputeFeaturePhasesFilter::k_FeaturePhasesArrayName_Key, std::make_any(k_PhasesName)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(dataStructure, args); + + // Validation + SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); + REQUIRE(executeResult.result.errors().front().code == -61861); +} + +// Case 8: Negative featureId value +TEST_CASE("SimplnxCore::ComputeFeaturePhasesFilter: Invalid: Negative FeatureId", "[SimplnxCore][ComputeFeaturePhasesFilter]") +{ + // featureIds = [-1, 1] — negative featureId → validator error -5355 + // cellPhases = [1, 1] + // Expected: error code -5355 + + DataStructure dataStructure; + // Construction + { + auto* cellAM = AttributeMatrix::Create(dataStructure, k_CellAMName, ShapeType{2}); + AttributeMatrix::Create(dataStructure, k_FeatureAMName, ShapeType{3}); + + auto* featureIds = Int32Array::CreateWithStore>(dataStructure, k_FeatureIdsName, cellAM->getShape(), ShapeType{1}, cellAM->getId()); + auto* cellPhases = Int32Array::CreateWithStore>(dataStructure, k_CellPhasesName, cellAM->getShape(), ShapeType{1}, cellAM->getId()); + + (*featureIds)[0] = -1; + (*featureIds)[1] = 1; + (*cellPhases)[0] = 1; + (*cellPhases)[1] = 1; + } + + // Execution + ComputeFeaturePhasesFilter filter; + Arguments args; + args.insert(ComputeFeaturePhasesFilter::k_CellPhasesArrayPath_Key, std::make_any(k_CellPhasesPath)); + args.insert(ComputeFeaturePhasesFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_FeatureIdsPath)); + args.insert(ComputeFeaturePhasesFilter::k_CellFeaturesAttributeMatrixPath_Key, std::make_any(k_FeatureAMPath)); + args.insert(ComputeFeaturePhasesFilter::k_FeaturePhasesArrayName_Key, std::make_any(k_PhasesName)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(dataStructure, args); + + // Validation + SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); + REQUIRE(executeResult.result.errors().front().code == -5355); +} + TEST_CASE("SimplnxCore::ComputeFeaturePhasesFilter: SIMPL Backwards Compatibility", "[SimplnxCore][ComputeFeaturePhasesFilter][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" / "ComputeFeaturePhasesFilter.json"}, @@ -100,8 +440,8 @@ TEST_CASE("SimplnxCore::ComputeFeaturePhasesFilter: SIMPL Backwards Compatibilit CHECK(pipelineFilter->getComments().empty()); const Arguments args = pipelineFilter->getArguments(); - CHECK(args.value(ComputeFeaturePhasesFilter::k_CellPhasesArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); - CHECK(args.value(ComputeFeaturePhasesFilter::k_CellFeatureIdsArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); + CHECK(args.value(ComputeFeaturePhasesFilter::k_CellPhasesArrayPath_Key) == DataPath({"DataContainer", k_CellAMName, "TestArray"})); + CHECK(args.value(ComputeFeaturePhasesFilter::k_CellFeatureIdsArrayPath_Key) == DataPath({"DataContainer", k_CellAMName, "TestArray"})); CHECK(args.value(ComputeFeaturePhasesFilter::k_CellFeaturesAttributeMatrixPath_Key) == DataPath({"DataContainer", "CellData"})); CHECK(args.value(ComputeFeaturePhasesFilter::k_FeaturePhasesArrayName_Key) == "TestArray"); } diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeaturePhasesFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeaturePhasesFilter.md new file mode 100644 index 0000000000..4c4e312558 --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/ComputeFeaturePhasesFilter.md @@ -0,0 +1,124 @@ +# V&V Report: ComputeFeaturePhasesFilter + +| | | +|---|---| +| **Plugin** | SimplnxCore | +| **SIMPLNX UUID** | `da5bb20e-4a8e-49d9-9434-fbab7bc434fc` | +| **DREAM3D 6.5.171 equivalent** | `FindFeaturePhases` (SIMPL UUID `6334ce16-cea5-5643-83b5-9573805873fa`) | +| **Verified commit** | *pending* | +| **Status** | COMPLETE | +| **Sign-off** | Nathan Young - 7/15/2026 | + +## At a glance + +| Aspect | State | +|---|---| +| Algorithm Relationship | **Port with post-port hardening** — core algorithm identical; D1–D4 hardening added post-Phase-9. See §Algorithm Relationship. | +| Oracle | **Class 1 + 4 (Analytical + Invariant)** — encoded. 8 inline TEST_CASEs cover all paths except cancel-signal injection. | +| Tests | 8 inline TEST_CASEs (Cases 1–8). `SIMPL Backwards Compatibility` retained. Old `(Valid Parameters)` circular-oracle test **retired**. | +| Legacy comparison | **Complete** (2026-07-10, 3 cases). `featurePhases[1..N]` bit-identical. D1 confirmed by Case C. See `vv/deviations/`. | +| Open items | None | + +## Summary + +`ComputeFeaturePhasesFilter` assigns each Feature an Ensemble phase by reading the Cell Phase of any Element belonging to that Feature, assuming all Cells of a Feature share one phase; a consolidated warning is emitted when they do not. + +**Headline result:** `featurePhases[1..N]` is bit-identical to DREAM3D 6.5.171 `FindFeaturePhases` on Cases A (consistent phases), B (inconsistent phases, warning), and C (background cells). Post-port hardening introduced four deviations D1–D4; none affect `featurePhases[1..N]` for valid inputs. D1 confirmed empirically by Case C: SIMPL writes `featurePhases[0]=2`; SIMPLNX leaves it `0`. + +The pre-V&V `(Valid Parameters)` test was a circular oracle (compared filter output against `Cell Feature Data/Phases` in `6_6_stats_test_v2.dream3d`, which was generated by this filter's SIMPL predecessor). It has been retired and replaced by 7 inline Class 1 analytical tests. + +## Algorithm Relationship + +*Classification:* **Port with post-port hardening** + +Core algorithm body (`featurePhases[featureIds[i]] = cellPhases[i]`) is structurally identical to `FindFeaturePhases::execute()`. Same SIMPL UUID retained. All differences are additions that do not change output for valid inputs. + +**Port-time additions:** +1. Cancel check (`m_ShouldCancel`) at top of per-cell loop (PR #1582) +2. `ValidateFeatureIdsToFeatureAttributeMatrixIndexing` guard before the loop (PR #1308) +3. Consolidated warning listing all affected feature IDs (PR #1455) + +**Post-Phase-9 hardening (2026-07-10):** +4. Background feature skip — `if(featureId == 0) { continue; }` (D1) +5. Negative cell phase guard — error -61861 (D2) +6. Cell array size mismatch guard — error -61860 (D3) +7. Warning truncation to 15 IDs + `"and N more occurrence(s)"` suffix (D4) + +**Material PRs since baseline (2025-10-01):** #1582, #1588, #1544, #1535, #1455, #1308 + +## Oracle + +*Class:* **1 (Analytical) primary + 4 (Invariant) companion** + +The algorithm is a simple indirection: `featurePhases[featureIds[i]] = cellPhases[i]`. For any toy dataset, `featurePhases[f]` equals the phase of the last cell belonging to feature `f` in index order. For consistent datasets the result is unambiguous. + +**Class 4 invariants:** +- `featurePhases[f] ∈ {phase values of cells with featureId == f}` for all `f > 0` +- `featurePhases` has exactly `numFeatures` tuples (matches Feature AM shape) +- Warning is emitted if and only if any `f > 0` has cells with differing phase values + +**Encoded:** 7 inline TEST_CASEs in `test/ComputeFeaturePhasesFilterTest.cpp`. See `vv/provenance/` for fixture derivations. + +## Code path coverage + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeaturePhases.cpp` + +| # | Path | Test fixture | +|---|---|---| +| 1a | Validator: featureId OOB → error -5351 | Case 5 | +| 1b | Validator: negative featureId → error -5355 | Case 8 | +| 2 | Size mismatch → error -61860 (D3) | Case 6 | +| 3 | `m_ShouldCancel` → early return | **Not tested** (cancel-signal injection not supported) | +| 4 | `featureId == 0` → `continue` (D1) | Case 2 | +| 5 | `currentPhaseId < 0` → error -61861 (D2) | Case 7 | +| 6 | First occurrence for feature: store phase | Cases 1, 2 | +| 7 | Subsequent occurrence, phase matches: no conflict | Case 1 | +| 8 | Subsequent occurrence, phase differs: `warnFeature[f] = true` | Cases 3, 4 | +| 9a | `count != 0` → build and push warning | Case 3 | +| 9b | `count > 15` → append truncation suffix | Case 4 | +| 10 | `count == 0` → return result with no warnings | Cases 1, 2 | + +## Test inventory + +| TEST_CASE | Status | Notes | +|---|---|---| +| `(Valid Parameters)` | **Retired** | Circular oracle — `Computed_Phases` compared against `Cell Feature Data/Phases` in `6_6_stats_test_v2.dream3d`, which was produced by this filter's SIMPL predecessor. Replaced by 7 Class 1 Oracle tests. | +| `Valid: Uniform Phases` | Active | Case 1: 7 cells, 3 features, consistent phases. Paths 6, 7, 10. | +| `Valid: Feature 0 Skip` | Active | Case 2: 6 cells, background + 2 features. D1 assertion. Paths 4, 10. | +| `Valid: Mixed Phase Warning` | Active | Case 3: 2 cells, phase conflict, warning -500. Paths 8, 9a. | +| `Valid: Truncated Mixed Feature Warning` | Active | Case 4: 16 conflicting features, truncation suffix. Path 9b. | +| `Invalid: FeatureId Out Of Bounds` | Active | Case 5: error -5351. Path 1a. | +| `Invalid: Cell Array Size Mismatch` | Active | Case 6: error -61860. Path 2. | +| `Invalid: Negative Cell Phase` | Active | Case 7: error -61861. Path 5. | +| `Invalid: Negative FeatureId` | Active | Case 8: error -5355. Path 1b. | +| `SIMPL Backwards Compatibility` | Kept, unchanged | `DYNAMIC_SECTION` over 6.4 and 6.5 SIMPL conversion fixtures; validates UUID, argument keys, and parameter conversion. | + +## Exemplar archive + +| Field | Value | +|---|---| +| **Archive** | `6_6_stats_test_v2.tar.gz` | +| **SHA512** | `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` | +| **Use for this filter** | `SIMPL Backwards Compatibility` test (parameter conversion only); no correctness oracle | +| **`Cell Feature Data/Phases` array** | Circular oracle — do NOT use as correctness reference | + +No new exemplar archive is needed. Oracle data is inlined in test source. + +## Deviations + +See `vv/deviations/ComputeFeaturePhasesFilter.md` for full entries and comparison run results. + +| ID | Summary | Affects `featurePhases[1..N]`? | Tested by | +|---|---|---|---| +| D1 | `featurePhases[0]` always 0 (background skip) | No — index 0 only | Case 2 | +| D2 | Negative cell phase → error -61861 | No — error path | Case 7 | +| D3 | Cell array size mismatch → error -61860 | No — error path | Case 6 | +| D4 | Warning: consolidated list, 15-ID truncation | No — warning text only | Cases 3, 4 | + +**D1 downstream note:** Consumers reading `featurePhases[0]` must treat it as undefined. + +--- + +## Note: ComputeFeaturePhasesBinaryFilter + +`ComputeFeaturePhasesBinaryFilter` (UUID `16010080-a913-443a-b5b3-bd43391fe3c0`, SIMPL equivalent `FindFeaturePhasesBinary` UUID `64d20c7b-697c-5ff1-9d1d-8a27b071f363`) is a distinct algorithm requiring its own V&V document at `vv/ComputeFeaturePhasesBinaryFilter.md`. Its existing execution test (`bin_feature_phases.tar.gz`) should be audited for circular-oracle issues. diff --git a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeaturePhasesFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeaturePhasesFilter.md new file mode 100644 index 0000000000..1846462ee9 --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeaturePhasesFilter.md @@ -0,0 +1,83 @@ +# Deviations from DREAM3D 6.5.171: ComputeFeaturePhasesFilter + +Behavioral differences between `ComputeFeaturePhasesFilter` and `FindFeaturePhases` (SIMPL UUID `6334ce16-cea5-5643-83b5-9573805873fa`). All four deviations were introduced in post-Phase-9 algorithm cleanup (branch `vv/fill_bad_data`, 2026-07-10). None affect `featurePhases[1..N]` for valid inputs. + +--- + +## D1 — Background feature (index 0) excluded from phase assignment + +| Field | Value | +|---|---| +| **Affects output** | Yes — `featurePhases[0]` only | +| **Severity** | Low | +| **Tested by** | Case 2 (`Background Feature Skip`) | + +**SIMPLNX:** Cells with `featureId == 0` are skipped. `featurePhases[0]` retains zero-initialization from `CreateArrayAction`. + +**SIMPL:** Background cells are processed; `featurePhases[0]` is written with the last background cell's phase. + +**Downstream note:** Any consumer reading `featurePhases[0]` will observe `0` from SIMPLNX instead of the last background cell's phase. `featurePhases[0]` should be treated as undefined. + +--- + +## D2 — Hard error on negative cell phase values + +| Field | Value | +|---|---| +| **Affects output** | No — error path only | +| **Severity** | Behavioral change | +| **Tested by** | Case 7 (`Negative Cell Phase`, error -61861) | + +**SIMPLNX:** Negative cell phase → immediate error -61861. + +**SIMPL:** No check; negative phase values processed silently. + +--- + +## D3 — Hard error on cell array size mismatch + +| Field | Value | +|---|---| +| **Affects output** | No — error path only | +| **Severity** | Behavioral change | +| **Tested by** | Case 6 (`Cell Array Size Mismatch`, error -61860) | + +**SIMPLNX:** `featureIds.getNumberOfTuples() != cellPhases.getNumberOfTuples()` → immediate error -61860. + +**SIMPL:** No check; mismatch would result in an out-of-bounds read. + +**Note:** Unreachable in normal use — `AttributeMatrixSelectionParameter` and `ArraySelectionParameter` enforce same-AM selection. Guard is defensive. + +--- + +## D4 — Warning message phrasing and truncation + +| Field | Value | +|---|---| +| **Affects output** | No — warning text only | +| **Severity** | None | +| **Tested by** | Cases 3 (`Inconsistent Phase Warning`) and 4 (`Warning Truncation`) | + +**SIMPLNX:** Single consolidated warning (code -500) listing up to 15 affected feature IDs by index, followed by `"and N more occurrence(s)"` for large conflict sets. Introduced at port time (PR #1455). + +**SIMPL:** Single generic message with no feature ID list. + +--- + +## Comparison runs + +### 2026-07-10 — current (post-cleanup, 3 cases) + +| Case | featureIds | cellPhases | Result | +|---|---|---|---| +| A | `[1,1,2,2,3,3,4,4]` | `[1,1,1,1,2,2,2,2]` | `featurePhases[1..4]` **BIT-IDENTICAL** | +| B | `[1,2,3,4,1,2,3,4]` | `[1,1,2,2,2,1,2,2]` | `featurePhases[1..4]` **BIT-IDENTICAL** | +| C | `[0,0,1,1,2,2,3,3]` | `[2,2,1,1,2,2,1,1]` | `featurePhases[1..3]` bit-identical; `featurePhases[0]`: SIMPL=2, SIMPLNX=0 — **D1 confirmed** | + +Scripts: `compute_feature_phases_vv/{generate_inputs.py,pipeline_simpl_{A,B,C}.json,pipeline_nx_{A,B,C}.d3dpipeline,compare_outputs.py}` + +**Conclusion:** `featurePhases[1..N]` is bit-identical across all three cases. The only deviation is D1 at `featurePhases[0]` when background cells are present. + +### 2026-07-09 — archived (pre-cleanup, 2 cases) + +Cases A and B only; no background cells; all elements bit-identical. `featurePhases[0]` not explicitly compared. Superseded by the 2026-07-10 run. diff --git a/src/Plugins/SimplnxCore/vv/provenance/ComputeFeaturePhasesFilter.md b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeaturePhasesFilter.md new file mode 100644 index 0000000000..20ed1704c2 --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeaturePhasesFilter.md @@ -0,0 +1,70 @@ +# Exemplar Provenance: ComputeFeaturePhasesFilter + +## Archive identity + +| Field | Value | +|---|---| +| **Archive** | None — oracle data is inlined in test source | +| **SHA512** | N/A | +| **Used by** | 8 inline TEST_CASEs in `test/ComputeFeaturePhasesFilterTest.cpp` | +| **Generated by** | N/A — fixtures hand-constructed in-test | + +No exemplar archive is required. The algorithm is a single-pass indirection (`featurePhases[featureIds[i]] = cellPhases[i]`) with integer arithmetic; expected outputs are trivially hand-computable. + +--- + +## Circular-oracle disposition + +The pre-V&V test `SimplnxCore::ComputeFeaturePhasesFilter(Valid Parameters)` loaded `6_6_stats_test_v2.dream3d` and compared the filter's output against `Cell Feature Data/Phases` already in that archive. That `Phases` array was generated by running `FindFeaturePhases` (the SIMPL predecessor) as part of the Small IN100 pipeline — making the comparison circular: filter output checked against its own prior output. + +| Field | Value | +|---|---| +| **Archive** | `6_6_stats_test_v2.tar.gz` | +| **SHA512** | `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` | +| **Circular array** | `DataContainer/Cell Feature Data/Phases` — not an independent oracle | +| **Retained for** | `SIMPL Backwards Compatibility` test (parameter conversion only); legacy comparison (Phase 9) | + +**Resolution (2026-07-10):** Retired `(Valid Parameters)` test. Replaced by 8 inline Class 1 oracle TEST_CASEs in `test/ComputeFeaturePhasesFilterTest.cpp` covering all code paths except cancel-signal injection. + +--- + +## Inline test fixtures + +All oracle assertions are embedded directly in `test/ComputeFeaturePhasesFilterTest.cpp`. No archived outputs are used. + +**Derivation methodology:** `featurePhases[f]` equals the phase of the last cell (ascending index) belonging to feature `f`. For consistent datasets the result is unambiguous. `featurePhases[0]` is always `0` (zero-initialized by `CreateArrayAction`; never written — deviation D1). + +| Case | TEST_CASE suffix | Purpose | Paths | +|---|---|---|---| +| 1 | `Valid: Uniform Phases` | 7 cells, 3 features, no conflict; normal accumulation, no warning | 6, 7, 10 | +| 2 | `Valid: Feature 0 Skip` | 6 cells, 2 features + background; `featurePhases[0]` stays 0 (D1) | 4, 10 | +| 3 | `Valid: Mixed Phase Warning` | 2 cells, 1 feature, phase conflict; warning code -500 | 8, 9 | +| 4 | `Valid: Truncated Mixed Feature Warning` | 16 conflicting features; truncation suffix "and 1 more occurrence" | 9 (truncation) | +| 5 | `Invalid: FeatureId Out Of Bounds` | featureId 5 with Feature AM of 3 tuples; error -5351 | 1 (partial — OOB only) | +| 6 | `Invalid: Cell Array Size Mismatch` | featureIds (4 tuples) vs cellPhases (5 tuples); error -61860 | 2 | +| 7 | `Invalid: Negative Cell Phase` | cellPhases contains -1; error -61861 | 5 | +| 8 | `Invalid: Negative FeatureId` | featureIds contains -1; error -5355 | 1b | + +--- + +## Canonical oracle output + +| Assertion | Class | Derivation | +|---|---|---| +| `featurePhases[1..3]` in Case 1 = `[1, 2, 1]` | 1 — Analytical | Last cell of each feature has unambiguous consistent phase | +| `featurePhases[1]` in Case 3 = 2 | 1 — Analytical | Last-cell-wins: cell 1 (phase 2) overwrites cell 0 (phase 1) | +| `featurePhases[0]` in all cases = 0 | 1 — Analytical | Zero-initialized; never written (deviation D1) | +| Warning count = 0 for Cases 1, 2 | 4 — Invariant | No feature has mixed-phase cells | +| Warning code = -500 for Cases 3, 4 | 1 — Analytical | Hard-coded in algorithm | +| Error code = -5351 for Case 5 | 1 — Analytical | Hard-coded in `ValidateFeatureIdsToFeatureAttributeMatrixIndexing` | +| Error code = -5355 for Case 8 | 1 — Analytical | Hard-coded in `ValidateFeatureIdsToFeatureAttributeMatrixIndexing` | +| Error code = -61860 for Case 6 | 1 — Analytical | Hard-coded in algorithm | +| Error code = -61861 for Case 7 | 1 — Analytical | Hard-coded in algorithm | +| `featurePhases[f] ∈ {phases of cells with featureId == f}` for `f > 0` | 4 — Invariant | Single-pass indirection | + +--- + +## Second-engineer oracle review + +- **Status:** Not yet conducted. +- **Rationale for skipping if necessary:** Cases 1–4 are single-pass indirections over ≤ 32 cells with integer values in {1, 2}; Cases 5–8 are hard-coded error codes. Derivation is reproducible from the algorithm source in under 5 minutes.