33#include " SimplnxCore/SimplnxCore_test_dirs.hpp"
44
55#include " simplnx/DataStructure/AttributeMatrix.hpp"
6+ #include " simplnx/DataStructure/DataArray.hpp"
67#include " simplnx/DataStructure/Geometry/ImageGeom.hpp"
7- #include " simplnx/DataStructure/IDataArray.hpp"
88#include " simplnx/Parameters/ChoicesParameter.hpp"
99#include " simplnx/UnitTest/UnitTestCommon.hpp"
1010#include " simplnx/Utilities/AlgorithmDispatch.hpp"
11+ #include " simplnx/Utilities/DataStoreUtilities.hpp"
1112
1213#include < catch2/catch.hpp>
1314
@@ -18,183 +19,178 @@ using namespace nx::core::UnitTest;
1819
1920namespace
2021{
21- const DataPath k_ExemplarArrayPath = Constants::k_DataContainerPath.createChildPath(Constants::k_CellData).createChildPath(" Mask Exemplar" );
22- }
23- TEST_CASE (" SimplnxCore::IdentifySampleFilter" , " [SimplnxCore][IdentifySampleFilter]" )
22+ // Exemplar archive
23+ const std::string k_ArchiveName = " identify_sample_exemplars.tar.gz" ;
24+ const std::string k_DataDirName = " identify_sample_exemplars" ;
25+ const fs::path k_DataDir = fs::path(unit_test::k_TestFilesDir.view()) / k_DataDirName;
26+ const fs::path k_ExemplarFile = k_DataDir / " identify_sample.dream3d" ;
27+
28+ // Geometry names
29+ constexpr StringLiteral k_GeomName = " DataContainer" ;
30+ constexpr StringLiteral k_CellDataName = " CellData" ;
31+
32+ // Output array paths
33+ const DataPath k_GeomPath ({k_GeomName});
34+ const DataPath k_MaskPath ({k_GeomName, k_CellDataName, " Mask" });
35+
36+ // Test dimensions
37+ constexpr usize k_Dim = 200 ;
38+
39+ /* *
40+ * @brief Builds an IdentifySample test dataset: a sphere of "good" voxels
41+ * with interior holes and exterior noise.
42+ */
43+ void BuildIdentifySampleTestData (DataStructure& ds, usize dimX, usize dimY, usize dimZ, const std::string& geomName = " DataContainer" )
2444{
25- UnitTest::LoadPlugins ();
26- bool forceOocAlgo = GENERATE (false , true );
27- const nx::core::ForceOocAlgorithmGuard guard (forceOocAlgo);
28- // 25x25x25 dataset, Mask (uint8, 1-comp) => 25*25*1 = 625 bytes/slice
29- const UnitTest::PreferencesSentinel prefsSentinel (" Zarr" , 625 , true );
30-
31- const nx::core::UnitTest::TestFileSentinel testDataSentinel (nx::core::unit_test::k_TestFilesDir, " identify_sample_v2.tar.gz" , " identify_sample_v2" );
32- using TestArgType = std::tuple<std::string, std::string, std::string>;
33- /* clang-format off */
34- std::vector<TestArgType> allTestParams = {
35- {" sliced" , " xy" , " fill" },
36- {" sliced" , " xy" , " nofill" },
37- {" sliced" , " xz" , " fill" },
38- {" sliced" , " xz" , " nofill" },
39- {" sliced" , " yz" , " fill" },
40- {" sliced" , " yz" , " nofill" },
41-
42- {" whole" , " xy" , " fill" },
43- {" whole" , " xy" , " nofill" },
44- {" whole" , " xz" , " fill" },
45- {" whole" , " xz" , " nofill" },
46- {" whole" , " yz" , " fill" },
47- {" whole" , " yz" , " nofill" },
48- };
49- /* clang-format on */
50- for (const auto & testParam : allTestParams)
51- {
52- std::string slice_by_slice = std::get<0 >(testParam);
53- bool sliceBySlice = slice_by_slice == " sliced" ;
45+ const ShapeType cellShape = {dimZ, dimY, dimX};
46+ auto * imageGeom = ImageGeom::Create (ds, geomName);
47+ imageGeom->setDimensions ({dimX, dimY, dimZ});
48+ imageGeom->setSpacing ({1 .0f , 1 .0f , 1 .0f });
49+ imageGeom->setOrigin ({0 .0f , 0 .0f , 0 .0f });
5450
55- std::string slice_plane = std::get<1 >(testParam);
51+ auto * cellAM = AttributeMatrix::Create (ds, " CellData" , cellShape, imageGeom->getId ());
52+ imageGeom->setCellData (*cellAM);
5653
57- ChoicesParameter::ValueType sliceBySlicePlane = 0 ;
58- if (slice_plane == " xz" )
59- sliceBySlicePlane = 1 ;
60- else if (slice_plane == " yz" )
61- sliceBySlicePlane = 2 ;
54+ auto maskDataStore = DataStoreUtilities::CreateDataStore<uint8>(cellShape, {1 }, IDataAction::Mode::Execute);
55+ auto * maskArray = DataArray<uint8>::Create (ds, " Mask" , maskDataStore, cellAM->getId ());
56+ auto & maskStore = maskArray->getDataStoreRef ();
6257
63- std::string fill_holes = std::get<2 >(testParam);
64- bool fillHoles = fill_holes == " fill" ;
58+ const float cx = dimX / 2 .0f ;
59+ const float cy = dimY / 2 .0f ;
60+ const float cz = dimZ / 2 .0f ;
61+ const float radius = dimX * 0 .4f ;
6562
66- SECTION (fmt::format (" {}_{}_{}" , slice_by_slice, slice_plane, fill_holes))
67- {
68- fs::path inputFilePath = fs::path (fmt::format (" {}/identify_sample_v2/{}_{}_{}.dream3d" , unit_test::k_TestFilesDir, slice_by_slice, slice_plane, fill_holes));
69- std::cout << inputFilePath.string () << std::endl;
70-
71- DataStructure dataStructure = LoadDataStructure (inputFilePath);
72- IdentifySampleFilter filter;
73- Arguments args;
74- args.insert (IdentifySampleFilter::k_SelectedImageGeometryPath_Key, std::make_any<DataPath>(Constants::k_DataContainerPath));
75- args.insert (IdentifySampleFilter::k_MaskArrayPath_Key, std::make_any<DataPath>(Constants::k_MaskArrayPath));
76- args.insert (IdentifySampleFilter::k_FillHoles_Key, std::make_any<bool >(fillHoles));
77- args.insert (IdentifySampleFilter::k_SliceBySlice_Key, std::make_any<bool >(sliceBySlice));
78- args.insert (IdentifySampleFilter::k_SliceBySlicePlane_Key, std::make_any<ChoicesParameter::ValueType>(sliceBySlicePlane));
79-
80- // Preflight the filter and check result
81- auto preflightResult = filter.preflight (dataStructure, args);
82- SIMPLNX_RESULT_REQUIRE_VALID (preflightResult.outputActions )
83-
84- // Execute the filter and check the result
85- auto executeResult = filter.execute (dataStructure, args);
86- SIMPLNX_RESULT_REQUIRE_VALID (executeResult.result )
87-
88- #ifdef SIMPLNX_WRITE_TEST_OUTPUT
89- WriteTestDataStructure (dataStructure, fmt::format (" {}/identify_sample_output_{}_{}_{}.dream3d" , unit_test::k_BinaryTestOutputDir, fillHoles, sliceBySlice, sliceBySlicePlane));
90- #endif
91-
92- const IDataArray& computedArray = dataStructure.getDataRefAs <IDataArray>(Constants::k_MaskArrayPath);
93- const IDataArray& exemplarArray = dataStructure.getDataRefAs <IDataArray>(k_ExemplarArrayPath);
94- CompareDataArrays<uint8>(computedArray, exemplarArray);
95-
96- UnitTest::CheckArraysInheritTupleDims (dataStructure);
97- }
98- }
99- }
100-
101- TEST_CASE (" SimplnxCore::IdentifySampleFilter: Benchmark 200x200x200" , " [SimplnxCore][IdentifySampleFilter][Benchmark]" )
102- {
103- UnitTest::LoadPlugins ();
104- // 200*200 * 1 byte = 40000 bytes per Z-slice for uint8 mask
105- const UnitTest::PreferencesSentinel prefsSentinel (" Zarr" , 40000 , true );
106-
107- constexpr usize k_DimX = 200 ;
108- constexpr usize k_DimY = 200 ;
109- constexpr usize k_DimZ = 200 ;
110- constexpr usize k_TotalVoxels = k_DimX * k_DimY * k_DimZ;
111- const ShapeType cellTupleShape = {k_DimZ, k_DimY, k_DimX};
112- const auto benchmarkFile = fs::path (fmt::format (" {}/identify_sample_benchmark.dream3d" , unit_test::k_BinaryTestOutputDir));
113-
114- // Stage 1: Build data programmatically and write to .dream3d
63+ for (usize z = 0 ; z < dimZ; z++)
11564 {
116- DataStructure buildDS;
117- auto * imageGeom = ImageGeom::Create (buildDS, " DataContainer" );
118- imageGeom->setDimensions ({k_DimX, k_DimY, k_DimZ});
119- imageGeom->setSpacing ({1 .0f , 1 .0f , 1 .0f });
120- imageGeom->setOrigin ({0 .0f , 0 .0f , 0 .0f });
121-
122- auto * cellAM = AttributeMatrix::Create (buildDS, " Cell Data" , cellTupleShape, imageGeom->getId ());
123- imageGeom->setCellData (*cellAM);
124-
125- // Create mask array: a sphere of "good" voxels with interior holes and exterior noise
126- auto * maskArray = CreateTestDataArray<uint8>(buildDS, " Mask" , cellTupleShape, {1 }, cellAM->getId ());
127- auto & maskStore = maskArray->getDataStoreRef ();
128-
129- const float cx = k_DimX / 2 .0f ;
130- const float cy = k_DimY / 2 .0f ;
131- const float cz = k_DimZ / 2 .0f ;
132- const float radius = 80 .0f ;
133-
134- for (usize z = 0 ; z < k_DimZ; z++)
65+ for (usize y = 0 ; y < dimY; y++)
13566 {
136- for (usize y = 0 ; y < k_DimY; y ++)
67+ for (usize x = 0 ; x < dimX; x ++)
13768 {
138- for (usize x = 0 ; x < k_DimX; x++)
69+ const usize idx = z * dimX * dimY + y * dimX + x;
70+ const float dx = static_cast <float >(x) - cx;
71+ const float dy = static_cast <float >(y) - cy;
72+ const float dz = static_cast <float >(z) - cz;
73+ const float dist = std::sqrt (dx * dx + dy * dy + dz * dz);
74+ bool good = dist < radius;
75+
76+ // Interior holes (positions relative to geometry size so they work at any dim)
77+ const float h1cx = cx + radius * 0 .3f ;
78+ const float h1cy = cy + radius * 0 .3f ;
79+ const float h1cz = cz + radius * 0 .3f ;
80+ const float h1r = dimX * 0 .053f ; // ~4 at 75, ~10.6 at 200
81+ const float h2cx = cx - radius * 0 .3f ;
82+ const float h2cy = cy - radius * 0 .3f ;
83+ const float h2cz = cz - radius * 0 .3f ;
84+ const float h2r = dimX * 0 .04f ; // ~3 at 75, ~8 at 200
85+
86+ if (good)
13987 {
140- const usize idx = z * k_DimX * k_DimY + y * k_DimX + x;
141- const float dx = static_cast <float >(x) - cx;
142- const float dy = static_cast <float >(y) - cy;
143- const float dz = static_cast <float >(z) - cz;
144- const float dist = std::sqrt (dx * dx + dy * dy + dz * dz);
145- bool good = dist < radius;
146-
147- // Create interior holes (small sphere cavities)
148- if (good)
88+ const float h1 = std::sqrt ((static_cast <float >(x) - h1cx) * (static_cast <float >(x) - h1cx) + (static_cast <float >(y) - h1cy) * (static_cast <float >(y) - h1cy) +
89+ (static_cast <float >(z) - h1cz) * (static_cast <float >(z) - h1cz));
90+ if (h1 < h1r)
14991 {
150- const float h1 = std::sqrt ((static_cast <float >(x) - 120 .0f ) * (static_cast <float >(x) - 120 .0f ) + (static_cast <float >(y) - 120 .0f ) * (static_cast <float >(y) - 120 .0f ) +
151- (static_cast <float >(z) - 120 .0f ) * (static_cast <float >(z) - 120 .0f ));
152- if (h1 < 10 .0f )
153- {
154- good = false ;
155- }
156- const float h2 = std::sqrt ((static_cast <float >(x) - 80 .0f ) * (static_cast <float >(x) - 80 .0f ) + (static_cast <float >(y) - 80 .0f ) * (static_cast <float >(y) - 80 .0f ) +
157- (static_cast <float >(z) - 80 .0f ) * (static_cast <float >(z) - 80 .0f ));
158- if (h2 < 8 .0f )
159- {
160- good = false ;
161- }
92+ good = false ;
16293 }
163-
164- // Add some isolated small clusters outside the main sphere
165- if (!good && dist < radius + 5 . 0f && dist > radius )
94+ const float h2 = std::sqrt (( static_cast < float >(x) - h2cx) * ( static_cast < float >(x) - h2cx) + ( static_cast < float >(y) - h2cy) * ( static_cast < float >(y) - h2cy) +
95+ ( static_cast < float >(z) - h2cz) * ( static_cast < float >(z) - h2cz));
96+ if (h2 < h2r )
16697 {
167- if ((x + y + z) % 7 == 0 )
168- {
169- good = true ;
170- }
98+ good = false ;
17199 }
100+ }
172101
173- maskStore[idx] = good ? 1 : 0 ;
102+ // Isolated noise outside the sphere
103+ if (!good && dist < radius + 5 .0f && dist > radius)
104+ {
105+ if ((x + y + z) % 7 == 0 )
106+ {
107+ good = true ;
108+ }
174109 }
110+
111+ maskStore[idx] = good ? 1 : 0 ;
175112 }
176113 }
114+ }
115+ }
177116
178- UnitTest::WriteTestDataStructure (buildDS, benchmarkFile);
117+ /* *
118+ * @brief Populates IdentifySampleFilter arguments from a test variant name.
119+ *
120+ * Name convention: "whole_fill", "sliced_xy_nofill", etc.
121+ */
122+ void SetupArgs (Arguments& args, const std::string& testName, const DataPath& geomPath, const DataPath& maskPath)
123+ {
124+ const bool fillHoles = (testName.find (" nofill" ) == std::string::npos);
125+ const bool sliceBySlice = (testName.find (" sliced" ) != std::string::npos);
126+ ChoicesParameter::ValueType slicePlane = 0 ;
127+ if (testName.find (" xz" ) != std::string::npos)
128+ {
129+ slicePlane = 1 ;
130+ }
131+ else if (testName.find (" yz" ) != std::string::npos)
132+ {
133+ slicePlane = 2 ;
179134 }
180135
181- // Stage 2: Reload (arrays become ZarrStore in OOC) and run filter
182- DataStructure dataStructure = UnitTest::LoadDataStructure (benchmarkFile);
136+ args.insertOrAssign (IdentifySampleFilter::k_SelectedImageGeometryPath_Key, std::make_any<DataPath>(geomPath));
137+ args.insertOrAssign (IdentifySampleFilter::k_MaskArrayPath_Key, std::make_any<DataPath>(maskPath));
138+ args.insertOrAssign (IdentifySampleFilter::k_FillHoles_Key, std::make_any<bool >(fillHoles));
139+ args.insertOrAssign (IdentifySampleFilter::k_SliceBySlice_Key, std::make_any<bool >(sliceBySlice));
140+ args.insertOrAssign (IdentifySampleFilter::k_SliceBySlicePlane_Key, std::make_any<ChoicesParameter::ValueType>(slicePlane));
141+ }
142+ } // namespace
143+
144+ TEST_CASE (" SimplnxCore::IdentifySampleFilter: 200x200x200 Exemplar Comparison" , " [SimplnxCore][IdentifySampleFilter]" )
145+ {
146+ UnitTest::LoadPlugins ();
147+ bool forceOocAlgo = GENERATE (false , true );
148+ const nx::core::ForceOocAlgorithmGuard guard (forceOocAlgo);
149+ // uint8 1-comp => 200*200*1 = 40,000 bytes/slice
150+ const UnitTest::PreferencesSentinel prefsSentinel (" Zarr" , 40000 , true );
183151
152+ const nx::core::UnitTest::TestFileSentinel testDataSentinel (nx::core::unit_test::k_TestFilesDir, k_ArchiveName, k_DataDirName);
153+ DataStructure exemplarDS = UnitTest::LoadDataStructure (k_ExemplarFile);
154+
155+ std::string testName = GENERATE (" whole_fill" , " whole_nofill" , " sliced_xy_fill" , " sliced_xy_nofill" , " sliced_xz_fill" , " sliced_xz_nofill" , " sliced_yz_fill" , " sliced_yz_nofill" );
156+ DYNAMIC_SECTION (" Variant: " << testName)
184157 {
158+ DataStructure dataStructure;
159+ BuildIdentifySampleTestData (dataStructure, k_Dim, k_Dim, k_Dim);
160+
185161 IdentifySampleFilter filter;
186162 Arguments args;
187- args.insert (IdentifySampleFilter::k_SelectedImageGeometryPath_Key, std::make_any<DataPath>(DataPath ({" DataContainer" })));
188- args.insert (IdentifySampleFilter::k_MaskArrayPath_Key, std::make_any<DataPath>(DataPath ({" DataContainer" , " Cell Data" , " Mask" })));
189- args.insert (IdentifySampleFilter::k_FillHoles_Key, std::make_any<bool >(true ));
190- args.insert (IdentifySampleFilter::k_SliceBySlice_Key, std::make_any<bool >(false ));
191- args.insert (IdentifySampleFilter::k_SliceBySlicePlane_Key, std::make_any<ChoicesParameter::ValueType>(0 ));
163+ SetupArgs (args, testName, k_GeomPath, k_MaskPath);
192164
193165 auto preflightResult = filter.preflight (dataStructure, args);
194166 SIMPLNX_RESULT_REQUIRE_VALID (preflightResult.outputActions );
195167 auto executeResult = filter.execute (dataStructure, args);
196168 SIMPLNX_RESULT_REQUIRE_VALID (executeResult.result );
169+
170+ // Compare against exemplar
171+ const std::string exemplarGeomName = testName + " _Exemplar" ;
172+ const DataPath exemplarMaskPath ({exemplarGeomName, std::string (k_CellDataName), " Mask" });
173+
174+ REQUIRE_NOTHROW (dataStructure.getDataRefAs <UInt8Array>(k_MaskPath));
175+ REQUIRE_NOTHROW (exemplarDS.getDataRefAs <UInt8Array>(exemplarMaskPath));
176+ CompareDataArrays<uint8>(exemplarDS.getDataRefAs <UInt8Array>(exemplarMaskPath),
177+ dataStructure.getDataRefAs <UInt8Array>(k_MaskPath));
178+
179+ UnitTest::CheckArraysInheritTupleDims (dataStructure);
197180 }
181+ }
198182
199- fs::remove (benchmarkFile);
183+ TEST_CASE (" SimplnxCore::IdentifySampleFilter: Generate Test Data" , " [SimplnxCore][IdentifySampleFilter][.GenerateTestData]" )
184+ {
185+ UnitTest::LoadPlugins ();
186+
187+ const auto outputDir = fs::path (fmt::format (" {}/generated_test_data/identify_sample" , unit_test::k_BinaryTestOutputDir));
188+ fs::create_directories (outputDir);
189+
190+ DataStructure ds;
191+ for (const auto & name : {" whole_fill" , " whole_nofill" , " sliced_xy_fill" , " sliced_xy_nofill" , " sliced_xz_fill" , " sliced_xz_nofill" , " sliced_yz_fill" , " sliced_yz_nofill" })
192+ {
193+ BuildIdentifySampleTestData (ds, k_Dim, k_Dim, k_Dim, name);
194+ }
195+ UnitTest::WriteTestDataStructure (ds, outputDir / " input.dream3d" );
200196}
0 commit comments