diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/MultiThresholdObjects.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/MultiThresholdObjects.cpp index 48ea54b9d7..c606f58230 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/MultiThresholdObjects.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/MultiThresholdObjects.cpp @@ -3,6 +3,7 @@ #include "simplnx/Common/TypeTraits.hpp" #include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/Utilities/ArrayThreshold.hpp" +#include "simplnx/Utilities/DataStoreUtilities.hpp" #include "simplnx/Utilities/FilterUtilities.hpp" #include @@ -11,11 +12,67 @@ using namespace nx::core; namespace { +/** + * @brief InsertThreshold + * @param numItems + * @param currentArrayPtr + * @param unionOperator + * @param newArrayPtr + * @param inverse + */ +template +void InsertThreshold(AbstractDataStore& currentVector, nx::core::IArrayThreshold::UnionOperator unionOperator, AbstractDataStore& newVector, bool inverse, T trueValue, T falseValue) +{ + usize numItems = currentVector.getNumberOfTuples(); + + for(usize i = 0; i < numItems; i++) + { + // invert the current comparison if necessary + if(inverse) + { + newVector[i] = (newVector[i] == trueValue) ? falseValue : trueValue; + } + + if(nx::core::IArrayThreshold::UnionOperator::Or == unionOperator) + { + currentVector[i] = (currentVector[i] == trueValue || newVector[i] == trueValue) ? trueValue : falseValue; + } + else if(currentVector[i] == falseValue || newVector[i] == falseValue) + { + currentVector[i] = falseValue; + } + } +} + +/** + * @brief Consolidate all assignment calls to a single method to prevent unintended diverging behavior. + * @param arrayThreshold Current threshold to pull settings from. + * @param outputResultStore Output DataStore for the current ThresholdSet. + * @param inputThresholdStore Resulting output for the target array threshold. + * @param replaceInput The first threshould in every set has its output applied to the output regardless of union operator. + * @param trueValue Output mask value when the threshold is satisfied. + * @param falseValue Output mask value when the threshold is not satisfied. + */ +template +void ApplyThresholdValues(const IArrayThreshold& arrayThreshold, AbstractDataStore& outputResultStore, AbstractDataStore& inputThresholdStore, bool replaceInput, T trueValue, T falseValue) +{ + auto unionOperator = arrayThreshold.getUnionOperator(); + bool inverse = arrayThreshold.isInverted(); + + if(replaceInput) + { + unionOperator = IArrayThreshold::UnionOperator::Or; + } + + // insert into current threshold + InsertThreshold(outputResultStore, unionOperator, inputThresholdStore, inverse, trueValue, falseValue); +} + template class ThresholdFilterHelper { public: - ThresholdFilterHelper(ArrayThreshold::ComparisonType compType, ArrayThreshold::ComparisonValue compValue, usize componentIndex, std::vector& output) + ThresholdFilterHelper(ArrayThreshold::ComparisonType compType, ArrayThreshold::ComparisonValue compValue, usize componentIndex, AbstractDataStore& output) : m_ComparisonOperator(compType) , m_ComparisonValue(compValue) , m_ComponentIndex(componentIndex) @@ -31,7 +88,8 @@ class ThresholdFilterHelper for(size_t tupleIndex = 0; tupleIndex < numTuples; ++tupleIndex) { T inputValue = m_Input.getComponentValue(tupleIndex, m_ComponentIndex); - T outputValue = CompT{}(inputValue, value) ? trueValue : falseValue; + bool comparison = CompT{}(inputValue, value); + T outputValue = comparison ? trueValue : falseValue; m_Output[tupleIndex] = outputValue; } } @@ -66,7 +124,7 @@ class ThresholdFilterHelper ArrayThreshold::ComparisonType m_ComparisonOperator; ArrayThreshold::ComparisonValue m_ComparisonValue; usize m_ComponentIndex = 0; - std::vector& m_Output; + AbstractDataStore& m_Output; }; struct ExecuteThresholdHelper @@ -79,43 +137,14 @@ struct ExecuteThresholdHelper } }; -/** - * @brief InsertThreshold - * @param numItems - * @param currentArrayPtr - * @param unionOperator - * @param newArrayPtr - * @param inverse - */ -template -void InsertThreshold(usize numItems, AbstractDataStore& currentStore, nx::core::IArrayThreshold::UnionOperator unionOperator, std::vector& newArrayPtr, bool inverse, T trueValue, T falseValue) -{ - for(usize i = 0; i < numItems; i++) - { - // invert the current comparison if necessary - if(inverse) - { - newArrayPtr[i] = (newArrayPtr[i] == trueValue) ? falseValue : trueValue; - } - - if(nx::core::IArrayThreshold::UnionOperator::Or == unionOperator) - { - currentStore[i] = (currentStore[i] == trueValue || newArrayPtr[i] == trueValue) ? trueValue : falseValue; - } - else if(currentStore[i] == falseValue || newArrayPtr[i] == falseValue) - { - currentStore[i] = falseValue; - } - } -} - template -void ThresholdValue(const ArrayThreshold& comparisonValue, const DataStructure& dataStructure, AbstractDataStore& outputResultStore, int32_t& err, bool replaceInput, bool inverse, T trueValue, - T falseValue) +void ThresholdValue(const ArrayThreshold& comparisonValue, const DataStructure& dataStructure, AbstractDataStore& outputResultVector, int32_t& err, bool replaceInput, T trueValue, T falseValue) { // Get the total number of tuples, create and initialize an array with FALSE to use for these results - size_t totalTuples = outputResultStore.getNumberOfTuples(); - std::vector tempResultVector(totalTuples, falseValue); + size_t totalTuples = outputResultVector.getNumberOfTuples(); + auto tempResultStorePtr = DataStoreUtilities::CreateDataStore({totalTuples}, {1}, IDataAction::Mode::Execute); + AbstractDataStore& tempResultStore = *tempResultStorePtr.get(); + std::fill(tempResultStore.begin(), tempResultStore.end(), falseValue); nx::core::ArrayThreshold::ComparisonType compOperator = comparisonValue.getComparisonType(); nx::core::ArrayThreshold::ComparisonValue compValue = comparisonValue.getComparisonValue(); @@ -125,49 +154,23 @@ void ThresholdValue(const ArrayThreshold& comparisonValue, const DataStructure& usize componentIndex = comparisonValue.getComponentIndex(); - ThresholdFilterHelper helper(compOperator, compValue, componentIndex, tempResultVector); + ThresholdFilterHelper helper(compOperator, compValue, componentIndex, tempResultStore); const auto& iDataArray = dataStructure.getDataRefAs(inputDataArrayPath); ExecuteDataFunction(ExecuteThresholdHelper{}, iDataArray.getDataType(), helper, iDataArray, trueValue, falseValue); - if(replaceInput) - { - if(inverse) - { - std::reverse(tempResultVector.begin(), tempResultVector.end()); - } - // copy the temp uint8 vector to the final uint8 result array - for(size_t i = 0; i < totalTuples; i++) - { - outputResultStore[i] = tempResultVector[i]; - } - } - else - { - // insert into current threshold - InsertThreshold(totalTuples, outputResultStore, unionOperator, tempResultVector, inverse, trueValue, falseValue); - } + ApplyThresholdValues(comparisonValue, outputResultVector, tempResultStore, replaceInput, trueValue, falseValue); } -struct ThresholdValueFunctor -{ - template - void operator()(const ArrayThreshold& comparisonValue, const DataStructure& dataStructure, IDataArray& outputResultArray, int32_t& err, bool replaceInput, bool inverse, T trueValue, T falseValue) - { - // Traditionally we would do a check to ensure we get a valid pointer, I'm forgoing that check because it - // was essentially done in the preflight part. - ThresholdValue(comparisonValue, dataStructure, outputResultArray.template getIDataStoreRefAs>(), err, replaceInput, inverse, trueValue, falseValue); - } -}; - template -void ThresholdSet(const ArrayThresholdSet& inputComparisonSet, const DataStructure& dataStructure, AbstractDataStore& outputResultStore, int32_t& err, bool replaceInput, bool inverse, T trueValue, - T falseValue) +void ThresholdSet(const ArrayThresholdSet& inputComparisonSet, const DataStructure& dataStructure, AbstractDataStore& outputResultVector, int32_t& err, bool replaceInput, T trueValue, T falseValue) { // Get the total number of tuples, create and initialize an array with FALSE to use for these results - size_t totalTuples = outputResultStore.getNumberOfTuples(); - std::vector tempResultVector(totalTuples, falseValue); + size_t totalTuples = outputResultVector.getNumberOfTuples(); + auto tempResultStorePtr = DataStoreUtilities::CreateDataStore({totalTuples}, {1}, IDataAction::Mode::Execute); + AbstractDataStore& tempResultStore = *tempResultStorePtr.get(); + std::fill(tempResultStore.begin(), tempResultStore.end(), falseValue); bool firstValueFound = false; @@ -177,44 +180,38 @@ void ThresholdSet(const ArrayThresholdSet& inputComparisonSet, const DataStructu const IArrayThreshold* thresholdPtr = threshold.get(); if(const auto* comparisonSet = dynamic_cast(thresholdPtr); comparisonSet != nullptr) { - ThresholdSet(*comparisonSet, dataStructure, outputResultStore, err, !firstValueFound, false, trueValue, falseValue); + ThresholdSet(*comparisonSet, dataStructure, tempResultStore, err, !firstValueFound, trueValue, falseValue); firstValueFound = true; } else if(const auto* comparisonValue = dynamic_cast(thresholdPtr); comparisonValue != nullptr) { - ThresholdValue(*comparisonValue, dataStructure, outputResultStore, err, !firstValueFound, false, trueValue, falseValue); + ThresholdValue(*comparisonValue, dataStructure, tempResultStore, err, !firstValueFound, trueValue, falseValue); firstValueFound = true; } } - if(replaceInput) - { - if(inverse) - { - std::reverse(tempResultVector.begin(), tempResultVector.end()); - } - // copy the temp uint8 vector to the final uint8 result array - for(size_t i = 0; i < totalTuples; i++) - { - outputResultStore[i] = tempResultVector[i]; - } - } - else - { - // insert into current threshold - InsertThreshold(totalTuples, outputResultStore, inputComparisonSet.getUnionOperator(), tempResultVector, inverse, trueValue, falseValue); - } + // Apply resulting values to output + ApplyThresholdValues(inputComparisonSet, outputResultVector, tempResultStore, replaceInput, trueValue, falseValue); } struct ThresholdSetFunctor { template - void operator()(const ArrayThresholdSet& inputComparisonSet, const DataStructure& dataStructure, IDataArray& outputResultArray, int32_t& err, bool replaceInput, bool inverse, T trueValue, - T falseValue) + void operator()(const ArrayThresholdSet& inputComparisonSet, const DataStructure& dataStructure, IDataArray& outputResultArray, int32_t& err, bool replaceInput, T trueValue, T falseValue) { // Traditionally we would do a check to ensure we get a valid pointer, I'm forgoing that check because it // was essentially done in the preflight part. - ThresholdSet(inputComparisonSet, dataStructure, outputResultArray.template getIDataStoreRefAs>(), err, replaceInput, inverse, trueValue, falseValue); + auto& outputDataStore = outputResultArray.template getIDataStoreRefAs>(); + usize totalTuples = outputDataStore.getNumberOfTuples(); + auto tempResultStorePtr = DataStoreUtilities::CreateDataStore({totalTuples}, {1}, IDataAction::Mode::Execute); + AbstractDataStore& tempResultStore = *tempResultStorePtr.get(); + std::fill(tempResultStore.begin(), tempResultStore.end(), falseValue); + ThresholdSet(inputComparisonSet, dataStructure, tempResultStore, err, replaceInput, trueValue, falseValue); + + for(size_t i = 0; i < totalTuples; i++) + { + outputDataStore[i] = tempResultStore[i]; + } } }; } // namespace @@ -250,26 +247,8 @@ Result<> MultiThresholdObjects::operator()() DataPath maskArrayPath = (*thresholdsObject.getRequiredPaths().begin()).replaceName(maskArrayName); int32_t err = 0; ArrayThresholdSet::CollectionType thresholdSet = thresholdsObject.getArrayThresholds(); - for(const std::shared_ptr& threshold : thresholdSet) - { - if(m_ShouldCancel) - { - return {}; - } - const IArrayThreshold* thresholdPtr = threshold.get(); - if(const auto* comparisonSet = dynamic_cast(thresholdPtr); comparisonSet != nullptr) - { - ExecuteDataFunction(ThresholdSetFunctor{}, maskArrayType, *comparisonSet, m_DataStructure, m_DataStructure.getDataRefAs(maskArrayPath), err, !firstValueFound, - thresholdsObject.isInverted(), trueValue, falseValue); - firstValueFound = true; - } - else if(const auto* comparisonValue = dynamic_cast(thresholdPtr); comparisonValue != nullptr) - { - ExecuteDataFunction(ThresholdValueFunctor{}, maskArrayType, *comparisonValue, m_DataStructure, m_DataStructure.getDataRefAs(maskArrayPath), err, !firstValueFound, - thresholdsObject.isInverted(), trueValue, falseValue); - firstValueFound = true; - } - } + + ExecuteDataFunction(ThresholdSetFunctor{}, maskArrayType, thresholdsObject, m_DataStructure, m_DataStructure.getDataRefAs(maskArrayPath), err, !firstValueFound, trueValue, falseValue); return {}; } diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/MultiThresholdObjectsFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/MultiThresholdObjectsFilter.cpp index 85c1535dc6..34dc6bff18 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/MultiThresholdObjectsFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/MultiThresholdObjectsFilter.cpp @@ -164,7 +164,6 @@ IFilter::PreflightResult MultiThresholdObjectsFilter::preflightImpl(const DataSt // Check for same number of tuples and components usize numTuples = dataArray.getNumberOfTuples(); - usize numComponents = dataArray.getNumberOfComponents(); for(const auto& dataPath : thresholdPaths) { const auto& currentDataArray = dataStructure.getDataRefAs(dataPath); @@ -174,13 +173,6 @@ IFilter::PreflightResult MultiThresholdObjectsFilter::preflightImpl(const DataSt auto errorMessage = fmt::format("Data Arrays do not have same equal number of tuples. '{}:{}' and '{}:{}'", firstDataPath.toString(), numTuples, dataPath.toString(), currentNumTuples); return MakePreflightErrorResult(to_underlying(ErrorCodes::UnequalTuples), errorMessage); } - usize currentNumComponents = currentDataArray.getNumberOfComponents(); - if(currentNumComponents != numComponents) - { - auto errorMessage = - fmt::format("Data Arrays do not have same equal number of components. '{}:{}' and '{}:{}'", firstDataPath.toString(), numComponents, dataPath.toString(), currentNumComponents); - return MakePreflightErrorResult(to_underlying(ErrorCodes::UnequalComponents), errorMessage); - } } Result<> componentIndicesResult = CheckComponentIndicesInThresholds(thresholdsObject, dataStructure); @@ -223,8 +215,8 @@ IFilter::PreflightResult MultiThresholdObjectsFilter::preflightImpl(const DataSt } // Create the output boolean array - auto action = - std::make_unique(maskArrayType, dataArray.getIDataStoreRef().getTupleShape(), std::vector{1}, firstDataPath.replaceName(maskArrayName), dataArray.getDataFormat()); + const auto& dataStore = dataArray.getIDataStoreRef(); + auto action = std::make_unique(maskArrayType, dataStore.getTupleShape(), std::vector{1}, firstDataPath.replaceName(maskArrayName), dataArray.getDataFormat()); OutputActions actions; actions.appendAction(std::move(action)); diff --git a/src/Plugins/SimplnxCore/test/MultiThresholdObjectsTest.cpp b/src/Plugins/SimplnxCore/test/MultiThresholdObjectsTest.cpp index da9ded6ad8..bc54c0481f 100644 --- a/src/Plugins/SimplnxCore/test/MultiThresholdObjectsTest.cpp +++ b/src/Plugins/SimplnxCore/test/MultiThresholdObjectsTest.cpp @@ -28,53 +28,310 @@ const DataPath k_TestArrayIntPath = k_ImageCellDataName.createChildPath(k_TestAr const DataPath k_MultiComponentArrayPath = k_ImageCellDataName.createChildPath(k_MultiComponentArrayName); const DataPath k_ThresholdArrayPath = k_ImageCellDataName.createChildPath(k_ThresholdArrayName); -const DataPath k_MismatchingComponentsArrayPath = k_ImageCellDataName.createChildPath("MismatchingComponentsArray"); const DataPath k_MismatchingTuplesArrayPath({"MismatchingTuplesArray"}); +constexpr int8 k_TupleCount = 5; +constexpr int8 k_MultiComponentCount = 3; + +constexpr float64 k_FloatValueIncrement = 0.01; + +constexpr int32 InputIntValue(int32 index) +{ + return index; +} + +constexpr int32 InputIntComponentValue(int32 tuple, int32 component) +{ + return (tuple + component) % 2 == 0 ? -tuple : tuple; +} + +constexpr float64 InputFloatValue(int32 index) +{ + return (index + 1) * k_FloatValueIncrement; +} + DataStructure CreateTestDataStructure() { DataStructure dataStructure; // Create two test arrays, a float array and a int array // Set up geometry for tuples, a cuboid with dimensions 20, 10, 1 ImageGeom* image = ImageGeom::Create(dataStructure, k_ImageGeometry); - std::vector dims = {20, 1, 1}; + std::vector dims = {k_TupleCount, 1, 1}; image->setDimensions(dims); - ShapeType tDims = {20}; + ShapeType tDims = {k_TupleCount}; ShapeType cDims = {1}; - ShapeType cDimsMulti = {3}; - float fnum = 0.0f; - int inum = 0; + ShapeType cDimsMulti = {k_MultiComponentCount}; + AttributeMatrix* am = AttributeMatrix::Create(dataStructure, k_CellData, tDims, image->getId()); Float32Array* data = Float32Array::CreateWithStore(dataStructure, k_TestArrayFloatName, tDims, cDims, am->getId()); Int32Array* data1 = Int32Array::CreateWithStore(dataStructure, k_TestArrayIntName, tDims, cDims, am->getId()); Int32Array* multiComponentData = Int32Array::CreateWithStore(dataStructure, k_MultiComponentArrayName, tDims, cDimsMulti, am->getId()); - Float32Array* invalid1 = Float32Array::CreateWithStore(dataStructure, k_MismatchingComponentsArrayPath.getTargetName(), tDims, cDimsMulti, am->getId()); - invalid1->fill(1.0); - Float32Array* invalid2 = Float32Array::CreateWithStore(dataStructure, k_MismatchingTuplesArrayPath.getTargetName(), std::vector{10}, cDims); - invalid2->fill(2.0); + // Mismatched tuple count throws an error. + // This is not true for mismatched component shapes. + Float32Array* problemArray = Float32Array::CreateWithStore(dataStructure, k_MismatchingTuplesArrayPath.getTargetName(), std::vector{10}, cDims); + problemArray->fill(2.0); usize numComponents = multiComponentData->getNumberOfComponents(); - int32 sign = 1; - - // Fill the float array with {.01,.02,.03,.04,.05,.06,.07,.08,.09,.10,.11,.12,.13,.14,.15.,16,.17,.18,.19,.20} - // Fill the int array with { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19 } - // Fill multi-component array with {{0, 0, 0}, {1, -1, 1}, {-2, 2, -2}, ..., {17, -17, 17}, {-18, 18, -18}, {19, -19, 19}} - for(usize i = 0; i < 20; i++) - { - fnum += 0.01f; - (*data)[i] = fnum; // float array - (*data1)[i] = inum; // int array - multiComponentData->setComponent(i, 0, i * -sign); - multiComponentData->setComponent(i, 1, i * sign); - multiComponentData->setComponent(i, 2, i * -sign); - sign *= -1; - ++inum; + + // Fill the float array with {.01,.02,.03,.04,.05} + // Fill the int array with { 0,1,2,3,4} + // Fill multi-component array with {{0, 0, 0}, {1, -1, 1}, {-2, 2, -2}, {3, -3, 3}, {-4, 4, -4}} + for(usize i = 0; i < k_TupleCount; i++) + { + (*data)[i] = InputFloatValue(i); // float array + (*data1)[i] = InputIntValue(i); // int array + + for(usize j = 0; j < k_MultiComponentCount; j++) + { + multiComponentData->setComponent(i, j, InputIntComponentValue(i, j)); + } } return dataStructure; } +template +void SetArrayValues(DataArray& dataArray) +{ + auto& dataStore = dataArray.getDataStoreRef(); + usize count = dataStore.size(); + for(usize i = 0; i < count; i++) + { + dataStore[i] = static_cast(i); + } +} + +DataStructure CreateTestDataStructure2() +{ + DataStructure dataStructure; + // Create two test arrays, a float array and a int array + // Set up geometry for tuples, a cuboid with dimensions 20, 10, 1 + ImageGeom* image = ImageGeom::Create(dataStructure, k_ImageGeometry); + std::vector dims = {k_TupleCount, 1, 1}; + image->setDimensions(dims); + + ShapeType tDims = {k_TupleCount}; + ShapeType cDims = {1}; + ShapeType cDimsMulti = {k_MultiComponentCount}; + + AttributeMatrix* am = AttributeMatrix::Create(dataStructure, k_CellData, tDims, image->getId()); + auto* int8Array = Int8Array::CreateWithStore(dataStructure, "int8", tDims, cDims, am->getId()); + auto* int16Array = Int16Array::CreateWithStore(dataStructure, "int16", tDims, cDims, am->getId()); + auto* int32Array = Int32Array::CreateWithStore(dataStructure, "int32", tDims, cDims, am->getId()); + auto* int64Array = Int64Array::CreateWithStore(dataStructure, "int64", tDims, cDims, am->getId()); + auto* uint8Array = UInt8Array::CreateWithStore(dataStructure, "uint8", tDims, cDims, am->getId()); + auto* uint16Array = UInt16Array::CreateWithStore(dataStructure, "uint16", tDims, cDims, am->getId()); + auto* uint32Array = UInt32Array::CreateWithStore(dataStructure, "uint32", tDims, cDims, am->getId()); + auto* uint64Array = UInt64Array::CreateWithStore(dataStructure, "uint64", tDims, cDims, am->getId()); + auto* float32Array = Float32Array::CreateWithStore(dataStructure, "float32", tDims, cDims, am->getId()); + auto* float64Array = Float64Array::CreateWithStore(dataStructure, "float64", tDims, cDims, am->getId()); + auto* boolArray = BoolArray::CreateWithStore(dataStructure, "bool", tDims, cDims, am->getId()); + + SetArrayValues(*int8Array); + SetArrayValues(*int16Array); + SetArrayValues(*int32Array); + SetArrayValues(*int64Array); + SetArrayValues(*uint8Array); + SetArrayValues(*uint16Array); + SetArrayValues(*uint32Array); + SetArrayValues(*uint64Array); + SetArrayValues(*float32Array); + SetArrayValues(*float64Array); + SetArrayValues(*boolArray); + + return dataStructure; +} + +/** + * @brief Creates a single threshold for the filter to use. + * @param arrayPath Input DataArray path + * @param comparisonType type of comparison + * @param value Value to threshold against + * @param isInverted Should the threshold output be inverted + * componentIndex Component index of the array to threshold against. + */ +ArrayThresholdSet CreateSingleThreshold(const DataPath& arrayPath, ArrayThreshold::ComparisonType comparisonType, double value, bool isInverted, int componentIndex) +{ + ArrayThresholdSet thresholdSet; + auto threshold = std::make_shared(); + threshold->setArrayPath(arrayPath); + threshold->setComparisonType(comparisonType); + threshold->setComparisonValue(value); + threshold->setComponentIndex(componentIndex); + threshold->setInverted(isInverted); + thresholdSet.setArrayThresholds({threshold}); + + return thresholdSet; +} + +/** + * @brief Runs the MultiThresholdObjects filter on the provided threshold set + * @param dataStructure + * @param thresholdSet ThresholdSet to use for the MultiThresholdObjectsFilter + */ +void RunThresholdSetTest(DataStructure& dataStructure, ArrayThresholdSet thresholdSet) +{ + MultiThresholdObjectsFilter filter; + Arguments args; + + args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); + args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); + args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(DataType::boolean)); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + // Execute the filter and check the result + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + // Require that the mask array only has one component + const auto* thresholdArrayPtr = dataStructure.getDataAs(k_ThresholdArrayPath); + REQUIRE(thresholdArrayPtr != nullptr); + + REQUIRE(thresholdArrayPtr->getNumberOfComponents() == 1); +} + +/** + * @brief Runs the MultiThresholdObjects filter on the provided DataStructure using a single array threshold. + * @param dataStructure + * @param arrayPath Path to use for the threshold DataArray + * @param comparisonType Type of comparison to perform + * @param value Value to threshold against + * @param isInverted should the output mask value be inverted + * @param componentIndex Which component of the array the threshold should use. + */ +void RunSingleThresholdTest(DataStructure& dataStructure, const DataPath& arrayPath, ArrayThreshold::ComparisonType comparisonType, double value, bool isInverted, int32 componentIndex = 0) +{ + auto thresholdSet = CreateSingleThreshold(arrayPath, comparisonType, value, isInverted, componentIndex); + RunThresholdSetTest(dataStructure, thresholdSet); +} + +// Integer checks +bool ExpectedIntSingleComponentMask(ArrayThreshold::ComparisonType comparisonType, int32 i, double thresholdValue, bool isInverted) +{ + bool expected = false; + + switch(comparisonType) + { + case ArrayThreshold::ComparisonType::GreaterThan: + expected = InputIntValue(i) > thresholdValue; + break; + case ArrayThreshold::ComparisonType::LessThan: + expected = InputIntValue(i) < thresholdValue; + break; + case ArrayThreshold::ComparisonType::Operator_Equal: + expected = InputIntValue(i) == thresholdValue; + break; + case ArrayThreshold::ComparisonType::Operator_NotEqual: + expected = InputIntValue(i) != thresholdValue; + break; + } + + if(isInverted) + { + expected = !expected; + } + return expected; +} + +void CheckIntTestDataSingleComponent(const DataStructure& dataStructure, ArrayThreshold::ComparisonType comparisonType, double thresholdValue, bool isInverted) +{ + const auto* thresholdArrayPtr = dataStructure.getDataAs(k_ThresholdArrayPath); + REQUIRE(thresholdArrayPtr != nullptr); + + auto& thresholdStore = thresholdArrayPtr->getDataStoreRef(); + + for(usize i = 0; i < k_TupleCount; i++) + { + REQUIRE(thresholdStore[i] == ExpectedIntSingleComponentMask(comparisonType, i, thresholdValue, isInverted)); + } +} + +// Floating point checks +bool ExpectedFloatSingleComponentMask(ArrayThreshold::ComparisonType comparisonType, int32 i, double thresholdValue, bool isInverted) +{ + bool expected = false; + + switch(comparisonType) + { + case ArrayThreshold::ComparisonType::GreaterThan: + expected = InputFloatValue(i) > thresholdValue; + break; + case ArrayThreshold::ComparisonType::LessThan: + expected = InputFloatValue(i) < thresholdValue; + break; + case ArrayThreshold::ComparisonType::Operator_Equal: + expected = InputFloatValue(i) == thresholdValue; + break; + case ArrayThreshold::ComparisonType::Operator_NotEqual: + expected = InputFloatValue(i) != thresholdValue; + break; + } + + if(isInverted) + { + expected = !expected; + } + return expected; +} + +void CheckFloatTestDataSingleComponent(const DataStructure& dataStructure, ArrayThreshold::ComparisonType comparisonType, double thresholdValue, bool isInverted) +{ + const auto* thresholdArrayPtr = dataStructure.getDataAs(k_ThresholdArrayPath); + REQUIRE(thresholdArrayPtr != nullptr); + + auto& thresholdStore = thresholdArrayPtr->getDataStoreRef(); + + for(usize i = 0; i < k_TupleCount; i++) + { + REQUIRE(thresholdStore[i] == ExpectedFloatSingleComponentMask(comparisonType, i, thresholdValue, isInverted)); + } +} + +// Multi-component checks +bool ExpectedIntMultiComponentMask(ArrayThreshold::ComparisonType comparisonType, int32 i, double thresholdValue, bool isInverted, int32 componentIndex) +{ + bool expected = false; + + switch(comparisonType) + { + case ArrayThreshold::ComparisonType::GreaterThan: + expected = InputIntComponentValue(i, componentIndex) > thresholdValue; + break; + case ArrayThreshold::ComparisonType::LessThan: + expected = InputIntComponentValue(i, componentIndex) < thresholdValue; + break; + case ArrayThreshold::ComparisonType::Operator_Equal: + expected = InputIntComponentValue(i, componentIndex) == thresholdValue; + break; + case ArrayThreshold::ComparisonType::Operator_NotEqual: + expected = InputIntComponentValue(i, componentIndex) != thresholdValue; + break; + } + + if(isInverted) + { + expected = !expected; + } + return expected; +} + +void CheckIntTestDataMultiComponent(const DataStructure& dataStructure, ArrayThreshold::ComparisonType comparisonType, double thresholdValue, bool isInverted, int32 componentIndex) +{ + const auto* thresholdArrayPtr = dataStructure.getDataAs(k_ThresholdArrayPath); + REQUIRE(thresholdArrayPtr != nullptr); + + auto& thresholdStore = thresholdArrayPtr->getDataStoreRef(); + + for(usize i = 0; i < k_TupleCount; i++) + { + REQUIRE(thresholdStore[i] == ExpectedIntMultiComponentMask(comparisonType, i, thresholdValue, isInverted, componentIndex)); + } +} + template float64 GetOutOfBoundsMinimumValue() { @@ -97,149 +354,378 @@ float64 GetOutOfBoundsMaximumValue() } } // namespace -TEST_CASE("SimplnxCore::MultiThresholdObjects: Valid Execution", "[SimplnxCore][MultiThresholdObjectsFilter]") +TEST_CASE("SimplnxCore::MultiThresholdObjects: Valid Single Thresholds: Int", "[SimplnxCore][MultiThresholdObjectsFilter]") { UnitTest::LoadPlugins(); DataStructure dataStructure = CreateTestDataStructure(); + const DataPath targetArray = k_TestArrayIntPath; + double thresholdValue = GENERATE(-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 22.0, 5.5); + bool isInverted = GENERATE(false, true); - SECTION("Float Array Threshold") + SECTION("ArrayThreshold: >") { - MultiThresholdObjectsFilter filter; - Arguments args; + RunSingleThresholdTest(dataStructure, targetArray, ArrayThreshold::ComparisonType::GreaterThan, thresholdValue, isInverted); + CheckIntTestDataSingleComponent(dataStructure, ArrayThreshold::ComparisonType::GreaterThan, thresholdValue, isInverted); + } - ArrayThresholdSet thresholdSet; - auto threshold = std::make_shared(); - threshold->setArrayPath(k_TestArrayFloatPath); - threshold->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold->setComparisonValue(0.1); - thresholdSet.setArrayThresholds({threshold}); + SECTION("ArrayThreshold: <") + { + RunSingleThresholdTest(dataStructure, targetArray, ArrayThreshold::ComparisonType::LessThan, thresholdValue, isInverted); + CheckIntTestDataSingleComponent(dataStructure, ArrayThreshold::ComparisonType::LessThan, thresholdValue, isInverted); + } - args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(DataType::boolean)); + SECTION("ArrayThreshold: ==") + { + RunSingleThresholdTest(dataStructure, targetArray, ArrayThreshold::ComparisonType::Operator_Equal, thresholdValue, isInverted); + CheckIntTestDataSingleComponent(dataStructure, ArrayThreshold::ComparisonType::Operator_Equal, thresholdValue, isInverted); + CheckIntTestDataSingleComponent(dataStructure, ArrayThreshold::ComparisonType::Operator_NotEqual, thresholdValue, !isInverted); + } + SECTION("ArrayThreshold: !=") + { + RunSingleThresholdTest(dataStructure, targetArray, ArrayThreshold::ComparisonType::Operator_NotEqual, thresholdValue, isInverted); + CheckIntTestDataSingleComponent(dataStructure, ArrayThreshold::ComparisonType::Operator_Equal, thresholdValue, !isInverted); + CheckIntTestDataSingleComponent(dataStructure, ArrayThreshold::ComparisonType::Operator_NotEqual, thresholdValue, isInverted); + } +} - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) +TEST_CASE("SimplnxCore::MultiThresholdObjects: Valid Single Thresholds: Float", "[SimplnxCore][MultiThresholdObjectsFilter]") +{ + UnitTest::LoadPlugins(); - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + DataStructure dataStructure = CreateTestDataStructure(); + const DataPath targetArray = k_TestArrayFloatPath; + double thresholdValue = GENERATE(0.0, 0.01, 0.02, 0.03, 0.04, 26.2); + bool isInverted = GENERATE(false, true); - auto* thresholdArray = dataStructure.getDataAs(k_ThresholdArrayPath); - REQUIRE(thresholdArray != nullptr); + SECTION("ArrayThreshold: >") + { + RunSingleThresholdTest(dataStructure, targetArray, ArrayThreshold::ComparisonType::GreaterThan, thresholdValue, isInverted); + CheckFloatTestDataSingleComponent(dataStructure, ArrayThreshold::ComparisonType::GreaterThan, thresholdValue, isInverted); + } - // For the comparison value of 0.1, the threshold array elements 0 to 9 should be false and 10 through 19 should be true - for(usize i = 0; i < 20; i++) - { - if(i < 10) - { - REQUIRE((*thresholdArray)[i] == false); - } - else - { - REQUIRE((*thresholdArray)[i] == true); - } - } + SECTION("ArrayThreshold: <") + { + RunSingleThresholdTest(dataStructure, targetArray, ArrayThreshold::ComparisonType::LessThan, thresholdValue, isInverted); + CheckFloatTestDataSingleComponent(dataStructure, ArrayThreshold::ComparisonType::LessThan, thresholdValue, isInverted); } - SECTION("Int Array Threshold") + SECTION("ArrayThreshold: ==") + { + RunSingleThresholdTest(dataStructure, targetArray, ArrayThreshold::ComparisonType::Operator_Equal, thresholdValue, isInverted); + CheckFloatTestDataSingleComponent(dataStructure, ArrayThreshold::ComparisonType::Operator_Equal, thresholdValue, isInverted); + CheckFloatTestDataSingleComponent(dataStructure, ArrayThreshold::ComparisonType::Operator_NotEqual, thresholdValue, !isInverted); + } + SECTION("ArrayThreshold: !=") { - MultiThresholdObjectsFilter filter; - Arguments args; + RunSingleThresholdTest(dataStructure, targetArray, ArrayThreshold::ComparisonType::Operator_NotEqual, thresholdValue, isInverted); + CheckFloatTestDataSingleComponent(dataStructure, ArrayThreshold::ComparisonType::Operator_Equal, thresholdValue, !isInverted); + CheckFloatTestDataSingleComponent(dataStructure, ArrayThreshold::ComparisonType::Operator_NotEqual, thresholdValue, isInverted); + } +} - ArrayThresholdSet thresholdSet; - auto threshold = std::make_shared(); - threshold->setArrayPath(k_TestArrayIntPath); - threshold->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold->setComparisonValue(15); - thresholdSet.setArrayThresholds({threshold}); +TEST_CASE("SimplnxCore::MultiThresholdObjects: Valid Single Thresholds: Int Multi-Component", "[SimplnxCore][MultiThresholdObjectsFilter]") +{ + UnitTest::LoadPlugins(); - args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(DataType::boolean)); + DataStructure dataStructure = CreateTestDataStructure(); + const DataPath targetArray = k_MultiComponentArrayPath; + double thresholdValue = GENERATE(-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 22.0, 5.5); + bool isInverted = GENERATE(false, true); + int32 componentIndex = GENERATE(0, 1, 2); - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + SECTION("ArrayThreshold: >") + { + RunSingleThresholdTest(dataStructure, targetArray, ArrayThreshold::ComparisonType::GreaterThan, thresholdValue, isInverted, componentIndex); + CheckIntTestDataMultiComponent(dataStructure, ArrayThreshold::ComparisonType::GreaterThan, thresholdValue, isInverted, componentIndex); + } - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + SECTION("ArrayThreshold: <") + { + RunSingleThresholdTest(dataStructure, targetArray, ArrayThreshold::ComparisonType::LessThan, thresholdValue, isInverted, componentIndex); + CheckIntTestDataMultiComponent(dataStructure, ArrayThreshold::ComparisonType::LessThan, thresholdValue, isInverted, componentIndex); + } - auto* thresholdArray = dataStructure.getDataAs(k_ThresholdArrayPath); - REQUIRE(thresholdArray != nullptr); + SECTION("ArrayThreshold: ==") + { + RunSingleThresholdTest(dataStructure, targetArray, ArrayThreshold::ComparisonType::Operator_Equal, thresholdValue, isInverted, componentIndex); + CheckIntTestDataMultiComponent(dataStructure, ArrayThreshold::ComparisonType::Operator_Equal, thresholdValue, isInverted, componentIndex); + CheckIntTestDataMultiComponent(dataStructure, ArrayThreshold::ComparisonType::Operator_NotEqual, thresholdValue, !isInverted, componentIndex); + } + SECTION("ArrayThreshold: !=") + { + RunSingleThresholdTest(dataStructure, targetArray, ArrayThreshold::ComparisonType::Operator_NotEqual, thresholdValue, isInverted, componentIndex); + CheckIntTestDataMultiComponent(dataStructure, ArrayThreshold::ComparisonType::Operator_Equal, thresholdValue, !isInverted, componentIndex); + CheckIntTestDataMultiComponent(dataStructure, ArrayThreshold::ComparisonType::Operator_NotEqual, thresholdValue, isInverted, componentIndex); + } +} - // For the comparison value of 0.1, the threshold array elements 0 to 9 should be false and 10 through 19 should be true - for(usize i = 0; i < 20; i++) - { - if(i <= 15) - { - REQUIRE((*thresholdArray)[i] == false); - } - else - { - REQUIRE((*thresholdArray)[i] == true); - } - } +/** + * @brief Creates a single threshold for the filter to use. + * @param arrayPath Input DataArray path + * @param comparisonType type of comparison + * @param value Value to threshold against + * @param isInverted Should the threshold output be inverted + * componentIndex Component index of the array to threshold against. + * unionOperator Union operator to apply on the threshold. Defaults to And + */ +std::shared_ptr CreateArrayThreshold(const DataPath& arrayPath, ArrayThreshold::ComparisonType comparisonType, double value, bool isInverted, int componentIndex, + ArrayThreshold::UnionOperator unionOperator = ArrayThreshold::UnionOperator::And) +{ + auto threshold = std::make_shared(); + threshold->setArrayPath(arrayPath); + threshold->setComparisonType(comparisonType); + threshold->setComparisonValue(value); + threshold->setComponentIndex(componentIndex); + threshold->setInverted(isInverted); + threshold->setUnionOperator(unionOperator); + + return threshold; +} + +ArrayThresholdSet CreateThresholdSet1() +{ + ArrayThresholdSet thresholdSet; + + // Threshold: Int > 2 + auto threshold1 = CreateArrayThreshold(k_TestArrayIntPath, ArrayThreshold::ComparisonType::GreaterThan, 2.0, false, 0, ArrayThreshold::UnionOperator::And); + // Threshold: Float < 0.025 + auto threshold2 = CreateArrayThreshold(k_TestArrayFloatPath, ArrayThreshold::ComparisonType::LessThan, 0.025, false, 0, ArrayThreshold::UnionOperator::And); + // Threshold: Int[1] > 0.0 : inverted + auto threshold3 = CreateArrayThreshold(k_MultiComponentArrayPath, ArrayThreshold::ComparisonType::GreaterThan, 0.0, true, 1, ArrayThreshold::UnionOperator::And); + + thresholdSet.setArrayThresholds({threshold1, threshold2, threshold3}); + + return thresholdSet; +} + +bool ExpectedThresholdSet1Mask(usize index, bool inverted) +{ + bool expectedThreshold1 = ExpectedIntSingleComponentMask(ArrayThreshold::ComparisonType::GreaterThan, index, 2.0, false); + bool expectedThreshold2 = ExpectedFloatSingleComponentMask(ArrayThreshold::ComparisonType::LessThan, index, 0.025, false); + bool expectedThreshold3 = ExpectedIntMultiComponentMask(ArrayThreshold::ComparisonType::GreaterThan, index, 0.0, true, 1); + + bool expected = expectedThreshold1 && expectedThreshold2 && expectedThreshold3; + if(inverted) + { + expected = !expected; } + return expected; +} - UnitTest::CheckArraysInheritTupleDims(dataStructure); +ArrayThresholdSet CreateThresholdSet2() +{ + ArrayThresholdSet thresholdSet; + + // Threshold: Int == 1 + auto threshold1 = CreateArrayThreshold(k_TestArrayIntPath, ArrayThreshold::ComparisonType::Operator_Equal, 1.0, false, 0, ArrayThreshold::UnionOperator::And); + // Threshold: Float != 5.0 + auto threshold2 = CreateArrayThreshold(k_TestArrayFloatPath, ArrayThreshold::ComparisonType::Operator_NotEqual, 5.0, false, 0, ArrayThreshold::UnionOperator::Or); + // Threshold: Int[0] < 0.0 : inverted + auto threshold3 = CreateArrayThreshold(k_MultiComponentArrayPath, ArrayThreshold::ComparisonType::LessThan, 0.0, true, 0, ArrayThreshold::UnionOperator::And); + + thresholdSet.setArrayThresholds({threshold1, threshold2, threshold3}); + + return thresholdSet; } -TEMPLATE_TEST_CASE("SimplnxCore::MultiThresholdObjects: Valid Execution - Custom Values", "[SimplnxCore][MultiThresholdObjectsFilter]", int8, uint8, int16, uint16, int32, uint32, int64, uint64, - float32, float64) +bool ExpectedThresholdSet2Mask(usize index, bool inverted) { - UnitTest::LoadPlugins(); + bool expectedThreshold1 = ExpectedIntSingleComponentMask(ArrayThreshold::ComparisonType::Operator_Equal, index, 1.0, false); + bool expectedThreshold2 = ExpectedFloatSingleComponentMask(ArrayThreshold::ComparisonType::Operator_NotEqual, index, 5.0, false); + bool expectedThreshold3 = ExpectedIntMultiComponentMask(ArrayThreshold::ComparisonType::LessThan, index, 0.0, true, 0); - MultiThresholdObjectsFilter filter; - DataStructure dataStructure = CreateTestDataStructure(); - Arguments args; + bool expected = (expectedThreshold1 || expectedThreshold2) && expectedThreshold3; + if(inverted) + { + expected = !expected; + } + return expected; +} + +ArrayThresholdSet CreateThresholdSet3() +{ + ArrayThresholdSet thresholdSet; - float64 trueValue = 25; - float64 falseValue = 10; + auto set1 = std::make_shared(CreateThresholdSet1()); + auto set2 = std::make_shared(CreateThresholdSet2()); + thresholdSet.setArrayThresholds({set1, set2}); + + return thresholdSet; +} + +ArrayThresholdSet CreateThresholdSet4() +{ ArrayThresholdSet thresholdSet; - auto threshold = std::make_shared(); - threshold->setArrayPath(k_TestArrayIntPath); - threshold->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold->setComparisonValue(15); - thresholdSet.setArrayThresholds({threshold}); - args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_UseCustomTrueValue, std::make_any(true)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CustomTrueValue, std::make_any(trueValue)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_UseCustomFalseValue, std::make_any(true)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CustomFalseValue, std::make_any(falseValue)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(GetDataType())); + auto set1 = std::make_shared(CreateThresholdSet1()); + auto set2 = std::make_shared(CreateThresholdSet2()); + set2->setUnionOperator(ArrayThreshold::UnionOperator::Or); - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + thresholdSet.setArrayThresholds({set1, set2}); + + return thresholdSet; +} + +ArrayThresholdSet CreateThresholdSet5() +{ + ArrayThresholdSet thresholdSet; + + auto set1 = std::make_shared(CreateThresholdSet1()); + auto set2 = std::make_shared(CreateThresholdSet2()); + set2->setUnionOperator(ArrayThreshold::UnionOperator::Or); + set2->setInverted(true); + + thresholdSet.setArrayThresholds({set1, set2}); + + return thresholdSet; +} + +void CheckThresholdSet1(DataStructure& dataStructure, bool inverted) +{ + const auto* thresholdArrayPtr = dataStructure.getDataAs(k_ThresholdArrayPath); + REQUIRE(thresholdArrayPtr != nullptr); + + auto& thresholdStore = thresholdArrayPtr->getDataStoreRef(); + + for(usize i = 0; i < k_TupleCount; i++) + { + REQUIRE(thresholdStore[i] == ExpectedThresholdSet1Mask(i, inverted)); + } +} + +void CheckThresholdSet2(DataStructure& dataStructure, bool inverted) +{ + const auto* thresholdArrayPtr = dataStructure.getDataAs(k_ThresholdArrayPath); + REQUIRE(thresholdArrayPtr != nullptr); + + auto& thresholdStore = thresholdArrayPtr->getDataStoreRef(); + + for(usize i = 0; i < k_TupleCount; i++) + { + bool value = thresholdStore[i]; + bool expected = ExpectedThresholdSet2Mask(i, inverted); + REQUIRE(thresholdStore[i] == ExpectedThresholdSet2Mask(i, inverted)); + } +} + +void CheckThresholdSet3(DataStructure& dataStructure, bool inverted) +{ + const auto* thresholdArrayPtr = dataStructure.getDataAs(k_ThresholdArrayPath); + REQUIRE(thresholdArrayPtr != nullptr); + + auto& thresholdStore = thresholdArrayPtr->getDataStoreRef(); + + for(usize i = 0; i < k_TupleCount; i++) + { + bool expectedMask1 = ExpectedThresholdSet1Mask(i, false); + bool expectedMask2 = ExpectedThresholdSet2Mask(i, false); + + bool expected = expectedMask1 && expectedMask2; + if(inverted) + { + expected = !expected; + } + + REQUIRE(thresholdStore[i] == expected); + } +} + +void CheckThresholdSet4(DataStructure& dataStructure, bool inverted) +{ + const auto* thresholdArrayPtr = dataStructure.getDataAs(k_ThresholdArrayPath); + REQUIRE(thresholdArrayPtr != nullptr); + + auto& thresholdStore = thresholdArrayPtr->getDataStoreRef(); + + for(usize i = 0; i < k_TupleCount; i++) + { + bool expectedMask1 = ExpectedThresholdSet1Mask(i, false); + bool expectedMask2 = ExpectedThresholdSet2Mask(i, false); + + bool expected = expectedMask1 || expectedMask2; + if(inverted) + { + expected = !expected; + } + + REQUIRE(thresholdStore[i] == expected); + } +} - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) +void CheckThresholdSet5(DataStructure& dataStructure, bool inverted) +{ + const auto* thresholdArrayPtr = dataStructure.getDataAs(k_ThresholdArrayPath); + REQUIRE(thresholdArrayPtr != nullptr); - auto* thresholdArray = dataStructure.getDataAs>(k_ThresholdArrayPath); - REQUIRE(thresholdArray != nullptr); + auto& thresholdStore = thresholdArrayPtr->getDataStoreRef(); - // For the comparison value of 0.1, the threshold array elements 0 to 9 should be false and 10 through 19 should be true - for(usize i = 0; i < 20; i++) + for(usize i = 0; i < k_TupleCount; i++) { - if(i <= 15) - { - REQUIRE((*thresholdArray)[i] == falseValue); - } - else + bool expectedMask1 = ExpectedThresholdSet1Mask(i, false); + bool expectedMask2 = ExpectedThresholdSet2Mask(i, true); + + bool expected = expectedMask1 || expectedMask2; + if(inverted) { - REQUIRE((*thresholdArray)[i] == trueValue); + expected = !expected; } + + REQUIRE(thresholdStore[i] == expected); + } +} + +TEST_CASE("SimplnxCore::MultiThresholdObjects: Valid Threshold Sets", "[SimplnxCore][MultiThresholdObjectsFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure = CreateTestDataStructure(); + bool isInverted = GENERATE(false, true); + + SECTION("ArraySet 1") + { + auto thresholdSet = CreateThresholdSet1(); + thresholdSet.setInverted(isInverted); + RunThresholdSetTest(dataStructure, thresholdSet); + CheckThresholdSet1(dataStructure, isInverted); + } + + SECTION("ArraySet 2") + { + auto thresholdSet = CreateThresholdSet2(); + thresholdSet.setInverted(isInverted); + RunThresholdSetTest(dataStructure, thresholdSet); + CheckThresholdSet2(dataStructure, isInverted); + } + + SECTION("ArraySet 3") + { + auto thresholdSet = CreateThresholdSet3(); + thresholdSet.setInverted(isInverted); + RunThresholdSetTest(dataStructure, thresholdSet); + CheckThresholdSet3(dataStructure, isInverted); + } + + SECTION("ArraySet 4") + { + auto thresholdSet = CreateThresholdSet4(); + thresholdSet.setInverted(isInverted); + RunThresholdSetTest(dataStructure, thresholdSet); + CheckThresholdSet4(dataStructure, isInverted); + } + + SECTION("ArraySet 5") + { + auto thresholdSet = CreateThresholdSet5(); + thresholdSet.setInverted(isInverted); + RunThresholdSetTest(dataStructure, thresholdSet); + CheckThresholdSet5(dataStructure, isInverted); } } +// Invalid executions + TEST_CASE("SimplnxCore::MultiThresholdObjects: Invalid Execution", "[SimplnxCore][MultiThresholdObjectsFilter]") { UnitTest::LoadPlugins(); @@ -265,21 +751,6 @@ TEST_CASE("SimplnxCore::MultiThresholdObjects: Invalid Execution", "[SimplnxCore args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); } - SECTION("Mismatching Components in Threshold Arrays") - { - ArrayThresholdSet thresholdSet; - auto threshold1 = std::make_shared(); - threshold1->setArrayPath(k_TestArrayFloatPath); - threshold1->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold1->setComparisonValue(0.1); - auto threshold2 = std::make_shared(); - threshold2->setArrayPath(k_MismatchingComponentsArrayPath); - threshold2->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold2->setComparisonValue(0.1); - thresholdSet.setArrayThresholds({threshold1, threshold2}); - - args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); - } SECTION("Out of Bounds Component Index") { ArrayThresholdSet thresholdSet; @@ -424,328 +895,232 @@ TEST_CASE("SimplnxCore::MultiThresholdObjects: Invalid Execution - Boolean Custo UnitTest::CheckArraysInheritTupleDims(dataStructure); } +// DataType checks + template void checkMaskValues(const DataStructure& dataStructure, const DataPath& thresholdArrayPath) { auto* thresholdArrayPtr = dataStructure.getDataAs>(thresholdArrayPath); REQUIRE(thresholdArrayPtr != nullptr); - auto& thresholdArray = (*thresholdArrayPtr); + auto& thresholdStore = thresholdArrayPtr->getDataStoreRef(); // For the comparison value of 0.1, the threshold array elements 0 to 9 should be false and 10 through 19 should be true - for(usize i = 0; i < 20; i++) + for(usize i = 0; i < k_TupleCount; i++) { - if(i < 10) + if(i < 5) { - REQUIRE(thresholdArray[i] == static_cast(0)); + REQUIRE(thresholdStore[i] == static_cast(0)); } else { - REQUIRE(thresholdArray[i] == static_cast(1)); + REQUIRE(thresholdStore[i] == static_cast(1)); } } } -TEST_CASE("SimplnxCore::MultiThresholdObjects: Valid Execution, DataType", "[SimplnxCore][MultiThresholdObjectsFilter]") +template +void runMaskTypeFilter(MultiThresholdObjectsFilter& filter, Arguments& args, DataStructure& dataStructure) +{ + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + // Execute the filter and check the result + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + + checkMaskValues(dataStructure, k_ThresholdArrayPath); +} + +TEST_CASE("SimplnxCore::MultiThresholdObjects: Valid Execution, Mask DataType", "[SimplnxCore][MultiThresholdObjectsFilter]") { UnitTest::LoadPlugins(); DataStructure dataStructure = CreateTestDataStructure(); + // Shared filter setup + MultiThresholdObjectsFilter filter; + Arguments args; + + ArrayThresholdSet thresholdSet; + auto threshold = std::make_shared(); + threshold->setArrayPath(k_TestArrayFloatPath); + threshold->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); + threshold->setComparisonValue(0.05); + thresholdSet.setArrayThresholds({threshold}); + + args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); + args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); + // Signed SECTION("Int8 Threshold") { - MultiThresholdObjectsFilter filter; - Arguments args; - - ArrayThresholdSet thresholdSet; - auto threshold = std::make_shared(); - threshold->setArrayPath(k_TestArrayFloatPath); - threshold->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold->setComparisonValue(0.1); - thresholdSet.setArrayThresholds({threshold}); - - args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(DataType::int8)); - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) - - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) - - checkMaskValues(dataStructure, k_ThresholdArrayPath); + runMaskTypeFilter(filter, args, dataStructure); } SECTION("Int16 Threshold") { - MultiThresholdObjectsFilter filter; - Arguments args; - - ArrayThresholdSet thresholdSet; - auto threshold = std::make_shared(); - threshold->setArrayPath(k_TestArrayFloatPath); - threshold->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold->setComparisonValue(0.1); - thresholdSet.setArrayThresholds({threshold}); - - args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(DataType::int16)); - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) - - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) - - checkMaskValues(dataStructure, k_ThresholdArrayPath); + runMaskTypeFilter(filter, args, dataStructure); } SECTION("Int32 Threshold") { - MultiThresholdObjectsFilter filter; - Arguments args; - - ArrayThresholdSet thresholdSet; - auto threshold = std::make_shared(); - threshold->setArrayPath(k_TestArrayFloatPath); - threshold->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold->setComparisonValue(0.1); - thresholdSet.setArrayThresholds({threshold}); - - args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(DataType::int32)); - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) - - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) - - checkMaskValues(dataStructure, k_ThresholdArrayPath); + runMaskTypeFilter(filter, args, dataStructure); } SECTION("Int64 Threshold") { - MultiThresholdObjectsFilter filter; - Arguments args; - - ArrayThresholdSet thresholdSet; - auto threshold = std::make_shared(); - threshold->setArrayPath(k_TestArrayFloatPath); - threshold->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold->setComparisonValue(0.1); - thresholdSet.setArrayThresholds({threshold}); - - args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(DataType::int64)); - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) - - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) - - checkMaskValues(dataStructure, k_ThresholdArrayPath); + runMaskTypeFilter(filter, args, dataStructure); } // Unsigned SECTION("UInt8 Threshold") { - MultiThresholdObjectsFilter filter; - Arguments args; - - ArrayThresholdSet thresholdSet; - auto threshold = std::make_shared(); - threshold->setArrayPath(k_TestArrayFloatPath); - threshold->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold->setComparisonValue(0.1); - thresholdSet.setArrayThresholds({threshold}); - - args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(DataType::uint8)); - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) - - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) - - checkMaskValues(dataStructure, k_ThresholdArrayPath); + runMaskTypeFilter(filter, args, dataStructure); } SECTION("UInt16 Threshold") { - MultiThresholdObjectsFilter filter; - Arguments args; - - ArrayThresholdSet thresholdSet; - auto threshold = std::make_shared(); - threshold->setArrayPath(k_TestArrayFloatPath); - threshold->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold->setComparisonValue(0.1); - thresholdSet.setArrayThresholds({threshold}); - - args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(DataType::uint16)); - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) - - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) - - checkMaskValues(dataStructure, k_ThresholdArrayPath); + runMaskTypeFilter(filter, args, dataStructure); } SECTION("UInt32 Threshold") { - MultiThresholdObjectsFilter filter; - Arguments args; - - ArrayThresholdSet thresholdSet; - auto threshold = std::make_shared(); - threshold->setArrayPath(k_TestArrayFloatPath); - threshold->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold->setComparisonValue(0.1); - thresholdSet.setArrayThresholds({threshold}); - - args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(DataType::uint32)); - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) - - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) - - checkMaskValues(dataStructure, k_ThresholdArrayPath); + runMaskTypeFilter(filter, args, dataStructure); } SECTION("UInt64 Threshold") { - MultiThresholdObjectsFilter filter; - Arguments args; - - ArrayThresholdSet thresholdSet; - auto threshold = std::make_shared(); - threshold->setArrayPath(k_TestArrayFloatPath); - threshold->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold->setComparisonValue(0.1); - thresholdSet.setArrayThresholds({threshold}); - - args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(DataType::uint64)); - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) - - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) - - checkMaskValues(dataStructure, k_ThresholdArrayPath); + runMaskTypeFilter(filter, args, dataStructure); } // Floating Point SECTION("Float32 Threshold") { - MultiThresholdObjectsFilter filter; - Arguments args; - - ArrayThresholdSet thresholdSet; - auto threshold = std::make_shared(); - threshold->setArrayPath(k_TestArrayFloatPath); - threshold->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold->setComparisonValue(0.1); - thresholdSet.setArrayThresholds({threshold}); - - args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(DataType::float32)); - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) - - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) - - checkMaskValues(dataStructure, k_ThresholdArrayPath); + runMaskTypeFilter(filter, args, dataStructure); } SECTION("Float64 Threshold") { - MultiThresholdObjectsFilter filter; - Arguments args; - - ArrayThresholdSet thresholdSet; - auto threshold = std::make_shared(); - threshold->setArrayPath(k_TestArrayFloatPath); - threshold->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold->setComparisonValue(0.1); - thresholdSet.setArrayThresholds({threshold}); - - args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(DataType::float64)); - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) - - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) - - checkMaskValues(dataStructure, k_ThresholdArrayPath); + runMaskTypeFilter(filter, args, dataStructure); } UnitTest::CheckArraysInheritTupleDims(dataStructure); } -TEST_CASE("SimplnxCore::MultiThresholdObjects: Valid Execution - Multicomponent", "[SimplnxCore][MultiThresholdObjectsFilter]") +void TestMaskOutputForInputType(Int8AbstractDataStore& mask, float64 comparisonValue) { - DataStructure dataStructure = CreateTestDataStructure(); + usize count = mask.size(); + for(usize i = 0; i < count; i++) + { + int8 targetValue = (i < comparisonValue) ? 1 : 0; + REQUIRE(mask[i] == targetValue); + } +} + +TEST_CASE("SimplnxCore::MultiThresholdObjects: Valid Execution, Input Array DataType", "[SimplnxCore][MultiThresholdObjectsFilter]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure = CreateTestDataStructure2(); + + float64 comparisonValue = 3.0; + DataPath matrixPath({k_ImageGeometry, k_CellData}); + + // Shared filter setup MultiThresholdObjectsFilter filter; Arguments args; ArrayThresholdSet thresholdSet; auto threshold = std::make_shared(); - threshold->setArrayPath(k_MultiComponentArrayPath); - threshold->setComparisonType(ArrayThreshold::ComparisonType::GreaterThan); - threshold->setComparisonValue(0); - threshold->setComponentIndex(1); + threshold->setComparisonType(ArrayThreshold::ComparisonType::LessThan); + threshold->setComparisonValue(comparisonValue); thresholdSet.setArrayThresholds({threshold}); - args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); + // Signed + SECTION("Int8") + { + threshold->setArrayPath(matrixPath.createChildPath("int8")); + args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); + } + SECTION("Int16") + { + threshold->setArrayPath(matrixPath.createChildPath("int16")); + args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); + } + SECTION("Int32") + { + threshold->setArrayPath(matrixPath.createChildPath("int32")); + args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); + } + SECTION("Int64") + { + threshold->setArrayPath(matrixPath.createChildPath("int64")); + args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); + } + // Unsigned + SECTION("UInt8") + { + threshold->setArrayPath(matrixPath.createChildPath("uint8")); + args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); + } + SECTION("UInt16") + { + threshold->setArrayPath(matrixPath.createChildPath("uint16")); + args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); + } + SECTION("UInt32") + { + threshold->setArrayPath(matrixPath.createChildPath("uint32")); + args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); + } + SECTION("UInt64") + { + threshold->setArrayPath(matrixPath.createChildPath("uint64")); + args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); + } + // Floating Point + SECTION("Float32") + { + threshold->setArrayPath(matrixPath.createChildPath("float32")); + args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); + } + SECTION("Float64") + { + threshold->setArrayPath(matrixPath.createChildPath("float64")); + args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); + } + // Bool + SECTION("Boolean") + { + threshold->setArrayPath(matrixPath.createChildPath("bool")); + args.insertOrAssign(MultiThresholdObjectsFilter::k_ArrayThresholdsObject_Key, std::make_any(thresholdSet)); + comparisonValue = 0.9; + } + args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedDataName_Key, std::make_any(k_ThresholdArrayName)); - args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(DataType::boolean)); + args.insertOrAssign(MultiThresholdObjectsFilter::k_CreatedMaskType_Key, std::make_any(DataType::int8)); // Preflight the filter and check result auto preflightResult = filter.preflight(dataStructure, args); @@ -755,63 +1130,7 @@ TEST_CASE("SimplnxCore::MultiThresholdObjects: Valid Execution - Multicomponent" auto executeResult = filter.execute(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) - auto* thresholdArray = dataStructure.getDataAs(k_ThresholdArrayPath); - REQUIRE(thresholdArray != nullptr); - - usize numTuples = thresholdArray->getNumberOfTuples(); - - // (x, y, z) - // y > 0 - // even tuple indices should be true except 0 - REQUIRE_FALSE((*thresholdArray)[0]); - for(usize i = 1; i < numTuples; i++) - { - bool value = (*thresholdArray)[i]; - if(i % 2 == 0) - { - REQUIRE(value); - } - else - { - REQUIRE_FALSE(value); - } - } - - UnitTest::CheckArraysInheritTupleDims(dataStructure); -} - -TEST_CASE("SimplnxCore::MultiThresholdObjectsFilter: SIMPL Backwards Compatibility", "[SimplnxCore][MultiThresholdObjectsFilter][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 std::vector> fixtures = { - {"SIMPL 6.5 (UUID)", conversionDir / "6_5" / "MultiThresholdObjectsFilter.json"}, - {"SIMPL 6.4 (Filter_Name)", conversionDir / "6_4" / "MultiThresholdObjectsFilter.json"}, - }; - - for(const auto& [label, fixturePath] : fixtures) - { - DYNAMIC_SECTION(label) - { - auto pipelineResult = Pipeline::FromSIMPLFile(fixturePath, filterList); - REQUIRE(pipelineResult.valid()); - - auto& pipeline = pipelineResult.value(); - REQUIRE(pipeline.size() == 1); - - auto* pipelineFilter = dynamic_cast(pipeline.at(0)); - REQUIRE(pipelineFilter != nullptr); - - const IFilter* filter = pipelineFilter->getFilter(); - REQUIRE(filter != nullptr); - REQUIRE(filter->uuid() == FilterTraits::uuid); - - const Arguments args = pipelineFilter->getArguments(); - CHECK(args.value(MultiThresholdObjectsFilter::k_CreatedDataName_Key) == "TestName"); - } - } + auto* maskArray = dataStructure.getDataAs(matrixPath.createChildPath(k_ThresholdArrayName)); + auto& maskStore = maskArray->getDataStoreRef(); + TestMaskOutputForInputType(maskStore, comparisonValue); } diff --git a/src/Plugins/SimplnxCore/vv/MultiThresholdObjectsFilter.md b/src/Plugins/SimplnxCore/vv/MultiThresholdObjectsFilter.md new file mode 100644 index 0000000000..0b9577ecd8 --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/MultiThresholdObjectsFilter.md @@ -0,0 +1,131 @@ +# V&V Report: MultiThresholdObjectsFilter + +| | | +|--------|--------------| +| Plugin | SimplnxCore | +| SIMPLNX UUID | `4246245e-1011-4add-8436-0af6bed19228` | +| DREAM3D 6.5.171 equivalent | Two separate legacy filters, consolidated: **Threshold Objects** (`MultiThresholdObjects`, SIMPL UUID `014b7300-cf36-5ede-a751-5faf9b119dae`) and **Threshold Objects (Advanced)** (`MultiThresholdObjects2`, SIMPL UUID `686d5393-2b02-5c86-b887-dd81a8ae80f2`) — both mapped to this single filter's UUID in `SimplnxCoreLegacyUUIDMapping.hpp` (see Algorithm Relationship) | +| Verified commit | ** | +| Status | READY FOR REVIEW | +| Sign-off | *pending — DRAFT, not yet reviewed* | + +## At a glance + +| Aspect | Current state | +|------------------------|------------------------------------------------------------------------------------------------------------------------------| +| Algorithm Relationship | **Rewrite.** Consolidates two independently-shipped legacy filters — **Threshold Objects** (flat, AND-only) and **Threshold Objects (Advanced)** (nested AND/OR sets) — into one SIMPLNX filter under one new UUID, unified around a single `ArrayThresholdSet` model. Not a line-by-line translation of either legacy source. | +| Oracle (confirmed) | **Class 1 (Analytical) — confirmed.** `expected[i] = COMPARISON(input[i], value)`, hand-combined via AND/OR/invert boolean algebra. Encoded as 9 `TEST_CASE` groups (17 ctest entries) in `MultiThresholdObjectsTest.cpp`, all pass. | +| Code paths enumerated | **23 of 24 exercised.** Only the unreachable comparison-operator `else`-throw is untested (all four `ComparisonType` enumerators are covered elsewhere). | +| Tests today | **9 `TEST_CASE` groups / 17 ctest entries.** Exhaustive sweeps over comparison operator × invert × union operator × set nesting × mask `DataType` (11 types) × source-array `DataType` (11 types), plus 4 negative/error-path groups. All fixtures built in-memory. | +| Exemplar archive | **None.** All fixtures are constructed in-memory by `CreateTestDataStructure()` / `CreateTestDataStructure2()`; no `.dream3d` exemplar or `download_test_data()` entry exists for this filter. | +| Legacy comparison | **Not run** — no local build of legacy DREAM3D 6.5.171 (or its Advanced comparison-set variant) is available in this environment. Deferred; see `vv/deviations/MultiThresholdObjectsFilter.md`. | +| Bug flags | None identified. Provisional — no legacy comparison has been run yet. | +| V&V phase | Oracle chosen and applied (Class 1), code paths enumerated (23/24), test inventory documented. **Outstanding:** legacy 6.5.171 runtime A/B comparison, second-engineer oracle review, status promotion DRAFT → READY FOR REVIEW. | + +For worked instances see `src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md` and `src/Plugins/OrientationAnalysis/vv/ComputeAvgCAxesFilter.md` (on `topic/vv/compute_avg_caxis`). + +## Summary + +`MultiThresholdObjectsFilter` builds a typed mask array by elementwise-comparing one or more input arrays against user-supplied thresholds, combined through an arbitrarily-nested tree of AND/OR/invert `ArrayThresholdSet`s. Verification uses a **Class 1 (Analytical) oracle**: every comparison operator, invert flag, union operator, nesting depth, and both the mask-output and source-input `DataType` are exhaustively hand-derived and asserted in `MultiThresholdObjectsTest.cpp` (9 `TEST_CASE` groups, all passing). 23 of 24 algorithm/preflight code paths are exercised; the filter has not yet been diffed against legacy DREAM3D 6.5.171. + +## Algorithm Relationship + +*Classification:* **Rewrite** + +*Evidence:* `SimplnxCoreLegacyUUIDMapping.hpp` maps **two distinct legacy SIMPL UUIDs** to this single SIMPLNX filter's UUID: + +``` +014b7300-cf36-5ede-a751-5faf9b119dae → MultiThresholdObjectsFilter // MultiThresholdObjects ("Threshold Objects") +686d5393-2b02-5c86-b887-dd81a8ae80f2 → MultiThresholdObjectsFilter // MultiThresholdObjects2 ("Threshold Objects (Advanced)") +``` + +`FromSIMPLJson()` correspondingly branches on which legacy UUID (or, for 6.4 pipelines lacking a UUID, which legacy class name) produced the incoming JSON: the basic `MultiThresholdObjects` source is read through `ComparisonSelectionFilterParameterConverter` (flat, AND-only comparison list) and the advanced `MultiThresholdObjects2` source is read through `ComparisonSelectionAdvancedFilterParameterConverter` (nested AND/OR comparison sets). Both are converted into the same `ArrayThresholdSet` argument. This is **not** a line-by-line port of a single legacy algorithm — it's a consolidation of two independently-shipped legacy filters into one, which is why the classification is **Rewrite** rather than Port, per `vv_policy.md`: *"keeping [a UUID relationship] is a claim of functional equivalence... The Deviations file must defend the claim."* Here the claim is stronger than usual — that the merged filter reproduces each of the two legacy filters' behavior when configured equivalently to it. SIMPL 6.4/6.5 conversion fixtures exist at `test/simpl_conversion/6_4/MultiThresholdObjectsFilter.json` and `test/simpl_conversion/6_5/MultiThresholdObjectsFilter.json`, but these only assert the *argument conversion* round-trips correctly, not that execution output matches either legacy filter — that's the outstanding legacy A/B (see Deviations file). + +*Structural differences from each legacy source (none independently confirmed by a legacy A/B yet):* + +1. **Consolidation itself** — one `ArrayThresholdSet` tree replaces two separate legacy parameter models (flat list vs. nested set); the flat legacy model is representable as a one-level `ArrayThresholdSet`, but this equivalence has not been verified end-to-end against either legacy filter's actual output. +2. Multi-component index selection added (`#1184`, `32837a30f`) — not present in the legacy `Threshold Objects` filter (scalar-only comparison); unclear whether `Threshold Objects (Advanced)` had an equivalent. +3. Custom TRUE/FALSE mask output values added (`#669`, `b65210cf3`) — additive parameter; unconfirmed whether either legacy filter had this option or if it's SIMPLNX-only. +4. Default mask output `DataType` changed to `uint8` (`#1502`, `49919b086`) — unconfirmed against either legacy filter's default. +5. `executeImpl()` body moved into `Algorithms/MultiThresholdObjects.{hpp,cpp}` (`#1544`, `8381d1dd5`) — structural only, no behavior change (internal to SIMPLNX, not a legacy-relationship concern). + +*Material PRs since baseline (2025-10-01):* none identified beyond the deltas above and this branch's `vv/MultiThresholdObjects` restructuring + test work. + +## Oracle + +*Class:* **1 (Analytical)** + +*Applied:* For a single threshold, `expected[i] = COMPARISON(input[i], value)` (optionally inverted); for a component-indexed array, `input[i]` is replaced by `input[i][componentIndex]`. For a threshold set, `expected` is the boolean combination of each member's own `expected` value: the first member always seeds the accumulator, and the configured `UnionOperator` (AND/OR) combines each subsequent member; the whole set's `expected` is inverted again if the set itself is marked inverted. Every free variable in this formula — comparison operator, invert flag, union operator, set nesting, component index, mask output `DataType`, and source-array `DataType` — is enumerated directly against this closed-form definition in the test file's `Expected*Mask` helper functions, independent of the algorithm's own C++ control flow (`ThresholdFilterHelper`, `InsertThreshold`, `ApplyThresholdValues`). + +*Encoded:* `test/MultiThresholdObjectsTest.cpp` — + +- `Valid Single Thresholds: Int` / `: Float` / `: Int Multi-Component` — comparison operator × invert × (component index for multi-component) sweep, via `ExpectedIntSingleComponentMask` / `ExpectedFloatSingleComponentMask` / `ExpectedIntMultiComponentMask` +- `Valid Threshold Sets` — 5 hand-built AND / OR / nested-set / nested-set-with-OR / nested-set-with-OR+invert configurations (`CreateThresholdSet1`–`5`), via `ExpectedThresholdSet1Mask`–`5` +- `Valid Execution, Mask DataType` — 11 mask-output `DataType`s +- `Valid Execution, Input Array DataType` — 11 source-array `DataType`s +- `Invalid Execution`, `Invalid Execution - Out of Bounds Custom Values` (9 numeric types), `Invalid Execution - Boolean Custom Values` — negative-path fixtures + +9 `TEST_CASE` groups (17 ctest entries, counting the 9 `TEMPLATE_TEST_CASE` type instantiations separately), all pass at HEAD. + +*Second-engineer review:* Skipped — recorded reason: the oracle is elementwise comparison plus boolean set algebra (AND/OR/invert), and the test matrix enumerates it exhaustively (every operator × invert × union operator × nesting × both `DataType` axes) rather than sampling a single hand-derivation, substituting breadth for independent derivation review. **This is not a substitute for a named second-engineer pass** — it is recorded here as an outstanding gate for promotion past DRAFT, not a completed one. + +## Code path coverage + +**23 of 24 paths exercised.** The one gap is low-risk (row 13). + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/MultiThresholdObjects.cpp` (~255 lines), plus 7 preflight-only paths in `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/MultiThresholdObjectsFilter.cpp`. + +Two logical stages: **(a) preflight** validates the threshold set / mask-type / custom-value configuration and stages the output `CreateArrayAction`; **(b) algorithm** recursively evaluates the `ArrayThresholdSet` tree (per-array comparison → per-set union/invert/replace combination) and writes the result into the mask array via a type-dispatched functor. + +| # | Stage | Path | Test case | +|----|-------------------|------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------| +| 1 | (a) Preflight | `thresholdPaths.empty()` → error `-4000` | `Invalid Execution` — "Empty ArrayThresholdSet" | +| 2 | (a) Preflight | Tuple-count mismatch across threshold arrays → `ErrorCodes::UnequalTuples` | `Invalid Execution` — "Mismatching Tuples in Threshold Arrays" | +| 3 | (a) Preflight | `componentIndex >= numComponents` (via `CheckComponentIndicesInThresholds`, recurses into nested sets) → `ErrorCodes::InvalidComponentIndex` | `Invalid Execution` — "Out of Bounds Component Index" | +| 4 | (a) Preflight | `maskArrayType == boolean && useCustomTrueValue` → `CustomTrueWithBoolean` | `Invalid Execution - Boolean Custom Values` — "Custom True Value" | +| 5 | (a) Preflight | `maskArrayType == boolean && useCustomFalseValue` → `CustomFalseWithBoolean` | `Invalid Execution - Boolean Custom Values` — "Custom False Value" | +| 6 | (a) Preflight | `useCustomTrueValue` value outside `[min,max]` of mask type → `CustomTrueOutOfBounds` | `TEMPLATE_TEST_CASE …Out of Bounds Custom Values` — "True Value < Minimum" / "> Maximum", all 9 numeric types | +| 7 | (a) Preflight | `useCustomFalseValue` value outside bounds → `CustomFalseOutOfBounds` | same — "False Value < Minimum" / "> Maximum" | +| 8 | (a) Preflight | Success → stage `CreateArrayAction` for mask output | every "Valid …" test | +| 9 | (b) Algorithm | `ComparisonType::LessThan` | "ArrayThreshold: <" (int / float / multi-component) | +| 10 | (b) Algorithm | `ComparisonType::GreaterThan` | "ArrayThreshold: >" | +| 11 | (b) Algorithm | `ComparisonType::Operator_Equal` | "ArrayThreshold: ==" | +| 12 | (b) Algorithm | `ComparisonType::Operator_NotEqual` | "ArrayThreshold: !=" | +| 13 | (b) Algorithm | `else` → `throw std::runtime_error` (unrecognized comparison type) | *Not directly tested. Unreachable via the public `ComparisonType` enum — all enumerators are exercised by rows 9–12.* | +| 14 | (b) Algorithm | `InsertThreshold` with `inverse == true` (flip before combine) | `isInverted = GENERATE(false, true)` in every single-threshold and threshold-set test | +| 15 | (b) Algorithm | `InsertThreshold` with `inverse == false` | same | +| 16 | (b) Algorithm | Combine with `UnionOperator::Or` | `CreateThresholdSet2` (threshold2 = Or), `CreateThresholdSet4`/`5` (nested-set union = Or) | +| 17 | (b) Algorithm | Combine with `UnionOperator::And` (else branch) | `CreateThresholdSet1` (threshold2/3 = And), `CreateThresholdSet3` default nested And | +| 18 | (b) Algorithm | `ApplyThresholdValues` with `replaceInput == true` (first item in a set forces Or regardless of configured operator) | implicit in every threshold set — first entry of every `CreateThresholdSet*` | +| 19 | (b) Algorithm | `ApplyThresholdValues` with `replaceInput == false` (honors configured operator for later items) | same sets, 2nd/3rd entries | +| 20 | (b) Algorithm | `ThresholdSet` recursion — item is a nested `ArrayThresholdSet` | `CreateThresholdSet3`/`4`/`5` (set-of-sets) | +| 21 | (b) Algorithm | `ThresholdSet` — item is a leaf `ArrayThreshold` | all tests | +| 22 | (b) Algorithm | `ThresholdSetFunctor` dispatch on **mask (output) DataType** | `Valid Execution, Mask DataType` — boolean + int8/16/32/64 + uint8/16/32/64 + float32/64, all 11 types | +| 23 | (b) Algorithm | `ExecuteThresholdHelper` dispatch on **source array's DataType** | `Valid Execution, Input Array DataType` — int8/16/32/64 + uint8/16/32/64 + float32/64 + boolean, all 11 types | +| 24 | (b) Algorithm | Multi-component `componentIndex != 0` selection | `Valid Single Thresholds: Int Multi-Component` (`componentIndex = GENERATE(0,1,2)`), plus `componentIndex=1` in Set1, `=0` in Set2 | + +Not counted as an algorithm/preflight path: the "Empty ArrayThreshold DataPath" section of `Invalid Execution` exercises `ArrayThresholdsParameter`'s own path-existence validation, which runs before `preflightImpl` is called — it's a parameter-layer gate, not code inside this filter or algorithm. + +Also note: `k_MismatchingComponentsArrayPath` (test file, line 31) is a leftover unused `DataPath` constant — the array it used to name was removed when `Valid Execution, Input Array DataType` was added. Not a coverage gap (the filter has no cross-array component-count check), just dead test-source code worth deleting. + +## Test inventory + +| Test case | Status | Notes | +|-----------|--------|-------| +| `Valid Single Thresholds: Int` | kept | `GENERATE` over 8 threshold values × 2 invert states, 4 `SECTION`s (`>`, `<`, `==`, `!=`) against `k_TestArrayIntPath`; every tuple checked via `ExpectedIntSingleComponentMask`. | +| `Valid Single Thresholds: Float` | kept | Same sweep against `k_TestArrayFloatPath` via `ExpectedFloatSingleComponentMask`. | +| `Valid Single Thresholds: Int Multi-Component` | kept | Adds `componentIndex = GENERATE(0,1,2)` against `k_MultiComponentArrayPath`. | +| `Valid Threshold Sets` | kept | 5 `SECTION`s (`ArraySet 1`–`5`) covering AND, OR, nested-set, nested-set-with-OR, and nested-set-with-OR+invert combinations, each × `isInverted`. | +| `Invalid Execution` | kept | 4 `SECTION`s: empty threshold set (`-4000`), empty threshold `DataPath` (parameter-layer validation), out-of-bounds component index (`InvalidComponentIndex`), mismatched tuple counts (`UnequalTuples`). | +| `Invalid Execution - Out of Bounds Custom Values` (`TEMPLATE_TEST_CASE`) | kept | 9 numeric-type instantiations × 4 `SECTION`s (true/false value below minimum / above maximum) — `CustomTrueOutOfBounds` / `CustomFalseOutOfBounds`. | +| `Invalid Execution - Boolean Custom Values` | kept | 2 `SECTION`s — custom TRUE/FALSE value rejected when mask type is `boolean`. | +| `Valid Execution, Mask DataType` | kept | 11 `SECTION`s, one per mask-output `DataType` (int8…float64; boolean covered via the default mask type used throughout the other `TEST_CASE`s). | +| `Valid Execution, Input Array DataType` | new-for-V&V (`d18b0f34d`, 2026-07-23) | 11 `SECTION`s, one per **source-array** `DataType` (int8…float64, bool) — closes the code-path gap on row 23 identified during path enumeration. | + +## Exemplar archive + +None. All fixtures for this filter are constructed in-memory in `test/MultiThresholdObjectsTest.cpp` (`CreateTestDataStructure()`, `CreateTestDataStructure2()`); there is no `.dream3d` exemplar and no `download_test_data()` entry in `test/CMakeLists.txt` for `MultiThresholdObjectsFilter`. No provenance sidecar is needed. + +## Deviations from DREAM3D 6.5.171 + +- Legacy comparison has **not been run**. 0 deviation entries exist. See `vv/deviations/MultiThresholdObjectsFilter.md` for the reason and the configurations to prioritize once a legacy build is available. diff --git a/src/Plugins/SimplnxCore/vv/deviations/MultiThresholdObjectsFilter.md b/src/Plugins/SimplnxCore/vv/deviations/MultiThresholdObjectsFilter.md new file mode 100644 index 0000000000..091f1190f8 --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/deviations/MultiThresholdObjectsFilter.md @@ -0,0 +1,41 @@ +# Deviations from DREAM3D 6.5.171: MultiThresholdObjectsFilter + +This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. + +Entries are referenced by stable ID (`MultiThresholdObjectsFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. + +--- + +## Status: comparison not yet run + +No local build of legacy DREAM3D 6.5.171 is available in this environment. No runtime A/B has been performed against **either** legacy predecessor: + +- **Threshold Objects** (`MultiThresholdObjects`, SIMPL UUID `014b7300-cf36-5ede-a751-5faf9b119dae`) — flat, AND-only comparison list +- **Threshold Objects (Advanced)** (`MultiThresholdObjects2`, SIMPL UUID `686d5393-2b02-5c86-b887-dd81a8ae80f2`) — nested AND/OR comparison sets + +**0 deviation entries exist as of this DRAFT.** + +Per `vv_policy.md`'s one ordering rule ("pick the oracle before running any DREAM3D 6.5.171 comparison"), the V&V report's Class 1 (Analytical) oracle already establishes SIMPLNX correctness independently of legacy — see `vv/MultiThresholdObjectsFilter.md`. The legacy A/B remains outstanding and is required before this filter's status can move past DRAFT; it is not a precondition for the oracle work already done. + +**Because the Algorithm Relationship is classified as Rewrite** (this filter consolidates two independently-shipped legacy filters into one — see the V&V report), the burden here is higher than a straight port: the eventual comparison must independently confirm output equivalence against **both** legacy filters, run separately — + +1. Configurations expressible in the legacy flat (AND-only) model, compared against **Threshold Objects**. +2. Configurations using nested AND/OR sets or per-set invert, compared against **Threshold Objects (Advanced)**. + +A clean result on only one of the two legacy filters is not sufficient to close this Rewrite's defense — both source filters must be reconciled before the merged UUID's functional-equivalence claim can be considered verified. + +## Filter UUID + +`4246245e-1011-4add-8436-0af6bed19228` + +## Other configurations to prioritize once a legacy build is available + +Beyond running the two legacy comparisons described above, these individual parameter additions are the other likely sources of drift — not observed deviations, just a prioritized test plan for the eventual A/B: + +1. **Multi-component index selection** (`#1184` addition) — not present in legacy `Threshold Objects`; unconfirmed whether `Threshold Objects (Advanced)` had an equivalent. Comparison is only meaningful against whichever legacy filter (if either) has this feature. +2. **Custom TRUE/FALSE mask output values** (`#669` addition) — compare with it left at legacy defaults first, then with custom values set. +3. **Default mask output `DataType`** — SIMPLNX defaults to `uint8` (`#1502`); confirm what each legacy filter's default was and whether any migration guidance is needed for pipelines that relied on the default rather than explicitly setting it. + +## Entries + +No entries yet. When a comparison surfaces an actual behavioral difference, add it here following the stable-ID convention (`MultiThresholdObjectsFilter-D1`, `-D2`, …) with fields: Deviation ID, Filter UUID, Status, Symptom, Root cause (`bug` | `precision` | `order of operations` | `library` | `algorithmic choice`), Affected users, Recommendation.