SIMPLNX Issue 1284 I would like to work on this issue more by converting Filters that have anything more than a trivial execute implementation to move that implementation to an "Algorithm" class like the bulk of the other filters.
The Algorithm files are located in src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/not_used.
Look at the other filters and understand how we create the Algorithm classes and follow that same style.
Each Algorithm class that gets updated should be moved out of the "not_used" folder.
If anything is ambiguous please ask.
src/simplnx/- Core library (Common, Core, DataStructure, Filter, Parameters, Pipeline, Plugin, Utilities)src/Plugins/- Plugin modulessrc/nxrunner/- CLI runnertest/- Test filescmake/- CMake configuration- Additional Plugin at "/Users/mjackson/Workspace1/DREAM3D_Plugins/SimplnxReview"
- Additional Plugin at "/Users/mjackson/Workspace1/DREAM3D_Plugins/FileStore"
- Additional Plugin at "/Users/mjackson/Workspace1/DREAM3D_Plugins/Synthetic"
- DREAM3DNX application located in "/Users/mjackson/Workspace1/DREAM3DNX"
scripts/- Build/utility scriptsconda/- Conda packaging
- C++20 standard
- Allman brace style (braces on new lines for classes, control statements, enums, functions, namespaces, structs, before else)
- 200 column limit
- 2-space indentation, no tabs
- Pointer alignment left (
int* ptrnotint *ptr) - No space before parentheses
- Sort includes alphabetically
- No short functions on single line
- Always break template declarations
- Constructor initializers break before comma
- C++ header files:
.hppextension - C++ source files:
.cppextension - Namespaces:
lower_case - Classes:
CamelCase - Structs:
CamelCase - Class methods:
camelBack - Functions:
camelBack - Variables:
camelBack - Private members:
m_prefix +CamelCase(e.g.,m_MemberVariable) - Global variables:
CamelCase - Global constants:
k_prefix +CamelCase(e.g.,k_DefaultValue) - Local pointers:
camelBack+Ptrsuffix (e.g.,dataPtr) - Type aliases:
CamelCase+Typesuffix (e.g.,ValueType) - Macros:
UPPER_CASE
Use suffixes to make variable types and purposes immediately clear:
Geometry variables use Geom suffix:
- Correct:
const auto& imageGeom = dataStructure.getDataRefAs<ImageGeom>(path); - Incorrect:
const auto& image = dataStructure.getDataRefAs<ImageGeom>(path);
DataStore references use Ref suffix:
- Correct:
const auto& verticesRef = vertexGeom.getVertices()->getDataStoreRef(); - Incorrect:
const auto& vertices = vertexGeom.getVertices()->getDataStoreRef();
Examples:
// Geometry variables
auto& imageGeom = dataStructure.getDataRefAs<ImageGeom>(imagePath);
const auto& rectGridGeom = dataStructure.getDataRefAs<RectGridGeom>(rectPath);
const auto& edgeGeom = dataStructure.getDataRefAs<EdgeGeom>(edgePath);
// DataStore references
const auto& xBoundsRef = rectGridGeom.getXBounds()->getDataStoreRef();
const auto& yBoundsRef = rectGridGeom.getYBounds()->getDataStoreRef();
const auto& verticesRef = edgeGeom.getVertices()->getDataStoreRef();These conventions improve code clarity and distinguish between geometry objects and their underlying data references.
- When creating a C++ based simplnx filter inside a plugin, the complete filter will have a "NameFilter.hpp" and "NameFilter.cpp" file, an "Algorithm/Name.hpp" and "Algorithm/Name.cpp".
- Filter documentation files are created in Markdown and are in the "docs" subfolder inside the Plugins directory
- Unit tests should be created in the 'test' subfolder and use the 'catch2' unit testing framework.
- Selection parameters (GeometrySelectionParameter, ArraySelectionParameter, DataGroupSelectionParameter, etc.) automatically validate that the selected object exists in the DataStructure. Do NOT add null checks for these in preflightImpl() or executeImpl().
- Only add explicit existence checks for objects that are not validated by a selection parameter.
- Use
getDataRefAs<T>()to get a reference when you know the object exists (e.g., validated by a selection parameter). - Use
getDataAs<T>()to get a pointer only when you need to check if an object exists or when the object may not be present. - IMPORTANT: In unit tests, always wrap
getDataRefAs<T>()calls withREQUIRE_NOTHROW()to provide clear test failure messages if the object doesn't exist.
Example - Correct:
// Parameter already validated this exists, use reference
const auto& imageGeom = dataStructure.getDataRefAs<ImageGeom>(pInputImageGeometryPathValue);
SizeVec3 dims = imageGeom.getDimensions();Example - Incorrect:
// Unnecessary null check - parameter already validated existence
const auto* imageGeomPtr = dataStructure.getDataAs<ImageGeom>(pInputImageGeometryPathValue);
if(imageGeomPtr == nullptr)
{
return {MakeErrorResult<OutputActions>(-1000, "Could not find geometry")};
}DataArray,DataStore, andAbstractDataStoreclasses are NOT thread-safe for concurrent read or write access.- The subscript operator (
operator[]) and other access methods may have internal state or go through virtual function calls that are not safe for concurrent access, even when accessing different indices. - Some
DataStoresubclasses use out-of-core implementations where data may not be resident in memory. Getting raw pointers to the underlying data is dangerous and should be avoided.
- When writing parallel algorithms using
ParallelDataAlgorithm, be aware that passingDataArrayorDataStorereferences to worker classes can cause random failures on different platforms. - If parallel access to data arrays is required, consider:
- Disabling parallelization with
parallelAlgorithm.setParallelizationEnabled(false)for correctness - Using thread-local storage for intermediate results
- Structuring the algorithm to avoid concurrent access to the same DataArray
- Disabling parallelization with
- Do NOT assume that writing to different indices of a DataArray from multiple threads is safe.
Example - Potentially Unsafe:
// This pattern can cause random failures even when threads write to different indices
class MyParallelWorker
{
DataArray<float32>& m_OutputArray; // NOT thread-safe for concurrent access
void operator()(const Range& range) const
{
for(usize i = range.min(); i < range.max(); i++)
{
m_OutputArray[i] = computeValue(i); // May fail randomly
}
}
};- vcpkg for dependency management
- CMake-based build system
Example configuring the project
cd /Users/mjackson/Workspace2/simplnx && cmake --preset simplnx-Rel- Build directory is located at "/Users/mjackson/Workspace2/DREAM3D-Build/simplnx-Rel"
Example building the project
cd /Users/mjackson/Workspace2/DREAM3D-Build/simplnx-Rel && cmake --build . --target allEnsuring all test data files are downloaded
cd /Users/mjackson/Workspace2/DREAM3D-Build/simplnx-Rel && cmake --build . --target Fetch_Remote_Data_Files- Python anaconda environment 'dream3d' can be used if needed
- Unit tests use the Catch2 framework.
- Each
TEST_CASEshould includeUnitTest::CheckArraysInheritTupleDims(dataStructure);near the end of the test to ensure all created data arrays have correct tuple dimensions inherited from their parent groups. - Use the
ctestto run unit tests
- Always use
ctestto run unit tests, NOT the test binary directly - The
ctestcommand handles test data extraction and cleanup automatically - Use the
-Rflag to run specific tests by name pattern
Example - Running a specific test:
cd /Users/mjackson/Workspace2/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel && ctest -R "SimplnxCore::FillBadData" --verboseExample - Running all SimplnxCore tests:
cd /Users/mjackson/Workspace2/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel && ctest -R "SimplnxCore::" --verboseExample - Correct
auto executeResult = filter.execute(dataStructure, args, nullptr, IFilter::MessageHandler{[](const IFilter::Message& message){ fmt::print("{}\n", message.message); }});
- Many tests use "exemplar" datasets - pre-generated golden reference data stored in
.dream3dfiles - Exemplar datasets are generated by running pipeline files (
.d3dpipeline) that configure and execute filters
- Generate test data locally: Create pipeline file with filter configurations and
WriteDREAM3DFilterto save results - Execute pipeline: Run the pipeline to generate exemplar
.dream3dfile and any input data files - Package as tar.gz: Compress test data (no
6_6_prefix needed - that was only for legacy DREAM3D data)tar -zvcf test_name.tar.gz test_directory/
- Compute SHA512 hash:
shasum -a 512 test_name.tar.gz
- Upload to GitHub: Upload to the DREAM3D data archive release
- Update CMakeLists.txt: Add
download_test_data()call insrc/Plugins/[PluginName]/test/CMakeLists.txt:download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME test_name.tar.gz SHA512 <hash_from_step_4>)
- Test data auto-downloads: When tests run, the sentinel mechanism automatically downloads and extracts the tar.gz to
unit_test::k_TestFilesDir
- Base naming: Use descriptive names that match the test:
test_name.tar.gz - Version suffixes: When updating existing test data, append version numbers:
test_name_v2.tar.gz,test_name_v3.tar.gz - When to version:
- Original archive already exists in GitHub data archive
- Test requirements changed (new exemplars, different parameters, additional data files)
- Cannot overwrite original because other code may depend on it
- CMakeLists.txt may reference both old and new versions for different tests
- Check before creating: Browse the Data_Archive release to see if your test data name already exists
- Legacy prefixes: The
6_6_and6_5_prefixes are for data from legacy DREAM3D/SIMPL versions - do NOT use for new DREAM3DNX test data
namespace
{
const std::string k_TestDataDirName = "test_name";
const fs::path k_TestDataDir = fs::path(unit_test::k_TestFilesDir.view()) / k_TestDataDirName;
const fs::path k_ExemplarFile = k_TestDataDir / "test_name.dream3d";
const fs::path k_InputImageFile = k_TestDataDir / "input_file.tif";
}Example: If import_image_stack_test.tar.gz exists in the archive and you need to upload updated test data with new exemplars, create import_image_stack_test_v2.tar.gz. Update CMakeLists.txt to reference the new version, and optionally keep the old version if other tests depend on it.
- Load exemplar DataStructure: Use
UnitTest::LoadDataStructure(exemplarFilePath)to load the .dream3d file - ALWAYS use
REQUIRE_NOTHROW()beforegetDataRefAs<T>(): This applies to ALLgetDataRefAscalls - both generated and exemplar data - Get generated data: Use
getDataRefAs<T>()wrapped inREQUIRE_NOTHROW()since objects were just created by the filter - Get exemplar data: Use
getDataRefAs<T>()wrapped inREQUIRE_NOTHROW()to verify the exemplar exists before accessing - Compare geometries: Use
UnitTest::CompareImageGeometry(&exemplarGeom, &generatedGeom)- takes two pointers - Compare arrays: Use
UnitTest::CompareDataArrays<T>(exemplarArray, generatedArray)- type-specific template - Switch on data type when comparing arrays to handle different types (uint8, uint16, uint32, float32, etc.)
Example pattern:
// Load exemplar
DataStructure exemplarDS = UnitTest::LoadDataStructure(k_ExemplarFile);
// Get geometries - ALWAYS wrap getDataRefAs with REQUIRE_NOTHROW
REQUIRE_NOTHROW(dataStructure.getDataRefAs<ImageGeom>(generatedGeomPath));
const auto& generatedGeom = dataStructure.getDataRefAs<ImageGeom>(generatedGeomPath);
REQUIRE_NOTHROW(exemplarDS.getDataRefAs<ImageGeom>(DataPath({exemplarGeomName})));
const auto& exemplarGeom = exemplarDS.getDataRefAs<ImageGeom>(DataPath({exemplarGeomName}));
// Compare geometries (dimensions, origin, spacing) - pass pointers
UnitTest::CompareImageGeometry(&exemplarGeom, &generatedGeom);
// Get arrays - ALWAYS wrap getDataRefAs with REQUIRE_NOTHROW
REQUIRE_NOTHROW(dataStructure.getDataRefAs<IDataArray>(generatedDataPath));
const auto& generatedArray = dataStructure.getDataRefAs<IDataArray>(generatedDataPath);
REQUIRE_NOTHROW(exemplarDS.getDataRefAs<IDataArray>(exemplarDataPath));
const auto& exemplarArray = exemplarDS.getDataRefAs<IDataArray>(exemplarDataPath);
// Compare arrays based on type
switch(generatedArray.getDataType())
{
case DataType::uint8:
UnitTest::CompareDataArrays<uint8>(exemplarArray, generatedArray);
break;
case DataType::uint16:
UnitTest::CompareDataArrays<uint16>(exemplarArray, generatedArray);
break;
// ... etc
}Important: Use the standardized UnitTest:: comparison methods directly in test code.
- Each test should call
UnitTest::LoadPlugins()before executing filters - Use
DYNAMIC_SECTION()for parameterized tests that generate multiple test cases
- JSON format with
.d3dpipelineextension - Contains array of filter configurations with arguments
- Each filter has:
args: Dictionary of parameter keys and valuescomments: Description of what the filter doesfilter: Name and UUIDisDisabled: Boolean to skip filter execution
- Common pattern: Multiple filter configurations followed by WriteDREAM3DFilter to save all results to one
.dream3dfile - Output geometry paths in pipeline must match exemplar names expected by tests