Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/sina/hdf5 output support #1480

Open
wants to merge 43 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3f09c5e
created a clean new branch from Gabriel's work to push to github repo
doutriaux1 Dec 19, 2024
e0cd5bf
Comments Implemented
gwaegner Dec 19, 2024
d2f38ac
More Comments
gwaegner Dec 19, 2024
6c32398
Fix?
gwaegner Dec 19, 2024
31242a6
Fix??
gwaegner Dec 19, 2024
aa9073d
Final Fix
gwaegner Dec 19, 2024
b553c44
Axom Changes
gwaegner Dec 20, 2024
c5c0cb4
Brian's Changes, Wait to hear back on createFromNode
gwaegner Jan 8, 2025
8dd8a84
Merge branch 'develop' into sina_hdf5_implementation
doutriaux1 Jan 9, 2025
8e6afc1
Create From Node Changes
gwaegner Jan 24, 2025
247591f
Forgot to delete some leftover comments
gwaegner Jan 24, 2025
703dbd1
Small fix
gwaegner Jan 24, 2025
e1d7631
Merge remote-tracking branch 'llnl/develop' into sina_hdf5_implementa…
doutriaux1 Jan 25, 2025
0df65b3
merged github's develop in
doutriaux1 Jan 25, 2025
e118d24
Pipeline Fix
gwaegner Jan 27, 2025
0f5851e
Clang Format
gwaegner Jan 28, 2025
1168000
More ClangFormat
gwaegner Jan 28, 2025
429d9ce
bringing sumbodule back to develop version
doutriaux1 Feb 3, 2025
b71b05d
New Changes + Guarding
gwaegner Feb 4, 2025
0b689db
Merge remote-tracking branch 'llnl/feature/sina/hdf5_output_support' …
doutriaux1 Feb 4, 2025
ccfa513
bringing in submodule updates
doutriaux1 Feb 4, 2025
c05979d
Build Fix
gwaegner Feb 4, 2025
7c5d7cc
Documentation Update
gwaegner Feb 6, 2025
d036b67
Merge branch 'develop' into feature/sina/hdf5_output_support
doutriaux1 Feb 10, 2025
79f3729
Fixes submodule pointers
kennyweiss Feb 18, 2025
f92080d
Merge branch 'develop' into feature/sina/hdf5_output_support
kennyweiss Feb 18, 2025
4310ee1
Fixes includes in sina
kennyweiss Feb 19, 2025
0fd3a35
Guards usage of sina::Protocol::HDF5
kennyweiss Feb 19, 2025
1145c29
Guarding and Fortran tests
gwaegner Feb 22, 2025
4043d3e
Merge branch 'develop' into feature/sina/hdf5_output_support
doutriaux1 Feb 24, 2025
3ccfbb3
submodule fix?
doutriaux1 Feb 24, 2025
9429b60
more submodule fix
doutriaux1 Feb 24, 2025
45ee75b
Fix Fortran Test
gwaegner Feb 24, 2025
0f016bb
CMakeLists Fix
gwaegner Feb 24, 2025
4a2cf7e
Minor Fix
gwaegner Feb 24, 2025
2499896
Another minor change
gwaegner Feb 25, 2025
209ee7d
Maybe this change?
gwaegner Feb 25, 2025
35bf45b
Syntax fixes
gwaegner Feb 26, 2025
64e5026
Test Update
gwaegner Feb 26, 2025
a0341da
Syntax Fix
gwaegner Feb 26, 2025
baf2492
Test Pipeline for Not HDF5
gwaegner Feb 27, 2025
3e03355
Reset
gwaegner Feb 28, 2025
8370a21
New Changes
gwaegner Mar 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 241 additions & 50 deletions src/axom/sina/core/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,24 @@

#include "axom/sina/core/Document.hpp"

#include "axom/config.hpp"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: We need to #include "axom/config.hpp" to bring in compiler defines such as AXOM_USE_HDF5. If we don't, they will always appear to be off.

I updated the includes in this branch to bring in 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 <cstdio>
#include <fstream>
#include <ios>
#include <iostream>
#include <utility>
#include <sstream>
#include <stdexcept>
#include <algorithm>

namespace axom
{
Expand All @@ -35,6 +46,26 @@ char const RELATIONSHIPS_KEY[] = "relationships";
char const SAVE_TMP_FILE_EXTENSION[] = ".sina.tmp";
} // namespace

void protocolWarn(std::string const protocol, std::string const &name)
{
std::unordered_map<std::string, std::string> 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, ...)"}};

Path path(name, '.');

if(protocol != '.' + path.baseName())
{
auto messageIt = protocolMessages.find(protocol);
if(messageIt != protocolMessages.end())
{
std::cerr << messageIt->second;
}
}
}

void Document::add(std::unique_ptr<Record> record)
{
records.emplace_back(std::move(record));
Expand Down Expand Up @@ -63,63 +94,184 @@ conduit::Node Document::toNode() const
return document;
}

void Document::createFromNode(conduit::Node const &asNode,
RecordLoader const &recordLoader)
void Document::createFromNode(const conduit::Node &asNode,
const RecordLoader &recordLoader)
{
if(asNode.has_child(RECORDS_KEY))
{
conduit::Node record_nodes = asNode[RECORDS_KEY];
if(record_nodes.dtype().is_list())
conduit::Node nodeCopy = asNode;

auto processChildNodes = [&](const char *key,
std::function<void(conduit::Node &)> addFunc) {
if(nodeCopy.has_child(key))
{
auto recordIter = record_nodes.children();
while(recordIter.has_next())
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())
{
auto record = recordIter.next();
add(recordLoader.load(record));
conduit::Node child = childIter.next();
addFunc(child);
}
}
else
};
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)
{
this->createFromNode(asNode, recordLoader);
}

Document::Document(std::string const &asJson, RecordLoader const &recordLoader)
{
conduit::Node asNode;
asNode.parse(asJson, "json");
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())
{
std::ostringstream message;
message << "The '" << RECORDS_KEY
<< "' element of a document must be an array";
throw std::invalid_argument(message.str());
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(asNode.has_child(RELATIONSHIPS_KEY))
if(modifiedNode.dtype().is_list())
{
conduit::Node relationship_nodes = asNode[RELATIONSHIPS_KEY];
if(relationship_nodes.dtype().is_list())
// If its empty with no children it's the end of a tree

for(auto it = modifiedNode.children(); it.has_next();)
{
auto relationshipsIter = relationship_nodes.children();
while(relationshipsIter.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())
{
auto &relationship = relationshipsIter.next();
add(Relationship {relationship});
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();)
{
std::ostringstream message;
message << "The '" << RELATIONSHIPS_KEY
<< "' element of a document must be an array";
throw std::invalid_argument(message.str());
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);
}
}
}
}

Document::Document(conduit::Node const &asNode, RecordLoader const &recordLoader)
void Document::toHDF5(const std::string &filename) const
{
this->createFromNode(asNode, recordLoader);
}
conduit::Node node;
conduit::Node &recordsNode = node["records"];
conduit::Node &relationshipsNode = node["relationships"];

Document::Document(std::string const &asJson, RecordLoader const &recordLoader)
{
conduit::Node asNode;
asNode.parse(asJson, "json");
this->createFromNode(asNode, recordLoader);
for(const auto &record : getRecords())
{
conduit::Node recordNode = record->toNode();

removeSlashes(recordNode, recordsNode.append());
}

// Process relationships
for(const auto &relationship : getRelationships())
{
conduit::Node relationshipNode = relationship.toNode();

removeSlashes(relationshipNode, relationshipsNode.append());
}

conduit::relay::io::save(node, filename, "hdf5");
}
#endif

//

std::string Document::toJson(conduit::index_t indent,
conduit::index_t depth,
Expand All @@ -129,7 +281,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)
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
Expand All @@ -138,12 +292,30 @@ void saveDocument(Document const &document, std::string const &fileName)
// 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(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();
}
#ifdef AXOM_USE_HDF5
else if(protocol == Protocol::HDF5)
{
protocolWarn(".hdf5", fileName);
document.toHDF5(tmpFileName);
}
#endif
else
{
throw std::invalid_argument(
"Invalid format choice. Please enter 'json' or 'hdf5'.");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the code ever get to this else?

Copy link

@gwaegner gwaegner Feb 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the above default, the code will hit this else if a user attempts Protocol::HDF5 with no HDF5 support since the else if(Protocol::HDF5) case will not be loaded


if(rename(tmpFileName.c_str(), fileName.c_str()) != 0)
{
Expand All @@ -154,20 +326,39 @@ void saveDocument(Document const &document, std::string const &fileName)
}
}

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};
conduit::Node node, modifiedNode;
std::ostringstream file_contents;
file_contents << file_in.rdbuf();
file_in.close();
nodeFromJson.parse(file_contents.str(), "json");
return Document {nodeFromJson, recordLoader};
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};

#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;
}
}

} // namespace sina
Expand Down
Loading