From 35c46a9fd8c8b023d6165b1fd81bfcc2dc71193f Mon Sep 17 00:00:00 2001 From: nyoungbq Date: Fri, 5 Jun 2026 08:38:59 -0400 Subject: [PATCH 1/3] Remove Legacy Test --- .../test/ComputeFeatureSizesTest.cpp | 72 ------------------- 1 file changed, 72 deletions(-) diff --git a/src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp b/src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp index 4348552717..6c915c4140 100644 --- a/src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp @@ -8,18 +8,11 @@ #include #include -#include using namespace nx::core; namespace fs = std::filesystem; -namespace LegacyTest -{ -const std::string k_Volumes("Volumes"); -const std::string k_EquivalentDiameters("EquivalentDiameters"); -} // namespace LegacyTest - namespace Test { // Geometry Level @@ -647,71 +640,6 @@ TEST_CASE("SimplnxCore::ComputeFeatureSizes: Invalid: Preflight Failure", "[Simp #endif } -TEST_CASE("SimplnxCore::ComputeFeatureSizes: Legacy: Small IN100 Test", "[SimplnxCore][ComputeFeatureSizes]") -{ - UnitTest::LoadPlugins(); - - 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({Constants::k_DataContainer}); - DataPath cellDataPath = smallIn100Group.createChildPath(Constants::k_CellData); - DataPath cellPhasesPath = cellDataPath.createChildPath(Constants::k_Phases); - DataPath featureIdsPath = cellDataPath.createChildPath(Constants::k_FeatureIds); - DataPath featureGroup = smallIn100Group.createChildPath(Constants::k_CellFeatureData); - std::string volumesName = "computed_volumes"; - std::string numElementsName = "computed_NumElements"; - std::string EquivalentDiametersName = "computed_EquivalentDiameters"; - - std::vector featureNames = {LegacyTest::k_Volumes, LegacyTest::k_EquivalentDiameters, Constants::k_NumElements}; - - { - ComputeFeatureSizesFilter filter; - Arguments args; - - args.insert(ComputeFeatureSizesFilter::k_GeometryPath_Key, std::make_any(smallIn100Group)); - args.insert(ComputeFeatureSizesFilter::k_SaveElementSizes_Key, std::make_any(false)); - args.insert(ComputeFeatureSizesFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(featureIdsPath)); - args.insert(ComputeFeatureSizesFilter::k_CellFeatureAttributeMatrixPath_Key, std::make_any(featureGroup)); - args.insert(ComputeFeatureSizesFilter::k_VolumesName_Key, std::make_any(volumesName)); - args.insert(ComputeFeatureSizesFilter::k_EquivalentDiametersName_Key, std::make_any(EquivalentDiametersName)); - args.insert(ComputeFeatureSizesFilter::k_NumElementsName_Key, std::make_any(numElementsName)); - - // 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); - } - - // Compare Outputs - { - DataPath exemplaryDataPath = featureGroup.createChildPath(LegacyTest::k_Volumes); - UnitTest::CompareArrays(dataStructure, exemplaryDataPath, featureGroup.createChildPath(volumesName)); - } - - { - DataPath exemplaryDataPath = featureGroup.createChildPath(LegacyTest::k_EquivalentDiameters); - UnitTest::CompareArrays(dataStructure, exemplaryDataPath, featureGroup.createChildPath(EquivalentDiametersName)); - } - - { - DataPath exemplaryDataPath = featureGroup.createChildPath(Constants::k_NumElements); - UnitTest::CompareArrays(dataStructure, exemplaryDataPath, featureGroup.createChildPath(numElementsName)); - } - -// Write the DataStructure out to the file system -#ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fs::path(fmt::format("{}/calculate_feature_sizes/legacy_test.dream3d", unit_test::k_BinaryTestOutputDir))); -#endif - - UnitTest::CheckArraysInheritTupleDims(dataStructure); -} - TEST_CASE("SimplnxCore::ComputeFeatureSizesFilter: SIMPL Backwards Compatibility", "[SimplnxCore][ComputeFeatureSizesFilter][BackwardsCompatibility]") { auto app = Application::GetOrCreateInstance(); From 7d8e90d1b39c6be256ad5652c60db12a207ee88c Mon Sep 17 00:00:00 2001 From: nyoungbq Date: Fri, 5 Jun 2026 13:05:14 -0400 Subject: [PATCH 2/3] provenance file --- .../test/ComputeFeatureSizesTest.cpp | 2 +- .../provenance/ComputeFeatureSizesFilter.md | 426 ++++++++++++++++++ 2 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md diff --git a/src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp b/src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp index 6c915c4140..27933bbfe3 100644 --- a/src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp @@ -64,7 +64,7 @@ DataStructure Create2DImageDataStructure() 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 1, 1, 3, - 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3 }; // clang-format on diff --git a/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md new file mode 100644 index 0000000000..d1466a0898 --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/provenance/ComputeFeatureSizesFilter.md @@ -0,0 +1,426 @@ +# Exemplar Archive Provenance: Inlined + +This sidecar records how an exemplar archive used in unit tests was generated. It is the answer to "where did this gold-standard data come from?" + +One sidecar per archive. The archive name and SHA512 must match `download_test_data()` in `src/Plugins/

/test/CMakeLists.txt`. + +--- + +## Archive identity + +| Field | Value | +|---|---| +| **Archive** | `Inlined - CreateRectGridDataStructure()` | +| **SHA512** | `N/A` | +| **Used by tests** | `Valid: Rectilinear Grid`, `Valid: Rectilinear Grid with Element Size` | +| **Generated by** | *Nathan Young* | +| **Generated on** | *2026-02-20* | +| **Generated at commit** | `34c86ab4e419725728ddb6270119bb40ea4c2945` | + +## How it was generated + +The `CreateRectGridDataStructure()` function generates a 4x4x4 Rectilinear Grid Geometry and all descendant arrays on each test invocation. The actual inlined data in the function was synthesized by hand to test typical functionality. The expected output is a Class 1 Oracle as it is hand derived. + +Relevant Information for the data set: + +```text +Image Info: +Dimensions: {4, 4, 4} +xBounds: {0.0f, 0.6f, 0.9f, 2.1f, 13.0f} +yBounds: {0.0f, 0.1f, 1.0f, 10.0f, 100.0f} +zBounds: {0.0f, 1.0f, 1.2f, 2.0f, 2.1f} + +Feature Ids: +1, 2, 2, 2, +1, 1, 1, 1, +1, 1, 1, 2, +2, 2, 1, 1, + +1, 1, 1, 1, +1, 1, 1, 1, +1, 1, 1, 1, +1, 1, 1, 1, + +3, 3, 3, 3, +1, 3, 1, 3, +2, 2, 1, 1, +2, 2, 1, 1, + +3, 2, 2, 1, +3, 2, 1, 1, +3, 1, 1, 1, +3, 2, 1, 2 +``` + +## Canonical oracle output + +| DataPath | Source of expected values | +|---|---| +| `Image/FeatureData/NumElements` | *Class 1 hand derivation* | +| `Image/FeatureData/Volumes` | *Class 1 hand derivation* | +| `Image/FeatureData/EquivalentDiameters` | *Class 1 hand derivation* | + +The expected outputs are as follows: + +```text +NumElements: {0, 39, 15, 10} +Volumes: {0.0, 2358.834, 352.462, 15.104} +EquivalentDiameters: {0.0, 16.516, 8.764, 3.0994} +``` + +In order to derive these values, the voxels must be processed individually and summed up. This is how it was done: + +```text +======================== +| Totals By Feature | +======================== +| IDs |Count| Volume | +| 1 | 39 | 2362.434 | +| 2 | 15 | 352.462 | +| 3 | 10 | 15.104 | +======================== +``` + +Format + +```text +:::::::::::::::::::::::::: +X Y Z | Volume | Feature | +:::::::::::::::::::::::::: <- Row Divider + <- Plane Divider +:::::::::::::::::::::::::: +``` + +Geometry Structure Information + +xBounds -> 0.0f, 0.6f, 0.9f, 2.1f, 13.0f +yBounds -> 0.0f, 0.1f, 1.0f, 10.0f, 100.0f +zBounds -> 0.0f, 1.0f, 1.2f, 2.0f, 2.1f + +Data + +```text +:::::::::::::::::::::::: +0.6 0.1 1.0 |0.06 | 1 | 0.06 +0.3 0.1 1.0 |0.03 | 2 | 0.03 +1.2 0.1 1.0 |0.12 | 2 | 0.15 +10.9 0.1 1.0 |1.09 | 2 | 1.24 +:::::::::::::::::::::::: +0.6 0.9 1.0 |0.54 | 1 | 0.60 +0.3 0.9 1.0 |0.27 | 1 | 0.87 +1.2 0.9 1.0 |1.08 | 1 | 1.95 +10.9 0.9 1.0 |9.81 | 1 | 11.76 +:::::::::::::::::::::::: +0.6 9.0 1.0 |5.4 | 1 | 17.16 +0.3 9.0 1.0 |2.7 | 1 | 19.86 +1.2 9.0 1.0 |10.8 | 1 | 30.66 +10.9 9.0 1.0 |98.1 | 2 | 99.34 +:::::::::::::::::::::::: +0.6 90.0 1.0 |54 | 2 | 153.34 +0.3 90.0 1.0 |27 | 2 | 180.34 +1.2 90.0 1.0 |108 | 1 | 138.66 +10.9 90.0 1.0|981 | 1 | 1119.66 +:::::::::::::::::::::::: + +======================== +| Slice 1 By Feature | +======================== +| IDs |Count| Volume | +| 1 | 10 | 1119.66 | +| 2 | 6 | 180.34 | +| 3 | 0 | 0.0 | +======================== + +:::::::::::::::::::::::: +0.6 0.1 0.2 |0.012| 1 | 0.012 +0.3 0.1 0.2 |0.006| 1 | 0.018 +1.2 0.1 0.2 |0.024| 1 | 0.042 +10.9 0.1 0.2 |0.218| 1 | 0.260 +:::::::::::::::::::::::: +0.6 0.9 0.2 |0.108| 1 | 0.368 +0.3 0.9 0.2 |0.054| 1 | 0.422 +1.2 0.9 0.2 |0.216| 1 | 0.638 +10.9 0.9 0.2 |1.962| 1 | 2.600 +:::::::::::::::::::::::: +0.6 9.0 0.2 |1.08 | 1 | 3.680 +0.3 9.0 0.2 |0.54 | 1 | 4.220 +1.2 9.0 0.2 |2.16 | 1 | 6.380 +10.9 9.0 0.2 |19.62| 1 | 26.00 +:::::::::::::::::::::::: +0.6 90.0 0.2 |10.8 | 1 | 36.80 +0.3 90.0 0.2 |5.4 | 1 | 42.20 +1.2 90.0 0.2 |21.6 | 1 | 63.80 +10.9 90.0 0.2|196.2| 1 | 260.0 +:::::::::::::::::::::::: + +======================== +| Slice 2 By Feature | +======================== +| IDs |Count| Volume | +| 1 | 16 | 260 | +| 2 | 0 | 0.0 | +| 3 | 0 | 0.0 | +======================== + +:::::::::::::::::::::::: +0.6 0.1 0.8 |0.048| 3 | 0.048 +0.3 0.1 0.8 |0.024| 3 | 0.072 +1.2 0.1 0.8 |0.096| 3 | 0.168 +10.9 0.1 0.8 |0.872| 3 | 1.04 +:::::::::::::::::::::::: +0.6 0.9 0.8 |0.432| 1 | 0.432 +0.3 0.9 0.8 |0.216| 3 | 1.256 +1.2 0.9 0.8 |0.864| 1 | 1.296 +10.9 0.9 0.8 |7.848| 3 | 9.104 +:::::::::::::::::::::::: +0.6 9.0 0.8 |4.32 | 2 | 4.320 +0.3 9.0 0.8 |2.16 | 2 | 6.480 +1.2 9.0 0.8 |8.64 | 1 | 9.936 +10.9 9.0 0.8 |78.48| 1 | 88.416 +:::::::::::::::::::::::: +0.6 90.0 0.8 |43.2 | 2 | 49.68 +0.3 90.0 0.8 |21.6 | 2 | 71.28 +1.2 90.0 0.8 |86.4 | 1 | 174.816 +10.9 90.0 0.8|784.8| 1 | 959.616 +:::::::::::::::::::::::: + +======================== +| Slice 3 By Feature | +======================== +| IDs |Count| Volume | +| 1 | 6 | 959.616 | +| 2 | 4 | 71.28 | +| 3 | 6 | 9.104 | +======================== + +:::::::::::::::::::::::: +0.6 0.1 0.1 |0.006| 3 | 0.006 +0.3 0.1 0.1 |0.003| 2 | 0.003 +1.2 0.1 0.1 |0.012| 2 | 0.015 +10.9 0.1 0.1 |0.109| 1 | 0.109 +:::::::::::::::::::::::: +0.6 0.9 0.1 |0.054| 3 | 0.060 +0.3 0.9 0.1 |0.027| 2 | 0.042 +1.2 0.9 0.1 |0.108| 1 | 0.217 +10.9 0.9 0.1 |0.981| 1 | 1.198 +:::::::::::::::::::::::: +0.6 9.0 0.1 |0.54 | 3 | 0.60 +0.3 9.0 0.1 |0.27 | 1 | 1.468 +1.2 9.0 0.1 |1.08 | 1 | 2.548 +10.9 9.0 0.1 |9.81 | 1 | 12.358 +:::::::::::::::::::::::: +0.6 90.0 0.1 |5.4 | 3 | 6.0 +0.3 90.0 0.1 |2.7 | 2 | 2.742 +1.2 90.0 0.1 |10.8 | 1 | 23.158 +10.9 90.0 0.1|98.1 | 2 | 100.842 +:::::::::::::::::::::::: + +======================== +| Slice 4 By Feature | +======================== +| IDs |Count| Volume | +| 1 | 7 | 23.158 | +| 2 | 5 | 100.842 | +| 3 | 4 | 6.0 | +======================== +``` + +```text +Calculated Output + + 0.06 0.03 0.12 1.09 + 0.54 0.27 1.08 9.81 + 5.4 2.7 10.8 98.1 + 54 27 108 981 + + 0.012 0.006 0.024 0.218 + 0.108 0.054 0.216 1.962 + 1.08 0.54 2.16 19.62 + 10.8 5.4 21.6 196.2 + + 0.048 0.024 0.096 0.872 + 0.432 0.216 0.864 7.848 + 4.32 2.16 8.64 78.48 + 43.2 21.6 86.4 784.8 + + 0.00599999 0.003 0.012 0.109 + 0.0539999 0.027 0.108 0.980999 + 0.539999 0.27 1.08 9.80999 + 5.39999 2.7 10.8 98.0999 + ``` + +We calculate the `EquivalentDiameters` with the volumes: + +Given that volume of a circle can be derived by `Volume = 4/3 * pi * r^3`, we can isolate the radius given volume. That formula becomes `radius = cubed_root((3 * Volume) / (4 * pi))`, and given that the radius is half of the diameter the formula becomes `Diameter = 2 * cubed_root((3 * Volume) / (4 * pi))`. + +## Oracle provenance (Classes 2, 3, 5 only) + +N/A + +## Second-engineer oracle review + +- **Reviewer:** ** OR *skipped* +- **Date:** *YYYY-MM-DD* +- **Skip reason** (if skipped): ** + +## Regenerated to fix a circular-oracle situation? + +N/A + +--- + +## Archive identity + +| Field | Value | +|---|---| +| **Archive** | `Inlined - Create2DImageDataStructure()` | +| **SHA512** | `N/A` | +| **Used by tests** | `Valid: Image 2D`, `Valid: Image 2D with Element Sizes` | +| **Generated by** | *Nathan Young* | +| **Generated on** | *2026-02-20* | +| **Generated at commit** | `34c86ab4e419725728ddb6270119bb40ea4c2945` | + +## How it was generated + +The `Create2DImageDataStructure()` function generates a 5x5x1 Image Geometry and all descendant arrays on each test invocation. The actual inlined data in the function was synthesized by hand to test typical functionality as well as an isolated voxel edge case. The expected output is a Class 1 Oracle as it is hand derived. + +Relevant Information for the data set: + +```text +Image Info: +Dimensions: {5,5,1} +Spacing: {20.2f, 0.1f, 1.0f} +Origin: {0.0f, 0.0f, 0.0f} + +Feature Ids: +1, 2, 3, 3, 3, +1, 1, 1, 1, 1, +1, 1, 1, 3, 3, +3, 3, 1, 1, 3, +3, 3, 3, 3, 3 +``` + +## Canonical oracle output + +| DataPath | Source of expected values | +|---|---| +| `Image/FeatureData/NumElements` | *Class 1 hand derivation* | +| `Image/FeatureData/Volumes` | *Class 1 hand derivation* | +| `Image/FeatureData/EquivalentDiameters` | *Class 1 hand derivation* | + +The expected outputs are as follows: + +```text +NumElements: {0, 11, 1, 13} +Areas: {0.0, 22.22, 2.02, 26.26} +EquivalentDiameters: {0.0, 5.319, 1.6037, 5.782} +``` + +In order to derive these values, one first calculates single voxel area which in this case is `2.02`. Then feature counts must be determined using the *Feature Ids* array, this gives the `NumElements` results. With the feature counts, we then determine the size of the feature via multiplying the number of elements in each feature by the voxel area, which gives the `Areas`. Finally, we calculate the `EquivalentDiameters` with the areas: + +Given that area of a circle can be derived by `Area = pi * radius^2`, we can isolate the radius given area. That formula becomes `radius = square_root(Area / pi)`, and given that the radius is half of the diameter the formula becomes `Diameter = 2 * square_root(A / pi)`. + +## Oracle provenance (Classes 2, 3, 5 only) + +N/A + +## Second-engineer oracle review + +- **Reviewer:** ** OR *skipped* +- **Date:** *YYYY-MM-DD* +- **Skip reason** (if skipped): ** + +## Regenerated to fix a circular-oracle situation? + +N/A + +--- + +## Archive identity + +| Field | Value | +|---|---| +| **Archive** | `Inlined - Create3DImageDataStructure()` | +| **SHA512** | `N/A` | +| **Used by tests** | `Valid: Image 3D`, `Valid: Image 3D with Element Sizes` | +| **Generated by** | *Nathan Young* | +| **Generated on** | *2026-02-20* | +| **Generated at commit** | `34c86ab4e419725728ddb6270119bb40ea4c2945` | + +## How it was generated + +The `Create3DImageDataStructure()` function generates a 5x5x5 Image Geometry and all descendant arrays on each test invocation. The actual inlined data in the function was synthesized by hand to test typical functionality. The expected output is a Class 1 Oracle as it is hand derived. + +Relevant Information for the data set: + +```text +Image Info: +Dimensions: {5,5,5} +Spacing: {1.2f, 0.9f, 2.1f} +Origin: {0.0f, 0.0f, 0.0f} + +Feature Ids: +1, 2, 2, 2, 2, +1, 1, 1, 1, 1, +1, 1, 1, 2, 2, +2, 2, 1, 1, 2, +2, 2, 2, 2, 2, + +1, 1, 1, 1, 1, +1, 1, 1, 1, 1, +1, 1, 1, 1, 1, +1, 1, 1, 1, 1, +1, 1, 1, 1, 1, + +3, 3, 3, 3, 1, +1, 3, 1, 3, 3, +2, 2, 1, 1, 1, +2, 2, 1, 1, 3, +2, 2, 1, 3, 3, + +3, 2, 2, 1, 1, +3, 2, 1, 1, 1, +3, 1, 1, 1, 1, +3, 2, 1, 2, 1, +3, 2, 2, 2, 2, + +3, 1, 3, 1, 1, +1, 1, 1, 1, 1, +3, 1, 1, 1, 3, +1, 1, 1, 3, 1, +3, 1, 3, 1, 3 +``` + +## Canonical oracle output + +| DataPath | Source of expected values | +|---|---| +| `Image/FeatureData/NumElements` | *Class 1 hand derivation* | +| `Image/FeatureData/Volumes` | *Class 1 hand derivation* | +| `Image/FeatureData/EquivalentDiameters` | *Class 1 hand derivation* | + +The expected outputs are as follows: + +```text +NumElements: {0, 73, 29, 23} +Volumes: {0.0, 165.564, 65.772, 52.164} +EquivalentDiameters: {0.0, 6.813, 5.008, 4.636} +``` + +In order to derive these values, one first calculates single voxel volume which in this case is `2.268`. Then feature counts must be determined using the *Feature Ids* array, this gives the `NumElements` results. With the feature counts, we then determine the size of the feature via multiplying the number of elements in each feature by the voxel volume, which gives the `Volumes`. Finally, we calculate the `EquivalentDiameters` with the volumes: + +Given that volume of a circle can be derived by `Volume = 4/3 * pi * r^3`, we can isolate the radius given volume. That formula becomes `radius = cubed_root((3 * Volume) / (4 * pi))`, and given that the radius is half of the diameter the formula becomes `Diameter = 2 * cubed_root((3 * Volume) / (4 * pi))`. + +## Oracle provenance (Classes 2, 3, 5 only) + +N/A + +## Second-engineer oracle review + +- **Reviewer:** ** OR *skipped* +- **Date:** *YYYY-MM-DD* +- **Skip reason** (if skipped): ** + +## Regenerated to fix a circular-oracle situation? + +N/A From aa689d3c22c082e12d6ac06828b2c26d3ec84d11 Mon Sep 17 00:00:00 2001 From: nyoungbq Date: Thu, 11 Jun 2026 14:39:46 -0400 Subject: [PATCH 3/3] - patched thread kahan summation bug - Compute Feature Sizes Deviations - Compute Feature Sizes V&V report --- .../Algorithms/ComputeFeatureSizes.cpp | 6 +- .../vv/ComputeFeatureSizesFilter.md | 106 ++++++++++++++++++ .../deviations/ComputeFeatureSizesFilter.md | 47 ++++++++ 3 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md create mode 100644 src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeatureSizes.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeatureSizes.cpp index 48993310b2..565e30ee66 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeatureSizes.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeFeatureSizes.cpp @@ -344,13 +344,13 @@ Result<> ProcessRectGridGeom(RectGridGeom& rectGridGeom, Float32AbstractDataStor // Use Kahan summation to determine overall volume // Attempt to recover low order into the value. The first instance is 0 - const float64 value = featureVolumes[featureIdx] - featureCompensators[featureIdx]; + const float64 value = localVolumes[featureIdx] - featureCompensators[featureIdx]; // low order may be lost - const float64 volSum = localVolumes[featureIdx] + value; + const float64 volSum = featureVolumes[featureIdx] + value; // recover and cache low order - featureCompensators[featureIdx] = (volSum - localVolumes[featureIdx]) - value; + featureCompensators[featureIdx] = (volSum - featureVolumes[featureIdx]) - value; // store volumes featureVolumes[featureIdx] = volSum; diff --git a/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md new file mode 100644 index 0000000000..83e4670bf5 --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/ComputeFeatureSizesFilter.md @@ -0,0 +1,106 @@ +# V&V Report: ComputeFeatureSizes + +| | | +|---|---| +| Plugin | `SimplnxCore` | +| SIMPLNX UUID | `c666ee17-ca58-4969-80d0-819986c72485` | +| DREAM3D 6.5.171 equivalent | `FindSizes` — UUID `656f144c-a120-5c3b-bee5-06deab438588` | +| Verified commit | *pending* | +| Status | COMPLETE | +| Sign-off | Nathan Young, June 10th, 2026 | + +## At a glance + +| Aspect | State | +|---|---| +| Algorithm relationship | **Port** of both `FindSizes::execute()` (ImageGeom) and `FindSizes::findSizesUnstructured()` (RectGridGeom) | +| Oracle | **Class 1 (Analytical)** — all test data inlined, hand-derived | +| Code paths | **14 of 19** exercised; 5 gaps (cancel ×2, INT32_MAX overflow ×2, other-geom no-op ×1) | +| Tests | **9 TEST_CASEs** — all pass | +| External archive | None | +| Deviations | **1 active** (`ComputeFeatureSizes-D1`): Kahan vs naive summation in RectGridGeom — see deviations file | +| Open bugs | **1 open** (`Bug-1`): 2D area formula uses all 3 spacings (= volume) instead of the 2 non-flat spacings | + +## Summary + +`ComputeFeatureSizesFilter` produces three arrays per feature: `NumElements` (voxel count, `int32`), `Volumes`/`Areas` (float32), and `EquivalentDiameters` (ESD or ECD, float32). For ImageGeom it uses voxel count × voxel size; for RectGridGeom it sums per-cell element sizes per feature. Both paths are parallelized via `ParallelDataAlgorithm` + TBB `tbb::combinable`. All tests use Class 1 oracles (hand-constructed fixtures with first-principles expected values). Source-inspection confirms both geometry paths are ports of legacy `FindSizes`. One precision deviation (`D1`) and one open bug (`Bug-1`) are documented below. + +## Algorithm Relationship + +**Port** of `FindSizes` (both ImageGeom and RectGridGeom paths). + +- **ImageGeom** (`ProcessImageGeom`): voxel count × voxel volume/area → ESD/ECD. Identical to `FindSizes::execute()`. Formulas: `ESD = 2·∛(V / (4π/3))`; `ECD = 2·√(A / π)`. +- **RectGridGeom** (`ProcessRectGridGeom`): per-cell `ΔxΔyΔz` sizes, summed per feature. Port of `FindSizes::findSizesUnstructured()`. SIMPLNX departs in precision: float32 element sizes are promoted to float64 and Kahan summation is applied at two levels (per-thread and post-reduction). See deviation `D1`. + +Port-time additions not in DREAM3D 6.5.171: +- **Parallel execution** — `ParallelDataAlgorithm` + `tbb::combinable`; legacy was serial. +- **Execute-time FeatureId bounds check** — `ValidateFeatureIdsToFeatureAttributeMatrixIndexing`; legacy accessed out-of-bounds silently. +- **Preflight dimension guard** — rejects ImageGeom with two or more dimensions equal to 1. This is because a single empty dimension converts the calculation from Volume to Area and "1D" Images would not make sense to get an Area formula. + +## Oracle + +**Class 1 (Analytical)** — all expected values are derived from first principles and asserted in `test/ComputeFeatureSizesTest.cpp`. + +| Fixture | Geometry | Derived quantity | +|---|---|---| +| `Create2DImageDataStructure()` | 5×5×1 ImageGeom, spacing 20.2×0.1×1.0 | `voxelArea = 20.2 × 0.1 × 1.0 = 2.02`; areas = count × 2.02; ECDs from circle formula | +| `Create3DImageDataStructure()` | 5×5×5 ImageGeom, spacing 1.2×0.9×2.1 | `voxelVol = 1.2 × 0.9 × 2.1 = 2.268`; volumes = count × 2.268; ESDs from sphere formula | +| `CreateRectGridDataStructure()` | 4×4×4 non-uniform RectGrid | Per-cell `ΔxΔyΔz` summed per feature; step-by-step trace in provenance sidecar | + +Negative tests use degenerate inputs (out-of-bounds FeatureId; degenerate dims) and assert an error result is returned. + +Second-engineer review pending: verify hand-derivations for all three fixtures and the tolerance choice (`std::numeric_limits::epsilon()`). + +## Code path coverage + +14 of 19 paths exercised. Source: `Algorithms/ComputeFeatureSizes.cpp`. + +| # | Path | Exercised by | +|---|---|---| +| 1 | Preflight: `emptyDimCount > 1` → error | `Invalid: Preflight Failure` (4 sub-checks) | +| 2 | Preflight: valid dims → pass | All 6 positive tests | +| 3 | Execute validate: FeatureId > numFeatures → error | `Invalid: Execution Failure` | +| 4 | Execute validate: passes → geometry dispatch | All 6 positive tests | +| 5 | Dispatch: ImageGeom → `ProcessImageGeom` | `Image 2D *`, `Image Stack 3D *` | +| 6 | Dispatch: RectGridGeom → `ProcessRectGridGeom` | `Rectilinear Grid *` | +| 7 | Dispatch: other geometry → no-op | *Not tested. `GeometrySelectionParameter` prevents this at runtime; gap acceptable.* | +| 8 | Image: any dim == 1 → area + ECD | `Image 2D *` | +| 9 | Image: all dims > 1 → volume + ESD | `Image Stack 3D *` | +| 10 | Image: voxelCount > `k_MaxVoxelCount` → error | *Not tested. Requires >2³¹ voxels in one feature; impractical. Gap acceptable.* | +| 11 | Image: `SaveElementSizes = false` | `Image 2D`, `Image Stack 3D` | +| 12 | Image: `SaveElementSizes = true` | `Image 2D with Element Sizes`, `Image Stack 3D with Element Size` | +| 13 | Image: `shouldCancel` in loop → early return | *Not tested. Cancel disregard would cause hangs in any test; low-value gap.* | +| 14 | RectGrid: `findElementSizes` → per-cell volumes available | `Rectilinear Grid *` | +| 15 | RectGrid: Kahan summation in `RectGridSummationImpl` + post-reduction | `Rectilinear Grid *` (non-uniform spacing exercises summation) | +| 16 | RectGrid: voxelCount > `k_MaxVoxelCount` → error | *Not tested. Same rationale as path 10.* | +| 17 | RectGrid: `SaveElementSizes = false` → `deleteElementSizes()` | `Rectilinear Grid` | +| 18 | RectGrid: `SaveElementSizes = true` → element sizes retained | `Rectilinear Grid with Element Size` | +| 19 | RectGrid: `shouldCancel` in loop → early return | *Not tested. Same rationale as path 13.* | + +## Test inventory + +| Test case | Notes | +|---|---| +| `Valid: Image 2D` | Class 1; 5×5×1, spacing 20.2×0.1×1.0; SaveElementSizes=false | +| `Valid: Image 2D with Element Sizes` | Same fixture; SaveElementSizes=true | +| `Valid: Image Stack 3D` | Class 1; 5×5×5, spacing 1.2×0.9×2.1; SaveElementSizes=false | +| `Valid: Image Stack 3D with Element Size` | Same fixture; SaveElementSizes=true | +| `Valid: Rectilinear Grid` | Class 1; 4×4×4 non-uniform; SaveElementSizes=false; Kahan path exercised | +| `Valid: Rectilinear Grid with Element Size` | Same fixture; SaveElementSizes=true | +| `Invalid: Execution Failure` | FeatureId 10 in a 4-feature AM; asserts execute result invalid | +| `Invalid: Preflight Failure` | 4 degenerate dim configs; asserts preflight result invalid | +| `SIMPL Backwards Compatibility` | `DYNAMIC_SECTION` over SIMPL 6.4 + 6.5; validates UUID + arg-key + param-value decoding | + +All 9 TEST_CASEs pass. + +## Exemplar archive + +None — all fixtures constructed in C++ at test time. Provenance in `vv/provenance/ComputeFeatureSizesFilter.md`. + +## Deviations from DREAM3D 6.5.171 + +**ImageGeom path:** No deviations. Formulas and logic identical to `FindSizes::execute()`. The float64 intermediate in the ESD/ECD computation differs from likely float32 in legacy by ≤1 ULP for typical EBSD spacings; non-material. + +**RectGridGeom path — `ComputeFeatureSizes-D1`:** Per-feature volumes differ from `FindSizes::findSizesUnstructured()` output due to two precision improvements in SIMPLNX: (1) element sizes promoted from float32 to float64 before accumulation; (2) Kahan compensated summation applied per-thread and during TBB post-reduction, reducing accumulated error from O(N·ε_float32) to O(ε_float64). SIMPLNX is more accurate. Users migrating from DREAM3D 6.5.171 should expect small shifts in per-feature volumes for RectGridGeom; largest for features with many cells on grids with high cell-volume variation. + +Full entry in `vv/deviations/ComputeFeatureSizesFilter.md`. diff --git a/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md new file mode 100644 index 0000000000..52652adaec --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/deviations/ComputeFeatureSizesFilter.md @@ -0,0 +1,47 @@ +# Deviations from DREAM3D 6.5.171: ComputeFeatureSizes + +This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. + +Entries are referenced by stable ID (`ComputeFeatureSizes-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. + +--- + +## ImageGeom path — no deviations + +Source-inspection comparison of `ProcessImageGeom` against DREAM3D 6.5.171 `FindSizes::execute()` confirmed identical algorithmic structure: + +- Per-feature voxel count: identical parallel accumulation logic (serial in legacy). +- 3D volume formula: `volume = voxelCount × voxelVolume`; ESD: `2·∛(volume / (4π/3))` — both use the standard sphere formula. +- 2D area formula (any dim == 1): `area = voxelCount × voxelArea`; ECD: `2·√(area / π)` — both use the standard circle formula. + +**Precision non-deviation (not flagged):** SIMPLNX promotes `voxelVolume` to `float64` and evaluates `cbrt` / `sqrt` in `float64` before casting to `float32` for storage. The legacy filter likely used `float32` throughout. The difference is within ~1 ULP of `float32` for typical EBSD spacings and is non-material for downstream morphological statistics. + +--- + +## ComputeFeatureSizes-D1 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeFeatureSizes-D1` | +| **Filter UUID** | `c666ee17-ca58-4969-80d0-819986c72485` | +| **Status** | Active — precision improvement; SIMPLNX output is more accurate | + +**Symptom:** Per-feature volumes for features in `RectGridGeom` geometries differ from DREAM3D 6.5.171 `FindSizes::findSizesUnstructured()` output. The discrepancy grows with feature size and with the variation in cell volumes across the grid, scaling approximately as `O(N · ε_float32)` for the legacy result vs `O(ε_float64)` for the SIMPLNX result, where N is the number of cells in the feature. + +**Root cause:** Precision — two compounding improvements in SIMPLNX's `ProcessRectGridGeom` over the legacy `findSizesUnstructured`: + +1. **float32 → float64 promotion.** Element sizes are stored as `float32` (the output of `findElementSizes`). The legacy `findSizesUnstructured` accumulated these directly in `float32`, so each per-cell addition carried a relative error of ~`ε_float32 ≈ 1.2×10⁻⁷`. SIMPLNX promotes each element size to `float64` before accumulation (`static_cast(m_ElemSizes.getValue(voxelIdx))`), reducing the per-operation rounding error to `ε_float64 ≈ 2.2×10⁻¹⁶`. + +2. **Kahan summation.** SIMPLNX applies Kahan compensated summation at two levels: + - *Per-thread, per-cell* in `RectGridSummationImpl::convert()` (lines 274–283): standard Kahan with `y = elemSize - c`, `t = sum + y`, `c′ = (t - sum) - y`. For a feature spanning N cells, naive float64 accumulation still has O(N · ε_float64) error; Kahan reduces this to O(ε_float64) by carrying a compensation term `c` that recovers the low-order bits lost in each `sum + y` operation. + - *Post-reduction, per thread-local vector* in `threadLocalVolumes.combine_each()` (lines 341–358): the same Kahan scheme is applied when combining the TBB thread-local partial sums into the final per-feature volume. + + For a non-uniform rectilinear grid where cell volumes span several orders of magnitude (e.g., a grid with fine near-surface resolution and coarse interior), naive summation of N cells can lose ~`log₂(V_max / V_min)` bits of precision in the smaller cell contributions. Kahan summation recovers those bits regardless of N or the dynamic range of cell volumes. + +**Affected users:** Any workflow that runs `ComputeFeatureSizes` on a `RectGridGeom` and then compares per-feature volume or ESD values against DREAM3D 6.5.171 output. The deviation is largest for large features (high N) on grids with high volume variation. On uniform RectGrids (all cell volumes equal), Kahan has no practical effect and the only deviation is the float32→float64 promotion. + +**Recommendation:** Trust SIMPLNX. The legacy `findSizesUnstructured` accumulated unnecessary floating-point error that grew with feature size and grid non-uniformity. The SIMPLNX result is strictly more accurate. Users migrating pipelines should expect small positive or negative shifts in per-feature volumes; ESD values are affected proportionally through the `cbrt` formula. + +--- + +*If a future comparison run against DREAM3D 6.5.171 output reveals additional deviations, add them here as `ComputeFeatureSizes-D2` etc.*