diff --git a/src/Plugins/SimplnxCore/CMakeLists.txt b/src/Plugins/SimplnxCore/CMakeLists.txt index 0ca373406c..9aac2adc4c 100644 --- a/src/Plugins/SimplnxCore/CMakeLists.txt +++ b/src/Plugins/SimplnxCore/CMakeLists.txt @@ -137,6 +137,7 @@ set(FilterList WriteDREAM3DFilter WriteFeatureDataCSVFilter WriteLosAlamosFFTFilter + WriteNodesAndElementsFilesFilter WriteStlFileFilter WriteVtkRectilinearGridFilter WriteVtkStructuredPointsFilter @@ -226,6 +227,7 @@ set(AlgorithmList WriteAvizoRectilinearCoordinate WriteAvizoUniformCoordinate WriteLosAlamosFFT + WriteNodesAndElementsFiles WriteStlFile WriteVtkRectilinearGrid WriteVtkStructuredPoints diff --git a/src/Plugins/SimplnxCore/docs/WriteNodesAndElementsFilesFilter.md b/src/Plugins/SimplnxCore/docs/WriteNodesAndElementsFilesFilter.md new file mode 100644 index 0000000000..f6c53892b5 --- /dev/null +++ b/src/Plugins/SimplnxCore/docs/WriteNodesAndElementsFilesFilter.md @@ -0,0 +1,24 @@ +# Write Nodes and Elements File(s) + +## Group (Subgroup) + +IO (Output) + +## Description + +This **Filter** exports geometric data into structured text files. It allows users to save the following: + +1. Node Data (Vertices): Export the coordinates of points that define the geometry. +2. Element Data (Connectivity): Export the node indices that make up the edges, faces, or volumes of the geometry. + +The filter gives the user the option to export a node file, element file, or both. It also allows the user to decide whether or not to number the nodes and elements and whether or not to include headers for the node and element files. + +% Auto generated parameter table will be inserted here + +## License & Copyright + +Please see the description file distributed with this **Plugin** + +## DREAM3D-NX Help + +If you need help, need to file a bug report or want to request a new feature, please head over to the [DREAM3DNX-Issues](https://github.com/BlueQuartzSoftware/DREAM3DNX-Issues/discussions) GitHub site where the community of DREAM3D-NX users can help answer your questions. diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteNodesAndElementsFiles.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteNodesAndElementsFiles.cpp new file mode 100644 index 0000000000..bd0988c34d --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteNodesAndElementsFiles.cpp @@ -0,0 +1,196 @@ +#include "WriteNodesAndElementsFiles.hpp" + +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/Geometry/EdgeGeom.hpp" +#include "simplnx/DataStructure/Geometry/HexahedralGeom.hpp" +#include "simplnx/DataStructure/Geometry/QuadGeom.hpp" +#include "simplnx/DataStructure/Geometry/TetrahedralGeom.hpp" +#include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" +#include "simplnx/DataStructure/Geometry/VertexGeom.hpp" +#include "simplnx/SIMPLNXVersion.hpp" +#include "simplnx/Utilities/StringUtilities.hpp" + +#include +#include + +namespace fs = std::filesystem; + +using namespace nx::core; + +namespace +{ +template +void WriteValue(std::ofstream& file, const T& value) +{ + if constexpr(std::is_floating_point::value) + { + // For floating-point numbers, use up to 4 decimal places + file << std::fixed << std::setprecision(4) << value; + } + else + { + // For non-floating-point types, reset to default format with no precision + file << std::defaultfloat << std::setprecision(0) << value; + } +} + +template +Result<> WriteFile(const fs::path& outputFilePath, const DataArray& array, bool includeArrayHeaders, std::vector arrayHeaders, bool numberRows, bool includeComponentCount) +{ + std::ofstream file(outputFilePath.string()); + if(!file.is_open()) + { + return MakeErrorResult(to_underlying(WriteNodesAndElementsFiles::ErrorCodes::FailedToOpenOutputFile), fmt::format("Failed to open output file \"{}\".", outputFilePath.string())); + } + + file << fmt::format("# This file was created by simplnx v{}", Version::Complete()) << std::endl; + + if(includeArrayHeaders) + { + WriteValue(file, StringUtilities::join(arrayHeaders, " ")); + file << std::endl; + } + usize numComps = array.getNumberOfComponents(); + for(usize i = 0; i < array.getNumberOfTuples(); i++) + { + if(numberRows) + { + WriteValue(file, i); + file << " "; + } + + if(includeComponentCount) + { + WriteValue(file, numComps); + file << " "; + } + + for(usize j = 0; j < numComps; j++) + { + WriteValue(file, array[i * numComps + j]); + if(j != numComps - 1) + { + file << " "; + } + } + file << std::endl; + } + + return {}; +} +} // namespace + +// ----------------------------------------------------------------------------- +WriteNodesAndElementsFiles::WriteNodesAndElementsFiles(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, + WriteNodesAndElementsFilesInputValues* inputValues) +: m_DataStructure(dataStructure) +, m_InputValues(inputValues) +, m_ShouldCancel(shouldCancel) +, m_MessageHandler(mesgHandler) +{ +} + +// ----------------------------------------------------------------------------- +WriteNodesAndElementsFiles::~WriteNodesAndElementsFiles() noexcept = default; + +// ----------------------------------------------------------------------------- +const std::atomic_bool& WriteNodesAndElementsFiles::getCancel() +{ + return m_ShouldCancel; +} + +// ----------------------------------------------------------------------------- +void WriteNodesAndElementsFiles::sendMessage(const std::string& message) +{ + m_MessageHandler(IFilter::Message::Type::Info, message); +} + +// ----------------------------------------------------------------------------- +Result<> WriteNodesAndElementsFiles::operator()() +{ + auto& iNodeGeometry = m_DataStructure.getDataRefAs(m_InputValues->SelectedGeometryPath); + auto geomType = iNodeGeometry.getGeomType(); + UInt64Array* cellsArray = nullptr; + + switch(geomType) + { + case IGeometry::Type::Edge: { + auto& geom = m_DataStructure.getDataRefAs(m_InputValues->SelectedGeometryPath); + cellsArray = geom.getEdges(); + break; + } + case IGeometry::Type::Triangle: { + auto& geom = m_DataStructure.getDataRefAs(m_InputValues->SelectedGeometryPath); + cellsArray = geom.getFaces(); + break; + } + case IGeometry::Type::Quad: { + auto& geom = m_DataStructure.getDataRefAs(m_InputValues->SelectedGeometryPath); + cellsArray = geom.getFaces(); + break; + } + case IGeometry::Type::Tetrahedral: { + auto& geom = m_DataStructure.getDataRefAs(m_InputValues->SelectedGeometryPath); + cellsArray = geom.getPolyhedra(); + break; + } + case IGeometry::Type::Hexahedral: { + auto& geom = m_DataStructure.getDataRefAs(m_InputValues->SelectedGeometryPath); + cellsArray = geom.getPolyhedra(); + break; + } + case IGeometry::Type::Vertex: { + break; + } + case IGeometry::Type::Image: + return MakeErrorResult(to_underlying(ErrorCodes::UnsupportedGeometryType), fmt::format("The Image geometry type is not supported by this filter. Please choose another geometry.")); + case IGeometry::Type::RectGrid: { + return MakeErrorResult(to_underlying(ErrorCodes::UnsupportedGeometryType), fmt::format("The Rectilinear Grid geometry type is not supported by this filter. Please choose another geometry.")); + } + } + + const Float32Array& vertices = iNodeGeometry.getVerticesRef(); + + if(m_InputValues->WriteNodeFile) + { + std::vector arrayHeaders; + if(m_InputValues->NumberNodes) + { + arrayHeaders.push_back("NODE_NUM"); + } + arrayHeaders.insert(arrayHeaders.end(), {"X", "Y", "Z"}); + + std::vector arrayHeadersViews(arrayHeaders.size()); + std::transform(arrayHeaders.begin(), arrayHeaders.end(), arrayHeadersViews.begin(), [](const std::string& s) { return std::string_view(s); }); + auto result = WriteFile(m_InputValues->NodeFilePath, vertices, m_InputValues->IncludeNodeFileHeader, arrayHeadersViews, m_InputValues->NumberNodes, false); + if(result.invalid()) + { + return result; + } + } + + if(m_InputValues->WriteElementFile && geomType != IGeometry::Type::Vertex) + { + std::vector arrayHeaders; + if(m_InputValues->NumberElements) + { + arrayHeaders.push_back("ELEMENT_NUM"); + } + arrayHeaders.push_back("NUM_VERTS_IN_ELEMENT"); + for(usize i = 0; i < cellsArray->getNumberOfComponents(); i++) + { + std::string vertexHeader = fmt::format("V{}_Index", i); + arrayHeaders.push_back(vertexHeader); + } + + std::vector arrayHeadersViews(arrayHeaders.size()); + std::transform(arrayHeaders.begin(), arrayHeaders.end(), arrayHeadersViews.begin(), [](const std::string& s) { return std::string_view(s); }); + auto result = WriteFile(m_InputValues->ElementFilePath, *cellsArray, m_InputValues->IncludeElementFileHeader, arrayHeadersViews, m_InputValues->NumberElements, true); + if(result.invalid()) + { + return result; + } + } + + return {}; +} diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteNodesAndElementsFiles.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteNodesAndElementsFiles.hpp new file mode 100644 index 0000000000..a59c27049e --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/WriteNodesAndElementsFiles.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include "SimplnxCore/SimplnxCore_export.hpp" + +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/DataStructure.hpp" +#include "simplnx/Filter/IFilter.hpp" +#include "simplnx/Parameters/ArraySelectionParameter.hpp" +#include "simplnx/Parameters/FileSystemPathParameter.hpp" +#include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Parameters/StringParameter.hpp" + +namespace fs = std::filesystem; + +namespace nx::core +{ + +struct SIMPLNXCORE_EXPORT WriteNodesAndElementsFilesInputValues +{ + DataPath SelectedGeometryPath; + bool WriteNodeFile; + bool NumberNodes; + bool IncludeNodeFileHeader; + fs::path NodeFilePath; + bool WriteElementFile; + bool NumberElements; + bool IncludeElementFileHeader; + fs::path ElementFilePath; +}; + +/** + * @class + */ +class SIMPLNXCORE_EXPORT WriteNodesAndElementsFiles +{ +public: + WriteNodesAndElementsFiles(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, WriteNodesAndElementsFilesInputValues* inputValues); + ~WriteNodesAndElementsFiles() noexcept; + + WriteNodesAndElementsFiles(const WriteNodesAndElementsFiles&) = delete; + WriteNodesAndElementsFiles(WriteNodesAndElementsFiles&&) noexcept = delete; + WriteNodesAndElementsFiles& operator=(const WriteNodesAndElementsFiles&) = delete; + WriteNodesAndElementsFiles& operator=(WriteNodesAndElementsFiles&&) noexcept = delete; + + enum class ErrorCodes : int64 + { + NoFileWriterChosen = -134, + FailedToOpenOutputFile = -135, + VertexGeomHasNoElements = -136, + UnsupportedGeometryType = -137 + }; + + Result<> operator()(); + + const std::atomic_bool& getCancel(); + + void sendMessage(const std::string& message); + +private: + DataStructure& m_DataStructure; + const WriteNodesAndElementsFilesInputValues* m_InputValues = nullptr; + const std::atomic_bool& m_ShouldCancel; + const IFilter::MessageHandler& m_MessageHandler; +}; + +} // namespace nx::core diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteNodesAndElementsFilesFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteNodesAndElementsFilesFilter.cpp new file mode 100644 index 0000000000..251609a985 --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteNodesAndElementsFilesFilter.cpp @@ -0,0 +1,141 @@ +#include "WriteNodesAndElementsFilesFilter.hpp" + +#include "simplnx/Common/TypeTraits.hpp" +#include "simplnx/DataStructure/DataGroup.hpp" +#include "simplnx/Parameters/BoolParameter.hpp" +#include "simplnx/Parameters/FileSystemPathParameter.hpp" +#include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Pipeline/Pipeline.hpp" +#include "simplnx/Pipeline/PipelineFilter.hpp" +#include "simplnx/Utilities/SIMPLConversion.hpp" + +#include "SimplnxCore/Filters/Algorithms/WriteNodesAndElementsFiles.hpp" + +#include +namespace fs = std::filesystem; + +namespace nx::core +{ +//------------------------------------------------------------------------------ +std::string WriteNodesAndElementsFilesFilter::name() const +{ + return FilterTraits::name; +} + +//------------------------------------------------------------------------------ +std::string WriteNodesAndElementsFilesFilter::className() const +{ + return FilterTraits::className; +} + +//------------------------------------------------------------------------------ +Uuid WriteNodesAndElementsFilesFilter::uuid() const +{ + return FilterTraits::uuid; +} + +//------------------------------------------------------------------------------ +std::string WriteNodesAndElementsFilesFilter::humanName() const +{ + return "Write Nodes And Elements File(s)"; +} + +//------------------------------------------------------------------------------ +std::vector WriteNodesAndElementsFilesFilter::defaultTags() const +{ + return {className(), "IO", "Output", "Write", "Export", "Nodes", "Elements", "Cells", "Vertices", "Geometry"}; +} + +//------------------------------------------------------------------------------ +Parameters WriteNodesAndElementsFilesFilter::parameters() const +{ + Parameters params; + + params.insertSeparator(Parameters::Separator{"Input Parameter(s)"}); + params.insert(std::make_unique(k_SelectedGeometry, "Geometry To Write", "The Geometry that will be written to the output file(s).", DataPath(), + GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Vertex, IGeometry::Type::Edge, IGeometry::Type::Triangle, IGeometry::Type::Quad, + IGeometry::Type::Tetrahedral, IGeometry::Type::Hexahedral})); + params.insertLinkableParameter(std::make_unique(k_WriteNodeFile, "Write Node File", "Whether or not to write the node information out to a file.", true)); + params.insert(std::make_unique(k_NumberNodes, "Number Nodes", "Whether or not to number each node in the node information output file.", true)); + params.insert(std::make_unique(k_IncludeNodeFileHeader, "Include Node File Header", "Whether or not to include the node file header in the node output file.", true)); + params.insertLinkableParameter(std::make_unique(k_WriteElementFile, "Write Element/Cell File", "Whether or not to write the element/cell information out to a file.", true)); + params.insert(std::make_unique(k_NumberElements, "Number Elements/Cells", "Whether or not to number each element/cell in the element information output file.", true)); + params.insert( + std::make_unique(k_IncludeElementFileHeader, "Include Element/Cell File Header", "Whether or not to include the element/cell file header in the element/cell output file.", true)); + + params.insertSeparator(Parameters::Separator{"Output Parameter(s)"}); + params.insert(std::make_unique(k_NodeFilePath, "Output Node File Path", "The node information will be written to this file path.", "Nodes.csv", + FileSystemPathParameter::ExtensionsType{".csv", ".node", ".txt"}, FileSystemPathParameter::PathType::OutputFile, true)); + params.insert(std::make_unique(k_ElementFilePath, "Output Element/Cell File Path", "The element/cell information will be written to this file path.", "Elements.csv", + FileSystemPathParameter::ExtensionsType{".csv", ".ele", ".txt"}, FileSystemPathParameter::PathType::OutputFile, true)); + + params.linkParameters(k_WriteNodeFile, k_NumberNodes, true); + params.linkParameters(k_WriteNodeFile, k_IncludeNodeFileHeader, true); + params.linkParameters(k_WriteNodeFile, k_NodeFilePath, true); + params.linkParameters(k_WriteElementFile, k_NumberElements, true); + params.linkParameters(k_WriteElementFile, k_IncludeElementFileHeader, true); + params.linkParameters(k_WriteElementFile, k_ElementFilePath, true); + + return params; +} + +//------------------------------------------------------------------------------ +IFilter::VersionType WriteNodesAndElementsFilesFilter::parametersVersion() const +{ + return 1; +} + +//------------------------------------------------------------------------------ +IFilter::UniquePointer WriteNodesAndElementsFilesFilter::clone() const +{ + return std::make_unique(); +} + +//------------------------------------------------------------------------------ +IFilter::PreflightResult WriteNodesAndElementsFilesFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& args, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel) const +{ + DataPath selectedGeometryPath = args.value(k_SelectedGeometry); + bool writeNodeFile = args.value(k_WriteNodeFile); + bool numberNodes = args.value(k_NumberNodes); + fs::path nodeFilePath = args.value(k_NodeFilePath); + bool writeElementFile = args.value(k_WriteElementFile); + bool numberElements = args.value(k_NumberElements); + fs::path elementFilePath = args.value(k_ElementFilePath); + + if(!writeNodeFile && !writeElementFile) + { + return {MakeErrorResult(to_underlying(WriteNodesAndElementsFiles::ErrorCodes::NoFileWriterChosen), + "Neither 'Write Node File' nor 'Write Element/Cell File' have been chosen. Please choose at least one of these options.")}; + } + + auto& selectedGeometry = dataStructure.getDataRefAs(selectedGeometryPath); + if(selectedGeometry.getGeomType() == IGeometry::Type::Vertex && writeElementFile) + { + return {MakeErrorResult( + to_underlying(WriteNodesAndElementsFiles::ErrorCodes::VertexGeomHasNoElements), + "The selected geometry is a vertex geometry, so an element file cannot be written. Please turn off 'Write Element/Cell File' or select a different geometry with a type other than Vertex.")}; + } + + return {}; +} + +//------------------------------------------------------------------------------ +Result<> WriteNodesAndElementsFilesFilter::executeImpl(DataStructure& dataStructure, const Arguments& args, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel) const +{ + WriteNodesAndElementsFilesInputValues inputValues; + + inputValues.SelectedGeometryPath = args.value(k_SelectedGeometry); + inputValues.WriteNodeFile = args.value(k_WriteNodeFile); + inputValues.NumberNodes = args.value(k_NumberNodes); + inputValues.IncludeNodeFileHeader = args.value(k_IncludeNodeFileHeader); + inputValues.NodeFilePath = args.value(k_NodeFilePath); + inputValues.WriteElementFile = args.value(k_WriteElementFile); + inputValues.NumberElements = args.value(k_NumberElements); + inputValues.IncludeElementFileHeader = args.value(k_IncludeElementFileHeader); + inputValues.ElementFilePath = args.value(k_ElementFilePath); + + return WriteNodesAndElementsFiles(dataStructure, messageHandler, shouldCancel, &inputValues)(); +} +} // namespace nx::core diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteNodesAndElementsFilesFilter.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteNodesAndElementsFilesFilter.hpp new file mode 100644 index 0000000000..7ec09b1871 --- /dev/null +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteNodesAndElementsFilesFilter.hpp @@ -0,0 +1,122 @@ +#pragma once + +#include "SimplnxCore/SimplnxCore_export.hpp" + +#include "simplnx/Filter/Arguments.hpp" +#include "simplnx/Filter/FilterTraits.hpp" +#include "simplnx/Filter/IFilter.hpp" +#include "simplnx/Filter/Parameters.hpp" + +namespace nx::core +{ +/** + * @class WriteNodesAndElementsFilesFilter + * @brief The WriteNodesAndElementsFilesFilter is an IFilter class designed to export the + * DataStructure to a target HDF5 file. + */ +class SIMPLNXCORE_EXPORT WriteNodesAndElementsFilesFilter : public IFilter +{ +public: + WriteNodesAndElementsFilesFilter() = default; + ~WriteNodesAndElementsFilesFilter() noexcept override = default; + + WriteNodesAndElementsFilesFilter(const WriteNodesAndElementsFilesFilter&) = delete; + WriteNodesAndElementsFilesFilter(WriteNodesAndElementsFilesFilter&&) noexcept = delete; + + WriteNodesAndElementsFilesFilter& operator=(const WriteNodesAndElementsFilesFilter&) = delete; + WriteNodesAndElementsFilesFilter& operator=(WriteNodesAndElementsFilesFilter&&) noexcept = delete; + + // Parameter Keys + static inline constexpr StringLiteral k_SelectedGeometry = "selected_geometry_path"; + static inline constexpr StringLiteral k_WriteNodeFile = "write_node_file"; + static inline constexpr StringLiteral k_NumberNodes = "number_nodes"; + static inline constexpr StringLiteral k_IncludeNodeFileHeader = "include_node_file_header"; + static inline constexpr StringLiteral k_NodeFilePath = "node_file_path"; + static inline constexpr StringLiteral k_WriteElementFile = "write_element_file"; + static inline constexpr StringLiteral k_NumberElements = "number_elements"; + static inline constexpr StringLiteral k_IncludeElementFileHeader = "include_element_file_header"; + static inline constexpr StringLiteral k_ElementFilePath = "element_file_path"; + + /** + * @brief Reads SIMPL json and converts it simplnx Arguments. + * @param json + * @return Result + */ + static Result FromSIMPLJson(const nlohmann::json& json); + + /** + * @brief Returns the name of the filter class. + * @return std::string + */ + std::string name() const override; + + /** + * @brief Returns the C++ classname of this filter. + * @return + */ + std::string className() const override; + + /** + * @brief Returns the WriteNodesAndElementsFilesFilter class's UUID. + * @return Uuid + */ + Uuid uuid() const override; + + /** + * @brief Returns the human readable name of the filter. + * @return std::string + */ + std::string humanName() const override; + + /** + * @brief Returns the default tags for this filter. + * @return + */ + std::vector defaultTags() const override; + + /** + * @brief Returns a collection of the filter's parameters (i.e. its inputs) + * @return Parameters + */ + Parameters parameters() const override; + + /** + * @brief Returns parameters version integer. + * Initial version should always be 1. + * Should be incremented everytime the parameters change. + * @return VersionType + */ + VersionType parametersVersion() const override; + + /** + * @brief Returns a copy of the filter as a std::unique_ptr. + * @return UniquePointer + */ + UniquePointer clone() const override; + +protected: + /** + * @brief Classes that implement IFilter must provide this function for preflight. + * Runs after the filter runs the checks in its parameters. + * @param dataStructure + * @param args + * @param messageHandler + * @return Result + */ + PreflightResult preflightImpl(const DataStructure& dataStructure, const Arguments& args, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel) const override; + + /** + * @brief Classes that implement IFilter must provide this function for execute. + * Runs after the filter applies the OutputActions from preflight. + * @param dataStructure + * @param args + * @param pipelineNode + * @param messageHandler + * @return Result<> + */ + Result<> executeImpl(DataStructure& dataStructure, const Arguments& args, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel) const override; +}; +} // namespace nx::core + +SIMPLNX_DEF_FILTER_TRAITS(nx::core, WriteNodesAndElementsFilesFilter, "8c563174-0183-45fe-8ef8-756104f215d5"); diff --git a/src/Plugins/SimplnxCore/test/CMakeLists.txt b/src/Plugins/SimplnxCore/test/CMakeLists.txt index 5ae4af0332..1cdd3295fa 100644 --- a/src/Plugins/SimplnxCore/test/CMakeLists.txt +++ b/src/Plugins/SimplnxCore/test/CMakeLists.txt @@ -137,6 +137,7 @@ set(${PLUGIN_NAME}UnitTest_SRCS WriteBinaryDataTest.cpp WriteFeatureDataCSVTest.cpp WriteLosAlamosFFTTest.cpp + WriteNodesAndElementsFilesTest.cpp WriteStlFileTest.cpp WriteVtkRectilinearGridTest.cpp ) diff --git a/src/Plugins/SimplnxCore/test/WriteNodesAndElementsFilesTest.cpp b/src/Plugins/SimplnxCore/test/WriteNodesAndElementsFilesTest.cpp new file mode 100644 index 0000000000..0a27d3e3f5 --- /dev/null +++ b/src/Plugins/SimplnxCore/test/WriteNodesAndElementsFilesTest.cpp @@ -0,0 +1,247 @@ +#include + +#include "SimplnxCore/Filters/Algorithms/WriteNodesAndElementsFiles.hpp" +#include "SimplnxCore/Filters/WriteNodesAndElementsFilesFilter.hpp" +#include "SimplnxCore/SimplnxCore_test_dirs.hpp" + +#include "simplnx/DataStructure/Geometry/EdgeGeom.hpp" +#include "simplnx/UnitTest/UnitTestCommon.hpp" +#include "simplnx/Utilities/StringUtilities.hpp" + +#include +namespace fs = std::filesystem; + +using namespace nx::core; +using namespace nx::core::Constants; +namespace +{ +const std::string k_GeometryName = "Geometry"; +const DataPath k_GeometryPath = DataPath({k_GeometryName}); +const fs::path k_OutputNodeFilePath = fs::temp_directory_path() / "nodes.node"; +const fs::path k_OutputElementFilePath = fs::temp_directory_path() / "elements.ele"; + +void Cleanup() +{ + if(fs::exists(k_OutputNodeFilePath)) + { + REQUIRE(fs::remove(k_OutputNodeFilePath)); + } + if(fs::exists(k_OutputElementFilePath)) + { + REQUIRE(fs::remove(k_OutputElementFilePath)); + } +} + +void CreateVertexGeometry(DataStructure& ds) +{ + VertexGeom* geom = VertexGeom::Create(ds, k_GeometryName); + auto vertexAttrMatrix = AttributeMatrix::Create(ds, "Vertex Data", {2}, geom->getId()); + geom->setVertexAttributeMatrix(*vertexAttrMatrix); + Float32Array* vertices = UnitTest::CreateTestDataArray(ds, "Vertices Store", {2}, {3}, geom->getId()); + std::vector verticesVec = {1, 1.5, 1.75, 2, 3, 4}; + std::copy(verticesVec.begin(), verticesVec.end(), vertices->begin()); + geom->setVertices(*vertices); +} + +void CreateEdgeGeometry(DataStructure& ds) +{ + EdgeGeom* geom = EdgeGeom::Create(ds, k_GeometryName); + auto edgeAttrMatrix = AttributeMatrix::Create(ds, "Edge Data", {1}, geom->getId()); + geom->setEdgeAttributeMatrix(*edgeAttrMatrix); + auto vertexAttrMatrix = AttributeMatrix::Create(ds, "Vertex Data", {2}, geom->getId()); + geom->setVertexAttributeMatrix(*vertexAttrMatrix); + Float32Array* vertices = UnitTest::CreateTestDataArray(ds, "Vertices Store", {2}, {3}, geom->getId()); + std::vector verticesVec = {1, 1.5, 1.75, 2, 3, 4}; + std::copy(verticesVec.begin(), verticesVec.end(), vertices->begin()); + geom->setVertices(*vertices); + DataArray* cells = UnitTest::CreateTestDataArray(ds, "Cells Store", {1}, {2}, geom->getId()); + std::vector cellsVec = {0, 1}; + std::copy(cellsVec.begin(), cellsVec.end(), cells->begin()); + geom->setEdgeList(*cells); +} + +void ValidateFile(const fs::path& filePath, const std::vector& expectedHeader, const std::vector>& expectedContent) +{ + std::ifstream file(filePath.string()); + REQUIRE(file.is_open()); + + auto validateTokens = [&](const std::string& line, const std::vector& expectedTokens) { + auto tokens = StringUtilities::split(line, ' '); + REQUIRE(tokens == expectedTokens); + }; + + std::string line; + + // Skip the comment line + std::getline(file, line); + + // Validate header if provided + if(!expectedHeader.empty()) + { + std::getline(file, line); + validateTokens(line, expectedHeader); + } + + // Validate content lines + for(const auto& expectedTokens : expectedContent) + { + std::getline(file, line); + validateTokens(line, expectedTokens); + } + + file.close(); +} +} // namespace + +TEST_CASE("SimplnxCore::WriteNodesAndElementsFilesFilter: Valid Execution", "[SimplnxCore][WriteNodesAndElementsFilesFilter]") +{ + // Instantiate the filter, a DataStructure object and an Arguments Object + WriteNodesAndElementsFilesFilter filter; + + DataStructure dataStructure; + Arguments args; + + CreateEdgeGeometry(dataStructure); + + bool writeNodeFile = false; + bool numberNodes = false; + bool includeNodeFileHeader = false; + bool writeElementFile = false; + bool numberElements = false; + bool includeElementFileHeader = false; + + SECTION("Node File") + { + writeNodeFile = true; + + SECTION("Number Nodes") + { + numberNodes = true; + } + SECTION("Include File Header") + { + includeNodeFileHeader = true; + } + SECTION("Both Number Nodes & Include File Header") + { + numberNodes = true; + includeNodeFileHeader = true; + } + } + SECTION("Element File") + { + writeElementFile = true; + + SECTION("Number Elements") + { + numberElements = true; + } + SECTION("Include File Header") + { + includeElementFileHeader = true; + } + SECTION("Both Number Elements & Include File Header") + { + numberElements = true; + includeElementFileHeader = true; + } + } + + // Create default Parameters for the filter. + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_SelectedGeometry, std::make_any(k_GeometryPath)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_WriteNodeFile, std::make_any(writeNodeFile)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_NumberNodes, std::make_any(numberNodes)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_IncludeNodeFileHeader, std::make_any(includeNodeFileHeader)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_NodeFilePath, std::make_any(k_OutputNodeFilePath)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_WriteElementFile, std::make_any(writeElementFile)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_NumberElements, std::make_any(numberElements)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_IncludeElementFileHeader, std::make_any(includeElementFileHeader)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_ElementFilePath, std::make_any(k_OutputElementFilePath)); + + // 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); + + if(writeNodeFile) + { + std::vector expectedHeader = includeNodeFileHeader ? std::vector{"X", "Y", "Z"} : std::vector{}; + std::vector> expectedContent = {{"1.0000", "1.5000", "1.7500"}, {"2.0000", "3.0000", "4.0000"}}; + if(numberNodes) + { + if(includeNodeFileHeader) + { + expectedHeader.insert(expectedHeader.begin(), "NODE_NUM"); + } + expectedContent[0].insert(expectedContent[0].begin(), "0"); + expectedContent[1].insert(expectedContent[1].begin(), "1"); + } + ValidateFile(k_OutputNodeFilePath, expectedHeader, expectedContent); + } + + if(writeElementFile) + { + std::vector expectedHeader = includeElementFileHeader ? std::vector{"NUM_VERTS_IN_ELEMENT", "V0_Index", "V1_Index"} : std::vector{}; + std::vector> expectedContent = {{"2", "0", "1"}}; + if(numberElements) + { + if(includeElementFileHeader) + { + expectedHeader.insert(expectedHeader.begin(), "ELEMENT_NUM"); + } + expectedContent[0].insert(expectedContent[0].begin(), "0"); + } + ValidateFile(k_OutputElementFilePath, expectedHeader, expectedContent); + } + + // Clean up the files + Cleanup(); +} + +TEST_CASE("SimplnxCore::WriteNodesAndElementsFilesFilter: Invalid Execution", "[SimplnxCore][WriteNodesAndElementsFilesFilter]") +{ + // Instantiate the filter, a DataStructure object and an Arguments Object + WriteNodesAndElementsFilesFilter filter; + + DataStructure dataStructure; + Arguments args; + + int32 code = -1; + SECTION("No File Writer Chosen") + { + CreateEdgeGeometry(dataStructure); + code = to_underlying(WriteNodesAndElementsFiles::ErrorCodes::NoFileWriterChosen); + + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_WriteNodeFile, std::make_any(false)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_NumberNodes, std::make_any(true)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_IncludeNodeFileHeader, std::make_any(true)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_WriteElementFile, std::make_any(false)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_NumberElements, std::make_any(true)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_IncludeElementFileHeader, std::make_any(true)); + } + SECTION("Writing A Node File Using A Vertex Geometry") + { + CreateVertexGeometry(dataStructure); + code = to_underlying(WriteNodesAndElementsFiles::ErrorCodes::VertexGeomHasNoElements); + + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_WriteNodeFile, std::make_any(false)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_NumberNodes, std::make_any(true)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_IncludeNodeFileHeader, std::make_any(true)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_WriteElementFile, std::make_any(true)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_NumberElements, std::make_any(true)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_IncludeElementFileHeader, std::make_any(true)); + } + + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_SelectedGeometry, std::make_any(k_GeometryPath)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_NodeFilePath, std::make_any(k_OutputNodeFilePath)); + args.insertOrAssign(WriteNodesAndElementsFilesFilter::k_ElementFilePath, std::make_any(k_OutputElementFilePath)); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + REQUIRE(preflightResult.outputActions.errors()[0].code == code); +}