-
Notifications
You must be signed in to change notification settings - Fork 27
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
base: develop
Are you sure you want to change the base?
Changes from 28 commits
3f09c5e
e0cd5bf
d2f38ac
6c32398
31242a6
aa9073d
b553c44
c5c0cb4
8dd8a84
8e6afc1
247591f
703dbd1
e1d7631
0df65b3
e118d24
0f5851e
1168000
429d9ce
b71b05d
0b689db
ccfa513
c05979d
7c5d7cc
d036b67
79f3729
f92080d
4310ee1
0fd3a35
1145c29
4043d3e
3ccfbb3
9429b60
45ee75b
0f016bb
4a2cf7e
2499896
209ee7d
35bf45b
64e5026
a0341da
baf2492
3e03355
8370a21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,13 +15,24 @@ | |
|
||
#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 <cstdio> | ||
#include <fstream> | ||
#include <ios> | ||
#include <iostream> | ||
#include <utility> | ||
#include <sstream> | ||
#include <stdexcept> | ||
#include <algorithm> | ||
|
||
namespace axom | ||
{ | ||
|
@@ -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)); | ||
|
@@ -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, | ||
|
@@ -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 | ||
|
@@ -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'."); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can the code ever get to this There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
{ | ||
|
@@ -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; | ||
kennyweiss marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
} // namespace sina | ||
|
There was a problem hiding this comment.
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 asAXOM_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