From 3f09c5ee24ded54e9d9733281ee800b8b6ce8364 Mon Sep 17 00:00:00 2001 From: Charles Doutriaux Date: Wed, 18 Dec 2024 16:19:42 -0800 Subject: [PATCH 01/38] created a clean new branch from Gabriel's work to push to github repo --- src/axom/sina/core/Document.cpp | 285 +++++++++++++++--- src/axom/sina/core/Document.hpp | 38 ++- src/axom/sina/docs/sphinx/documents.rst | 21 ++ src/axom/sina/docs/sphinx/tutorial.rst | 26 +- src/axom/sina/examples/sina_basic.cpp | 3 + .../sina/examples/sina_document_assembly.cpp | 5 + src/axom/sina/examples/sina_fortran.f | 3 + src/axom/sina/examples/sina_tutorial.cpp | 4 +- .../sina/interface/sina_fortran_interface.cpp | 13 +- .../sina/interface/sina_fortran_interface.f | 45 ++- .../sina/interface/sina_fortran_interface.h | 3 +- src/axom/sina/tests/sina_Document.cpp | 162 +++++++++- src/spack | 1 + 13 files changed, 541 insertions(+), 68 deletions(-) create mode 160000 src/spack diff --git a/src/axom/sina/core/Document.cpp b/src/axom/sina/core/Document.cpp index d53892e477..2a3aed89cc 100644 --- a/src/axom/sina/core/Document.cpp +++ b/src/axom/sina/core/Document.cpp @@ -22,6 +22,10 @@ #include #include #include +#include +#include "conduit.hpp" +#include "conduit_relay.hpp" +#include "conduit_relay_io.hpp" namespace axom { @@ -33,7 +37,133 @@ namespace char const RECORDS_KEY[] = "records"; char const RELATIONSHIPS_KEY[] = "relationships"; char const SAVE_TMP_FILE_EXTENSION[] = ".sina.tmp"; -} // namespace +} + +void protocol_warn(std::string protocol, std::string const &name) { + size_t pos = name.rfind('.'); + + if (pos != std::string::npos) { + std::string found = name.substr(pos+1); + + if (("." + found) != protocol) { + for (const std::string& file_type : supported_types) { + std::string lower_case_type = file_type; + std::transform(lower_case_type.begin(), lower_case_type.end(), lower_case_type.begin(), [](unsigned char c) { return std::tolower(c); }); + + if ((lower_case_type != protocol) && (lower_case_type == found)) { + std::cout << "|| WARNING: INCORRECT FILE EXTENSION FOUND (FOUND: " << found << "; EXPECTED: " << protocol << "), DID YOU MEAN TO INCLUDE ONE OF " << protocol << "'s SUPPORTED TYPES? ||"; + } + } + std::cout << "|| WARNING: BROKEN FILE EXTENSION FOUND, SINA WILL BE APPENDING THE " << protocol << " FILE EXTENSION ||"; + } else { + return; + } + } else { + std::cout << "|| WARNING: NO FILE EXTENSION FOUND, DID YOU MEAN TO INCLUDE ONE OF " << protocol << "'s SUPPORTED TYPES?"; + } +} + +void removeSlashes(const conduit::Node& originalNode, conduit::Node& modifiedNode) +{ + for (auto it = originalNode.children(); it.has_next();) + { + it.next(); + std::string key = it.name(); + std::string modifiedKey = key; + //modifiedKey.erase(std::remove(modifiedKey.begin(), modifiedKey.end(), '/'), modifiedKey.end()); + + std::string toReplace = "/"; + std::string replacement = "__SINA_SLASHREPLACE__"; + + size_t pos = 0; + // Find and replace all occurrences of "/" + while ((pos = modifiedKey.find(toReplace, pos)) != std::string::npos) { + modifiedKey.replace(pos, toReplace.length(), replacement); + pos += replacement.length(); // Move past the replaced substring + } + + modifiedNode[modifiedKey] = it.node(); + + if (it.node().dtype().is_object()) + { + conduit::Node nestedNode; + removeSlashes(it.node(), nestedNode); + modifiedNode[modifiedKey].set(nestedNode); + } + } +} + +void restoreSlashes(const conduit::Node& modifiedNode, conduit::Node& restoredNode) +{ + // Check if List or Object, if its a list the else statement would turn it into an object + // which breaks the Document + + if (modifiedNode.dtype().is_list()) + { + // If its empty with no children it's the end of a tree + + for (auto it = modifiedNode.children(); it.has_next();) + { + it.next(); + conduit::Node& newChild = restoredNode.append(); + + // Leaves empty nodes empty, if null data is set the + // Document breaks + + if (it.node().dtype().is_string() || it.node().dtype().is_number()) + { + newChild.set(it.node()); // Lists need .set + } + + // Recursive Call + if (it.node().number_of_children() > 0) + { + restoreSlashes(it.node(), newChild); + } + } + } + else + { + for (auto it = modifiedNode.children(); it.has_next();) + { + it.next(); + std::string key = it.name(); + std::string restoredKey = key; + std::string toReplace = "__SINA_SLASHREPLACE__"; + std::string replacement = "/"; + + size_t pos = 0; + // Find and replace all occurrences of "__SLASH__" + + while ((pos = restoredKey.find(toReplace, pos)) != std::string::npos) { + restoredKey.replace(pos, toReplace.length(), replacement); + pos += replacement.length(); + } + + + // Initialize a new node for the restored key + conduit::Node& newChild = restoredNode.add_child(restoredKey); + + // Leaves empty keys empty but continues recursive call if its a list + if (it.node().dtype().is_string() || it.node().dtype().is_number() || it.node().dtype().is_object()) + { + newChild.set(it.node()); + } + else if (it.node().dtype().is_list()) + { + restoreSlashes(it.node(), newChild); // Handle nested lists + } + + // If the node has children, recursively restore them + if (it.node().number_of_children() > 0) + { + conduit::Node nestedNode; + restoreSlashes(it.node(), nestedNode); + newChild.set(nestedNode); + } + } + } +} void Document::add(std::unique_ptr record) { @@ -87,26 +217,27 @@ void Document::createFromNode(conduit::Node const &asNode, } } - if(asNode.has_child(RELATIONSHIPS_KEY)) - { - conduit::Node relationship_nodes = asNode[RELATIONSHIPS_KEY]; - if(relationship_nodes.dtype().is_list()) - { - auto relationshipsIter = relationship_nodes.children(); - while(relationshipsIter.has_next()) - { - auto &relationship = relationshipsIter.next(); - add(Relationship {relationship}); - } - } - else + if (asNode.has_child(RELATIONSHIPS_KEY)) { - std::ostringstream message; - message << "The '" << RELATIONSHIPS_KEY - << "' element of a document must be an array"; - throw std::invalid_argument(message.str()); + conduit::Node relationship_nodes = asNode[RELATIONSHIPS_KEY]; + if (relationship_nodes.number_of_children() == 0) + { + relationship_nodes.set(conduit::DataType::list()); + } + else if (!relationship_nodes.dtype().is_list()) + { + std::ostringstream message; + message << "The '" << RELATIONSHIPS_KEY << "' element of a document must be an array"; + throw std::invalid_argument(message.str()); + } + + auto relationshipsIter = relationship_nodes.children(); + while (relationshipsIter.has_next()) + { + auto &relationship = relationshipsIter.next(); + add(Relationship{relationship}); + } } - } } Document::Document(conduit::Node const &asNode, RecordLoader const &recordLoader) @@ -121,6 +252,37 @@ Document::Document(std::string const &asJson, RecordLoader const &recordLoader) this->createFromNode(asNode, recordLoader); } +void Document::toHDF5(const std::string &filename) const +{ + conduit::Node node; + conduit::Node &recordsNode = node["records"]; + conduit::Node &relationshipsNode = node["relationships"]; + + for (const auto& record : getRecords()) + { + conduit::Node recordNode = record->toNode(); + conduit::Node modifiedRecordNode; + + removeSlashes(recordNode, modifiedRecordNode); + + recordsNode.append() = modifiedRecordNode; + } + + // Process relationships + for (const auto& relationship : getRelationships()) + { + conduit::Node relationshipNode = relationship.toNode(); + conduit::Node modifiedRelationshipsNode; + + removeSlashes(relationshipNode, modifiedRelationshipsNode); + relationshipsNode.append() = modifiedRelationshipsNode; + } + + conduit::relay::io::save(node, filename, "hdf5"); +} + +// + std::string Document::toJson(conduit::index_t indent, conduit::index_t depth, const std::string &pad, @@ -129,45 +291,72 @@ std::string Document::toJson(conduit::index_t indent, return this->toNode().to_json("json", indent, depth, pad, eoe); } -void saveDocument(Document const &document, std::string const &fileName) +void saveDocument(Document const &document, std::string const &fileName, Protocol protocol) { - // It is a common use case for users to want to overwrite their files as - // the simulation progresses. However, this operation should be atomic so - // that if a write fails, the old file is left intact. For this reason, - // we write to a temporary file first and then move the file. The temporary - // file is in the same directory to ensure that it is part of the same - // file system as the destination file so that the move operation is - // atomic. std::string tmpFileName = fileName + SAVE_TMP_FILE_EXTENSION; - auto asJson = document.toJson(); - std::ofstream fout {tmpFileName}; - fout.exceptions(std::ostream::failbit | std::ostream::badbit); - fout << asJson; - fout.close(); - if(rename(tmpFileName.c_str(), fileName.c_str()) != 0) + try { - std::string message {"Could not save to '"}; - message += fileName; - message += "'"; - throw std::ios::failure {message}; - } + if (protocol == Protocol::JSON) + { + protocol_warn(".json", fileName); + auto asJson = document.toJson(); + std::ofstream fout {tmpFileName}; + fout.exceptions(std::ostream::failbit | std::ostream::badbit); + fout << asJson; + fout.close(); + } + else if (protocol == Protocol::HDF5) + { + protocol_warn(".hdf5", fileName); + document.toHDF5(tmpFileName); + } + else + { + throw std::invalid_argument("Invalid format choice. Please enter 'json' or 'hdf5'."); + } + + if (rename(tmpFileName.c_str(), fileName.c_str()) != 0) + { + std::string message {"Could not save to '"}; + message += fileName; + message += "'"; + throw std::ios::failure {message}; + } + } + catch (const std::exception &e) + { + std::cerr << "An error occurred: " << e.what() << "\n"; + std::remove(tmpFileName.c_str()); + throw; + } } -Document loadDocument(std::string const &path) +Document loadDocument(std::string const &path, Protocol protocol) { - return loadDocument(path, createRecordLoaderWithAllKnownTypes()); + return loadDocument(path, createRecordLoaderWithAllKnownTypes(), protocol); } -Document loadDocument(std::string const &path, RecordLoader const &recordLoader) +Document loadDocument(std::string const &path, RecordLoader const &recordLoader, Protocol protocol) { - conduit::Node nodeFromJson; - std::ifstream file_in {path}; - std::ostringstream file_contents; - file_contents << file_in.rdbuf(); - file_in.close(); - nodeFromJson.parse(file_contents.str(), "json"); - return Document {nodeFromJson, recordLoader}; + conduit::Node node; + + // Load the file depending on the protocol + if (protocol == Protocol::JSON) { + std::ifstream file_in {path}; + std::ostringstream file_contents; + file_contents << file_in.rdbuf(); + file_in.close(); + node.parse(file_contents.str(), "json"); + return Document {node, recordLoader}; + } else if (protocol == Protocol::HDF5) { + conduit::Node modifiedNode; + conduit::relay::io::load(path, "hdf5", node); + restoreSlashes(node, modifiedNode); + return Document {modifiedNode, recordLoader}; + } + + // Finally, use the node to create the Document object (existing logic) } } // namespace sina diff --git a/src/axom/sina/core/Document.hpp b/src/axom/sina/core/Document.hpp index aead416b79..f0f163b813 100644 --- a/src/axom/sina/core/Document.hpp +++ b/src/axom/sina/core/Document.hpp @@ -32,6 +32,17 @@ namespace axom namespace sina { +enum class Protocol +{ + JSON, + HDF5 +}; + +const std::string supported_types[] = { + "JSON", + "HDF5" +}; + /** * \brief An object representing the top-level object of a Sina JSON file * @@ -177,6 +188,13 @@ class Document */ conduit::Node toNode() const; + /** + * \brief Dump this document as an HDF5 File + * + * \return None, conduit automatically dumps the hdf5 file without a return + */ + void toHDF5(const std::string &filename) const; + /** * \brief Convert this document to a JSON string. * @@ -203,9 +221,21 @@ class Document * * \param document the Document to save * \param fileName the location to which to save the file + * \param protocol the file type requested to save as, default = JSON * \throws std::ios::failure if there are any IO errors */ -void saveDocument(Document const &document, std::string const &fileName); +void saveDocument(Document const &document, std::string const &fileName, Protocol protocol = Protocol::JSON); + +/** + * \brief Get the current file format version. + * + * \return A string representing the file format version. + */ +inline std::string getSinaFileFormatVersion() +{ + return std::to_string(SINA_FILE_FORMAT_VERSION_MAJOR) + "." + + std::to_string(SINA_FILE_FORMAT_VERSION_MINOR); +} /** * \brief Get the current file format version. @@ -223,9 +253,10 @@ inline std::string getSinaFileFormatVersion() * knows about will be able to be loaded. * * \param path the file system path from which to load the document + * \param protocol the type of file being loaded, default = JSON * \return the loaded Document */ -Document loadDocument(std::string const &path); +Document loadDocument(std::string const &path, Protocol protocol = Protocol::JSON); /** * \brief Load a document from the given path. @@ -233,9 +264,10 @@ Document loadDocument(std::string const &path); * \param path the file system path from which to load the document * \param recordLoader the RecordLoader to use to load the different types * of records + * \param protocol the type of file being loaded, default = JSON * \return the loaded Document */ -Document loadDocument(std::string const &path, RecordLoader const &recordLoader); +Document loadDocument(std::string const &path, RecordLoader const &recordLoader, Protocol protocol = Protocol::JSON); } // namespace sina } // namespace axom diff --git a/src/axom/sina/docs/sphinx/documents.rst b/src/axom/sina/docs/sphinx/documents.rst index 8cdc6ebfb6..853f6f433f 100644 --- a/src/axom/sina/docs/sphinx/documents.rst +++ b/src/axom/sina/docs/sphinx/documents.rst @@ -106,6 +106,27 @@ of the ``Document`` that way: std::cout << myDocument.toJson() << std::endl; } + +------------------------------ +Generating Documents From HDF5 +------------------------------ + +In addition to assembling ``Document`` instances from existing JSON files, it +is possible to generate ``Document`` objects from existing HDF5 files using +conduit. + +Sina's ``saveDocument()`` and ``loadDocument()`` functions support HDF5 assembly if we provide it +the optional Protocol variable set to HDF5: + +.. code:: cpp + + #include "axom/sina.hpp" + + int main (void) { + axom::sina::Document myDocument = axom::sina::loadDocument("MySinaData.hdf5", axom::sina::Protocol::HDF5); + } + + --------------------------------------------------------- Obtaining Records & Relationships from Existing Documents --------------------------------------------------------- diff --git a/src/axom/sina/docs/sphinx/tutorial.rst b/src/axom/sina/docs/sphinx/tutorial.rst index 70131b748d..b0cc5cec35 100644 --- a/src/axom/sina/docs/sphinx/tutorial.rst +++ b/src/axom/sina/docs/sphinx/tutorial.rst @@ -53,7 +53,7 @@ Adding Data Once we have a Record, we can add different types of data to it. Any Datum object that is added will end up in the "data" section of the record in -the JSON file. +the file. .. literalinclude:: ../../examples/sina_tutorial.cpp :language: cpp @@ -146,9 +146,12 @@ users will be able to search for "temperature" (value = 450), Input and Output ---------------- -Once you have a document, it is easy to save it to a file. After executing -the below, your will output a file named "my_output.json" which you can ingest -into a Sina datastore. +Once you have a document, it is easy to save it to a file. To save to a JSON, we +run the saveDocument() with the optional argument Protocol set to JSON or set as +nothing. Alternatively, if you wish to save the document to an HDF5 file, you must set +saveDocument()'s optional Protocol parameter to HDF5. After executing the below, you +will output a file named "my_output.json" and a file named "my_output.hdf5", both of +which you can ingest into a Sina datastore. .. literalinclude:: ../../examples/sina_tutorial.cpp :language: cpp @@ -157,7 +160,20 @@ into a Sina datastore. If needed, you can also load a document from a file. This can be useful, for example, if you wrote a document when writing a restart and you want to -continue from where you left off. +continue from where you left off. To load from a JSON file simply run loadDocument() +with the optional argument Protocol set to JSON or set as nothing, and to load from +an HDF5 set the Protocol to HDF5. + +Note that due to HDF5's handling of '/' as indicators for nested structures, +our saveDocument() function will string-replace '/' in parent nodes to +'__SINA_SLASHREPLACE__' in the resulting HDF5 file, while our loadDocument() +function will string-replace them back to normal. + +Additionally, Sina is also equipped to handle missing, broken, or incorrect file +extensions. In such an event (for example: saving a JSON file as Example, Example.jso, +or Example.hdf5), Sina will replace incorrect file extensions and append to missing or +broken ones while outputting a WARNING message to let you know of said occurance. + .. literalinclude:: ../../examples/sina_tutorial.cpp :language: cpp diff --git a/src/axom/sina/examples/sina_basic.cpp b/src/axom/sina/examples/sina_basic.cpp index 2d59f4429d..2c81605188 100644 --- a/src/axom/sina/examples/sina_basic.cpp +++ b/src/axom/sina/examples/sina_basic.cpp @@ -17,5 +17,8 @@ int main(void) // Add the run to the document document.add(std::move(run)); // Save the document directly to a file. + // since we gave saveDocument no protocol parameter, it will default to JSON axom::sina::saveDocument(document, "MySinaData.json"); + // by specifying Protocol::HDF5, we also save a copy as an HDF5 file. + axom::sina::saveDocument(document, "MySinaData.hdf5", axom::sina::Protocol::HDF5); } \ No newline at end of file diff --git a/src/axom/sina/examples/sina_document_assembly.cpp b/src/axom/sina/examples/sina_document_assembly.cpp index 428c076767..201add1987 100644 --- a/src/axom/sina/examples/sina_document_assembly.cpp +++ b/src/axom/sina/examples/sina_document_assembly.cpp @@ -32,5 +32,10 @@ int main(void) document.add(relationship); // Save the document directly to a file. + // since we gave saveDocument no optional protocol parameter, it will default to JSON axom::sina::saveDocument(document, "MySinaData.json"); + + // We will also save a copy of the document as an HDF5 file + // which can be done by passing the protocol as HDF5 + axom::sina::saveDocument(document, "MySinaData.hdf5", axom::sina::Protocol::HDF5); } \ No newline at end of file diff --git a/src/axom/sina/examples/sina_fortran.f b/src/axom/sina/examples/sina_fortran.f index 8dd2a5e0d4..73ff678122 100644 --- a/src/axom/sina/examples/sina_fortran.f +++ b/src/axom/sina/examples/sina_fortran.f @@ -28,6 +28,7 @@ program example character(:), allocatable :: tag character(:), allocatable :: units character(20) :: json_fn + character(20) :: hdf5_fn character(15) :: name character(25) :: curve @@ -56,6 +57,7 @@ program example full_path = make_cstring(wrk_dir//''//fle_nme) ofull_path = make_cstring(wrk_dir//''//ofle_nme) json_fn = make_cstring('sina_dump.json') + hdf5_fn = make_cstring('sina_dump.hdf5') mime_type = make_cstring('') @@ -149,6 +151,7 @@ program example ! write out the Sina Document print *,'Writing out the Sina Document' call write_sina_document(json_fn) + call write_sina_document(hdf5_fn, 1) contains diff --git a/src/axom/sina/examples/sina_tutorial.cpp b/src/axom/sina/examples/sina_tutorial.cpp index 13445f0fe9..e4db0f5c6c 100644 --- a/src/axom/sina/examples/sina_tutorial.cpp +++ b/src/axom/sina/examples/sina_tutorial.cpp @@ -130,13 +130,15 @@ void gatherAllData(axom::sina::Record &record) void save(axom::sina::Document const &doc) { axom::sina::saveDocument(doc, "my_output.json"); + axom::sina::saveDocument(doc, "my_output.hdf5", axom::sina::Protocol::HDF5); } //! [end io write] //! [begin io read] void load() { - axom::sina::Document doc = axom::sina::loadDocument("my_output.json"); + axom::sina::Document doc1 = axom::sina::loadDocument("my_output.json"); + axom::sina::Document doc2 = axom::sina::loadDocument("my_output.json", axom::sina::Protocol::HDF5); } //! [end io read] diff --git a/src/axom/sina/interface/sina_fortran_interface.cpp b/src/axom/sina/interface/sina_fortran_interface.cpp index 0f3c55fcec..4b2ff057f2 100644 --- a/src/axom/sina/interface/sina_fortran_interface.cpp +++ b/src/axom/sina/interface/sina_fortran_interface.cpp @@ -258,7 +258,18 @@ extern "C" void sina_add_file_(char *filename, char *mime_type) } } -extern "C" void write_sina_document_(char *input_fn) +extern "C" void write_sina_document_protocol_(char *input_fn, int *protocol) +{ + std::string filename(input_fn); + axom::sina::Protocol proto = static_cast(*protocol); + // Save everything + if(sina_document) + { + axom::sina::saveDocument(*sina_document, filename.c_str(), proto); + } +} + +extern "C" void write_sina_document_noprotocol_(char *input_fn) { std::string filename(input_fn); // Save everything diff --git a/src/axom/sina/interface/sina_fortran_interface.f b/src/axom/sina/interface/sina_fortran_interface.f index 56fe05ebe6..8362ab8d10 100644 --- a/src/axom/sina/interface/sina_fortran_interface.f +++ b/src/axom/sina/interface/sina_fortran_interface.f @@ -1,5 +1,8 @@ module sina_functions + integer, parameter :: JSON = 0 + integer, parameter :: HDF5 = 1 + interface subroutine create_document_and_record(id) @@ -17,14 +20,6 @@ end subroutine sina_add_file end interface - interface - - subroutine write_sina_document(file_nm) - character(*) file_nm - end subroutine write_sina_document - - end interface - interface sina_add subroutine sina_add_long(key, value, units, tags) @@ -114,5 +109,39 @@ subroutine sina_add_curve_long(name, curve, values, n, independent) end subroutine sina_add_curve_long end interface + + interface + + subroutine write_sina_document_protocol(file_nm, protocol) + character(*) file_nm + integer protocol + end subroutine write_sina_document_protocol + + end interface + + interface + + subroutine write_sina_document_noprotocol(file_nm) + character(*) file_nm + end subroutine write_sina_document_noprotocol + + end interface + + + interface write_sina_document + module procedure save_with_protocol + module procedure save_without_protocol + end interface + +contains + subroutine save_with_protocol(fname, proto) + character(*) fname + integer proto + call write_sina_document_protocol(fname, proto) + end subroutine save_with_protocol + subroutine save_without_protocol(fname) + character(*) fname + call write_sina_document_noprotocol(fname) + end subroutine save_without_protocol end module \ No newline at end of file diff --git a/src/axom/sina/interface/sina_fortran_interface.h b/src/axom/sina/interface/sina_fortran_interface.h index ed004c81be..12f1be620d 100644 --- a/src/axom/sina/interface/sina_fortran_interface.h +++ b/src/axom/sina/interface/sina_fortran_interface.h @@ -13,7 +13,8 @@ extern "C" void create_document_and_run_(char *); extern "C" axom::sina::Record *Sina_Get_Run(); extern "C" void sina_add_file_to_record_(char *); extern "C" void sina_add_file_with_mimetype_to_record_(char *, char *); -extern "C" void write_sina_document_(char *); +extern "C" void write_sina_document_protocol_(char *, int *); +extern "C" void write_sina_document_noprotocol_(char *); extern "C" void sina_add_long_(char *, long long int *, char *, char *); extern "C" void sina_add_int_(char *, int *, char *, char *); extern "C" void sina_add_float_(char *, float *, char *, char *); diff --git a/src/axom/sina/tests/sina_Document.cpp b/src/axom/sina/tests/sina_Document.cpp index b8186a0408..88381a9bac 100644 --- a/src/axom/sina/tests/sina_Document.cpp +++ b/src/axom/sina/tests/sina_Document.cpp @@ -17,6 +17,9 @@ #include "axom/sina/core/Run.hpp" #include "axom/sina/tests/TestRecord.hpp" +#include "conduit.hpp" +#include "conduit_relay.hpp" +#include "conduit_relay_io.hpp" namespace axom { @@ -34,6 +37,7 @@ char const TEST_RECORD_TYPE[] = "test type"; char const EXPECTED_RECORDS_KEY[] = "records"; char const EXPECTED_RELATIONSHIPS_KEY[] = "relationships"; + TEST(Document, create_fromNode_empty) { conduit::Node documentAsNode; @@ -349,7 +353,136 @@ NamedTempFile::~NamedTempFile() axom::utilities::filesystem::removeFile(fileName.data()); } -TEST(Document, saveDocument) + +TEST(Document, create_fromJson_roundtrip_json) +{ + std::string orig_json = + "{\"records\": [{\"type\": \"test_rec\",\"id\": " + "\"test\"}],\"relationships\": []}"; + axom::sina::Document myDocument = + Document(orig_json, createRecordLoaderWithAllKnownTypes()); + EXPECT_EQ(0, myDocument.getRelationships().size()); + ASSERT_EQ(1, myDocument.getRecords().size()); + EXPECT_EQ("test_rec", myDocument.getRecords()[0]->getType()); + std::string returned_json1 = myDocument.toJson(0, 0, "", ""); + EXPECT_EQ(orig_json, returned_json1); +} + +TEST(Document, create_fromJson_roundtrip_hdf5) +{ + std::string orig_json = + "{\"records\": [{\"type\": \"test_rec\",\"id\": " + "\"test\"}],\"relationships\": []}"; + axom::sina::Document myDocument = + Document(orig_json, createRecordLoaderWithAllKnownTypes()); + saveDocument(myDocument, "round_json.hdf5", Protocol::HDF5); + Document loadedDocument = loadDocument("round_json.hdf5", Protocol::HDF5); + EXPECT_EQ(0, loadedDocument.getRelationships().size()); + ASSERT_EQ(1, loadedDocument.getRecords().size()); + EXPECT_EQ("test_rec", loadedDocument.getRecords()[0]->getType()); + std::string returned_json2 = loadedDocument.toJson(0, 0, "", ""); + EXPECT_EQ(orig_json, returned_json2); +} + +TEST(Document, create_fromJson_full_json) +{ + std::string long_json = + "{\"records\": [{\"type\": \"foo\",\"id\": " + "\"test_1\",\"user_defined\":{\"name\":\"bob\"},\"files\":{\"foo/" + "bar.png\":{\"mimetype\":\"image\"}},\"data\":{\"scalar\": {\"value\": " + "500,\"units\": \"miles\"}}},{\"type\":\"bar\",\"id\": " + "\"test_2\",\"data\": {\"scalar_list\": {\"value\": [1, 2, 3]}, " + "\"string_list\": {\"value\": [\"a\",\"wonderful\",\"world\"], " + "\"tags\":[\"observation\"]}}},{\"type\": " + "\"run\",\"application\":\"sina_test\",\"id\": " + "\"test_3\",\"data\":{\"scalar\": {\"value\": 12.3, \"units\": \"g/s\", " + "\"tags\": [\"hi\"]}, \"scalar_list\": {\"value\": [1,2,3.0,4]}}}, " + "{\"type\": \"bar\",\"id\": \"test_4\",\"data\":{\"string\": {\"value\": " + "\"yarr\"}, \"string_list\": {\"value\": [\"y\",\"a\",\"r\"]}}, " + "\"files\":{\"test/test.png\":{}}, " + "\"user_defined\":{\"hello\":\"there\"}}],\"relationships\": " + "[{\"predicate\": \"completes\",\"subject\": \"test_2\",\"object\": " + "\"test_1\"},{\"subject\": \"test_3\", \"predicate\": \"overrides\", " + "\"object\": \"test_4\"}]}"; + axom::sina::Document myDocument = + Document(long_json, createRecordLoaderWithAllKnownTypes()); + EXPECT_EQ(2, myDocument.getRelationships().size()); + auto &records1 = myDocument.getRecords(); + EXPECT_EQ(4, records1.size()); +} + +TEST(Document, create_fromJson_full_hdf5) +{ + std::string long_json = + "{\"records\": [{\"type\": \"foo\",\"id\": " + "\"test_1\",\"user_defined\":{\"name\":\"bob\"},\"files\":{\"foo/" + "bar.png\":{\"mimetype\":\"image\"}},\"data\":{\"scalar\": {\"value\": " + "500,\"units\": \"miles\"}}},{\"type\":\"bar\",\"id\": " + "\"test_2\",\"data\": {\"scalar_list\": {\"value\": [1, 2, 3]}, " + "\"string_list\": {\"value\": [\"a\",\"wonderful\",\"world\"], " + "\"tags\":[\"observation\"]}}},{\"type\": " + "\"run\",\"application\":\"sina_test\",\"id\": " + "\"test_3\",\"data\":{\"scalar\": {\"value\": 12.3, \"units\": \"g/s\", " + "\"tags\": [\"hi\"]}, \"scalar_list\": {\"value\": [1,2,3.0,4]}}}, " + "{\"type\": \"bar\",\"id\": \"test_4\",\"data\":{\"string\": {\"value\": " + "\"yarr\"}, \"string_list\": {\"value\": [\"y\",\"a\",\"r\"]}}, " + "\"files\":{\"test/test.png\":{}}, " + "\"user_defined\":{\"hello\":\"there\"}}],\"relationships\": " + "[{\"predicate\": \"completes\",\"subject\": \"test_2\",\"object\": " + "\"test_1\"},{\"subject\": \"test_3\", \"predicate\": \"overrides\", " + "\"object\": \"test_4\"}]}"; + axom::sina::Document myDocument = + Document(long_json, createRecordLoaderWithAllKnownTypes()); + saveDocument(myDocument, "long_json.hdf5", Protocol::HDF5); + Document loadedDocument = loadDocument("long_json.hdf5", Protocol::HDF5); + EXPECT_EQ(2, loadedDocument.getRelationships().size()); + auto &records2 = loadedDocument.getRecords(); + EXPECT_EQ(4, records2.size()); +} + +TEST(Document, create_fromJson_value_check_json) +{ + std::string data_json = + "{\"records\": [{\"type\": \"run\", \"application\":\"test\", \"id\": " + "\"test_1\",\"data\":{\"int\": {\"value\": 500,\"units\": \"miles\"}, " + "\"str/ings\": {\"value\":[\"z\", \"o\", \"o\"]}}, " + "\"files\":{\"test/test.png\":{}}}]}"; + axom::sina::Document myDocument = + Document(data_json, createRecordLoaderWithAllKnownTypes()); + EXPECT_EQ(0, myDocument.getRelationships().size()); + auto &records1 = myDocument.getRecords(); + EXPECT_EQ(1, records1.size()); + EXPECT_EQ(records1[0]->getType(), "run"); + auto &data1 = records1[0]->getData(); + EXPECT_EQ(data1.at("int").getScalar(), 500.0); + std::vector expected_string_vals = {"z", "o", "o"}; + EXPECT_EQ(data1.at("str/ings").getStringArray(), expected_string_vals); + EXPECT_EQ(records1[0]->getFiles().count(File {"test/test.png"}), 1); +} + +TEST(Document, create_fromJson_value_check_hdf5) +{ + std::string data_json = + "{\"records\": [{\"type\": \"run\", \"application\":\"test\", \"id\": " + "\"test_1\",\"data\":{\"int\": {\"value\": 500,\"units\": \"miles\"}, " + "\"str/ings\": {\"value\":[\"z\", \"o\", \"o\"]}}, " + "\"files\":{\"test/test.png\":{}}}]}"; + axom::sina::Document myDocument = + Document(data_json, createRecordLoaderWithAllKnownTypes()); + std::vector expected_string_vals = {"z", "o", "o"}; + saveDocument(myDocument, "data_json.hdf5", Protocol::HDF5); + Document loadedDocument = loadDocument("data_json.hdf5", Protocol::HDF5); + EXPECT_EQ(0, loadedDocument.getRelationships().size()); + auto &records2 = loadedDocument.getRecords(); + EXPECT_EQ(1, records2.size()); + EXPECT_EQ(records2[0]->getType(), "run"); + auto &data2 = records2[0]->getData(); + EXPECT_EQ(data2.at("int").getScalar(), 500.0); + EXPECT_EQ(data2.at("str/ings").getStringArray(), expected_string_vals); + EXPECT_EQ(records2[0]->getFiles().count(File {"test/test.png"}), 1); +} + +TEST(Document, saveDocument_json) { NamedTempFile tmpFile; @@ -381,6 +514,33 @@ TEST(Document, saveDocument) EXPECT_EQ("the type", readRecord["type"].as_string()); } +TEST(Document, saveDocument_hdf5) +{ + NamedTempFile tmpFile; + + // First, write some random stuff to the temp file to make sure it is + // overwritten. + { + std::ofstream fout {tmpFile.getName()}; + fout << "Initial contents"; + } + + Document document; + document.add( + std::make_unique(ID {"the id", IDType::Global}, "the type")); + + saveDocument(document, tmpFile.getName(), Protocol::HDF5); + + conduit::Node readContents; + conduit::relay::io::load(tmpFile.getName(), "hdf5", readContents); + + ASSERT_TRUE(readContents[EXPECTED_RECORDS_KEY].dtype().is_list()); + EXPECT_EQ(1, readContents[EXPECTED_RECORDS_KEY].number_of_children()); + auto &readRecord = readContents[EXPECTED_RECORDS_KEY][0]; + EXPECT_EQ("the id", readRecord["id"].as_string()); + EXPECT_EQ("the type", readRecord["type"].as_string()); +} + TEST(Document, load_specifiedRecordLoader) { using RecordType = TestRecord; diff --git a/src/spack b/src/spack new file mode 160000 index 0000000000..d7a86421d2 --- /dev/null +++ b/src/spack @@ -0,0 +1 @@ +Subproject commit d7a86421d28369062b77080f18be7460a6ac4a00 From e0cd5bf948fa08d42d3830e20263cd68ee817a1b Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Thu, 19 Dec 2024 10:43:25 -0800 Subject: [PATCH 02/38] Comments Implemented --- src/axom/sina/core/Document.cpp | 34 +++++------ src/axom/sina/core/Document.hpp | 4 +- src/axom/sina/docs/sphinx/tutorial.rst | 11 +--- src/axom/sina/tests/sina_Document.cpp | 79 +++++++++----------------- 4 files changed, 51 insertions(+), 77 deletions(-) diff --git a/src/axom/sina/core/Document.cpp b/src/axom/sina/core/Document.cpp index 2a3aed89cc..d4456e1187 100644 --- a/src/axom/sina/core/Document.cpp +++ b/src/axom/sina/core/Document.cpp @@ -45,21 +45,15 @@ void protocol_warn(std::string protocol, std::string const &name) { if (pos != std::string::npos) { std::string found = name.substr(pos+1); - if (("." + found) != protocol) { - for (const std::string& file_type : supported_types) { - std::string lower_case_type = file_type; - std::transform(lower_case_type.begin(), lower_case_type.end(), lower_case_type.begin(), [](unsigned char c) { return std::tolower(c); }); - - if ((lower_case_type != protocol) && (lower_case_type == found)) { - std::cout << "|| WARNING: INCORRECT FILE EXTENSION FOUND (FOUND: " << found << "; EXPECTED: " << protocol << "), DID YOU MEAN TO INCLUDE ONE OF " << protocol << "'s SUPPORTED TYPES? ||"; - } - } - std::cout << "|| WARNING: BROKEN FILE EXTENSION FOUND, SINA WILL BE APPENDING THE " << protocol << " FILE EXTENSION ||"; + if (("." + found) != protocol && protocol == ".json") { + std::cout << ".json extension not found, did you mean to save to this format?"; + } else if (("." + found) != protocol && protocol == ".hdf5") { + std::cout << ".hdf5 extension not found, did you use one of its other supported types? (h5, hdf, ...)"; } else { return; } } else { - std::cout << "|| WARNING: NO FILE EXTENSION FOUND, DID YOU MEAN TO INCLUDE ONE OF " << protocol << "'s SUPPORTED TYPES?"; + std::cout << "No file extension found, did you mean to use one of " << protocol << "'s supported types?"; } } @@ -73,13 +67,12 @@ void removeSlashes(const conduit::Node& originalNode, conduit::Node& modifiedNod //modifiedKey.erase(std::remove(modifiedKey.begin(), modifiedKey.end(), '/'), modifiedKey.end()); std::string toReplace = "/"; - std::string replacement = "__SINA_SLASHREPLACE__"; size_t pos = 0; // Find and replace all occurrences of "/" while ((pos = modifiedKey.find(toReplace, pos)) != std::string::npos) { - modifiedKey.replace(pos, toReplace.length(), replacement); - pos += replacement.length(); // Move past the replaced substring + modifiedKey.replace(pos, toReplace.length(), slashSubstitute); + pos += slashSubstitute.length(); // Move past the replaced substring } modifiedNode[modifiedKey] = it.node(); @@ -129,13 +122,12 @@ void restoreSlashes(const conduit::Node& modifiedNode, conduit::Node& restoredNo it.next(); std::string key = it.name(); std::string restoredKey = key; - std::string toReplace = "__SINA_SLASHREPLACE__"; std::string replacement = "/"; size_t pos = 0; // Find and replace all occurrences of "__SLASH__" - while ((pos = restoredKey.find(toReplace, pos)) != std::string::npos) { + while ((pos = restoredKey.find(slashSubstitute, pos)) != std::string::npos) { restoredKey.replace(pos, toReplace.length(), replacement); pos += replacement.length(); } @@ -293,6 +285,14 @@ std::string Document::toJson(conduit::index_t indent, void saveDocument(Document const &document, std::string const &fileName, Protocol protocol) { + // It is a common use case for users to want to overwrite their files as + // the simulation progresses. However, this operation should be atomic so + // that if a write fails, the old file is left intact. For this reason, + // we write to a temporary file first and then move the file. The temporary + // file is in the same directory to ensure that it is part of the same + // file system as the destination file so that the move operation is + // atomic. + std::string tmpFileName = fileName + SAVE_TMP_FILE_EXTENSION; try @@ -360,4 +360,4 @@ Document loadDocument(std::string const &path, RecordLoader const &recordLoader, } } // namespace sina -} // namespace axom +} // namespace axom \ No newline at end of file diff --git a/src/axom/sina/core/Document.hpp b/src/axom/sina/core/Document.hpp index f0f163b813..24bf21373e 100644 --- a/src/axom/sina/core/Document.hpp +++ b/src/axom/sina/core/Document.hpp @@ -43,6 +43,8 @@ const std::string supported_types[] = { "HDF5" }; +const std::string slashSubstitute = "__SINA_SLASHREPLACE__"; + /** * \brief An object representing the top-level object of a Sina JSON file * @@ -272,4 +274,4 @@ Document loadDocument(std::string const &path, RecordLoader const &recordLoader, } // namespace sina } // namespace axom -#endif //SINA_DOCUMENT_HPP +#endif //SINA_DOCUMENT_HPP \ No newline at end of file diff --git a/src/axom/sina/docs/sphinx/tutorial.rst b/src/axom/sina/docs/sphinx/tutorial.rst index b0cc5cec35..35f9742a57 100644 --- a/src/axom/sina/docs/sphinx/tutorial.rst +++ b/src/axom/sina/docs/sphinx/tutorial.rst @@ -165,14 +165,8 @@ with the optional argument Protocol set to JSON or set as nothing, and to load f an HDF5 set the Protocol to HDF5. Note that due to HDF5's handling of '/' as indicators for nested structures, -our saveDocument() function will string-replace '/' in parent nodes to -'__SINA_SLASHREPLACE__' in the resulting HDF5 file, while our loadDocument() -function will string-replace them back to normal. - -Additionally, Sina is also equipped to handle missing, broken, or incorrect file -extensions. In such an event (for example: saving a JSON file as Example, Example.jso, -or Example.hdf5), Sina will replace incorrect file extensions and append to missing or -broken ones while outputting a WARNING message to let you know of said occurance. +parent nodes will have '/' changed to '__SINA_SLASHREPLACE__' when saved with +as an HDF5 with saveDocument(). loadDocument() will restore them to normal. .. literalinclude:: ../../examples/sina_tutorial.cpp @@ -200,3 +194,4 @@ convert to and from JSON. The user-defined section is exposed as a :language: cpp :start-after: //! [begin user defined] :end-before: //! [end user defined] + diff --git a/src/axom/sina/tests/sina_Document.cpp b/src/axom/sina/tests/sina_Document.cpp index 88381a9bac..272e9915b5 100644 --- a/src/axom/sina/tests/sina_Document.cpp +++ b/src/axom/sina/tests/sina_Document.cpp @@ -38,6 +38,34 @@ char const EXPECTED_RECORDS_KEY[] = "records"; char const EXPECTED_RELATIONSHIPS_KEY[] = "relationships"; +// Large JSONs Used For JSON and HDF5 Save Tests +std::string data_json = + "{\"records\": [{\"type\": \"run\", \"application\":\"test\", \"id\": " + "\"test_1\",\"data\":{\"int\": {\"value\": 500,\"units\": \"miles\"}, " + "\"str/ings\": {\"value\":[\"z\", \"o\", \"o\"]}}, " + "\"files\":{\"test/test.png\":{}}}]}"; + +std::string long_json = + "{\"records\": [{\"type\": \"foo\",\"id\": " + "\"test_1\",\"user_defined\":{\"name\":\"bob\"},\"files\":{\"foo/" + "bar.png\":{\"mimetype\":\"image\"}},\"data\":{\"scalar\": {\"value\": " + "500,\"units\": \"miles\"}}},{\"type\":\"bar\",\"id\": " + "\"test_2\",\"data\": {\"scalar_list\": {\"value\": [1, 2, 3]}, " + "\"string_list\": {\"value\": [\"a\",\"wonderful\",\"world\"], " + "\"tags\":[\"observation\"]}}},{\"type\": " + "\"run\",\"application\":\"sina_test\",\"id\": " + "\"test_3\",\"data\":{\"scalar\": {\"value\": 12.3, \"units\": \"g/s\", " + "\"tags\": [\"hi\"]}, \"scalar_list\": {\"value\": [1,2,3.0,4]}}}, " + "{\"type\": \"bar\",\"id\": \"test_4\",\"data\":{\"string\": {\"value\": " + "\"yarr\"}, \"string_list\": {\"value\": [\"y\",\"a\",\"r\"]}}, " + "\"files\":{\"test/test.png\":{}}, " + "\"user_defined\":{\"hello\":\"there\"}}],\"relationships\": " + "[{\"predicate\": \"completes\",\"subject\": \"test_2\",\"object\": " + "\"test_1\"},{\"subject\": \"test_3\", \"predicate\": \"overrides\", " + "\"object\": \"test_4\"}]}"; + + +// Tests TEST(Document, create_fromNode_empty) { conduit::Node documentAsNode; @@ -218,11 +246,6 @@ TEST(Document, create_fromJson_full) TEST(Document, create_fromJson_value_check) { - std::string data_json = - "{\"records\": [{\"type\": \"run\", \"application\":\"test\", \"id\": " - "\"test_1\",\"data\":{\"int\": {\"value\": 500,\"units\": \"miles\"}, " - "\"str/ings\": {\"value\":[\"z\", \"o\", \"o\"]}}, " - "\"files\":{\"test/test.png\":{}}}]}"; axom::sina::Document myDocument = Document(data_json, createRecordLoaderWithAllKnownTypes()); EXPECT_EQ(0, myDocument.getRelationships().size()); @@ -386,24 +409,6 @@ TEST(Document, create_fromJson_roundtrip_hdf5) TEST(Document, create_fromJson_full_json) { - std::string long_json = - "{\"records\": [{\"type\": \"foo\",\"id\": " - "\"test_1\",\"user_defined\":{\"name\":\"bob\"},\"files\":{\"foo/" - "bar.png\":{\"mimetype\":\"image\"}},\"data\":{\"scalar\": {\"value\": " - "500,\"units\": \"miles\"}}},{\"type\":\"bar\",\"id\": " - "\"test_2\",\"data\": {\"scalar_list\": {\"value\": [1, 2, 3]}, " - "\"string_list\": {\"value\": [\"a\",\"wonderful\",\"world\"], " - "\"tags\":[\"observation\"]}}},{\"type\": " - "\"run\",\"application\":\"sina_test\",\"id\": " - "\"test_3\",\"data\":{\"scalar\": {\"value\": 12.3, \"units\": \"g/s\", " - "\"tags\": [\"hi\"]}, \"scalar_list\": {\"value\": [1,2,3.0,4]}}}, " - "{\"type\": \"bar\",\"id\": \"test_4\",\"data\":{\"string\": {\"value\": " - "\"yarr\"}, \"string_list\": {\"value\": [\"y\",\"a\",\"r\"]}}, " - "\"files\":{\"test/test.png\":{}}, " - "\"user_defined\":{\"hello\":\"there\"}}],\"relationships\": " - "[{\"predicate\": \"completes\",\"subject\": \"test_2\",\"object\": " - "\"test_1\"},{\"subject\": \"test_3\", \"predicate\": \"overrides\", " - "\"object\": \"test_4\"}]}"; axom::sina::Document myDocument = Document(long_json, createRecordLoaderWithAllKnownTypes()); EXPECT_EQ(2, myDocument.getRelationships().size()); @@ -413,24 +418,6 @@ TEST(Document, create_fromJson_full_json) TEST(Document, create_fromJson_full_hdf5) { - std::string long_json = - "{\"records\": [{\"type\": \"foo\",\"id\": " - "\"test_1\",\"user_defined\":{\"name\":\"bob\"},\"files\":{\"foo/" - "bar.png\":{\"mimetype\":\"image\"}},\"data\":{\"scalar\": {\"value\": " - "500,\"units\": \"miles\"}}},{\"type\":\"bar\",\"id\": " - "\"test_2\",\"data\": {\"scalar_list\": {\"value\": [1, 2, 3]}, " - "\"string_list\": {\"value\": [\"a\",\"wonderful\",\"world\"], " - "\"tags\":[\"observation\"]}}},{\"type\": " - "\"run\",\"application\":\"sina_test\",\"id\": " - "\"test_3\",\"data\":{\"scalar\": {\"value\": 12.3, \"units\": \"g/s\", " - "\"tags\": [\"hi\"]}, \"scalar_list\": {\"value\": [1,2,3.0,4]}}}, " - "{\"type\": \"bar\",\"id\": \"test_4\",\"data\":{\"string\": {\"value\": " - "\"yarr\"}, \"string_list\": {\"value\": [\"y\",\"a\",\"r\"]}}, " - "\"files\":{\"test/test.png\":{}}, " - "\"user_defined\":{\"hello\":\"there\"}}],\"relationships\": " - "[{\"predicate\": \"completes\",\"subject\": \"test_2\",\"object\": " - "\"test_1\"},{\"subject\": \"test_3\", \"predicate\": \"overrides\", " - "\"object\": \"test_4\"}]}"; axom::sina::Document myDocument = Document(long_json, createRecordLoaderWithAllKnownTypes()); saveDocument(myDocument, "long_json.hdf5", Protocol::HDF5); @@ -442,11 +429,6 @@ TEST(Document, create_fromJson_full_hdf5) TEST(Document, create_fromJson_value_check_json) { - std::string data_json = - "{\"records\": [{\"type\": \"run\", \"application\":\"test\", \"id\": " - "\"test_1\",\"data\":{\"int\": {\"value\": 500,\"units\": \"miles\"}, " - "\"str/ings\": {\"value\":[\"z\", \"o\", \"o\"]}}, " - "\"files\":{\"test/test.png\":{}}}]}"; axom::sina::Document myDocument = Document(data_json, createRecordLoaderWithAllKnownTypes()); EXPECT_EQ(0, myDocument.getRelationships().size()); @@ -462,11 +444,6 @@ TEST(Document, create_fromJson_value_check_json) TEST(Document, create_fromJson_value_check_hdf5) { - std::string data_json = - "{\"records\": [{\"type\": \"run\", \"application\":\"test\", \"id\": " - "\"test_1\",\"data\":{\"int\": {\"value\": 500,\"units\": \"miles\"}, " - "\"str/ings\": {\"value\":[\"z\", \"o\", \"o\"]}}, " - "\"files\":{\"test/test.png\":{}}}]}"; axom::sina::Document myDocument = Document(data_json, createRecordLoaderWithAllKnownTypes()); std::vector expected_string_vals = {"z", "o", "o"}; From d2f38ac2f138f8253825d216a63e9868e2d9cfc9 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Thu, 19 Dec 2024 13:46:02 -0800 Subject: [PATCH 03/38] More Comments --- src/axom/sina/core/Document.cpp | 58 +++++++++++--------------- src/axom/sina/docs/sphinx/tutorial.rst | 5 ++- 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/src/axom/sina/core/Document.cpp b/src/axom/sina/core/Document.cpp index d4456e1187..4d3bcb2e91 100644 --- a/src/axom/sina/core/Document.cpp +++ b/src/axom/sina/core/Document.cpp @@ -64,7 +64,6 @@ void removeSlashes(const conduit::Node& originalNode, conduit::Node& modifiedNod it.next(); std::string key = it.name(); std::string modifiedKey = key; - //modifiedKey.erase(std::remove(modifiedKey.begin(), modifiedKey.end(), '/'), modifiedKey.end()); std::string toReplace = "/"; @@ -295,41 +294,32 @@ void saveDocument(Document const &document, std::string const &fileName, Protoco std::string tmpFileName = fileName + SAVE_TMP_FILE_EXTENSION; - try + if (protocol == Protocol::JSON) { - if (protocol == Protocol::JSON) - { - protocol_warn(".json", fileName); - auto asJson = document.toJson(); - std::ofstream fout {tmpFileName}; - fout.exceptions(std::ostream::failbit | std::ostream::badbit); - fout << asJson; - fout.close(); - } - else if (protocol == Protocol::HDF5) - { - protocol_warn(".hdf5", fileName); - document.toHDF5(tmpFileName); - } - else - { - throw std::invalid_argument("Invalid format choice. Please enter 'json' or 'hdf5'."); - } + protocol_warn(".json", fileName); + auto asJson = document.toJson(); + std::ofstream fout {tmpFileName}; + fout.exceptions(std::ostream::failbit | std::ostream::badbit); + fout << asJson; + fout.close(); + } + else if (protocol == Protocol::HDF5) + { + protocol_warn(".hdf5", fileName); + document.toHDF5(tmpFileName); + } + else + { + throw std::invalid_argument("Invalid format choice. Please enter 'json' or 'hdf5'."); + } - if (rename(tmpFileName.c_str(), fileName.c_str()) != 0) - { - std::string message {"Could not save to '"}; - message += fileName; - message += "'"; - throw std::ios::failure {message}; - } - } - catch (const std::exception &e) - { - std::cerr << "An error occurred: " << e.what() << "\n"; - std::remove(tmpFileName.c_str()); - throw; - } + if (rename(tmpFileName.c_str(), fileName.c_str()) != 0) + { + std::string message {"Could not save to '"}; + message += fileName; + message += "'"; + throw std::ios::failure {message}; + } } Document loadDocument(std::string const &path, Protocol protocol) diff --git a/src/axom/sina/docs/sphinx/tutorial.rst b/src/axom/sina/docs/sphinx/tutorial.rst index 35f9742a57..b9cb0636f7 100644 --- a/src/axom/sina/docs/sphinx/tutorial.rst +++ b/src/axom/sina/docs/sphinx/tutorial.rst @@ -165,8 +165,9 @@ with the optional argument Protocol set to JSON or set as nothing, and to load f an HDF5 set the Protocol to HDF5. Note that due to HDF5's handling of '/' as indicators for nested structures, -parent nodes will have '/' changed to '__SINA_SLASHREPLACE__' when saved with -as an HDF5 with saveDocument(). loadDocument() will restore them to normal. +parent nodes will have '/' changed to the following string when saved with +as an HDF5 with saveDocument(). loadDocument() will restore them to normal: +.. doxygenvariable:: slashSubstitute .. literalinclude:: ../../examples/sina_tutorial.cpp From 6c32398a6be0c70560d81e1815740bb3b86934ee Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Thu, 19 Dec 2024 14:14:45 -0800 Subject: [PATCH 04/38] Fix? --- src/axom/sina/core/Document.hpp | 3 +++ src/axom/sina/docs/sphinx/tutorial.rst | 1 + 2 files changed, 4 insertions(+) diff --git a/src/axom/sina/core/Document.hpp b/src/axom/sina/core/Document.hpp index 24bf21373e..1d65884f66 100644 --- a/src/axom/sina/core/Document.hpp +++ b/src/axom/sina/core/Document.hpp @@ -43,6 +43,9 @@ const std::string supported_types[] = { "HDF5" }; +/** + * \brief The string used to replace '/' in parent node names when saving to HDF5. + */ const std::string slashSubstitute = "__SINA_SLASHREPLACE__"; /** diff --git a/src/axom/sina/docs/sphinx/tutorial.rst b/src/axom/sina/docs/sphinx/tutorial.rst index b9cb0636f7..02e1de285c 100644 --- a/src/axom/sina/docs/sphinx/tutorial.rst +++ b/src/axom/sina/docs/sphinx/tutorial.rst @@ -167,6 +167,7 @@ an HDF5 set the Protocol to HDF5. Note that due to HDF5's handling of '/' as indicators for nested structures, parent nodes will have '/' changed to the following string when saved with as an HDF5 with saveDocument(). loadDocument() will restore them to normal: + .. doxygenvariable:: slashSubstitute From 31242a618d7d007b3df09e6700c624417326c1be Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Thu, 19 Dec 2024 14:18:42 -0800 Subject: [PATCH 05/38] Fix?? --- src/axom/sina/docs/sphinx/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axom/sina/docs/sphinx/tutorial.rst b/src/axom/sina/docs/sphinx/tutorial.rst index 02e1de285c..8ab282fd5b 100644 --- a/src/axom/sina/docs/sphinx/tutorial.rst +++ b/src/axom/sina/docs/sphinx/tutorial.rst @@ -168,7 +168,7 @@ Note that due to HDF5's handling of '/' as indicators for nested structures, parent nodes will have '/' changed to the following string when saved with as an HDF5 with saveDocument(). loadDocument() will restore them to normal: -.. doxygenvariable:: slashSubstitute +.. doxygenvariable:: axom::sina::slashSubstitute .. literalinclude:: ../../examples/sina_tutorial.cpp From aa9073da151004f22a2b3fd3c8e91e085f511bc1 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Thu, 19 Dec 2024 14:40:07 -0800 Subject: [PATCH 06/38] Final Fix --- src/axom/sina/docs/sphinx/tutorial.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/axom/sina/docs/sphinx/tutorial.rst b/src/axom/sina/docs/sphinx/tutorial.rst index 8ab282fd5b..07ffed8648 100644 --- a/src/axom/sina/docs/sphinx/tutorial.rst +++ b/src/axom/sina/docs/sphinx/tutorial.rst @@ -165,10 +165,9 @@ with the optional argument Protocol set to JSON or set as nothing, and to load f an HDF5 set the Protocol to HDF5. Note that due to HDF5's handling of '/' as indicators for nested structures, -parent nodes will have '/' changed to the following string when saved with -as an HDF5 with saveDocument(). loadDocument() will restore them to normal: - -.. doxygenvariable:: axom::sina::slashSubstitute +parent nodes will have '/' changed to the ``slashSubstitute`` variable located in +``axom/sina/core/Document.hpp`` as an HDF5 with saveDocument(). loadDocument() +will restore them to normal: .. literalinclude:: ../../examples/sina_tutorial.cpp From b553c4435ffeb7141722c12ce1706640d0f3e72e Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Fri, 20 Dec 2024 15:01:10 -0800 Subject: [PATCH 07/38] Axom Changes --- src/axom/sina/core/Document.cpp | 60 +++++++++++++++++++-------------- src/spack | 1 - 2 files changed, 35 insertions(+), 26 deletions(-) delete mode 160000 src/spack diff --git a/src/axom/sina/core/Document.cpp b/src/axom/sina/core/Document.cpp index 4d3bcb2e91..4e8483bc43 100644 --- a/src/axom/sina/core/Document.cpp +++ b/src/axom/sina/core/Document.cpp @@ -127,7 +127,7 @@ void restoreSlashes(const conduit::Node& modifiedNode, conduit::Node& restoredNo // Find and replace all occurrences of "__SLASH__" while ((pos = restoredKey.find(slashSubstitute, pos)) != std::string::npos) { - restoredKey.replace(pos, toReplace.length(), replacement); + restoredKey.replace(pos, slashSubstitute.length(), replacement); pos += replacement.length(); } @@ -187,13 +187,13 @@ conduit::Node Document::toNode() const void Document::createFromNode(conduit::Node const &asNode, RecordLoader const &recordLoader) { - if(asNode.has_child(RECORDS_KEY)) + if (asNode.has_child(RECORDS_KEY)) { - conduit::Node record_nodes = asNode[RECORDS_KEY]; - if(record_nodes.dtype().is_list()) + const conduit::Node &record_nodes = asNode[RECORDS_KEY]; + if (record_nodes.dtype().is_list()) { auto recordIter = record_nodes.children(); - while(recordIter.has_next()) + while (recordIter.has_next()) { auto record = recordIter.next(); add(recordLoader.load(record)); @@ -209,26 +209,39 @@ void Document::createFromNode(conduit::Node const &asNode, } if (asNode.has_child(RELATIONSHIPS_KEY)) - { - conduit::Node relationship_nodes = asNode[RELATIONSHIPS_KEY]; - if (relationship_nodes.number_of_children() == 0) - { - relationship_nodes.set(conduit::DataType::list()); - } - else if (!relationship_nodes.dtype().is_list()) - { - std::ostringstream message; - message << "The '" << RELATIONSHIPS_KEY << "' element of a document must be an array"; - throw std::invalid_argument(message.str()); - } + { + const conduit::Node &relationships_node = asNode[RELATIONSHIPS_KEY]; - auto relationshipsIter = relationship_nodes.children(); + if (!relationships_node.dtype().is_list()) + { + if (relationships_node.number_of_children() == 0) + { + // Create a temporary mutable node for transformation + conduit::Node temp_node(conduit::DataType::list()); + auto relationshipsIter = temp_node.children(); while (relationshipsIter.has_next()) { - auto &relationship = relationshipsIter.next(); - add(Relationship{relationship}); + auto &relationship = relationshipsIter.next(); + add(Relationship{relationship}); } + } + else + { + std::ostringstream message; + message << "The '" << RELATIONSHIPS_KEY << "' element of a document must be an array"; + throw std::invalid_argument(message.str()); + } } + else + { + auto relationshipsIter = relationships_node.children(); + while (relationshipsIter.has_next()) + { + auto &relationship = relationshipsIter.next(); + add(Relationship{relationship}); + } + } + } } Document::Document(conduit::Node const &asNode, RecordLoader const &recordLoader) @@ -254,9 +267,7 @@ void Document::toHDF5(const std::string &filename) const conduit::Node recordNode = record->toNode(); conduit::Node modifiedRecordNode; - removeSlashes(recordNode, modifiedRecordNode); - - recordsNode.append() = modifiedRecordNode; + removeSlashes(recordNode, recordsNode.append()); } // Process relationships @@ -265,8 +276,7 @@ void Document::toHDF5(const std::string &filename) const conduit::Node relationshipNode = relationship.toNode(); conduit::Node modifiedRelationshipsNode; - removeSlashes(relationshipNode, modifiedRelationshipsNode); - relationshipsNode.append() = modifiedRelationshipsNode; + removeSlashes(relationshipNode, relationshipsNode.append()); } conduit::relay::io::save(node, filename, "hdf5"); diff --git a/src/spack b/src/spack deleted file mode 160000 index d7a86421d2..0000000000 --- a/src/spack +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d7a86421d28369062b77080f18be7460a6ac4a00 From c5c0cb441939b56a55174f762232361162d9f154 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Wed, 8 Jan 2025 14:55:49 -0800 Subject: [PATCH 08/38] Brian's Changes, Wait to hear back on createFromNode --- src/axom/sina/core/Document.cpp | 205 ++++++++++++++--------- src/axom/sina/core/Document.hpp | 11 -- src/axom/sina/docs/sphinx/tutorial.rst | 2 +- src/axom/sina/examples/sina_tutorial.cpp | 2 +- 4 files changed, 125 insertions(+), 95 deletions(-) diff --git a/src/axom/sina/core/Document.cpp b/src/axom/sina/core/Document.cpp index 4e8483bc43..cc9dcf824d 100644 --- a/src/axom/sina/core/Document.cpp +++ b/src/axom/sina/core/Document.cpp @@ -39,21 +39,25 @@ char const RELATIONSHIPS_KEY[] = "relationships"; char const SAVE_TMP_FILE_EXTENSION[] = ".sina.tmp"; } -void protocol_warn(std::string protocol, std::string const &name) { - size_t pos = name.rfind('.'); +void protocolWarn(std::string const protocol, std::string const &name) { + std::unordered_map protocolMessages = { + {".json", ".json extension not found, did you mean to save to this format?"}, + {".hdf5", ".hdf5 extension not found, did you use one of its other supported types? (h5, hdf, ...)"} + }; + size_t pos = name.rfind('.'); if (pos != std::string::npos) { - std::string found = name.substr(pos+1); - - if (("." + found) != protocol && protocol == ".json") { - std::cout << ".json extension not found, did you mean to save to this format?"; - } else if (("." + found) != protocol && protocol == ".hdf5") { - std::cout << ".hdf5 extension not found, did you use one of its other supported types? (h5, hdf, ...)"; - } else { - return; - } + std::string found = name.substr(pos); + + if (found != protocol) { + auto messageIt = protocolMessages.find(protocol); + if (messageIt != protocolMessages.end()) { + std::cout << messageIt->second << std::endl; + } + } } else { - std::cout << "No file extension found, did you mean to use one of " << protocol << "'s supported types?"; + std::cout << "No file extension found, did you mean to use one of " + << protocol << "'s supported types?" << std::endl; } } @@ -184,64 +188,106 @@ conduit::Node Document::toNode() const return document; } -void Document::createFromNode(conduit::Node const &asNode, - RecordLoader const &recordLoader) +// void Document::createFromNode(conduit::Node const &asNode, +// RecordLoader const &recordLoader) +// { +// if (asNode.has_child(RECORDS_KEY)) +// { +// const conduit::Node &record_nodes = asNode[RECORDS_KEY]; +// if (record_nodes.dtype().is_list()) +// { +// auto recordIter = record_nodes.children(); +// while (recordIter.has_next()) +// { +// auto record = recordIter.next(); +// add(recordLoader.load(record)); +// } +// } +// else +// { +// std::ostringstream message; +// message << "The '" << RECORDS_KEY +// << "' element of a document must be an array"; +// throw std::invalid_argument(message.str()); +// } +// } + +// if (asNode.has_child(RELATIONSHIPS_KEY)) +// { +// const conduit::Node &relationships_node = asNode[RELATIONSHIPS_KEY]; + +// if (!relationships_node.dtype().is_list()) +// { +// if (relationships_node.number_of_children() == 0) +// { +// // Create a temporary mutable node for transformation +// conduit::Node temp_node(conduit::DataType::list()); +// auto relationshipsIter = temp_node.children(); +// while (relationshipsIter.has_next()) +// { +// auto &relationship = relationshipsIter.next(); +// add(Relationship{relationship}); +// } +// } +// else +// { +// std::ostringstream message; +// message << "The '" << RELATIONSHIPS_KEY << "' element of a document must be an array"; +// throw std::invalid_argument(message.str()); +// } +// } +// else +// { +// auto relationshipsIter = relationships_node.children(); +// while (relationshipsIter.has_next()) +// { +// auto &relationship = relationshipsIter.next(); +// add(Relationship{relationship}); +// } +// } +// } +// } + +void Document::createFromNode(const conduit::Node &asNode, + const RecordLoader &recordLoader) { - if (asNode.has_child(RECORDS_KEY)) - { - const conduit::Node &record_nodes = asNode[RECORDS_KEY]; - if (record_nodes.dtype().is_list()) + conduit::Node nodeCopy = asNode; + auto processChildNodes = [&](const char* key, + std::function addFunc) { - auto recordIter = record_nodes.children(); - while (recordIter.has_next()) - { - auto record = recordIter.next(); - add(recordLoader.load(record)); - } - } - else - { - std::ostringstream message; - message << "The '" << RECORDS_KEY - << "' element of a document must be an array"; - throw std::invalid_argument(message.str()); - } - } + if (nodeCopy.has_child(key)) + { + conduit::Node &childNodes = nodeCopy[key]; - if (asNode.has_child(RELATIONSHIPS_KEY)) - { - const conduit::Node &relationships_node = asNode[RELATIONSHIPS_KEY]; + if (childNodes.number_of_children() == 0) + { + childNodes.set(conduit::DataType::list()); + } + if (!childNodes.dtype().is_list()) + { + std::ostringstream message; + message << "The '" << key << "' element of a document must be an array"; + throw std::invalid_argument(message.str()); + } - if (!relationships_node.dtype().is_list()) - { - if (relationships_node.number_of_children() == 0) - { - // Create a temporary mutable node for transformation - conduit::Node temp_node(conduit::DataType::list()); - auto relationshipsIter = temp_node.children(); - while (relationshipsIter.has_next()) - { - auto &relationship = relationshipsIter.next(); - add(Relationship{relationship}); + auto childIter = childNodes.children(); + while (childIter.has_next()) + { + conduit::Node child = childIter.next(); + addFunc(child); + } } - } - else - { - std::ostringstream message; - message << "The '" << RELATIONSHIPS_KEY << "' element of a document must be an array"; - throw std::invalid_argument(message.str()); - } - } - else + }; + + processChildNodes(RECORDS_KEY, [&](conduit::Node &record) + { + add(recordLoader.load(record)); + }); + + processChildNodes(RELATIONSHIPS_KEY, [&](conduit::Node &relationship) { - auto relationshipsIter = relationships_node.children(); - while (relationshipsIter.has_next()) - { - auto &relationship = relationshipsIter.next(); add(Relationship{relationship}); - } - } - } + }); } Document::Document(conduit::Node const &asNode, RecordLoader const &recordLoader) @@ -265,7 +311,6 @@ void Document::toHDF5(const std::string &filename) const for (const auto& record : getRecords()) { conduit::Node recordNode = record->toNode(); - conduit::Node modifiedRecordNode; removeSlashes(recordNode, recordsNode.append()); } @@ -274,7 +319,6 @@ void Document::toHDF5(const std::string &filename) const for (const auto& relationship : getRelationships()) { conduit::Node relationshipNode = relationship.toNode(); - conduit::Node modifiedRelationshipsNode; removeSlashes(relationshipNode, relationshipsNode.append()); } @@ -304,22 +348,18 @@ void saveDocument(Document const &document, std::string const &fileName, Protoco std::string tmpFileName = fileName + SAVE_TMP_FILE_EXTENSION; - if (protocol == Protocol::JSON) - { - protocol_warn(".json", fileName); + if (protocol == Protocol::JSON) { + protocolWarn(".json", fileName); auto asJson = document.toJson(); std::ofstream fout {tmpFileName}; fout.exceptions(std::ostream::failbit | std::ostream::badbit); fout << asJson; fout.close(); } - else if (protocol == Protocol::HDF5) - { - protocol_warn(".hdf5", fileName); + else if (protocol == Protocol::HDF5) { + protocolWarn(".hdf5", fileName); document.toHDF5(tmpFileName); - } - else - { + } else { throw std::invalid_argument("Invalid format choice. Please enter 'json' or 'hdf5'."); } @@ -339,24 +379,25 @@ Document loadDocument(std::string const &path, Protocol protocol) Document loadDocument(std::string const &path, RecordLoader const &recordLoader, Protocol protocol) { - conduit::Node node; + conduit::Node node, modifiedNode; + std::ostringstream file_contents; + std::ifstream file_in {path}; // Load the file depending on the protocol - if (protocol == Protocol::JSON) { - std::ifstream file_in {path}; - std::ostringstream file_contents; + switch (protocol) + { + case Protocol::JSON: file_contents << file_in.rdbuf(); file_in.close(); node.parse(file_contents.str(), "json"); return Document {node, recordLoader}; - } else if (protocol == Protocol::HDF5) { - conduit::Node modifiedNode; + case Protocol::HDF5: conduit::relay::io::load(path, "hdf5", node); restoreSlashes(node, modifiedNode); return Document {modifiedNode, recordLoader}; + default: + break; } - - // Finally, use the node to create the Document object (existing logic) } } // namespace sina diff --git a/src/axom/sina/core/Document.hpp b/src/axom/sina/core/Document.hpp index 1d65884f66..26e6839758 100644 --- a/src/axom/sina/core/Document.hpp +++ b/src/axom/sina/core/Document.hpp @@ -242,17 +242,6 @@ inline std::string getSinaFileFormatVersion() std::to_string(SINA_FILE_FORMAT_VERSION_MINOR); } -/** - * \brief Get the current file format version. - * - * \return A string representing the file format version. - */ -inline std::string getSinaFileFormatVersion() -{ - return std::to_string(SINA_FILE_FORMAT_VERSION_MAJOR) + "." + - std::to_string(SINA_FILE_FORMAT_VERSION_MINOR); -} - /** * \brief Load a document from the given path. Only records which this library * knows about will be able to be loaded. diff --git a/src/axom/sina/docs/sphinx/tutorial.rst b/src/axom/sina/docs/sphinx/tutorial.rst index 07ffed8648..cd82f2783b 100644 --- a/src/axom/sina/docs/sphinx/tutorial.rst +++ b/src/axom/sina/docs/sphinx/tutorial.rst @@ -53,7 +53,7 @@ Adding Data Once we have a Record, we can add different types of data to it. Any Datum object that is added will end up in the "data" section of the record in -the file. +the output file. .. literalinclude:: ../../examples/sina_tutorial.cpp :language: cpp diff --git a/src/axom/sina/examples/sina_tutorial.cpp b/src/axom/sina/examples/sina_tutorial.cpp index e4db0f5c6c..5ca9d18742 100644 --- a/src/axom/sina/examples/sina_tutorial.cpp +++ b/src/axom/sina/examples/sina_tutorial.cpp @@ -138,7 +138,7 @@ void save(axom::sina::Document const &doc) void load() { axom::sina::Document doc1 = axom::sina::loadDocument("my_output.json"); - axom::sina::Document doc2 = axom::sina::loadDocument("my_output.json", axom::sina::Protocol::HDF5); + axom::sina::Document doc2 = axom::sina::loadDocument("my_output.hdf5", axom::sina::Protocol::HDF5); } //! [end io read] From 8e6afc1701b5128a0515736d59c8c4e24fcf5b88 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Fri, 24 Jan 2025 14:40:06 -0800 Subject: [PATCH 09/38] Create From Node Changes --- src/axom/sina/core/Document.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/axom/sina/core/Document.cpp b/src/axom/sina/core/Document.cpp index cc9dcf824d..94ef7ff462 100644 --- a/src/axom/sina/core/Document.cpp +++ b/src/axom/sina/core/Document.cpp @@ -252,6 +252,7 @@ void Document::createFromNode(const conduit::Node &asNode, const RecordLoader &recordLoader) { conduit::Node nodeCopy = asNode; + auto processChildNodes = [&](const char* key, std::function addFunc) { @@ -259,17 +260,35 @@ void Document::createFromNode(const conduit::Node &asNode, { conduit::Node &childNodes = nodeCopy[key]; + // -- 1. Check if this node is a primitive leaf (throw immediately if so) + // Customize these checks to match exactly what you consider "primitive." + if (childNodes.dtype().is_number() || + childNodes.dtype().is_char8_str() || + childNodes.dtype().is_string()) + { + std::ostringstream message; + std::cout << "The '" << key + << "' element of a document cannot be a primitive value."; + throw std::invalid_argument(message.str()); + } + + // -- 2. Not a primitive. Check if it has no children. if (childNodes.number_of_children() == 0) { + // Turn it into an empty list childNodes.set(conduit::DataType::list()); } + + // -- 3. If it's still not a list, throw if (!childNodes.dtype().is_list()) { std::ostringstream message; - message << "The '" << key << "' element of a document must be an array"; + message << "The '" << key + << "' element of a document must be an array/list."; throw std::invalid_argument(message.str()); } + // -- 4. Now it's guaranteed to be a list, so iterate auto childIter = childNodes.children(); while (childIter.has_next()) { @@ -279,11 +298,13 @@ void Document::createFromNode(const conduit::Node &asNode, } }; + // Example usage for your "records" array: processChildNodes(RECORDS_KEY, [&](conduit::Node &record) { add(recordLoader.load(record)); }); + // Example usage for your "relationships" array: processChildNodes(RELATIONSHIPS_KEY, [&](conduit::Node &relationship) { add(Relationship{relationship}); From 247591f08244815557246992b21dfcf0a1bd5c98 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Fri, 24 Jan 2025 14:46:09 -0800 Subject: [PATCH 10/38] Forgot to delete some leftover comments --- src/axom/sina/core/Document.cpp | 63 --------------------------------- 1 file changed, 63 deletions(-) diff --git a/src/axom/sina/core/Document.cpp b/src/axom/sina/core/Document.cpp index 94ef7ff462..932af46fcf 100644 --- a/src/axom/sina/core/Document.cpp +++ b/src/axom/sina/core/Document.cpp @@ -188,66 +188,6 @@ conduit::Node Document::toNode() const return document; } -// void Document::createFromNode(conduit::Node const &asNode, -// RecordLoader const &recordLoader) -// { -// if (asNode.has_child(RECORDS_KEY)) -// { -// const conduit::Node &record_nodes = asNode[RECORDS_KEY]; -// if (record_nodes.dtype().is_list()) -// { -// auto recordIter = record_nodes.children(); -// while (recordIter.has_next()) -// { -// auto record = recordIter.next(); -// add(recordLoader.load(record)); -// } -// } -// else -// { -// std::ostringstream message; -// message << "The '" << RECORDS_KEY -// << "' element of a document must be an array"; -// throw std::invalid_argument(message.str()); -// } -// } - -// if (asNode.has_child(RELATIONSHIPS_KEY)) -// { -// const conduit::Node &relationships_node = asNode[RELATIONSHIPS_KEY]; - -// if (!relationships_node.dtype().is_list()) -// { -// if (relationships_node.number_of_children() == 0) -// { -// // Create a temporary mutable node for transformation -// conduit::Node temp_node(conduit::DataType::list()); -// auto relationshipsIter = temp_node.children(); -// while (relationshipsIter.has_next()) -// { -// auto &relationship = relationshipsIter.next(); -// add(Relationship{relationship}); -// } -// } -// else -// { -// std::ostringstream message; -// message << "The '" << RELATIONSHIPS_KEY << "' element of a document must be an array"; -// throw std::invalid_argument(message.str()); -// } -// } -// else -// { -// auto relationshipsIter = relationships_node.children(); -// while (relationshipsIter.has_next()) -// { -// auto &relationship = relationshipsIter.next(); -// add(Relationship{relationship}); -// } -// } -// } -// } - void Document::createFromNode(const conduit::Node &asNode, const RecordLoader &recordLoader) { @@ -297,14 +237,11 @@ void Document::createFromNode(const conduit::Node &asNode, } } }; - - // Example usage for your "records" array: processChildNodes(RECORDS_KEY, [&](conduit::Node &record) { add(recordLoader.load(record)); }); - // Example usage for your "relationships" array: processChildNodes(RELATIONSHIPS_KEY, [&](conduit::Node &relationship) { add(Relationship{relationship}); From 703dbd1aa78b075706237a16fbc2c59b1f63ef7c Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Fri, 24 Jan 2025 15:06:00 -0800 Subject: [PATCH 11/38] Small fix --- src/axom/sina/core/Document.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/axom/sina/core/Document.cpp b/src/axom/sina/core/Document.cpp index 932af46fcf..6308056c36 100644 --- a/src/axom/sina/core/Document.cpp +++ b/src/axom/sina/core/Document.cpp @@ -350,6 +350,7 @@ Document loadDocument(std::string const &path, RecordLoader const &recordLoader, node.parse(file_contents.str(), "json"); return Document {node, recordLoader}; case Protocol::HDF5: + file_in.close(); conduit::relay::io::load(path, "hdf5", node); restoreSlashes(node, modifiedNode); return Document {modifiedNode, recordLoader}; From 0df65b392b21c3e756bb3ae92da4d6f64d5d4d86 Mon Sep 17 00:00:00 2001 From: Charles Doutriaux Date: Fri, 24 Jan 2025 16:18:46 -0800 Subject: [PATCH 12/38] merged github's develop in --- data | 2 +- scripts/spack/radiuss-spack-configs | 2 +- src/cmake/blt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data b/data index 1bff47e373..c0c33018bd 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit 1bff47e3735122ce7f79a7f741b51e15227a9f57 +Subproject commit c0c33018bd6796cfe2ff64e46bb2a16402f00f9c diff --git a/scripts/spack/radiuss-spack-configs b/scripts/spack/radiuss-spack-configs index 22f5feaabf..d43946bed0 160000 --- a/scripts/spack/radiuss-spack-configs +++ b/scripts/spack/radiuss-spack-configs @@ -1 +1 @@ -Subproject commit 22f5feaabfe618339cf621d2734fe0d66077da39 +Subproject commit d43946bed028933a8b8770adca3dde78987f6626 diff --git a/src/cmake/blt b/src/cmake/blt index 9cfe8aedb1..c98f320835 160000 --- a/src/cmake/blt +++ b/src/cmake/blt @@ -1 +1 @@ -Subproject commit 9cfe8aedb12c17da04b4df1859ec5e802030795c +Subproject commit c98f320835d71f778fbffcc48f07675142c08635 From e118d2405e809534936a40bbac00bfa0b65553b3 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Mon, 27 Jan 2025 13:13:48 -0800 Subject: [PATCH 13/38] Pipeline Fix --- src/axom/sina/core/Document.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axom/sina/core/Document.cpp b/src/axom/sina/core/Document.cpp index 6308056c36..7696e463c8 100644 --- a/src/axom/sina/core/Document.cpp +++ b/src/axom/sina/core/Document.cpp @@ -207,7 +207,7 @@ void Document::createFromNode(const conduit::Node &asNode, childNodes.dtype().is_string()) { std::ostringstream message; - std::cout << "The '" << key + message << "The '" << key << "' element of a document cannot be a primitive value."; throw std::invalid_argument(message.str()); } From 0f5851e8d238bfcf1ac9bc133f4f153f45855a01 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Tue, 28 Jan 2025 09:26:42 -0800 Subject: [PATCH 14/38] Clang Format --- src/axom/sina/core/Document.cpp | 434 ++++++++++++++++---------------- 1 file changed, 224 insertions(+), 210 deletions(-) diff --git a/src/axom/sina/core/Document.cpp b/src/axom/sina/core/Document.cpp index 7696e463c8..e9864efb65 100644 --- a/src/axom/sina/core/Document.cpp +++ b/src/axom/sina/core/Document.cpp @@ -37,127 +37,136 @@ namespace char const RECORDS_KEY[] = "records"; char const RELATIONSHIPS_KEY[] = "relationships"; char const SAVE_TMP_FILE_EXTENSION[] = ".sina.tmp"; -} +} // namespace -void protocolWarn(std::string const protocol, std::string const &name) { +void protocolWarn(std::string const protocol, std::string const &name) +{ std::unordered_map protocolMessages = { - {".json", ".json extension not found, did you mean to save to this format?"}, - {".hdf5", ".hdf5 extension not found, did you use one of its other supported types? (h5, hdf, ...)"} - }; + {".json", ".json extension not found, did you mean to save to this format?"}, + {".hdf5", + ".hdf5 extension not found, did you use one of its other supported types? " + "(h5, hdf, ...)"}}; size_t pos = name.rfind('.'); - if (pos != std::string::npos) { - std::string found = name.substr(pos); - - if (found != protocol) { - auto messageIt = protocolMessages.find(protocol); - if (messageIt != protocolMessages.end()) { - std::cout << messageIt->second << std::endl; - } + if(pos != std::string::npos) + { + std::string found = name.substr(pos); + + if(found != protocol) + { + auto messageIt = protocolMessages.find(protocol); + if(messageIt != protocolMessages.end()) + { + std::cout << messageIt->second << std::endl; } - } else { - std::cout << "No file extension found, did you mean to use one of " - << protocol << "'s supported types?" << std::endl; + } + } + else + { + std::cout << "No file extension found, did you mean to use one of " + << protocol << "'s supported types?" << std::endl; } } -void removeSlashes(const conduit::Node& originalNode, conduit::Node& modifiedNode) +void removeSlashes(const conduit::Node &originalNode, conduit::Node &modifiedNode) { - for (auto it = originalNode.children(); it.has_next();) + for(auto it = originalNode.children(); it.has_next();) + { + it.next(); + std::string key = it.name(); + std::string modifiedKey = key; + + std::string toReplace = "/"; + + size_t pos = 0; + // Find and replace all occurrences of "/" + while((pos = modifiedKey.find(toReplace, pos)) != std::string::npos) + { + modifiedKey.replace(pos, toReplace.length(), slashSubstitute); + pos += slashSubstitute.length(); // Move past the replaced substring + } + + modifiedNode[modifiedKey] = it.node(); + + if(it.node().dtype().is_object()) { - it.next(); - std::string key = it.name(); - std::string modifiedKey = key; - - std::string toReplace = "/"; - - size_t pos = 0; - // Find and replace all occurrences of "/" - while ((pos = modifiedKey.find(toReplace, pos)) != std::string::npos) { - modifiedKey.replace(pos, toReplace.length(), slashSubstitute); - pos += slashSubstitute.length(); // Move past the replaced substring - } - - modifiedNode[modifiedKey] = it.node(); - - if (it.node().dtype().is_object()) - { - conduit::Node nestedNode; - removeSlashes(it.node(), nestedNode); - modifiedNode[modifiedKey].set(nestedNode); - } + conduit::Node nestedNode; + removeSlashes(it.node(), nestedNode); + modifiedNode[modifiedKey].set(nestedNode); } + } } -void restoreSlashes(const conduit::Node& modifiedNode, conduit::Node& restoredNode) +void restoreSlashes(const conduit::Node &modifiedNode, conduit::Node &restoredNode) { - // Check if List or Object, if its a list the else statement would turn it into an object - // which breaks the Document + // Check if List or Object, if its a list the else statement would turn it into an object + // which breaks the Document + + if(modifiedNode.dtype().is_list()) + { + // If its empty with no children it's the end of a tree - if (modifiedNode.dtype().is_list()) + for(auto it = modifiedNode.children(); it.has_next();) { - // If its empty with no children it's the end of a tree - - for (auto it = modifiedNode.children(); it.has_next();) - { - it.next(); - conduit::Node& newChild = restoredNode.append(); - - // Leaves empty nodes empty, if null data is set the - // Document breaks - - if (it.node().dtype().is_string() || it.node().dtype().is_number()) - { - newChild.set(it.node()); // Lists need .set - } - - // Recursive Call - if (it.node().number_of_children() > 0) - { - restoreSlashes(it.node(), newChild); - } - } + it.next(); + conduit::Node &newChild = restoredNode.append(); + + // Leaves empty nodes empty, if null data is set the + // Document breaks + + if(it.node().dtype().is_string() || it.node().dtype().is_number()) + { + newChild.set(it.node()); // Lists need .set + } + + // Recursive Call + if(it.node().number_of_children() > 0) + { + restoreSlashes(it.node(), newChild); + } } - else + } + else + { + for(auto it = modifiedNode.children(); it.has_next();) { - for (auto it = modifiedNode.children(); it.has_next();) - { - it.next(); - std::string key = it.name(); - std::string restoredKey = key; - std::string replacement = "/"; - - size_t pos = 0; - // Find and replace all occurrences of "__SLASH__" - - while ((pos = restoredKey.find(slashSubstitute, pos)) != std::string::npos) { - restoredKey.replace(pos, slashSubstitute.length(), replacement); - pos += replacement.length(); - } - - - // Initialize a new node for the restored key - conduit::Node& newChild = restoredNode.add_child(restoredKey); - - // Leaves empty keys empty but continues recursive call if its a list - if (it.node().dtype().is_string() || it.node().dtype().is_number() || it.node().dtype().is_object()) - { - newChild.set(it.node()); - } - else if (it.node().dtype().is_list()) - { - restoreSlashes(it.node(), newChild); // Handle nested lists - } - - // If the node has children, recursively restore them - if (it.node().number_of_children() > 0) - { - conduit::Node nestedNode; - restoreSlashes(it.node(), nestedNode); - newChild.set(nestedNode); - } - } + it.next(); + std::string key = it.name(); + std::string restoredKey = key; + std::string replacement = "/"; + + size_t pos = 0; + // Find and replace all occurrences of "__SLASH__" + + while((pos = restoredKey.find(slashSubstitute, pos)) != std::string::npos) + { + restoredKey.replace(pos, slashSubstitute.length(), replacement); + pos += replacement.length(); + } + + // Initialize a new node for the restored key + conduit::Node &newChild = restoredNode.add_child(restoredKey); + + // Leaves empty keys empty but continues recursive call if its a list + if(it.node().dtype().is_string() || it.node().dtype().is_number() || + it.node().dtype().is_object()) + { + newChild.set(it.node()); + } + else if(it.node().dtype().is_list()) + { + restoreSlashes(it.node(), newChild); // Handle nested lists + } + + // If the node has children, recursively restore them + if(it.node().number_of_children() > 0) + { + conduit::Node nestedNode; + restoreSlashes(it.node(), nestedNode); + newChild.set(nestedNode); + } } + } } void Document::add(std::unique_ptr record) @@ -188,64 +197,60 @@ conduit::Node Document::toNode() const return document; } -void Document::createFromNode(const conduit::Node &asNode, +void Document::createFromNode(const conduit::Node &asNode, const RecordLoader &recordLoader) { - conduit::Node nodeCopy = asNode; + conduit::Node nodeCopy = asNode; - auto processChildNodes = [&](const char* key, - std::function addFunc) - { - if (nodeCopy.has_child(key)) - { - conduit::Node &childNodes = nodeCopy[key]; - - // -- 1. Check if this node is a primitive leaf (throw immediately if so) - // Customize these checks to match exactly what you consider "primitive." - if (childNodes.dtype().is_number() || - childNodes.dtype().is_char8_str() || - childNodes.dtype().is_string()) - { - std::ostringstream message; - message << "The '" << key - << "' element of a document cannot be a primitive value."; - throw std::invalid_argument(message.str()); - } - - // -- 2. Not a primitive. Check if it has no children. - if (childNodes.number_of_children() == 0) - { - // Turn it into an empty list - childNodes.set(conduit::DataType::list()); - } - - // -- 3. If it's still not a list, throw - if (!childNodes.dtype().is_list()) - { - std::ostringstream message; - message << "The '" << key - << "' element of a document must be an array/list."; - throw std::invalid_argument(message.str()); - } - - // -- 4. Now it's guaranteed to be a list, so iterate - auto childIter = childNodes.children(); - while (childIter.has_next()) - { - conduit::Node child = childIter.next(); - addFunc(child); - } - } - }; - processChildNodes(RECORDS_KEY, [&](conduit::Node &record) + auto processChildNodes = [&](const char *key, + std::function addFunc) { + if(nodeCopy.has_child(key)) { - add(recordLoader.load(record)); - }); + conduit::Node &childNodes = nodeCopy[key]; + + // -- 1. Check if this node is a primitive leaf (throw immediately if so) + // Customize these checks to match exactly what you consider "primitive." + if(childNodes.dtype().is_number() || childNodes.dtype().is_char8_str() || + childNodes.dtype().is_string()) + { + std::ostringstream message; + message << "The '" << key + << "' element of a document cannot be a primitive value."; + throw std::invalid_argument(message.str()); + } - processChildNodes(RELATIONSHIPS_KEY, [&](conduit::Node &relationship) - { - add(Relationship{relationship}); - }); + // -- 2. Not a primitive. Check if it has no children. + if(childNodes.number_of_children() == 0) + { + // Turn it into an empty list + childNodes.set(conduit::DataType::list()); + } + + // -- 3. If it's still not a list, throw + if(!childNodes.dtype().is_list()) + { + std::ostringstream message; + message << "The '" << key + << "' element of a document must be an array/list."; + throw std::invalid_argument(message.str()); + } + + // -- 4. Now it's guaranteed to be a list, so iterate + auto childIter = childNodes.children(); + while(childIter.has_next()) + { + conduit::Node child = childIter.next(); + addFunc(child); + } + } + }; + processChildNodes(RECORDS_KEY, [&](conduit::Node &record) { + add(recordLoader.load(record)); + }); + + processChildNodes(RELATIONSHIPS_KEY, [&](conduit::Node &relationship) { + add(Relationship {relationship}); + }); } Document::Document(conduit::Node const &asNode, RecordLoader const &recordLoader) @@ -260,28 +265,28 @@ Document::Document(std::string const &asJson, RecordLoader const &recordLoader) this->createFromNode(asNode, recordLoader); } -void Document::toHDF5(const std::string &filename) const +void Document::toHDF5(const std::string &filename) const { - conduit::Node node; - conduit::Node &recordsNode = node["records"]; - conduit::Node &relationshipsNode = node["relationships"]; + conduit::Node node; + conduit::Node &recordsNode = node["records"]; + conduit::Node &relationshipsNode = node["relationships"]; - for (const auto& record : getRecords()) - { - conduit::Node recordNode = record->toNode(); + for(const auto &record : getRecords()) + { + conduit::Node recordNode = record->toNode(); - removeSlashes(recordNode, recordsNode.append()); - } + removeSlashes(recordNode, recordsNode.append()); + } - // Process relationships - for (const auto& relationship : getRelationships()) - { - conduit::Node relationshipNode = relationship.toNode(); + // Process relationships + for(const auto &relationship : getRelationships()) + { + conduit::Node relationshipNode = relationship.toNode(); - removeSlashes(relationshipNode, relationshipsNode.append()); - } + removeSlashes(relationshipNode, relationshipsNode.append()); + } - conduit::relay::io::save(node, filename, "hdf5"); + conduit::relay::io::save(node, filename, "hdf5"); } // @@ -294,7 +299,9 @@ std::string Document::toJson(conduit::index_t indent, return this->toNode().to_json("json", indent, depth, pad, eoe); } -void saveDocument(Document const &document, std::string const &fileName, Protocol protocol) +void saveDocument(Document const &document, + std::string const &fileName, + Protocol protocol) { // It is a common use case for users to want to overwrite their files as // the simulation progresses. However, this operation should be atomic so @@ -306,27 +313,32 @@ void saveDocument(Document const &document, std::string const &fileName, Protoco std::string tmpFileName = fileName + SAVE_TMP_FILE_EXTENSION; - if (protocol == Protocol::JSON) { - protocolWarn(".json", fileName); - auto asJson = document.toJson(); - std::ofstream fout {tmpFileName}; - fout.exceptions(std::ostream::failbit | std::ostream::badbit); - fout << asJson; - fout.close(); + if(protocol == Protocol::JSON) + { + protocolWarn(".json", fileName); + auto asJson = document.toJson(); + std::ofstream fout {tmpFileName}; + fout.exceptions(std::ostream::failbit | std::ostream::badbit); + fout << asJson; + fout.close(); } - else if (protocol == Protocol::HDF5) { - protocolWarn(".hdf5", fileName); - document.toHDF5(tmpFileName); - } else { - throw std::invalid_argument("Invalid format choice. Please enter 'json' or 'hdf5'."); + else if(protocol == Protocol::HDF5) + { + protocolWarn(".hdf5", fileName); + document.toHDF5(tmpFileName); + } + else + { + throw std::invalid_argument( + "Invalid format choice. Please enter 'json' or 'hdf5'."); } - if (rename(tmpFileName.c_str(), fileName.c_str()) != 0) + if(rename(tmpFileName.c_str(), fileName.c_str()) != 0) { - std::string message {"Could not save to '"}; - message += fileName; - message += "'"; - throw std::ios::failure {message}; + std::string message {"Could not save to '"}; + message += fileName; + message += "'"; + throw std::ios::failure {message}; } } @@ -335,28 +347,30 @@ Document loadDocument(std::string const &path, Protocol protocol) return loadDocument(path, createRecordLoaderWithAllKnownTypes(), protocol); } -Document loadDocument(std::string const &path, RecordLoader const &recordLoader, Protocol protocol) +Document loadDocument(std::string const &path, + RecordLoader const &recordLoader, + Protocol protocol) { - conduit::Node node, modifiedNode; - std::ostringstream file_contents; - std::ifstream file_in {path}; - - // Load the file depending on the protocol - switch (protocol) - { - case Protocol::JSON: - file_contents << file_in.rdbuf(); - file_in.close(); - node.parse(file_contents.str(), "json"); - return Document {node, recordLoader}; - case Protocol::HDF5: - file_in.close(); - conduit::relay::io::load(path, "hdf5", node); - restoreSlashes(node, modifiedNode); - return Document {modifiedNode, recordLoader}; - default: - break; - } + conduit::Node node, modifiedNode; + std::ostringstream file_contents; + std::ifstream file_in {path}; + + // Load the file depending on the protocol + switch(protocol) + { + case Protocol::JSON: + file_contents << file_in.rdbuf(); + file_in.close(); + node.parse(file_contents.str(), "json"); + return Document {node, recordLoader}; + case Protocol::HDF5: + file_in.close(); + conduit::relay::io::load(path, "hdf5", node); + restoreSlashes(node, modifiedNode); + return Document {modifiedNode, recordLoader}; + default: + break; + } } } // namespace sina From 1168000c40259a81968cf0d0e539abebabcccd74 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Tue, 28 Jan 2025 10:17:09 -0800 Subject: [PATCH 15/38] More ClangFormat --- src/axom/sina/core/Document.hpp | 18 +++++----- src/axom/sina/examples/sina_tutorial.cpp | 3 +- src/axom/sina/tests/sina_Document.cpp | 43 +++++++++++------------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/axom/sina/core/Document.hpp b/src/axom/sina/core/Document.hpp index 26e6839758..167e95eeca 100644 --- a/src/axom/sina/core/Document.hpp +++ b/src/axom/sina/core/Document.hpp @@ -38,15 +38,12 @@ enum class Protocol HDF5 }; -const std::string supported_types[] = { - "JSON", - "HDF5" -}; +const std::string supported_types[] = {"JSON", "HDF5"}; /** * \brief The string used to replace '/' in parent node names when saving to HDF5. */ -const std::string slashSubstitute = "__SINA_SLASHREPLACE__"; +const std::string slashSubstitute = "__SINA_SLASHREPLACE__"; /** * \brief An object representing the top-level object of a Sina JSON file @@ -229,7 +226,9 @@ class Document * \param protocol the file type requested to save as, default = JSON * \throws std::ios::failure if there are any IO errors */ -void saveDocument(Document const &document, std::string const &fileName, Protocol protocol = Protocol::JSON); +void saveDocument(Document const &document, + std::string const &fileName, + Protocol protocol = Protocol::JSON); /** * \brief Get the current file format version. @@ -250,7 +249,8 @@ inline std::string getSinaFileFormatVersion() * \param protocol the type of file being loaded, default = JSON * \return the loaded Document */ -Document loadDocument(std::string const &path, Protocol protocol = Protocol::JSON); +Document loadDocument(std::string const &path, + Protocol protocol = Protocol::JSON); /** * \brief Load a document from the given path. @@ -261,7 +261,9 @@ Document loadDocument(std::string const &path, Protocol protocol = Protocol::JSO * \param protocol the type of file being loaded, default = JSON * \return the loaded Document */ -Document loadDocument(std::string const &path, RecordLoader const &recordLoader, Protocol protocol = Protocol::JSON); +Document loadDocument(std::string const &path, + RecordLoader const &recordLoader, + Protocol protocol = Protocol::JSON); } // namespace sina } // namespace axom diff --git a/src/axom/sina/examples/sina_tutorial.cpp b/src/axom/sina/examples/sina_tutorial.cpp index 5ca9d18742..1a3636fffa 100644 --- a/src/axom/sina/examples/sina_tutorial.cpp +++ b/src/axom/sina/examples/sina_tutorial.cpp @@ -138,7 +138,8 @@ void save(axom::sina::Document const &doc) void load() { axom::sina::Document doc1 = axom::sina::loadDocument("my_output.json"); - axom::sina::Document doc2 = axom::sina::loadDocument("my_output.hdf5", axom::sina::Protocol::HDF5); + axom::sina::Document doc2 = + axom::sina::loadDocument("my_output.hdf5", axom::sina::Protocol::HDF5); } //! [end io read] diff --git a/src/axom/sina/tests/sina_Document.cpp b/src/axom/sina/tests/sina_Document.cpp index 272e9915b5..646d357bc3 100644 --- a/src/axom/sina/tests/sina_Document.cpp +++ b/src/axom/sina/tests/sina_Document.cpp @@ -37,7 +37,6 @@ char const TEST_RECORD_TYPE[] = "test type"; char const EXPECTED_RECORDS_KEY[] = "records"; char const EXPECTED_RELATIONSHIPS_KEY[] = "relationships"; - // Large JSONs Used For JSON and HDF5 Save Tests std::string data_json = "{\"records\": [{\"type\": \"run\", \"application\":\"test\", \"id\": " @@ -64,7 +63,6 @@ std::string long_json = "\"test_1\"},{\"subject\": \"test_3\", \"predicate\": \"overrides\", " "\"object\": \"test_4\"}]}"; - // Tests TEST(Document, create_fromNode_empty) { @@ -376,12 +374,11 @@ NamedTempFile::~NamedTempFile() axom::utilities::filesystem::removeFile(fileName.data()); } - TEST(Document, create_fromJson_roundtrip_json) { std::string orig_json = "{\"records\": [{\"type\": \"test_rec\",\"id\": " - "\"test\"}],\"relationships\": []}"; + "\"test\"}],\"relationships\": []}"; axom::sina::Document myDocument = Document(orig_json, createRecordLoaderWithAllKnownTypes()); EXPECT_EQ(0, myDocument.getRelationships().size()); @@ -395,7 +392,7 @@ TEST(Document, create_fromJson_roundtrip_hdf5) { std::string orig_json = "{\"records\": [{\"type\": \"test_rec\",\"id\": " - "\"test\"}],\"relationships\": []}"; + "\"test\"}],\"relationships\": []}"; axom::sina::Document myDocument = Document(orig_json, createRecordLoaderWithAllKnownTypes()); saveDocument(myDocument, "round_json.hdf5", Protocol::HDF5); @@ -493,29 +490,29 @@ TEST(Document, saveDocument_json) TEST(Document, saveDocument_hdf5) { - NamedTempFile tmpFile; + NamedTempFile tmpFile; - // First, write some random stuff to the temp file to make sure it is - // overwritten. - { - std::ofstream fout {tmpFile.getName()}; - fout << "Initial contents"; - } + // First, write some random stuff to the temp file to make sure it is + // overwritten. + { + std::ofstream fout {tmpFile.getName()}; + fout << "Initial contents"; + } - Document document; - document.add( - std::make_unique(ID {"the id", IDType::Global}, "the type")); + Document document; + document.add( + std::make_unique(ID {"the id", IDType::Global}, "the type")); - saveDocument(document, tmpFile.getName(), Protocol::HDF5); + saveDocument(document, tmpFile.getName(), Protocol::HDF5); - conduit::Node readContents; - conduit::relay::io::load(tmpFile.getName(), "hdf5", readContents); + conduit::Node readContents; + conduit::relay::io::load(tmpFile.getName(), "hdf5", readContents); - ASSERT_TRUE(readContents[EXPECTED_RECORDS_KEY].dtype().is_list()); - EXPECT_EQ(1, readContents[EXPECTED_RECORDS_KEY].number_of_children()); - auto &readRecord = readContents[EXPECTED_RECORDS_KEY][0]; - EXPECT_EQ("the id", readRecord["id"].as_string()); - EXPECT_EQ("the type", readRecord["type"].as_string()); + ASSERT_TRUE(readContents[EXPECTED_RECORDS_KEY].dtype().is_list()); + EXPECT_EQ(1, readContents[EXPECTED_RECORDS_KEY].number_of_children()); + auto &readRecord = readContents[EXPECTED_RECORDS_KEY][0]; + EXPECT_EQ("the id", readRecord["id"].as_string()); + EXPECT_EQ("the type", readRecord["type"].as_string()); } TEST(Document, load_specifiedRecordLoader) From 429d9cebfb4dcea5aceea258c542f3d61a58b671 Mon Sep 17 00:00:00 2001 From: Charles Doutriaux Date: Mon, 3 Feb 2025 06:36:18 -0800 Subject: [PATCH 16/38] bringing sumbodule back to develop version --- data | 2 +- scripts/spack/radiuss-spack-configs | 2 +- src/cmake/blt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data b/data index c0c33018bd..1bff47e373 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit c0c33018bd6796cfe2ff64e46bb2a16402f00f9c +Subproject commit 1bff47e3735122ce7f79a7f741b51e15227a9f57 diff --git a/scripts/spack/radiuss-spack-configs b/scripts/spack/radiuss-spack-configs index d43946bed0..22f5feaabf 160000 --- a/scripts/spack/radiuss-spack-configs +++ b/scripts/spack/radiuss-spack-configs @@ -1 +1 @@ -Subproject commit d43946bed028933a8b8770adca3dde78987f6626 +Subproject commit 22f5feaabfe618339cf621d2734fe0d66077da39 diff --git a/src/cmake/blt b/src/cmake/blt index c98f320835..9cfe8aedb1 160000 --- a/src/cmake/blt +++ b/src/cmake/blt @@ -1 +1 @@ -Subproject commit c98f320835d71f778fbffcc48f07675142c08635 +Subproject commit 9cfe8aedb12c17da04b4df1859ec5e802030795c From b71b05d6c908a99fa07d1d48c4ebc23004099c7b Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Tue, 4 Feb 2025 10:01:39 -0800 Subject: [PATCH 17/38] New Changes + Guarding --- src/axom/sina/core/Document.cpp | 219 +++++++------- src/axom/sina/core/Document.hpp | 10 +- src/axom/sina/tests/sina_Document.cpp | 392 ++++++++++++-------------- 3 files changed, 290 insertions(+), 331 deletions(-) diff --git a/src/axom/sina/core/Document.cpp b/src/axom/sina/core/Document.cpp index e9864efb65..146720ea8f 100644 --- a/src/axom/sina/core/Document.cpp +++ b/src/axom/sina/core/Document.cpp @@ -24,8 +24,14 @@ #include #include #include "conduit.hpp" -#include "conduit_relay.hpp" -#include "conduit_relay_io.hpp" +#include "axom/core/Path.hpp" + +#ifdef AXOM_USE_HDF5 + #include "conduit_relay.hpp" + #include "conduit_relay_io.hpp" + + #include "axom/core/utilities/StringUtilities.hpp" +#endif namespace axom { @@ -47,124 +53,14 @@ void protocolWarn(std::string const protocol, std::string const &name) ".hdf5 extension not found, did you use one of its other supported types? " "(h5, hdf, ...)"}}; - size_t pos = name.rfind('.'); - if(pos != std::string::npos) - { - std::string found = name.substr(pos); - - if(found != protocol) - { - auto messageIt = protocolMessages.find(protocol); - if(messageIt != protocolMessages.end()) - { - std::cout << messageIt->second << std::endl; - } - } - } - else - { - std::cout << "No file extension found, did you mean to use one of " - << protocol << "'s supported types?" << std::endl; - } -} + Path path(name, '.'); -void removeSlashes(const conduit::Node &originalNode, conduit::Node &modifiedNode) -{ - for(auto it = originalNode.children(); it.has_next();) + if(protocol != '.' + path.baseName()) { - it.next(); - std::string key = it.name(); - std::string modifiedKey = key; - - std::string toReplace = "/"; - - size_t pos = 0; - // Find and replace all occurrences of "/" - while((pos = modifiedKey.find(toReplace, pos)) != std::string::npos) - { - modifiedKey.replace(pos, toReplace.length(), slashSubstitute); - pos += slashSubstitute.length(); // Move past the replaced substring - } - - modifiedNode[modifiedKey] = it.node(); - - if(it.node().dtype().is_object()) + auto messageIt = protocolMessages.find(protocol); + if(messageIt != protocolMessages.end()) { - conduit::Node nestedNode; - removeSlashes(it.node(), nestedNode); - modifiedNode[modifiedKey].set(nestedNode); - } - } -} - -void restoreSlashes(const conduit::Node &modifiedNode, conduit::Node &restoredNode) -{ - // Check if List or Object, if its a list the else statement would turn it into an object - // which breaks the Document - - if(modifiedNode.dtype().is_list()) - { - // If its empty with no children it's the end of a tree - - for(auto it = modifiedNode.children(); it.has_next();) - { - it.next(); - conduit::Node &newChild = restoredNode.append(); - - // Leaves empty nodes empty, if null data is set the - // Document breaks - - if(it.node().dtype().is_string() || it.node().dtype().is_number()) - { - newChild.set(it.node()); // Lists need .set - } - - // Recursive Call - if(it.node().number_of_children() > 0) - { - restoreSlashes(it.node(), newChild); - } - } - } - else - { - for(auto it = modifiedNode.children(); it.has_next();) - { - it.next(); - std::string key = it.name(); - std::string restoredKey = key; - std::string replacement = "/"; - - size_t pos = 0; - // Find and replace all occurrences of "__SLASH__" - - while((pos = restoredKey.find(slashSubstitute, pos)) != std::string::npos) - { - restoredKey.replace(pos, slashSubstitute.length(), replacement); - pos += replacement.length(); - } - - // Initialize a new node for the restored key - conduit::Node &newChild = restoredNode.add_child(restoredKey); - - // Leaves empty keys empty but continues recursive call if its a list - if(it.node().dtype().is_string() || it.node().dtype().is_number() || - it.node().dtype().is_object()) - { - newChild.set(it.node()); - } - else if(it.node().dtype().is_list()) - { - restoreSlashes(it.node(), newChild); // Handle nested lists - } - - // If the node has children, recursively restore them - if(it.node().number_of_children() > 0) - { - conduit::Node nestedNode; - restoreSlashes(it.node(), nestedNode); - newChild.set(nestedNode); - } + std::cerr << messageIt->second; } } } @@ -265,6 +161,90 @@ Document::Document(std::string const &asJson, RecordLoader const &recordLoader) this->createFromNode(asNode, recordLoader); } +#ifdef AXOM_USE_HDF5 +void removeSlashes(const conduit::Node &originalNode, conduit::Node &modifiedNode) +{ + for(auto it = originalNode.children(); it.has_next();) + { + it.next(); + std::string key = it.name(); + std::string modifiedKey = + axom::utilities::string::replaceAllInstances(key, "/", slashSubstitute); + + modifiedNode[modifiedKey] = it.node(); + + if(it.node().dtype().is_object()) + { + conduit::Node nestedNode; + removeSlashes(it.node(), nestedNode); + modifiedNode[modifiedKey].set(nestedNode); + } + } +} + +void restoreSlashes(const conduit::Node &modifiedNode, conduit::Node &restoredNode) +{ + // Check if List or Object, if its a list the else statement would turn it into an object + // which breaks the Document + + if(modifiedNode.dtype().is_list()) + { + // If its empty with no children it's the end of a tree + + for(auto it = modifiedNode.children(); it.has_next();) + { + it.next(); + conduit::Node &newChild = restoredNode.append(); + + // Leaves empty nodes empty, if null data is set the + // Document breaks + + if(it.node().dtype().is_string() || it.node().dtype().is_number()) + { + newChild.set(it.node()); // Lists need .set + } + + // Recursive Call + if(it.node().number_of_children() > 0) + { + restoreSlashes(it.node(), newChild); + } + } + } + else + { + for(auto it = modifiedNode.children(); it.has_next();) + { + it.next(); + std::string key = it.name(); + std::string restoredKey = + axom::utilities::string::replaceAllInstances(key, slashSubstitute, "/"); + + // Initialize a new node for the restored key + conduit::Node &newChild = restoredNode.add_child(restoredKey); + + // Leaves empty keys empty but continues recursive call if its a list + if(it.node().dtype().is_string() || it.node().dtype().is_number() || + it.node().dtype().is_object()) + { + newChild.set(it.node()); + } + else if(it.node().dtype().is_list()) + { + restoreSlashes(it.node(), newChild); // Handle nested lists + } + + // If the node has children, recursively restore them + if(it.node().number_of_children() > 0) + { + conduit::Node nestedNode; + restoreSlashes(it.node(), nestedNode); + newChild.set(nestedNode); + } + } + } +} + void Document::toHDF5(const std::string &filename) const { conduit::Node node; @@ -288,6 +268,7 @@ void Document::toHDF5(const std::string &filename) const conduit::relay::io::save(node, filename, "hdf5"); } +#endif // @@ -363,11 +344,15 @@ Document loadDocument(std::string const &path, file_in.close(); node.parse(file_contents.str(), "json"); return Document {node, recordLoader}; + +#ifdef AXOM_USE_HDF5 case Protocol::HDF5: file_in.close(); conduit::relay::io::load(path, "hdf5", node); restoreSlashes(node, modifiedNode); return Document {modifiedNode, recordLoader}; +#endif + default: break; } diff --git a/src/axom/sina/core/Document.hpp b/src/axom/sina/core/Document.hpp index 167e95eeca..b98c944208 100644 --- a/src/axom/sina/core/Document.hpp +++ b/src/axom/sina/core/Document.hpp @@ -35,10 +35,16 @@ namespace sina enum class Protocol { JSON, +#ifdef AXOM_USE_HDF5 HDF5 +#endif }; -const std::string supported_types[] = {"JSON", "HDF5"}; +const std::string supported_types[] = {"JSON", +#ifdef AXOM_USE_HDF5 + "HDF5" +#endif +}; /** * \brief The string used to replace '/' in parent node names when saving to HDF5. @@ -190,12 +196,14 @@ class Document */ conduit::Node toNode() const; +#ifdef AXOM_USE_HDF5 /** * \brief Dump this document as an HDF5 File * * \return None, conduit automatically dumps the hdf5 file without a return */ void toHDF5(const std::string &filename) const; +#endif /** * \brief Convert this document to a JSON string. diff --git a/src/axom/sina/tests/sina_Document.cpp b/src/axom/sina/tests/sina_Document.cpp index 646d357bc3..69b161495a 100644 --- a/src/axom/sina/tests/sina_Document.cpp +++ b/src/axom/sina/tests/sina_Document.cpp @@ -38,30 +38,112 @@ char const EXPECTED_RECORDS_KEY[] = "records"; char const EXPECTED_RELATIONSHIPS_KEY[] = "relationships"; // Large JSONs Used For JSON and HDF5 Save Tests -std::string data_json = - "{\"records\": [{\"type\": \"run\", \"application\":\"test\", \"id\": " - "\"test_1\",\"data\":{\"int\": {\"value\": 500,\"units\": \"miles\"}, " - "\"str/ings\": {\"value\":[\"z\", \"o\", \"o\"]}}, " - "\"files\":{\"test/test.png\":{}}}]}"; - -std::string long_json = - "{\"records\": [{\"type\": \"foo\",\"id\": " - "\"test_1\",\"user_defined\":{\"name\":\"bob\"},\"files\":{\"foo/" - "bar.png\":{\"mimetype\":\"image\"}},\"data\":{\"scalar\": {\"value\": " - "500,\"units\": \"miles\"}}},{\"type\":\"bar\",\"id\": " - "\"test_2\",\"data\": {\"scalar_list\": {\"value\": [1, 2, 3]}, " - "\"string_list\": {\"value\": [\"a\",\"wonderful\",\"world\"], " - "\"tags\":[\"observation\"]}}},{\"type\": " - "\"run\",\"application\":\"sina_test\",\"id\": " - "\"test_3\",\"data\":{\"scalar\": {\"value\": 12.3, \"units\": \"g/s\", " - "\"tags\": [\"hi\"]}, \"scalar_list\": {\"value\": [1,2,3.0,4]}}}, " - "{\"type\": \"bar\",\"id\": \"test_4\",\"data\":{\"string\": {\"value\": " - "\"yarr\"}, \"string_list\": {\"value\": [\"y\",\"a\",\"r\"]}}, " - "\"files\":{\"test/test.png\":{}}, " - "\"user_defined\":{\"hello\":\"there\"}}],\"relationships\": " - "[{\"predicate\": \"completes\",\"subject\": \"test_2\",\"object\": " - "\"test_1\"},{\"subject\": \"test_3\", \"predicate\": \"overrides\", " - "\"object\": \"test_4\"}]}"; +std::string data_json = R"( +{ + "records": [ + { + "type": "run", + "application": "test", + "id": "test_1", + "data": { + "int": { + "value": 500, + "units": "miles" + }, + "str/ings": { + "value": ["z", "o", "o"] + } + }, + "files": { + "test/test.png": {} + } + } + ] +} +)"; + +std::string long_json = R"( +{ + "records": [ + { + "type": "foo", + "id": "test_1", + "user_defined": { + "name": "bob" + }, + "files": { + "foo/bar.png": { + "mimetype": "image" + } + }, + "data": { + "scalar": { + "value": 500, + "units": "miles" + } + } + }, + { + "type": "bar", + "id": "test_2", + "data": { + "scalar_list": { + "value": [1, 2, 3] + }, + "string_list": { + "value": ["a", "wonderful", "world"], + "tags": ["observation"] + } + } + }, + { + "type": "run", + "application": "sina_test", + "id": "test_3", + "data": { + "scalar": { + "value": 12.3, + "units": "g/s", + "tags": ["hi"] + }, + "scalar_list": { + "value": [1, 2, 3.0, 4] + } + } + }, + { + "type": "bar", + "id": "test_4", + "data": { + "string": { + "value": "yarr" + }, + "string_list": { + "value": ["y", "a", "r"] + } + }, + "files": { + "test/test.png": {} + }, + "user_defined": { + "hello": "there" + } + } + ], + "relationships": [ + { + "predicate": "completes", + "subject": "test_2", + "object": "test_1" + }, + { + "subject": "test_3", + "predicate": "overrides", + "object": "test_4" + } + ] +} +)"; // Tests TEST(Document, create_fromNode_empty) @@ -139,124 +221,6 @@ TEST(Document, create_fromNode_withRelationships) EXPECT_EQ("is related to", relationships[0].getPredicate()); } -TEST(Document, create_fromJson_roundtrip) -{ - std::string orig_json = - "{\"records\": [{\"type\": \"test_rec\",\"id\": " - "\"test\"}],\"relationships\": []}"; - axom::sina::Document myDocument = - Document(orig_json, createRecordLoaderWithAllKnownTypes()); - EXPECT_EQ(0, myDocument.getRelationships().size()); - ASSERT_EQ(1, myDocument.getRecords().size()); - EXPECT_EQ("test_rec", myDocument.getRecords()[0]->getType()); - std::string returned_json = myDocument.toJson(0, 0, "", ""); - EXPECT_EQ(orig_json, returned_json); -} - -TEST(Document, create_fromJson_full) -{ - std::string long_json = R"({ - "records": [ - { - "type": "foo", - "id": "test_1", - "user_defined": { - "name": "bob" - }, - "files": { - "foo/bar.png": { - "mimetype": "image" - } - }, - "data": { - "scalar": { - "value": 500, - "units": "miles" - } - } - }, - { - "type": "bar", - "id": "test_2", - "data": { - "scalar_list": { - "value": [1, 2, 3] - }, - "string_list": { - "value": ["a", "wonderful", "world"], - "tags": ["observation"] - } - } - }, - { - "type": "run", - "application": "sina_test", - "id": "test_3", - "data": { - "scalar": { - "value": 12.3, - "units": "g/s", - "tags": ["hi"] - }, - "scalar_list": { - "value": [1, 2, 3.0, 4] - } - } - }, - { - "type": "bar", - "id": "test_4", - "data": { - "string": { - "value": "yarr" - }, - "string_list": { - "value": ["y", "a", "r"] - } - }, - "files": { - "test/test.png": {} - }, - "user_defined": { - "hello": "there" - } - } - ], - "relationships": [ - { - "predicate": "completes", - "subject": "test_2", - "object": "test_1" - }, - { - "subject": "test_3", - "predicate": "overrides", - "object": "test_4" - } - ] - })"; - axom::sina::Document myDocument = - Document(long_json, createRecordLoaderWithAllKnownTypes()); - EXPECT_EQ(2, myDocument.getRelationships().size()); - auto &records = myDocument.getRecords(); - EXPECT_EQ(4, records.size()); -} - -TEST(Document, create_fromJson_value_check) -{ - axom::sina::Document myDocument = - Document(data_json, createRecordLoaderWithAllKnownTypes()); - EXPECT_EQ(0, myDocument.getRelationships().size()); - auto &records = myDocument.getRecords(); - EXPECT_EQ(1, records.size()); - EXPECT_EQ(records[0]->getType(), "run"); - auto &data = records[0]->getData(); - EXPECT_EQ(data.at("int").getScalar(), 500.0); - std::vector expected_string_vals = {"z", "o", "o"}; - EXPECT_EQ(data.at("str/ings").getStringArray(), expected_string_vals); - EXPECT_EQ(records[0]->getFiles().count(File {"test/test.png"}), 1); -} - TEST(Document, toNode_empty) { // A sina document should always have, at minimum, both records and @@ -388,22 +352,6 @@ TEST(Document, create_fromJson_roundtrip_json) EXPECT_EQ(orig_json, returned_json1); } -TEST(Document, create_fromJson_roundtrip_hdf5) -{ - std::string orig_json = - "{\"records\": [{\"type\": \"test_rec\",\"id\": " - "\"test\"}],\"relationships\": []}"; - axom::sina::Document myDocument = - Document(orig_json, createRecordLoaderWithAllKnownTypes()); - saveDocument(myDocument, "round_json.hdf5", Protocol::HDF5); - Document loadedDocument = loadDocument("round_json.hdf5", Protocol::HDF5); - EXPECT_EQ(0, loadedDocument.getRelationships().size()); - ASSERT_EQ(1, loadedDocument.getRecords().size()); - EXPECT_EQ("test_rec", loadedDocument.getRecords()[0]->getType()); - std::string returned_json2 = loadedDocument.toJson(0, 0, "", ""); - EXPECT_EQ(orig_json, returned_json2); -} - TEST(Document, create_fromJson_full_json) { axom::sina::Document myDocument = @@ -413,17 +361,6 @@ TEST(Document, create_fromJson_full_json) EXPECT_EQ(4, records1.size()); } -TEST(Document, create_fromJson_full_hdf5) -{ - axom::sina::Document myDocument = - Document(long_json, createRecordLoaderWithAllKnownTypes()); - saveDocument(myDocument, "long_json.hdf5", Protocol::HDF5); - Document loadedDocument = loadDocument("long_json.hdf5", Protocol::HDF5); - EXPECT_EQ(2, loadedDocument.getRelationships().size()); - auto &records2 = loadedDocument.getRecords(); - EXPECT_EQ(4, records2.size()); -} - TEST(Document, create_fromJson_value_check_json) { axom::sina::Document myDocument = @@ -439,23 +376,6 @@ TEST(Document, create_fromJson_value_check_json) EXPECT_EQ(records1[0]->getFiles().count(File {"test/test.png"}), 1); } -TEST(Document, create_fromJson_value_check_hdf5) -{ - axom::sina::Document myDocument = - Document(data_json, createRecordLoaderWithAllKnownTypes()); - std::vector expected_string_vals = {"z", "o", "o"}; - saveDocument(myDocument, "data_json.hdf5", Protocol::HDF5); - Document loadedDocument = loadDocument("data_json.hdf5", Protocol::HDF5); - EXPECT_EQ(0, loadedDocument.getRelationships().size()); - auto &records2 = loadedDocument.getRecords(); - EXPECT_EQ(1, records2.size()); - EXPECT_EQ(records2[0]->getType(), "run"); - auto &data2 = records2[0]->getData(); - EXPECT_EQ(data2.at("int").getScalar(), 500.0); - EXPECT_EQ(data2.at("str/ings").getStringArray(), expected_string_vals); - EXPECT_EQ(records2[0]->getFiles().count(File {"test/test.png"}), 1); -} - TEST(Document, saveDocument_json) { NamedTempFile tmpFile; @@ -488,33 +408,7 @@ TEST(Document, saveDocument_json) EXPECT_EQ("the type", readRecord["type"].as_string()); } -TEST(Document, saveDocument_hdf5) -{ - NamedTempFile tmpFile; - - // First, write some random stuff to the temp file to make sure it is - // overwritten. - { - std::ofstream fout {tmpFile.getName()}; - fout << "Initial contents"; - } - - Document document; - document.add( - std::make_unique(ID {"the id", IDType::Global}, "the type")); - - saveDocument(document, tmpFile.getName(), Protocol::HDF5); - - conduit::Node readContents; - conduit::relay::io::load(tmpFile.getName(), "hdf5", readContents); - - ASSERT_TRUE(readContents[EXPECTED_RECORDS_KEY].dtype().is_list()); - EXPECT_EQ(1, readContents[EXPECTED_RECORDS_KEY].number_of_children()); - auto &readRecord = readContents[EXPECTED_RECORDS_KEY][0]; - EXPECT_EQ("the id", readRecord["id"].as_string()); - EXPECT_EQ("the type", readRecord["type"].as_string()); -} - +#ifdef AXOM_USE_HDF5 TEST(Document, load_specifiedRecordLoader) { using RecordType = TestRecord; @@ -567,6 +461,78 @@ TEST(Document, load_defaultRecordLoaders) EXPECT_NE(nullptr, loadedRun); } +TEST(Document, create_fromJson_roundtrip_hdf5) +{ + std::string orig_json = + "{\"records\": [{\"type\": \"test_rec\",\"id\": " + "\"test\"}],\"relationships\": []}"; + axom::sina::Document myDocument = + Document(orig_json, createRecordLoaderWithAllKnownTypes()); + saveDocument(myDocument, "round_json.hdf5", Protocol::HDF5); + Document loadedDocument = loadDocument("round_json.hdf5", Protocol::HDF5); + EXPECT_EQ(0, loadedDocument.getRelationships().size()); + ASSERT_EQ(1, loadedDocument.getRecords().size()); + EXPECT_EQ("test_rec", loadedDocument.getRecords()[0]->getType()); + std::string returned_json2 = loadedDocument.toJson(0, 0, "", ""); + EXPECT_EQ(orig_json, returned_json2); +} + +TEST(Document, create_fromJson_full_hdf5) +{ + axom::sina::Document myDocument = + Document(long_json, createRecordLoaderWithAllKnownTypes()); + saveDocument(myDocument, "long_json.hdf5", Protocol::HDF5); + Document loadedDocument = loadDocument("long_json.hdf5", Protocol::HDF5); + EXPECT_EQ(2, loadedDocument.getRelationships().size()); + auto &records2 = loadedDocument.getRecords(); + EXPECT_EQ(4, records2.size()); +} + +TEST(Document, create_fromJson_value_check_hdf5) +{ + axom::sina::Document myDocument = + Document(data_json, createRecordLoaderWithAllKnownTypes()); + std::vector expected_string_vals = {"z", "o", "o"}; + saveDocument(myDocument, "data_json.hdf5", Protocol::HDF5); + Document loadedDocument = loadDocument("data_json.hdf5", Protocol::HDF5); + EXPECT_EQ(0, loadedDocument.getRelationships().size()); + auto &records2 = loadedDocument.getRecords(); + EXPECT_EQ(1, records2.size()); + EXPECT_EQ(records2[0]->getType(), "run"); + auto &data2 = records2[0]->getData(); + EXPECT_EQ(data2.at("int").getScalar(), 500.0); + EXPECT_EQ(data2.at("str/ings").getStringArray(), expected_string_vals); + EXPECT_EQ(records2[0]->getFiles().count(File {"test/test.png"}), 1); +} + +TEST(Document, saveDocument_hdf5) +{ + NamedTempFile tmpFile; + + // First, write some random stuff to the temp file to make sure it is + // overwritten. + { + std::ofstream fout {tmpFile.getName()}; + fout << "Initial contents"; + } + + Document document; + document.add( + std::make_unique(ID {"the id", IDType::Global}, "the type")); + + saveDocument(document, tmpFile.getName(), Protocol::HDF5); + + conduit::Node readContents; + conduit::relay::io::load(tmpFile.getName(), "hdf5", readContents); + + ASSERT_TRUE(readContents[EXPECTED_RECORDS_KEY].dtype().is_list()); + EXPECT_EQ(1, readContents[EXPECTED_RECORDS_KEY].number_of_children()); + auto &readRecord = readContents[EXPECTED_RECORDS_KEY][0]; + EXPECT_EQ("the id", readRecord["id"].as_string()); + EXPECT_EQ("the type", readRecord["type"].as_string()); +} +#endif + } // namespace } // namespace testing } // namespace sina From ccfa51332c631506adb171a5ba391c99fe5ef84d Mon Sep 17 00:00:00 2001 From: Charles Doutriaux Date: Tue, 4 Feb 2025 11:25:56 -0800 Subject: [PATCH 18/38] bringing in submodule updates --- data | 2 +- scripts/spack/radiuss-spack-configs | 2 +- src/cmake/blt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data b/data index 1bff47e373..c0c33018bd 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit 1bff47e3735122ce7f79a7f741b51e15227a9f57 +Subproject commit c0c33018bd6796cfe2ff64e46bb2a16402f00f9c diff --git a/scripts/spack/radiuss-spack-configs b/scripts/spack/radiuss-spack-configs index 22f5feaabf..d43946bed0 160000 --- a/scripts/spack/radiuss-spack-configs +++ b/scripts/spack/radiuss-spack-configs @@ -1 +1 @@ -Subproject commit 22f5feaabfe618339cf621d2734fe0d66077da39 +Subproject commit d43946bed028933a8b8770adca3dde78987f6626 diff --git a/src/cmake/blt b/src/cmake/blt index 9cfe8aedb1..c98f320835 160000 --- a/src/cmake/blt +++ b/src/cmake/blt @@ -1 +1 @@ -Subproject commit 9cfe8aedb12c17da04b4df1859ec5e802030795c +Subproject commit c98f320835d71f778fbffcc48f07675142c08635 From c05979d4c9d27c2ae26001bae94a1e31b47d96e4 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Tue, 4 Feb 2025 12:17:24 -0800 Subject: [PATCH 19/38] Build Fix --- src/axom/sina/core/Document.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/axom/sina/core/Document.cpp b/src/axom/sina/core/Document.cpp index 146720ea8f..60b776d875 100644 --- a/src/axom/sina/core/Document.cpp +++ b/src/axom/sina/core/Document.cpp @@ -303,11 +303,13 @@ void saveDocument(Document const &document, fout << asJson; fout.close(); } +#ifdef AXOM_USE_HDF5 else if(protocol == Protocol::HDF5) { protocolWarn(".hdf5", fileName); document.toHDF5(tmpFileName); } +#endif else { throw std::invalid_argument( From 7c5d7cc99078399c5ba7ec3d39028990438bed9a Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Wed, 5 Feb 2025 16:42:10 -0800 Subject: [PATCH 20/38] Documentation Update --- src/axom/sina/core/Document.hpp | 20 +++++-- src/axom/sina/docs/imgs/JSON_vs_HDF5_size.png | Bin 0 -> 29939 bytes .../sina/docs/imgs/JSON_vs_HDF5_speed.png | Bin 0 -> 30194 bytes src/axom/sina/docs/sphinx/hdf5_vs_json.rst | 55 ++++++++++++++++++ 4 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 src/axom/sina/docs/imgs/JSON_vs_HDF5_size.png create mode 100644 src/axom/sina/docs/imgs/JSON_vs_HDF5_speed.png create mode 100644 src/axom/sina/docs/sphinx/hdf5_vs_json.rst diff --git a/src/axom/sina/core/Document.hpp b/src/axom/sina/core/Document.hpp index b98c944208..2483198cc8 100644 --- a/src/axom/sina/core/Document.hpp +++ b/src/axom/sina/core/Document.hpp @@ -52,11 +52,12 @@ const std::string supported_types[] = {"JSON", const std::string slashSubstitute = "__SINA_SLASHREPLACE__"; /** - * \brief An object representing the top-level object of a Sina JSON file + * \brief An object representing the top-level object of a Sina file * - * A Document represents the top-level object of a JSON file conforming to the + * A Document represents the top-level object of a file conforming to the * Sina schema. When serialized, these documents can be ingested into a - * Sina database and used with the Sina tool. + * Sina database and used with the Sina tool. Sina files are defaulted to + * JSON but optionally support HDF5. * * Documents contain at most two objects: a list of Records and a list of Relationships. A simple, empty document: * \code{.json} @@ -94,6 +95,13 @@ const std::string slashSubstitute = "__SINA_SLASHREPLACE__"; * \code * axom::sina::saveDocument(myDocument, "path/to/outfile.json") * \endcode + * + * Loading and Saving documents will default to the JSON file type, but if an optional file type is + * loaded the Protocol parameter will control your file type. For example with HDF5: + * \code + * axom::sina::Document myDocument = axom::sina::loadDocument("path/to/infile.hdf5, Protocol::HDF5"); + * axom::sina::saveDocument(myDocument, "path/to/outfile.hdf5", Protocol::HDF5) + * \endcode * * Check the Sina file format version with: * \code @@ -200,7 +208,7 @@ class Document /** * \brief Dump this document as an HDF5 File * - * \return None, conduit automatically dumps the hdf5 file without a return + * \param filename the location of which to save the file */ void toHDF5(const std::string &filename) const; #endif @@ -230,8 +238,8 @@ class Document * it will be overwritten. * * \param document the Document to save - * \param fileName the location to which to save the file - * \param protocol the file type requested to save as, default = JSON + * \param fileName the location of which to save the file + * \param protocol the file type requested to save as contained in supported_types, default = JSON * \throws std::ios::failure if there are any IO errors */ void saveDocument(Document const &document, diff --git a/src/axom/sina/docs/imgs/JSON_vs_HDF5_size.png b/src/axom/sina/docs/imgs/JSON_vs_HDF5_size.png new file mode 100644 index 0000000000000000000000000000000000000000..c028463e9c660735f3de9bcfe6843576692ca6cd GIT binary patch literal 29939 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 Date: Tue, 18 Feb 2025 14:26:55 -0800 Subject: [PATCH 21/38] Fixes submodule pointers --- data | 2 +- scripts/spack/radiuss-spack-configs | 2 +- src/cmake/blt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data b/data index c0c33018bd..1bff47e373 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit c0c33018bd6796cfe2ff64e46bb2a16402f00f9c +Subproject commit 1bff47e3735122ce7f79a7f741b51e15227a9f57 diff --git a/scripts/spack/radiuss-spack-configs b/scripts/spack/radiuss-spack-configs index d43946bed0..22f5feaabf 160000 --- a/scripts/spack/radiuss-spack-configs +++ b/scripts/spack/radiuss-spack-configs @@ -1 +1 @@ -Subproject commit d43946bed028933a8b8770adca3dde78987f6626 +Subproject commit 22f5feaabfe618339cf621d2734fe0d66077da39 diff --git a/src/cmake/blt b/src/cmake/blt index c98f320835..9cfe8aedb1 160000 --- a/src/cmake/blt +++ b/src/cmake/blt @@ -1 +1 @@ -Subproject commit c98f320835d71f778fbffcc48f07675142c08635 +Subproject commit 9cfe8aedb12c17da04b4df1859ec5e802030795c From 4310ee1e42bced2bc8da396dbd458b04df52eb33 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 18 Feb 2025 16:18:59 -0800 Subject: [PATCH 22/38] Fixes includes in sina Must include `axom/config.hpp` before checking defines like `AXOM_USE_HDF5` --- src/axom/sina/core/Document.cpp | 21 +++++++++--------- src/axom/sina/core/Document.hpp | 11 +++++----- src/axom/sina/examples/sina_basic.cpp | 3 ++- .../sina/examples/sina_document_assembly.cpp | 3 ++- src/axom/sina/examples/sina_tutorial.cpp | 1 + .../sina/interface/sina_fortran_interface.h | 3 --- src/axom/sina/tests/sina_Document.cpp | 22 ++++++++++--------- 7 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/axom/sina/core/Document.cpp b/src/axom/sina/core/Document.cpp index f3f57e7c23..b3423ea9ce 100644 --- a/src/axom/sina/core/Document.cpp +++ b/src/axom/sina/core/Document.cpp @@ -15,6 +15,16 @@ #include "axom/sina/core/Document.hpp" +#include "axom/config.hpp" +#include "axom/core/Path.hpp" +#include "axom/core/utilities/StringUtilities.hpp" + +#include "conduit.hpp" +#ifdef AXOM_USE_HDF5 + #include "conduit_relay.hpp" + #include "conduit_relay_io.hpp" +#endif + #include #include #include @@ -23,15 +33,6 @@ #include #include #include -#include "conduit.hpp" -#include "axom/core/Path.hpp" - -#ifdef AXOM_USE_HDF5 - #include "conduit_relay.hpp" - #include "conduit_relay_io.hpp" - - #include "axom/core/utilities/StringUtilities.hpp" -#endif namespace axom { @@ -361,4 +362,4 @@ Document loadDocument(std::string const &path, } } // namespace sina -} // namespace axom \ No newline at end of file +} // namespace axom diff --git a/src/axom/sina/core/Document.hpp b/src/axom/sina/core/Document.hpp index ed57bf4ab8..bddd23be72 100644 --- a/src/axom/sina/core/Document.hpp +++ b/src/axom/sina/core/Document.hpp @@ -16,13 +16,14 @@ ****************************************************************************** */ -#include -#include +#include "axom/config.hpp" +#include "axom/sina/core/Record.hpp" +#include "axom/sina/core/Relationship.hpp" #include "conduit.hpp" -#include "axom/sina/core/Record.hpp" -#include "axom/sina/core/Relationship.hpp" +#include +#include #define SINA_FILE_FORMAT_VERSION_MAJOR 1 #define SINA_FILE_FORMAT_VERSION_MINOR 0 @@ -284,4 +285,4 @@ Document loadDocument(std::string const &path, } // namespace sina } // namespace axom -#endif //SINA_DOCUMENT_HPP \ No newline at end of file +#endif //SINA_DOCUMENT_HPP diff --git a/src/axom/sina/examples/sina_basic.cpp b/src/axom/sina/examples/sina_basic.cpp index 2de4fa674c..0a689035d7 100644 --- a/src/axom/sina/examples/sina_basic.cpp +++ b/src/axom/sina/examples/sina_basic.cpp @@ -3,6 +3,7 @@ // // SPDX-License-Identifier: (BSD-3-Clause) +#include "axom/config.hpp" #include "axom/sina.hpp" int main(void) @@ -21,4 +22,4 @@ int main(void) axom::sina::saveDocument(document, "MySinaData.json"); // by specifying Protocol::HDF5, we also save a copy as an HDF5 file. axom::sina::saveDocument(document, "MySinaData.hdf5", axom::sina::Protocol::HDF5); -} \ No newline at end of file +} diff --git a/src/axom/sina/examples/sina_document_assembly.cpp b/src/axom/sina/examples/sina_document_assembly.cpp index a9d8f2239b..0b32e1e49e 100644 --- a/src/axom/sina/examples/sina_document_assembly.cpp +++ b/src/axom/sina/examples/sina_document_assembly.cpp @@ -3,6 +3,7 @@ // // SPDX-License-Identifier: (BSD-3-Clause) +#include "axom/config.hpp" #include "axom/sina.hpp" int main(void) @@ -38,4 +39,4 @@ int main(void) // We will also save a copy of the document as an HDF5 file // which can be done by passing the protocol as HDF5 axom::sina::saveDocument(document, "MySinaData.hdf5", axom::sina::Protocol::HDF5); -} \ No newline at end of file +} diff --git a/src/axom/sina/examples/sina_tutorial.cpp b/src/axom/sina/examples/sina_tutorial.cpp index 6efcb29b33..0470dfc3db 100644 --- a/src/axom/sina/examples/sina_tutorial.cpp +++ b/src/axom/sina/examples/sina_tutorial.cpp @@ -3,6 +3,7 @@ // // SPDX-License-Identifier: (BSD-3-Clause) +#include "axom/config.hpp" #include "axom/sina.hpp" #include diff --git a/src/axom/sina/interface/sina_fortran_interface.h b/src/axom/sina/interface/sina_fortran_interface.h index ec357bf930..0a11010c9c 100644 --- a/src/axom/sina/interface/sina_fortran_interface.h +++ b/src/axom/sina/interface/sina_fortran_interface.h @@ -3,9 +3,6 @@ // // SPDX-License-Identifier: (BSD-3-Clause) -#include "axom/sina/core/Document.hpp" -#include "axom/sina/core/Record.hpp" -#include "axom/sina/core/Run.hpp" #include "axom/sina.hpp" extern "C" char *Get_File_Extension(char *); diff --git a/src/axom/sina/tests/sina_Document.cpp b/src/axom/sina/tests/sina_Document.cpp index 0789e9109e..9786440edd 100644 --- a/src/axom/sina/tests/sina_Document.cpp +++ b/src/axom/sina/tests/sina_Document.cpp @@ -3,24 +3,26 @@ // // SPDX-License-Identifier: (BSD-3-Clause) -#include -#include -#include -#include -#include - -#include "gtest/gtest.h" -#include "gmock/gmock.h" - +#include "axom/config.hpp" #include "axom/core/utilities/FileUtilities.hpp" + #include "axom/sina/core/Document.hpp" #include "axom/sina/core/Run.hpp" - #include "axom/sina/tests/TestRecord.hpp" + #include "conduit.hpp" #include "conduit_relay.hpp" #include "conduit_relay_io.hpp" +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include +#include +#include +#include +#include + namespace axom { namespace sina From 0fd3a354c034a7b280d073aefc0863fbfa91479b Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 18 Feb 2025 16:27:38 -0800 Subject: [PATCH 23/38] Guards usage of sina::Protocol::HDF5 --- src/axom/sina/examples/sina_basic.cpp | 3 +++ src/axom/sina/examples/sina_document_assembly.cpp | 2 ++ src/axom/sina/examples/sina_tutorial.cpp | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/src/axom/sina/examples/sina_basic.cpp b/src/axom/sina/examples/sina_basic.cpp index 0a689035d7..3ea779cb4f 100644 --- a/src/axom/sina/examples/sina_basic.cpp +++ b/src/axom/sina/examples/sina_basic.cpp @@ -20,6 +20,9 @@ int main(void) // Save the document directly to a file. // since we gave saveDocument no protocol parameter, it will default to JSON axom::sina::saveDocument(document, "MySinaData.json"); + +#ifdef AXOM_USE_HDF5 // by specifying Protocol::HDF5, we also save a copy as an HDF5 file. axom::sina::saveDocument(document, "MySinaData.hdf5", axom::sina::Protocol::HDF5); +#endif } diff --git a/src/axom/sina/examples/sina_document_assembly.cpp b/src/axom/sina/examples/sina_document_assembly.cpp index 0b32e1e49e..6dc4002254 100644 --- a/src/axom/sina/examples/sina_document_assembly.cpp +++ b/src/axom/sina/examples/sina_document_assembly.cpp @@ -36,7 +36,9 @@ int main(void) // since we gave saveDocument no optional protocol parameter, it will default to JSON axom::sina::saveDocument(document, "MySinaData.json"); +#ifdef AXOM_USE_HDF5 // We will also save a copy of the document as an HDF5 file // which can be done by passing the protocol as HDF5 axom::sina::saveDocument(document, "MySinaData.hdf5", axom::sina::Protocol::HDF5); +#endif } diff --git a/src/axom/sina/examples/sina_tutorial.cpp b/src/axom/sina/examples/sina_tutorial.cpp index 0470dfc3db..92e5ccac11 100644 --- a/src/axom/sina/examples/sina_tutorial.cpp +++ b/src/axom/sina/examples/sina_tutorial.cpp @@ -131,7 +131,9 @@ void gatherAllData(axom::sina::Record &record) void save(axom::sina::Document const &doc) { axom::sina::saveDocument(doc, "my_output.json"); +#ifdef AXOM_USE_HDF5 axom::sina::saveDocument(doc, "my_output.hdf5", axom::sina::Protocol::HDF5); +#endif } //! [end io write] @@ -139,8 +141,10 @@ void save(axom::sina::Document const &doc) void load() { axom::sina::Document doc1 = axom::sina::loadDocument("my_output.json"); +#ifdef AXOM_USE_HDF5 axom::sina::Document doc2 = axom::sina::loadDocument("my_output.hdf5", axom::sina::Protocol::HDF5); +#endif } //! [end io read] From 1145c295b2cffea958adfc02686007c127a9cdbd Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Fri, 21 Feb 2025 17:10:30 -0800 Subject: [PATCH 24/38] Guarding and Fortran tests --- data | 2 +- scripts/spack/radiuss-spack-configs | 2 +- src/axom/sina/core/Document.cpp | 30 ++- src/axom/sina/core/Document.hpp | 17 +- src/axom/sina/docs/sphinx/documents.rst | 2 +- src/axom/sina/tests/sina_Document.cpp | 2 +- .../sina/tests/test_fortran_integration.py | 173 ++++++++++++++++-- src/cmake/blt | 2 +- 8 files changed, 196 insertions(+), 34 deletions(-) diff --git a/data b/data index 1bff47e373..c0c33018bd 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit 1bff47e3735122ce7f79a7f741b51e15227a9f57 +Subproject commit c0c33018bd6796cfe2ff64e46bb2a16402f00f9c diff --git a/scripts/spack/radiuss-spack-configs b/scripts/spack/radiuss-spack-configs index 22f5feaabf..d43946bed0 160000 --- a/scripts/spack/radiuss-spack-configs +++ b/scripts/spack/radiuss-spack-configs @@ -1 +1 @@ -Subproject commit 22f5feaabfe618339cf621d2734fe0d66077da39 +Subproject commit d43946bed028933a8b8770adca3dde78987f6626 diff --git a/src/axom/sina/core/Document.cpp b/src/axom/sina/core/Document.cpp index b3423ea9ce..dc98593a1c 100644 --- a/src/axom/sina/core/Document.cpp +++ b/src/axom/sina/core/Document.cpp @@ -12,7 +12,6 @@ * ****************************************************************************** */ - #include "axom/sina/core/Document.hpp" #include "axom/config.hpp" @@ -66,6 +65,21 @@ void protocolWarn(std::string const protocol, std::string const &name) } } +std::string get_supported_file_types() +{ + std::string types = "["; + for(size_t i = 0; i < supported_types.size(); ++i) + { + types += supported_types[i]; + if(i < supported_types.size() - 1) + { + types += ", "; + } + } + types += "]"; + return types; +} + void Document::add(std::unique_ptr record) { records.emplace_back(std::move(record)); @@ -313,8 +327,11 @@ void saveDocument(Document const &document, #endif else { - throw std::invalid_argument( - "Invalid format choice. Please enter 'json' or 'hdf5'."); + std::ostringstream message; + message << "Invalid format choice. Please choose from one of the supported " + "protocols: " + << get_supported_file_types(); + throw std::invalid_argument(message.str()); } if(rename(tmpFileName.c_str(), fileName.c_str()) != 0) @@ -347,7 +364,6 @@ Document loadDocument(std::string const &path, file_in.close(); node.parse(file_contents.str(), "json"); return Document {node, recordLoader}; - #ifdef AXOM_USE_HDF5 case Protocol::HDF5: file_in.close(); @@ -355,8 +371,12 @@ Document loadDocument(std::string const &path, restoreSlashes(node, modifiedNode); return Document {modifiedNode, recordLoader}; #endif - default: + std::ostringstream message; + message << "Invalid format choice. Please choose from one of the supported " + "protocols: " + << get_supported_file_types(); + throw std::invalid_argument(message.str()); break; } } diff --git a/src/axom/sina/core/Document.hpp b/src/axom/sina/core/Document.hpp index bddd23be72..d179982448 100644 --- a/src/axom/sina/core/Document.hpp +++ b/src/axom/sina/core/Document.hpp @@ -36,14 +36,12 @@ namespace sina enum class Protocol { JSON, -#ifdef AXOM_USE_HDF5 HDF5 -#endif }; -const std::string supported_types[] = {"JSON", +const std::vector supported_types = {"JSON", #ifdef AXOM_USE_HDF5 - "HDF5" + "HDF5" #endif }; @@ -224,6 +222,13 @@ class Document const std::string &pad = "", const std::string &eoe = "") const; + /** + * \brief Get the list of file types currently supported by the implementation. + * + * \return a string of supported file types + */ + std::string get_supported_file_types(); + private: /** * Constructor helper method, extracts info from a conduit Node. @@ -237,11 +242,12 @@ class Document /** * \brief Save the given Document to the specified location. If the given file exists, * it will be overwritten. - * + * * \param document the Document to save * \param fileName the location of which to save the file * \param protocol the file type requested to save as contained in supported_types, default = JSON * \throws std::ios::failure if there are any IO errors + * std::invalid_argument if the protocol given is an undefined, optional protocol */ void saveDocument(Document const &document, std::string const &fileName, @@ -276,6 +282,7 @@ Document loadDocument(std::string const &path, * \param recordLoader the RecordLoader to use to load the different types * of records * \param protocol the type of file being loaded, default = JSON + * \throws std::invalid_argument if the protocol given is an undefined, optional protocol * \return the loaded Document */ Document loadDocument(std::string const &path, diff --git a/src/axom/sina/docs/sphinx/documents.rst b/src/axom/sina/docs/sphinx/documents.rst index 8bf4036e26..853f6f433f 100644 --- a/src/axom/sina/docs/sphinx/documents.rst +++ b/src/axom/sina/docs/sphinx/documents.rst @@ -1,4 +1,4 @@ -.. ## Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +.. ## Copyright (c) 2017-2024, Lawrence Livermore National Security, LLC and .. ## other Axom Project Developers. See the top-level LICENSE file for details. .. ## .. ## SPDX-License-Identifier: (BSD-3-Clause) diff --git a/src/axom/sina/tests/sina_Document.cpp b/src/axom/sina/tests/sina_Document.cpp index 9786440edd..74f8278005 100644 --- a/src/axom/sina/tests/sina_Document.cpp +++ b/src/axom/sina/tests/sina_Document.cpp @@ -410,7 +410,6 @@ TEST(Document, saveDocument_json) EXPECT_EQ("the type", readRecord["type"].as_string()); } -#ifdef AXOM_USE_HDF5 TEST(Document, load_specifiedRecordLoader) { using RecordType = TestRecord; @@ -463,6 +462,7 @@ TEST(Document, load_defaultRecordLoaders) EXPECT_NE(nullptr, loadedRun); } +#ifdef AXOM_USE_HDF5 TEST(Document, create_fromJson_roundtrip_hdf5) { std::string orig_json = diff --git a/src/axom/sina/tests/test_fortran_integration.py b/src/axom/sina/tests/test_fortran_integration.py index 08cb20cf15..21fd875c12 100644 --- a/src/axom/sina/tests/test_fortran_integration.py +++ b/src/axom/sina/tests/test_fortran_integration.py @@ -4,7 +4,12 @@ import os import subprocess import unittest - +import numpy as np +try: + import h5py + AXOM_USE_HDF5 = True +except ImportError: + AXOM_USE_HDF5 = False def parse_args(): """Helper function to obtain the binary directory path of Axom from CLI""" @@ -14,28 +19,34 @@ def parse_args(): # Add other arguments as needed return parser.parse_args() - -class TestFortranExampleIntegration(unittest.TestCase): +# JSON Tests: Will always run +class TestFortranExampleIntegrationJSON(unittest.TestCase): @classmethod def setUpClass(cls): """ - Obtain the binary directory from the CLI. + Obtain the binary directory from the CLI and compile the sina fortran + example needed for these tests if necessary. """ + cwd = os.getcwd() + args = parse_args() cls.binary_dir = args.binary_dir if cls.binary_dir is None: # Assume we're at /path/to/build_dir/axom/sina/tests so move up to build_dir - cls.binary_dir = os.path.join(os.getcwd(), "..", "..", "..") + cls.binary_dir = f"{cwd}/../../../" + + os.chdir(cls.binary_dir) + + if not os.path.exists(f"{cls.binary_dir}/examples/sina_fortran_ex"): + subprocess.run(["make", "sina_fortran_ex"]) + + os.chdir(cwd) + def setUp(self): """ Invoke example Fortran application to dump a sina file """ - sina_fortran_ex_path = os.path.join(self.binary_dir, "examples", "sina_fortran_ex") - if not os.path.exists(sina_fortran_ex_path): - raise FileNotFoundError( - f"The sina_fortran_ex needed for running fortran tests could not be found at path '{sina_fortran_ex_path}'" - ) - subprocess.run([sina_fortran_ex_path]) + subprocess.run([f"{self.binary_dir}/examples/sina_fortran_ex"]) self.dump_file = "sina_dump.json" def tearDown(self): @@ -47,6 +58,7 @@ def test_file_validity(self): try: import jsonschema schema_file = os.path.join(self.binary_dir, "tests", "sina_schema.json") + with io.open(schema_file, "r", encoding="utf-8") as schema: schema = json.load(schema) with io.open(self.dump_file, "r", encoding="utf-8") as loaded_test: @@ -55,7 +67,6 @@ def test_file_validity(self): except ModuleNotFoundError: print("jsonschema module not found. Skipping test_file_validity.") pass - def test_validate_contents_of_record(self): """ Ensure that the record written out matches what we expect """ @@ -69,12 +80,9 @@ def test_validate_contents_of_record(self): self.assertEqual("my_type", record["type"]) # Test the files - path_to_my_file = os.path.join("/path", "to", "my", "file") - my_file = os.path.join(path_to_my_file, "my_file.txt") - other_file = os.path.join(path_to_my_file, "my_other_file.txt") - self.assertEqual(list(record["files"].keys()), [other_file, my_file]) - self.assertEqual(record["files"][other_file]["mimetype"], "png") - self.assertEqual(record["files"][my_file]["mimetype"], "txt") + self.assertEqual(list(record["files"].keys()), ["/path/to/my/file/my_other_file.txt", "/path/to/my/file/my_file.txt"]) + self.assertEqual(record["files"]["/path/to/my/file/my_other_file.txt"]["mimetype"], "png") + self.assertEqual(record["files"]["/path/to/my/file/my_file.txt"]["mimetype"], "txt") # Test the signed variants self.assertEqual("A", record["data"]["char"]["value"]) @@ -116,8 +124,135 @@ def test_validate_contents_of_record(self): self.assertEqual(double_arr, record["curve_sets"][curveset]["dependent"][double_2_name]["value"]) +#HDF5 Test +@unittest.skipUnless(AXOM_USE_HDF5, "Requires h5py for HDF5-dependent tests") +class TestFortranExampleIntegrationHDF5(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cwd = os.getcwd() + args = parse_args() + cls.binary_dir = args.binary_dir or f"{cwd}/../../../" + os.chdir(cls.binary_dir) + if not os.path.exists(f"{cls.binary_dir}/examples/sina_fortran_ex"): + subprocess.run(["make", "sina_fortran_ex"]) + os.chdir(cwd) + + def setUp(self): + subprocess.run([f"{self.binary_dir}/examples/sina_fortran_ex"]) + self.dump_file = "sina_dump.hdf5" + + def tearDown(self): + os.remove(self.dump_file) + + def extract_hdf5_value(self, value): + if isinstance(value, h5py.Dataset): + value = value[()] + + if isinstance(value, bytes): + return value.decode("utf-8").strip("\0").strip() + + if isinstance(value, np.ndarray): + if value.dtype.kind in {'U', 'S'}: + result = b"".join(value.astype("S")).decode("utf-8").strip("\0").strip() + else: + result = value.tolist() + if isinstance(result, list) and len(result) == 1: + result = result[0] + return result + + if isinstance(value, (np.integer, np.floating)): + return value.item() + + return value + + def test_validate_contents_of_record(self): + with h5py.File(self.dump_file, "r") as f: + record = f["records"]["0"] + + # Validate metadata + self.assertEqual("my_rec_id", self.extract_hdf5_value(record["id"])) + self.assertEqual("my_type", self.extract_hdf5_value(record["type"])) + + # Validate Files + files_group = record["files"] + expected_file_keys = [ + "__SINA_SLASHREPLACE__path__SINA_SLASHREPLACE__to__SINA_SLASHREPLACE__my__SINA_SLASHREPLACE__file__SINA_SLASHREPLACE__my_other_file.txt", + "__SINA_SLASHREPLACE__path__SINA_SLASHREPLACE__to__SINA_SLASHREPLACE__my__SINA_SLASHREPLACE__file__SINA_SLASHREPLACE__my_file.txt" + ] + self.assertEqual(sorted(list(files_group.keys())), sorted(expected_file_keys)) + self.assertEqual(self.extract_hdf5_value( + files_group["__SINA_SLASHREPLACE__path__SINA_SLASHREPLACE__to__SINA_SLASHREPLACE__my__SINA_SLASHREPLACE__file__SINA_SLASHREPLACE__my_other_file.txt"]["mimetype"]), + "png") + self.assertEqual(self.extract_hdf5_value( + files_group["__SINA_SLASHREPLACE__path__SINA_SLASHREPLACE__to__SINA_SLASHREPLACE__my__SINA_SLASHREPLACE__file__SINA_SLASHREPLACE__my_file.txt"]["mimetype"]), + "txt") + + # Validate Data + data_group = record["data"] + self.assertEqual(self.extract_hdf5_value(data_group["char"]["value"]), "A") + self.assertEqual(self.extract_hdf5_value(data_group["int"]["value"]), 10) + self.assertEqual(self.extract_hdf5_value(data_group["logical"]["value"]), 0) + self.assertEqual(self.extract_hdf5_value(data_group["long"]["value"]), 1000000000.0) + self.assertAlmostEqual(self.extract_hdf5_value(data_group["real"]["value"]), 1.23456704616547) + self.assertAlmostEqual(self.extract_hdf5_value(data_group["double"]["value"]), 0.810000002384186) + + self.assertEqual(self.extract_hdf5_value(data_group["u_char"]["value"]), "A") + self.assertEqual(self.extract_hdf5_value(data_group["u_char"]["units"]), "kg") + self.assertEqual(self.extract_hdf5_value(data_group["u_int"]["value"]), 10) + self.assertEqual(self.extract_hdf5_value(data_group["u_int"]["units"]), "kg") + self.assertEqual(self.extract_hdf5_value(data_group["u_logical"]["value"]), 1.0) + self.assertEqual(self.extract_hdf5_value(data_group["u_logical"]["units"]), "kg") + self.assertEqual(self.extract_hdf5_value(data_group["u_long"]["value"]), 1000000000.0) + self.assertEqual(self.extract_hdf5_value(data_group["u_long"]["units"]), "kg") + self.assertAlmostEqual(self.extract_hdf5_value(data_group["u_real"]["value"]), 1.23456704616547) + self.assertEqual(self.extract_hdf5_value(data_group["u_real"]["units"]), "kg") + self.assertAlmostEqual(self.extract_hdf5_value(data_group["u_double"]["value"]), 0.810000002384186) + self.assertEqual(self.extract_hdf5_value(data_group["u_double"]["units"]), "kg") + self.assertAlmostEqual(self.extract_hdf5_value(data_group["u_double_w_tag"]["value"]), 0.810000002384186) + self.assertEqual(self.extract_hdf5_value(data_group["u_double_w_tag"]["units"]), "kg") + + tags_value = data_group["u_double_w_tag"]["tags"] + if isinstance(tags_value, h5py.Group): + if len(tags_value) == 1: + inner_dataset = list(tags_value.values())[0] + tags_value = self.extract_hdf5_value(inner_dataset) + if isinstance(tags_value, str): + tags_value = [tag.strip() for tag in tags_value.split(',')] + self.assertEqual(tags_value, ["new_fancy_tag"]) + + # Validate Curves + curveset_group = record["curve_sets"]["my_curveset"] + independent_group = curveset_group["independent"] + dependent_group = curveset_group["dependent"] + + nums = list(range(1, 21)) + real_arr = [i for i in nums] + double_arr = [i * 2 for i in nums] + int_arr = [i * 3 for i in nums] + long_arr = [i * 4 for i in nums] + + for kind, grp in (("indep", independent_group), ("dep", dependent_group)): + for val_type, target in (("real", real_arr), + ("double", double_arr), + ("int", int_arr), + ("long", long_arr)): + curve_name = f"my_{kind}_curve_{val_type}" + self.assertIn(curve_name, grp) + curve_val = self.extract_hdf5_value(grp[curve_name]["value"]) + self.assertEqual(curve_val, target) + + double_2_name = "my_dep_curve_double_2" + self.assertIn(double_2_name, dependent_group) + curve_double_2 = self.extract_hdf5_value(dependent_group[double_2_name]["value"]) + self.assertEqual(curve_double_2, double_arr) + + if __name__ == "__main__": # Doing the below instead of unittest.main() so that we can print to stdout - suite = unittest.TestLoader().loadTestsFromTestCase(TestFortranExampleIntegration) + suite = unittest.TestSuite() + suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestFortranExampleIntegrationJSON)) + if AXOM_USE_HDF5: + suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestFortranExampleIntegrationHDF5)) runner = unittest.TextTestRunner(buffer=False) runner.run(suite) \ No newline at end of file diff --git a/src/cmake/blt b/src/cmake/blt index 9cfe8aedb1..c98f320835 160000 --- a/src/cmake/blt +++ b/src/cmake/blt @@ -1 +1 @@ -Subproject commit 9cfe8aedb12c17da04b4df1859ec5e802030795c +Subproject commit c98f320835d71f778fbffcc48f07675142c08635 From 3ccfbb36db4535065bda0279ce0eead02c5828e4 Mon Sep 17 00:00:00 2001 From: Charles Doutriaux Date: Mon, 24 Feb 2025 10:34:52 -0800 Subject: [PATCH 25/38] submodule fix? --- data | 2 +- src/cmake/blt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data b/data index c0c33018bd..1bff47e373 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit c0c33018bd6796cfe2ff64e46bb2a16402f00f9c +Subproject commit 1bff47e3735122ce7f79a7f741b51e15227a9f57 diff --git a/src/cmake/blt b/src/cmake/blt index c98f320835..9cfe8aedb1 160000 --- a/src/cmake/blt +++ b/src/cmake/blt @@ -1 +1 @@ -Subproject commit c98f320835d71f778fbffcc48f07675142c08635 +Subproject commit 9cfe8aedb12c17da04b4df1859ec5e802030795c From 9429b60c710f6e7bc589e0408edf0b8240574071 Mon Sep 17 00:00:00 2001 From: Charles Doutriaux Date: Mon, 24 Feb 2025 10:38:02 -0800 Subject: [PATCH 26/38] more submodule fix --- scripts/spack/radiuss-spack-configs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/spack/radiuss-spack-configs b/scripts/spack/radiuss-spack-configs index d43946bed0..22f5feaabf 160000 --- a/scripts/spack/radiuss-spack-configs +++ b/scripts/spack/radiuss-spack-configs @@ -1 +1 @@ -Subproject commit d43946bed028933a8b8770adca3dde78987f6626 +Subproject commit 22f5feaabfe618339cf621d2734fe0d66077da39 From 45ee75ba35885eeec80993c0260af858d8521170 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Mon, 24 Feb 2025 13:05:58 -0800 Subject: [PATCH 27/38] Fix Fortran Test --- src/axom/sina/CMakeLists.txt | 20 +++++++++++++++-- src/axom/sina/docs/sphinx/documents.rst | 2 +- src/axom/sina/examples/CMakeLists.txt | 22 ++++++++++++++----- ...erface.f => sina_fortran_interface.f90.in} | 7 +++++- .../sina/tests/test_fortran_integration.py | 2 +- 5 files changed, 42 insertions(+), 11 deletions(-) rename src/axom/sina/interface/{sina_fortran_interface.f => sina_fortran_interface.f90.in} (96%) diff --git a/src/axom/sina/CMakeLists.txt b/src/axom/sina/CMakeLists.txt index c7cb0c0827..c689a0b46d 100644 --- a/src/axom/sina/CMakeLists.txt +++ b/src/axom/sina/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +# Copyright (c) 2017-2024, Lawrence Livermore National Security, LLC and # other Axom Project Developers. See the top-level LICENSE file for details. # # SPDX-License-Identifier: (BSD-3-Clause) @@ -14,6 +14,15 @@ axom_component_requires(NAME Sina TPLS Conduit ) +#------------------------------------------------------------------------------ +# Set sina version +#------------------------------------------------------------------------------ +set(SINA_VERSION_MAJOR 1) +set(SINA_VERSION_MINOR 14) +set(SINA_VERSION_PATCH 0) +axom_configure_file ( config.hpp.in + ${PROJECT_BINARY_DIR}/include/axom/sina/config.hpp ) + #------------------------------------------------------------------------------ # Specify the sina headers/sources #------------------------------------------------------------------------------ @@ -49,11 +58,18 @@ set(sina_sources blt_list_append( TO sina_headers ELEMENTS core/AdiakWriter.hpp IF AXOM_USE_ADIAK ) blt_list_append( TO sina_sources ELEMENTS core/AdiakWriter.cpp IF AXOM_USE_ADIAK ) +if(AXOM_USE_HDF5 AND ENABLE_FORTRAN) + set(AXOM_USE_HDF5_FORTRAN ".true.") +else() + set(AXOM_USE_HDF5_FORTRAN ".false.") +endif() + # Add fortran interface for Sina if (ENABLE_FORTRAN) blt_list_append( TO sina_headers ELEMENTS interface/sina_fortran_interface.h) blt_list_append( TO sina_sources - ELEMENTS interface/sina_fortran_interface.cpp interface/sina_fortran_interface.f) + ELEMENTS interface/sina_fortran_interface.cpp interface/sina_fortran_interface.f90.in) + configure_file(interface/sina_fortran_interface.f90.in interface/sina_fortran_interface.f90 @ONLY) endif() #------------------------------------------------------------------------------ diff --git a/src/axom/sina/docs/sphinx/documents.rst b/src/axom/sina/docs/sphinx/documents.rst index 853f6f433f..8bf4036e26 100644 --- a/src/axom/sina/docs/sphinx/documents.rst +++ b/src/axom/sina/docs/sphinx/documents.rst @@ -1,4 +1,4 @@ -.. ## Copyright (c) 2017-2024, Lawrence Livermore National Security, LLC and +.. ## Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and .. ## other Axom Project Developers. See the top-level LICENSE file for details. .. ## .. ## SPDX-License-Identifier: (BSD-3-Clause) diff --git a/src/axom/sina/examples/CMakeLists.txt b/src/axom/sina/examples/CMakeLists.txt index 93e307e99f..0d874db411 100644 --- a/src/axom/sina/examples/CMakeLists.txt +++ b/src/axom/sina/examples/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +# Copyright (c) 2017-2024, Lawrence Livermore National Security, LLC and # other Axom Project Developers. See the top-level LICENSE file for details. # # SPDX-License-Identifier: (BSD-3-Clause) @@ -27,10 +27,10 @@ set(sina_example_sources sina_view_datum_values.cpp ) -set(sina_example_depends sina conduit slic) +set(sina_example_depends sina conduit::conduit) if (ENABLE_FORTRAN) - blt_list_append( TO sina_example_sources ELEMENTS sina_fortran.f) + blt_list_append( TO sina_example_sources ELEMENTS sina_fortran.f90) endif() #------------------------------------------------------------------------------ @@ -38,15 +38,25 @@ endif() #------------------------------------------------------------------------------ foreach(src ${sina_example_sources}) get_filename_component(exe_name ${src} NAME_WE) + + if (exe_name STREQUAL "sina_fortran") + set(sina_mod_src ${CMAKE_CURRENT_BINARY_DIR}/../interface/sina_fortran_interface.f90) + else() + set(sina_mod_src "") + endif() + axom_add_executable( NAME ${exe_name}_ex - SOURCES ${src} + SOURCES ${src} ${sina_mod_src} OUTPUT_DIR ${EXAMPLE_OUTPUT_DIRECTORY} DEPENDS_ON ${sina_example_depends} FOLDER axom/sina/examples - ) + ) + + if (exe_name STREQUAL "sina_fortran") + target_include_directories(${exe_name}_ex PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/../interface) + endif() - # Need to add this flag so XL will ignore trailing underscores in fortran function names if (${exe_name}_ex STREQUAL "sina_fortran_ex" AND CMAKE_Fortran_COMPILER_ID STREQUAL "XL") target_compile_options(${exe_name}_ex PRIVATE -qextname) endif() diff --git a/src/axom/sina/interface/sina_fortran_interface.f b/src/axom/sina/interface/sina_fortran_interface.f90.in similarity index 96% rename from src/axom/sina/interface/sina_fortran_interface.f rename to src/axom/sina/interface/sina_fortran_interface.f90.in index 8362ab8d10..320c8ada8a 100644 --- a/src/axom/sina/interface/sina_fortran_interface.f +++ b/src/axom/sina/interface/sina_fortran_interface.f90.in @@ -144,4 +144,9 @@ subroutine save_without_protocol(fname) call write_sina_document_noprotocol(fname) end subroutine save_without_protocol -end module \ No newline at end of file +end module + +module hdf5_config + implicit none + logical, parameter :: use_hdf5 = @AXOM_USE_HDF5_FORTRAN@ +end module hdf5_config \ No newline at end of file diff --git a/src/axom/sina/tests/test_fortran_integration.py b/src/axom/sina/tests/test_fortran_integration.py index 21fd875c12..d78f509dad 100644 --- a/src/axom/sina/tests/test_fortran_integration.py +++ b/src/axom/sina/tests/test_fortran_integration.py @@ -21,7 +21,7 @@ def parse_args(): # JSON Tests: Will always run class TestFortranExampleIntegrationJSON(unittest.TestCase): - + @classmethod def setUpClass(cls): """ From 0f016bb8eef752abfe455544170093f5c1a0ad63 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Mon, 24 Feb 2025 15:33:37 -0800 Subject: [PATCH 28/38] CMakeLists Fix --- src/axom/sina/CMakeLists.txt | 17 ++++------------- src/axom/sina/examples/CMakeLists.txt | 5 +++-- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/axom/sina/CMakeLists.txt b/src/axom/sina/CMakeLists.txt index c689a0b46d..c14eec8b3e 100644 --- a/src/axom/sina/CMakeLists.txt +++ b/src/axom/sina/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2017-2024, Lawrence Livermore National Security, LLC and +# Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and # other Axom Project Developers. See the top-level LICENSE file for details. # # SPDX-License-Identifier: (BSD-3-Clause) @@ -14,15 +14,6 @@ axom_component_requires(NAME Sina TPLS Conduit ) -#------------------------------------------------------------------------------ -# Set sina version -#------------------------------------------------------------------------------ -set(SINA_VERSION_MAJOR 1) -set(SINA_VERSION_MINOR 14) -set(SINA_VERSION_PATCH 0) -axom_configure_file ( config.hpp.in - ${PROJECT_BINARY_DIR}/include/axom/sina/config.hpp ) - #------------------------------------------------------------------------------ # Specify the sina headers/sources #------------------------------------------------------------------------------ @@ -58,17 +49,17 @@ set(sina_sources blt_list_append( TO sina_headers ELEMENTS core/AdiakWriter.hpp IF AXOM_USE_ADIAK ) blt_list_append( TO sina_sources ELEMENTS core/AdiakWriter.cpp IF AXOM_USE_ADIAK ) +# Add fortran interface for Sina if(AXOM_USE_HDF5 AND ENABLE_FORTRAN) set(AXOM_USE_HDF5_FORTRAN ".true.") else() set(AXOM_USE_HDF5_FORTRAN ".false.") endif() -# Add fortran interface for Sina if (ENABLE_FORTRAN) blt_list_append( TO sina_headers ELEMENTS interface/sina_fortran_interface.h) blt_list_append( TO sina_sources - ELEMENTS interface/sina_fortran_interface.cpp interface/sina_fortran_interface.f90.in) + ELEMENTS interface/sina_fortran_interface.cpp interface/sina_fortran_interface.f90.in) configure_file(interface/sina_fortran_interface.f90.in interface/sina_fortran_interface.f90 @ONLY) endif() @@ -103,4 +94,4 @@ endif() if(AXOM_ENABLE_EXAMPLES) add_subdirectory(examples) -endif() +endif() \ No newline at end of file diff --git a/src/axom/sina/examples/CMakeLists.txt b/src/axom/sina/examples/CMakeLists.txt index 0d874db411..48c96a4b53 100644 --- a/src/axom/sina/examples/CMakeLists.txt +++ b/src/axom/sina/examples/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2017-2024, Lawrence Livermore National Security, LLC and +# Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and # other Axom Project Developers. See the top-level LICENSE file for details. # # SPDX-License-Identifier: (BSD-3-Clause) @@ -27,6 +27,7 @@ set(sina_example_sources sina_view_datum_values.cpp ) +set(sina_example_depends sina conduit slic) set(sina_example_depends sina conduit::conduit) if (ENABLE_FORTRAN) @@ -60,4 +61,4 @@ foreach(src ${sina_example_sources}) if (${exe_name}_ex STREQUAL "sina_fortran_ex" AND CMAKE_Fortran_COMPILER_ID STREQUAL "XL") target_compile_options(${exe_name}_ex PRIVATE -qextname) endif() -endforeach() +endforeach() \ No newline at end of file From 4a2cf7e5a7802405184de853f8b20a540852b6c6 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Mon, 24 Feb 2025 15:37:00 -0800 Subject: [PATCH 29/38] Minor Fix --- src/axom/sina/examples/{sina_fortran.f => sina_fortran.f90} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/axom/sina/examples/{sina_fortran.f => sina_fortran.f90} (100%) diff --git a/src/axom/sina/examples/sina_fortran.f b/src/axom/sina/examples/sina_fortran.f90 similarity index 100% rename from src/axom/sina/examples/sina_fortran.f rename to src/axom/sina/examples/sina_fortran.f90 From 2499896ccd71c296189ff6189a346375b88f83df Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Mon, 24 Feb 2025 17:18:17 -0800 Subject: [PATCH 30/38] Another minor change --- src/axom/sina/examples/CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/axom/sina/examples/CMakeLists.txt b/src/axom/sina/examples/CMakeLists.txt index 48c96a4b53..59b77620e4 100644 --- a/src/axom/sina/examples/CMakeLists.txt +++ b/src/axom/sina/examples/CMakeLists.txt @@ -27,8 +27,7 @@ set(sina_example_sources sina_view_datum_values.cpp ) -set(sina_example_depends sina conduit slic) -set(sina_example_depends sina conduit::conduit) +set(sina_example_depends sina conduit::conduit slic) if (ENABLE_FORTRAN) blt_list_append( TO sina_example_sources ELEMENTS sina_fortran.f90) From 209ee7d36ec0346a7e2a48720f2625ed32ce5db9 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Tue, 25 Feb 2025 11:30:19 -0800 Subject: [PATCH 31/38] Maybe this change? --- src/axom/sina/examples/sina_fortran.f90 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/axom/sina/examples/sina_fortran.f90 b/src/axom/sina/examples/sina_fortran.f90 index 73ff678122..021e3cd0ac 100644 --- a/src/axom/sina/examples/sina_fortran.f90 +++ b/src/axom/sina/examples/sina_fortran.f90 @@ -1,5 +1,6 @@ program example use sina_functions + use hdf5_config implicit none ! data types @@ -57,7 +58,9 @@ program example full_path = make_cstring(wrk_dir//''//fle_nme) ofull_path = make_cstring(wrk_dir//''//ofle_nme) json_fn = make_cstring('sina_dump.json') - hdf5_fn = make_cstring('sina_dump.hdf5') + if (use_hdf5) then + hdf5_fn = make_cstring('sina_dump.hdf5') + end if mime_type = make_cstring('') @@ -151,7 +154,9 @@ program example ! write out the Sina Document print *,'Writing out the Sina Document' call write_sina_document(json_fn) - call write_sina_document(hdf5_fn, 1) + if (use_hdf5) then + call write_sina_document(hdf5_fn, 1) + end if contains From 35bf45bbe7282049d7a32c81fe2a9a136a7291a8 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Wed, 26 Feb 2025 10:06:35 -0800 Subject: [PATCH 32/38] Syntax fixes --- src/axom/sina/CMakeLists.txt | 4 +-- src/axom/sina/examples/CMakeLists.txt | 4 +-- .../{sina_fortran.f90 => sina_fortran.f} | 0 ...ace.f90.in => sina_fortran_interface.f.in} | 0 .../sina/tests/test_fortran_integration.py | 31 +++++++++++++------ 5 files changed, 25 insertions(+), 14 deletions(-) rename src/axom/sina/examples/{sina_fortran.f90 => sina_fortran.f} (100%) rename src/axom/sina/interface/{sina_fortran_interface.f90.in => sina_fortran_interface.f.in} (100%) diff --git a/src/axom/sina/CMakeLists.txt b/src/axom/sina/CMakeLists.txt index c14eec8b3e..6bb56e162f 100644 --- a/src/axom/sina/CMakeLists.txt +++ b/src/axom/sina/CMakeLists.txt @@ -59,8 +59,8 @@ endif() if (ENABLE_FORTRAN) blt_list_append( TO sina_headers ELEMENTS interface/sina_fortran_interface.h) blt_list_append( TO sina_sources - ELEMENTS interface/sina_fortran_interface.cpp interface/sina_fortran_interface.f90.in) - configure_file(interface/sina_fortran_interface.f90.in interface/sina_fortran_interface.f90 @ONLY) + ELEMENTS interface/sina_fortran_interface.cpp interface/sina_fortran_interface.f.in) + configure_file(interface/sina_fortran_interface.f.in interface/sina_fortran_interface.f @ONLY) endif() #------------------------------------------------------------------------------ diff --git a/src/axom/sina/examples/CMakeLists.txt b/src/axom/sina/examples/CMakeLists.txt index 59b77620e4..f9904282a7 100644 --- a/src/axom/sina/examples/CMakeLists.txt +++ b/src/axom/sina/examples/CMakeLists.txt @@ -30,7 +30,7 @@ set(sina_example_sources set(sina_example_depends sina conduit::conduit slic) if (ENABLE_FORTRAN) - blt_list_append( TO sina_example_sources ELEMENTS sina_fortran.f90) + blt_list_append( TO sina_example_sources ELEMENTS sina_fortran.f) endif() #------------------------------------------------------------------------------ @@ -40,7 +40,7 @@ foreach(src ${sina_example_sources}) get_filename_component(exe_name ${src} NAME_WE) if (exe_name STREQUAL "sina_fortran") - set(sina_mod_src ${CMAKE_CURRENT_BINARY_DIR}/../interface/sina_fortran_interface.f90) + set(sina_mod_src ${CMAKE_CURRENT_BINARY_DIR}/../interface/sina_fortran_interface.f) else() set(sina_mod_src "") endif() diff --git a/src/axom/sina/examples/sina_fortran.f90 b/src/axom/sina/examples/sina_fortran.f similarity index 100% rename from src/axom/sina/examples/sina_fortran.f90 rename to src/axom/sina/examples/sina_fortran.f diff --git a/src/axom/sina/interface/sina_fortran_interface.f90.in b/src/axom/sina/interface/sina_fortran_interface.f.in similarity index 100% rename from src/axom/sina/interface/sina_fortran_interface.f90.in rename to src/axom/sina/interface/sina_fortran_interface.f.in diff --git a/src/axom/sina/tests/test_fortran_integration.py b/src/axom/sina/tests/test_fortran_integration.py index d78f509dad..948cb76828 100644 --- a/src/axom/sina/tests/test_fortran_integration.py +++ b/src/axom/sina/tests/test_fortran_integration.py @@ -21,7 +21,7 @@ def parse_args(): # JSON Tests: Will always run class TestFortranExampleIntegrationJSON(unittest.TestCase): - + @classmethod def setUpClass(cls): """ @@ -33,20 +33,19 @@ def setUpClass(cls): args = parse_args() cls.binary_dir = args.binary_dir if cls.binary_dir is None: - # Assume we're at /path/to/build_dir/axom/sina/tests so move up to build_dir - cls.binary_dir = f"{cwd}/../../../" - + # Move up three levels and resolve an absolute path + cls.binary_dir = os.path.abspath(os.path.join(cwd, "../")) + os.chdir(cls.binary_dir) - if not os.path.exists(f"{cls.binary_dir}/examples/sina_fortran_ex"): + if not os.path.exists(os.path.join(cls.binary_dir, "examples/sina_fortran_ex")): subprocess.run(["make", "sina_fortran_ex"]) os.chdir(cwd) - def setUp(self): """ Invoke example Fortran application to dump a sina file """ - subprocess.run([f"{self.binary_dir}/examples/sina_fortran_ex"]) + subprocess.run([os.path.join(self.binary_dir, "examples/sina_fortran_ex")]) self.dump_file = "sina_dump.json" def tearDown(self): @@ -130,16 +129,28 @@ class TestFortranExampleIntegrationHDF5(unittest.TestCase): @classmethod def setUpClass(cls): + """ + Obtain the binary directory from the CLI and compile the sina fortran + example needed for these tests if necessary. + """ cwd = os.getcwd() + args = parse_args() - cls.binary_dir = args.binary_dir or f"{cwd}/../../../" + cls.binary_dir = args.binary_dir + if cls.binary_dir is None: + # Move up three levels and resolve an absolute path + cls.binary_dir = os.path.abspath(os.path.join(cwd, "../")) + os.chdir(cls.binary_dir) - if not os.path.exists(f"{cls.binary_dir}/examples/sina_fortran_ex"): + + if not os.path.exists(os.path.join(cls.binary_dir, "examples/sina_fortran_ex")): subprocess.run(["make", "sina_fortran_ex"]) + os.chdir(cwd) def setUp(self): - subprocess.run([f"{self.binary_dir}/examples/sina_fortran_ex"]) + """ Invoke example Fortran application to dump a sina file """ + subprocess.run([os.path.join(self.binary_dir, "examples/sina_fortran_ex")]) self.dump_file = "sina_dump.hdf5" def tearDown(self): From 64e5026feb7f5226e9b3a26cea4ea4775b88b04a Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Wed, 26 Feb 2025 13:21:18 -0800 Subject: [PATCH 33/38] Test Update --- .../sina/tests/test_fortran_integration.py | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/axom/sina/tests/test_fortran_integration.py b/src/axom/sina/tests/test_fortran_integration.py index 948cb76828..e7d7ad8a72 100644 --- a/src/axom/sina/tests/test_fortran_integration.py +++ b/src/axom/sina/tests/test_fortran_integration.py @@ -157,24 +157,30 @@ def tearDown(self): os.remove(self.dump_file) def extract_hdf5_value(self, value): + # If the value is an h5py.Dataset, retrieve its underlying data. if isinstance(value, h5py.Dataset): value = value[()] - + + # If the value is a bytes instance, decode it. if isinstance(value, bytes): return value.decode("utf-8").strip("\0").strip() - - if isinstance(value, np.ndarray): - if value.dtype.kind in {'U', 'S'}: - result = b"".join(value.astype("S")).decode("utf-8").strip("\0").strip() - else: - result = value.tolist() - if isinstance(result, list) and len(result) == 1: - result = result[0] - return result - - if isinstance(value, (np.integer, np.floating)): - return value.item() - + + # If the value is a list or tuple of bytes, join them into a single bytes object and decode. + if isinstance(value, (list, tuple)) and value and all(isinstance(item, bytes) for item in value): + joined = b"".join(value) + return joined.decode("utf-8").strip("\0").strip() + + # If the value has a 'tolist' method (e.g., from an array-like object), convert it and process. + if hasattr(value, "tolist"): + converted = value.tolist() + if isinstance(converted, (list, tuple)) and converted and all(isinstance(item, bytes) for item in converted): + joined = b"".join(converted) + return joined.decode("utf-8").strip("\0").strip() + if isinstance(converted, list) and len(converted) == 1: + return converted[0] + return converted + + # Otherwise, return the value as-is. return value def test_validate_contents_of_record(self): From a0341dab91ff9c6c2e6c43335b7c6aed71a3deac Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Wed, 26 Feb 2025 14:33:25 -0800 Subject: [PATCH 34/38] Syntax Fix --- src/axom/sina/tests/test_fortran_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/axom/sina/tests/test_fortran_integration.py b/src/axom/sina/tests/test_fortran_integration.py index e7d7ad8a72..8ed21dae1d 100644 --- a/src/axom/sina/tests/test_fortran_integration.py +++ b/src/axom/sina/tests/test_fortran_integration.py @@ -4,7 +4,7 @@ import os import subprocess import unittest -import numpy as np + try: import h5py AXOM_USE_HDF5 = True From baf2492d39c8a9cf8993f28dbb40fa9787b16d90 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Thu, 27 Feb 2025 13:41:42 -0800 Subject: [PATCH 35/38] Test Pipeline for Not HDF5 --- src/cmake/axom-config.cmake.in | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cmake/axom-config.cmake.in b/src/cmake/axom-config.cmake.in index 19a2fa0882..73fd9635aa 100644 --- a/src/cmake/axom-config.cmake.in +++ b/src/cmake/axom-config.cmake.in @@ -64,6 +64,11 @@ if(NOT AXOM_FOUND) set(AXOM_USE_CAMP "@AXOM_USE_CAMP@") set(AXOM_USE_CONDUIT "@AXOM_USE_CONDUIT@") set(AXOM_USE_HDF5 "@AXOM_USE_HDF5@") + if(AXOM_USE_HDF5) + set(AXOM_USE_HDF5 FALSE) + else() + set(AXOM_USE_HDF5 TRUE) + endif() set(AXOM_USE_LUA "@AXOM_USE_LUA@") set(AXOM_USE_MFEM "@AXOM_USE_MFEM@") set(AXOM_USE_OPENCASCADE "@AXOM_USE_OPENCASCADE@") From 3e03355b12720c9dcb1ae5724c57af466c5fcb33 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Thu, 27 Feb 2025 17:04:35 -0800 Subject: [PATCH 36/38] Reset --- src/cmake/axom-config.cmake.in | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/cmake/axom-config.cmake.in b/src/cmake/axom-config.cmake.in index 73fd9635aa..19a2fa0882 100644 --- a/src/cmake/axom-config.cmake.in +++ b/src/cmake/axom-config.cmake.in @@ -64,11 +64,6 @@ if(NOT AXOM_FOUND) set(AXOM_USE_CAMP "@AXOM_USE_CAMP@") set(AXOM_USE_CONDUIT "@AXOM_USE_CONDUIT@") set(AXOM_USE_HDF5 "@AXOM_USE_HDF5@") - if(AXOM_USE_HDF5) - set(AXOM_USE_HDF5 FALSE) - else() - set(AXOM_USE_HDF5 TRUE) - endif() set(AXOM_USE_LUA "@AXOM_USE_LUA@") set(AXOM_USE_MFEM "@AXOM_USE_MFEM@") set(AXOM_USE_OPENCASCADE "@AXOM_USE_OPENCASCADE@") From 8370a21bfa151326b727064a72f61a36861f9503 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Fri, 7 Mar 2025 12:00:03 -0800 Subject: [PATCH 37/38] New Changes --- src/axom/sina/CMakeLists.txt | 16 ++++++++++++---- src/axom/sina/docs/sphinx/documents.rst | 7 +++++-- src/axom/sina/docs/sphinx/hdf5_vs_json.rst | 4 ++-- src/axom/sina/docs/sphinx/tutorial.rst | 12 ++++++------ src/axom/sina/examples/sina_basic.cpp | 5 +++-- src/axom/sina/tests/CMakeLists.txt | 14 ++++++++++++++ src/axom/sina/tests/config.py.in | 1 + src/axom/sina/tests/test_fortran_integration.py | 11 +++++------ 8 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 src/axom/sina/tests/config.py.in diff --git a/src/axom/sina/CMakeLists.txt b/src/axom/sina/CMakeLists.txt index 6bb56e162f..b0d6f20ea0 100644 --- a/src/axom/sina/CMakeLists.txt +++ b/src/axom/sina/CMakeLists.txt @@ -45,6 +45,9 @@ set(sina_sources core/Run.cpp ) +# Set CMake Policy +cmake_policy(SET CMP0115 NEW) + # Add Adiak header and source blt_list_append( TO sina_headers ELEMENTS core/AdiakWriter.hpp IF AXOM_USE_ADIAK ) blt_list_append( TO sina_sources ELEMENTS core/AdiakWriter.cpp IF AXOM_USE_ADIAK ) @@ -57,10 +60,15 @@ else() endif() if (ENABLE_FORTRAN) - blt_list_append( TO sina_headers ELEMENTS interface/sina_fortran_interface.h) - blt_list_append( TO sina_sources - ELEMENTS interface/sina_fortran_interface.cpp interface/sina_fortran_interface.f.in) - configure_file(interface/sina_fortran_interface.f.in interface/sina_fortran_interface.f @ONLY) + blt_list_append(TO sina_headers + ELEMENTS interface/sina_fortran_interface.h) + blt_list_append(TO sina_sources + ELEMENTS interface/sina_fortran_interface.cpp interface/sina_fortran_interface.f) + configure_file(interface/sina_fortran_interface.f.in + interface/sina_fortran_interface.f + @ONLY) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/interface/sina_fortran_interface.f + DESTINATION interface) endif() #------------------------------------------------------------------------------ diff --git a/src/axom/sina/docs/sphinx/documents.rst b/src/axom/sina/docs/sphinx/documents.rst index 8bf4036e26..2a0d270a32 100644 --- a/src/axom/sina/docs/sphinx/documents.rst +++ b/src/axom/sina/docs/sphinx/documents.rst @@ -115,8 +115,11 @@ In addition to assembling ``Document`` instances from existing JSON files, it is possible to generate ``Document`` objects from existing HDF5 files using conduit. -Sina's ``saveDocument()`` and ``loadDocument()`` functions support HDF5 assembly if we provide it -the optional Protocol variable set to HDF5: +Sina's ``saveDocument()`` and ``loadDocument()`` functions support HDF5 assembly if we both +build axom with HDF5 support and provide it with the optional Protocol variable set to HDF5. +If Protocol::HDF5 is attempted while axom does not have HDF5 support, the functions will +recognize they won't have the support for what you are attempting and will return a list of +supported types instead as a runtime error. .. code:: cpp diff --git a/src/axom/sina/docs/sphinx/hdf5_vs_json.rst b/src/axom/sina/docs/sphinx/hdf5_vs_json.rst index 4395097da0..f33679fb36 100644 --- a/src/axom/sina/docs/sphinx/hdf5_vs_json.rst +++ b/src/axom/sina/docs/sphinx/hdf5_vs_json.rst @@ -1,4 +1,4 @@ -.. ## Copyright (c) 2017-2024, Lawrence Livermore National Security, LLC and +.. ## Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and .. ## other Axom Project Developers. See the top-level LICENSE file for details. .. ## .. ## SPDX-License-Identifier: (BSD-3-Clause) @@ -34,7 +34,7 @@ Why You Should Use HDF5 * HDF5 offers better size and speed efficiency when dealing with larger files/curve sets and only outperforms more dramatically as size increases -* Hierarchal structure leads to being 2.5x more size efficient and 5x faster at our largest +* Hierarchical structure leads to being 2.5x more size efficient and 5x faster at our largest tested files ========== diff --git a/src/axom/sina/docs/sphinx/tutorial.rst b/src/axom/sina/docs/sphinx/tutorial.rst index 6cc8bbd237..da9c2af7f1 100644 --- a/src/axom/sina/docs/sphinx/tutorial.rst +++ b/src/axom/sina/docs/sphinx/tutorial.rst @@ -148,10 +148,10 @@ Input and Output Once you have a document, it is easy to save it to a file. To save to a JSON, we run the saveDocument() with the optional argument Protocol set to JSON or set as -nothing. Alternatively, if you wish to save the document to an HDF5 file, you must set -saveDocument()'s optional Protocol parameter to HDF5. After executing the below, you -will output a file named "my_output.json" and a file named "my_output.hdf5", both of -which you can ingest into a Sina datastore. +nothing. Alternatively if you wish to save the document to an HDF5 file: Configure +axom for HDF5 support then you can set saveDocument()'s optional Protocol parameter +to HDF5. After executing the below, you will output a file named "my_output.json" +and a file named "my_output.hdf5", both of which you can ingest into a Sina datastore. .. literalinclude:: ../../examples/sina_tutorial.cpp :language: cpp @@ -161,8 +161,8 @@ which you can ingest into a Sina datastore. If needed, you can also load a document from a file. This can be useful, for example, if you wrote a document when writing a restart and you want to continue from where you left off. To load from a JSON file simply run loadDocument() -with the optional argument Protocol set to JSON or set as nothing, and to load from -an HDF5 set the Protocol to HDF5. +with the optional argument Protocol set to JSON or set as nothing. If you've configured +for, and wish to load from an HDF5 simply set the Protocol to HDF5. Note that due to HDF5's handling of '/' as indicators for nested structures, parent nodes will have '/' changed to the ``slashSubstitute`` variable located in diff --git a/src/axom/sina/examples/sina_basic.cpp b/src/axom/sina/examples/sina_basic.cpp index 3ea779cb4f..4e2e937b61 100644 --- a/src/axom/sina/examples/sina_basic.cpp +++ b/src/axom/sina/examples/sina_basic.cpp @@ -18,8 +18,9 @@ int main(void) // Add the run to the document document.add(std::move(run)); // Save the document directly to a file. - // since we gave saveDocument no protocol parameter, it will default to JSON - axom::sina::saveDocument(document, "MySinaData.json"); + // by specifying Protocol::JSON, we set the file to save as a JSON. + // if we wished, we could provide no third argument since saveDocument defaults to JSON. + axom::sina::saveDocument(document, "MySinaData.json", axom::sina::Protocol::JSON); #ifdef AXOM_USE_HDF5 // by specifying Protocol::HDF5, we also save a copy as an HDF5 file. diff --git a/src/axom/sina/tests/CMakeLists.txt b/src/axom/sina/tests/CMakeLists.txt index 109572e3b2..34cb313402 100644 --- a/src/axom/sina/tests/CMakeLists.txt +++ b/src/axom/sina/tests/CMakeLists.txt @@ -110,6 +110,20 @@ if (ENABLE_FORTRAN AND DEFINED PYTHON_EXECUTABLE) COPYONLY ) + # Define Python-friendly boolean for HDF5 guarding in Fortran Test. + if(AXOM_USE_HDF5) + set(AXOM_USE_HDF5_PY "True") + else() + set(AXOM_USE_HDF5_PY "False") + endif() + + # Generate config.py in the test output directory + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/config.py.in + ${TEST_OUTPUT_DIRECTORY}/config.py + @ONLY + ) + axom_add_test( NAME sina_fortran_integration_test COMMAND ${PYTHON_EXECUTABLE} ${TEST_OUTPUT_DIRECTORY}/test_fortran_integration.py -bd ${PROJECT_BINARY_DIR} ) diff --git a/src/axom/sina/tests/config.py.in b/src/axom/sina/tests/config.py.in new file mode 100644 index 0000000000..089b761b15 --- /dev/null +++ b/src/axom/sina/tests/config.py.in @@ -0,0 +1 @@ +AXOM_USE_HDF5 = @AXOM_USE_HDF5_PY@ \ No newline at end of file diff --git a/src/axom/sina/tests/test_fortran_integration.py b/src/axom/sina/tests/test_fortran_integration.py index 8ed21dae1d..a40e102867 100644 --- a/src/axom/sina/tests/test_fortran_integration.py +++ b/src/axom/sina/tests/test_fortran_integration.py @@ -4,12 +4,11 @@ import os import subprocess import unittest +import config -try: +if (config.AXOM_USE_HDF5): import h5py - AXOM_USE_HDF5 = True -except ImportError: - AXOM_USE_HDF5 = False + def parse_args(): """Helper function to obtain the binary directory path of Axom from CLI""" @@ -124,7 +123,7 @@ def test_validate_contents_of_record(self): #HDF5 Test -@unittest.skipUnless(AXOM_USE_HDF5, "Requires h5py for HDF5-dependent tests") +@unittest.skipUnless(config.AXOM_USE_HDF5, "Requires h5py for HDF5-dependent tests") class TestFortranExampleIntegrationHDF5(unittest.TestCase): @classmethod @@ -269,7 +268,7 @@ def test_validate_contents_of_record(self): # Doing the below instead of unittest.main() so that we can print to stdout suite = unittest.TestSuite() suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestFortranExampleIntegrationJSON)) - if AXOM_USE_HDF5: + if config.AXOM_USE_HDF5: suite.addTests(unittest.defaultTestLoader.loadTestsFromTestCase(TestFortranExampleIntegrationHDF5)) runner = unittest.TextTestRunner(buffer=False) runner.run(suite) \ No newline at end of file From 50f17e132aabb2fc80708d380e210a21418a87e5 Mon Sep 17 00:00:00 2001 From: Gabriel Waegner Date: Tue, 11 Mar 2025 09:35:46 -0700 Subject: [PATCH 38/38] Minor Fix --- src/axom/sina/tests/config.py.in | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/axom/sina/tests/config.py.in b/src/axom/sina/tests/config.py.in index 089b761b15..b6d0a41e78 100644 --- a/src/axom/sina/tests/config.py.in +++ b/src/axom/sina/tests/config.py.in @@ -1 +1,6 @@ +# Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +# other Axom Project Developers. See the top-level LICENSE file for details. +# +# SPDX-License-Identifier: (BSD-3-Clause) + AXOM_USE_HDF5 = @AXOM_USE_HDF5_PY@ \ No newline at end of file