diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f8f0a25f49..11680cde18 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -74,11 +74,11 @@ jobs: HOST_CONFIG: 'clang@14.0.0' osx_gcc: VM_ImageName: 'macos-12' - CMAKE_EXTRA_FLAGS: '-DAXOM_ENABLE_SIDRE:BOOL=OFF -DAXOM_ENABLE_INLET:BOOL=OFF -DAXOM_ENABLE_KLEE:BOOL=OFF' + CMAKE_EXTRA_FLAGS: '-DAXOM_ENABLE_SIDRE:BOOL=OFF -DAXOM_ENABLE_INLET:BOOL=OFF -DAXOM_ENABLE_KLEE:BOOL=OFF -DAXOM_ENABLE_SINA:BOOL=OFF' TEST_TARGET: 'osx_gcc' windows: VM_ImageName: 'windows-2019' - CMAKE_EXTRA_FLAGS: '-DAXOM_ENABLE_SIDRE:BOOL=OFF -DAXOM_ENABLE_INLET:BOOL=OFF -DAXOM_ENABLE_KLEE:BOOL=OFF' + CMAKE_EXTRA_FLAGS: '-DAXOM_ENABLE_SIDRE:BOOL=OFF -DAXOM_ENABLE_INLET:BOOL=OFF -DAXOM_ENABLE_KLEE:BOOL=OFF -DAXOM_ENABLE_SINA:BOOL=OFF' TEST_TARGET: 'win_vs' pool: diff --git a/src/axom/CMakeLists.txt b/src/axom/CMakeLists.txt index a78442837e..a2fe71cc7c 100644 --- a/src/axom/CMakeLists.txt +++ b/src/axom/CMakeLists.txt @@ -28,6 +28,7 @@ endif() # Add components so that later ones depend on earlier ones # (i.e. quest depends on mint, so quest follows mint) axom_add_component(COMPONENT_NAME slic DEFAULT_STATE ${AXOM_ENABLE_ALL_COMPONENTS}) +axom_add_component(COMPONENT_NAME sina DEFAULT_STATE ${AXOM_ENABLE_ALL_COMPONENTS}) axom_add_component(COMPONENT_NAME slam DEFAULT_STATE ${AXOM_ENABLE_ALL_COMPONENTS}) axom_add_component(COMPONENT_NAME primal DEFAULT_STATE ${AXOM_ENABLE_ALL_COMPONENTS}) axom_add_component(COMPONENT_NAME sidre DEFAULT_STATE ${AXOM_ENABLE_ALL_COMPONENTS}) diff --git a/src/axom/config.hpp.in b/src/axom/config.hpp.in index 4cbdee0e29..281ea197cf 100644 --- a/src/axom/config.hpp.in +++ b/src/axom/config.hpp.in @@ -102,6 +102,7 @@ #cmakedefine AXOM_USE_PRIMAL #cmakedefine AXOM_USE_QUEST #cmakedefine AXOM_USE_SIDRE +#cmakedefine AXOM_USE_SINA #cmakedefine AXOM_USE_SLAM #cmakedefine AXOM_USE_SLIC #cmakedefine AXOM_USE_SPIN diff --git a/src/axom/core/utilities/About.cpp.in b/src/axom/core/utilities/About.cpp.in index 6e8a60d611..468f2f0ba9 100644 --- a/src/axom/core/utilities/About.cpp.in +++ b/src/axom/core/utilities/About.cpp.in @@ -102,6 +102,10 @@ void about(std::ostream &oss) comps.push_back("sidre"); #endif +#ifdef AXOM_USE_SINA + comps.push_back("sina"); +#endif + #ifdef AXOM_USE_SLAM comps.push_back("slam"); #endif diff --git a/src/axom/doxygen_mainpage.md b/src/axom/doxygen_mainpage.md index 47e818fd85..fd3662f7c2 100644 --- a/src/axom/doxygen_mainpage.md +++ b/src/axom/doxygen_mainpage.md @@ -14,6 +14,7 @@ Axom provides libraries that address common computer science needs. It grew fro * @subpage primaltop provides an API for geometric primitives and computational geometry tests. * @subpage questtop provides an API to query point distance and position relative to meshes. * @subpage sidretop provides a data store with hierarchical structure. +* @subpage sinatop ([S]imulation [In]sight and [A]nalysis) unified output library collects data directly within codes, outputting them to a common file output format co-designed with application developers and users. * @subpage slamtop provides an API to construct and process meshes. * @subpage slictop provides infrastructure for logging application messages. * @subpage spintop provides spatial acceleration data structures, also known as spatial indexes. @@ -29,6 +30,7 @@ Dependencies between components are as follows: - Quest depends on Slam, Primal, Spin, and Mint - Klee depends on Sidre, Inlet and Primal - Multimat depends on Slic, and Slam + - Sina only depends on Core The figure below summarizes the dependencies between the modules. Solid links indicate hard dependencies; dashed links indicate optional dependencies. diff --git a/src/axom/sina/CMakeLists.txt b/src/axom/sina/CMakeLists.txt new file mode 100644 index 0000000000..cf93a99bdb --- /dev/null +++ b/src/axom/sina/CMakeLists.txt @@ -0,0 +1,99 @@ +# 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) +#------------------------------------------------------------------------------ +# Sina -- API to write data to Sina's common file format +#------------------------------------------------------------------------------ + +#------------------------------------------------------------------------------ +# Specify necessary dependencies +# +# Note: Sina also optionally depends on Adiak when AXOM_USE_ADIAK=ON +#------------------------------------------------------------------------------ +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 +#------------------------------------------------------------------------------ +set(sina_headers + core/ConduitUtil.hpp + core/Curve.hpp + core/CurveSet.hpp + core/DataHolder.hpp + core/Datum.hpp + core/Document.hpp + core/File.hpp + core/ID.hpp + core/Record.hpp + core/Relationship.hpp + core/Run.hpp + ) + +set(sina_sources + core/ConduitUtil.cpp + core/Curve.cpp + core/CurveSet.cpp + core/DataHolder.cpp + core/Datum.cpp + core/Document.cpp + core/File.cpp + core/ID.cpp + core/Record.cpp + core/Relationship.cpp + core/Run.cpp + ) + +# 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 ) + +# 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) +endif() + +#------------------------------------------------------------------------------ +# Build and install the library +#------------------------------------------------------------------------------ +set(sina_depends + core + conduit::conduit + ) + +blt_list_append( TO sina_depends ELEMENTS adiak::adiak IF AXOM_USE_ADIAK ) + +axom_add_library(NAME sina + SOURCES ${sina_sources} + HEADERS ${sina_headers} + DEPENDS_ON ${sina_depends} + FOLDER axom/sina) + +axom_write_unified_header(NAME sina + HEADERS ${sina_headers}) + +axom_install_component(NAME sina + HEADERS ${sina_headers}) + +#------------------------------------------------------------------------------ +# Add tests and examples +#------------------------------------------------------------------------------ +if(AXOM_ENABLE_TESTS) + add_subdirectory(tests) +endif() + +if(AXOM_ENABLE_EXAMPLES) + add_subdirectory(examples) +endif() diff --git a/src/axom/sina/config.hpp.in b/src/axom/sina/config.hpp.in new file mode 100644 index 0000000000..f6fe736066 --- /dev/null +++ b/src/axom/sina/config.hpp.in @@ -0,0 +1,13 @@ +#ifndef SINA_CONFIG_HPP_ +#define SINA_CONFIG_HPP_ + +/// \file + +#define SINA_VERSION_MAJOR @SINA_VERSION_MAJOR@ +#define SINA_VERSION_MINOR @SINA_VERSION_MINOR@ +#define SINA_VERSION_PATCH @SINA_VERSION_PATCH@ + +#define SINA_VERSION "@SINA_VERSION_MAJOR@.@SINA_VERSION_MINOR@.@SINA_VERSION_PATCH@" + +#endif // SINA_CONFIG_HPP_ + diff --git a/src/axom/sina/core/AdiakWriter.cpp b/src/axom/sina/core/AdiakWriter.cpp new file mode 100644 index 0000000000..ca0e548a38 --- /dev/null +++ b/src/axom/sina/core/AdiakWriter.cpp @@ -0,0 +1,330 @@ +// 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) + +/*! + ****************************************************************************** + * + * \file AdiakWriter.cpp + * + * \brief Implementation file for the Adiak Sina callback function. + * + ****************************************************************************** + */ + +#include "axom/sina/core/AdiakWriter.hpp" + +#ifdef AXOM_USE_ADIAK + + #include + #include + #include + #include + #include + +extern "C" { + #include "adiak_tool.h" +} + + #include "axom/sina/core/ConduitUtil.hpp" + #include "axom/sina/core/Record.hpp" + #include "axom/sina/core/Datum.hpp" + #include "axom/sina/core/Document.hpp" + +namespace axom +{ +namespace sina +{ +namespace +{ + +/** +* Adiak has a much wider array of supported types than Sina. We will convert +* Adiak types to ones Sina understands; SinaType holds the possibilities. +**/ +enum SinaType +{ + sina_scalar, + sina_string, + sina_list, + sina_file, + sina_unknown +}; + +/** +* Add a axom::sina::Datum object to a Record. These are the sina equivalent +* of an Adiak datapoint. Since we track slightly different info, this function +* harvests what it can and hands it off to the Record. +**/ +template +void addDatum(const std::string &name, + T sina_safe_val, + const std::vector &tags, + axom::sina::Record *record) +{ + axom::sina::Datum datum {sina_safe_val}; + datum.setTags(std::move(tags)); + record->add(name, datum); +} + +/** +* Add a axom::sina::File object to our current Record. Adiak stores paths, +* which are essentially the same as Sina's idea of storing files. +**/ +void addFile(const std::string &name, + const std::string &uri, + axom::sina::Record *record) +{ + // We don't care about type here, there's only one adiak type that acts as a file + axom::sina::File file {uri}; + file.setTags(std::vector {name}); + record->add(std::move(file)); +} + +/** +* Given an Adiak type, return its corresponding Sina type. +**/ +SinaType findSinaType(adiak_datatype_t *t) +{ + switch(t->dtype) + { + case adiak_long: + case adiak_ulong: + case adiak_int: + case adiak_uint: + case adiak_double: + case adiak_timeval: + return sina_scalar; + case adiak_date: + case adiak_version: + case adiak_string: + case adiak_catstring: + return sina_string; + case adiak_path: + return sina_file; + case adiak_set: + case adiak_tuple: + case adiak_range: + case adiak_list: + return sina_list; + case adiak_type_unset: + return sina_unknown; + default: + return sina_unknown; + } +} + +/** +* Several Adiak types become what Sina views as a "scalar" (a double). +* Manage the conversions from various Adiak types to the final double +* representation +**/ +double toScalar(adiak_value_t *val, adiak_datatype_t *adiak_type) +{ + switch(adiak_type->dtype) + { + case adiak_long: + case adiak_ulong: + return static_cast(val->v_long); + case adiak_int: + case adiak_uint: + return static_cast(val->v_int); + case adiak_double: + return val->v_double; + case adiak_timeval: + { + struct timeval *tval = static_cast(val->v_ptr); + return static_cast(tval->tv_sec) + + (static_cast(tval->tv_usec) / 1000000.0); + } + // None of the rest of these should ever be reachable, so special error message + case adiak_date: + case adiak_version: + case adiak_string: + case adiak_catstring: + case adiak_path: + case adiak_set: + case adiak_tuple: + case adiak_range: + case adiak_list: + case adiak_type_unset: + { + std::string msg( + "Logic error, contact maintainer: Adiak-to-Sina double converter given "); + char *s = adiak_type_to_string(adiak_type, 1); + msg += s; + free(s); + throw std::runtime_error(msg); + } + default: + throw std::runtime_error( + "Adiak-to-Sina double converter given something not convertible to " + "double"); + } +} + +/** +* Some Adiak types become what Sina views as a string. +* Manage the conversions from various Adiak types to said string. +**/ +std::string toString(adiak_value_t *val, adiak_datatype_t *adiak_type) +{ + switch(adiak_type->dtype) + { + case adiak_date: + { + char datestr[512]; + signed long seconds_since_epoch = static_cast(val->v_long); + struct tm *loc = localtime(&seconds_since_epoch); + strftime(datestr, sizeof(datestr), "%a, %d %b %Y %T %z", loc); + return static_cast(datestr); + } + case adiak_catstring: + case adiak_version: + case adiak_string: + case adiak_path: + return std::string(static_cast(val->v_ptr)); + case adiak_long: + case adiak_ulong: + case adiak_int: + case adiak_uint: + case adiak_double: + case adiak_timeval: + case adiak_set: + case adiak_tuple: + case adiak_range: + case adiak_list: + case adiak_type_unset: + { + std::string msg( + "Logic error, contact maintainer: Adiak-to-Sina string converter given "); + char *s = adiak_type_to_string(adiak_type, 1); + msg += s; + free(s); + throw std::runtime_error(msg); + } + default: + throw std::runtime_error( + "Adiak-to-Sina string converter given something not convertible to " + "string"); + } +} + +/** +* Some Adiak types become a list of some form. Sina, being concerned +* with queries and visualization, only handles lists that are all scalars +* or all strings. Manage conversions from various Adiak list types that +* contain scalars to a simple list (vector) of scalars. +**/ +std::vector toScalarList(adiak_value_t *subvals, adiak_datatype_t *t) +{ + std::vector sina_safe_list; + for(int i = 0; i < t->num_elements; i++) + { + sina_safe_list.emplace_back(toScalar(subvals + i, t->subtype[0])); + } + return sina_safe_list; +} + +/** +* Partner method to toScalarList, invoked when the children of an adiak list +* type are strings (according to Sina). +**/ +std::vector toStringList(adiak_value_t *subvals, adiak_datatype_t *t) +{ + std::vector sina_safe_list; + for(int i = 0; i < t->num_elements; i++) + { + sina_safe_list.emplace_back(toString(subvals + i, t->subtype[0])); + } + return sina_safe_list; +} + +} // namespace + +void adiakSinaCallback(const char *name, + adiak_category_t, + const char *subcategory, + adiak_value_t *val, + adiak_datatype_t *adiak_type, + void *void_record) +{ + const SinaType sina_type = findSinaType(adiak_type); + axom::sina::Record *record = static_cast(void_record); + std::vector tags; + if(subcategory && subcategory[0] != '\0') + { + tags.emplace_back(subcategory); + } + switch(sina_type) + { + case sina_unknown: + // If we don't know what it is, we can't store it, so as above... + throw std::runtime_error( + "Unknown Adiak type cannot be added to Sina record."); + case sina_scalar: + { + char *s = adiak_type_to_string(adiak_type, 1); + tags.emplace_back(s); + free(s); + addDatum(name, toScalar(val, adiak_type), tags, record); + break; + } + case sina_string: + { + char *s = adiak_type_to_string(adiak_type, 1); + tags.emplace_back(s); + free(s); + addDatum(name, toString(val, adiak_type), tags, record); + break; + } + case sina_file: + addFile(name, toString(val, adiak_type), record); + break; + case sina_list: + { + // Sina doesn't really know/care the difference between list, tuple, set + // Further simplification: everything has to be the same type + // Even further simplification: nothing nested. In the future, depth>1 lists + // should be sent to user_defined + adiak_value_t *subvals = static_cast(val->v_ptr); + SinaType list_type = findSinaType(adiak_type->subtype[0]); + char *s = adiak_type_to_string(adiak_type->subtype[0], 1); + tags.emplace_back(s); + free(s); + switch(list_type) + { + case sina_string: + addDatum(name, toStringList(subvals, adiak_type), tags, record); + break; + // Weird case wherein we're given a list of filenames, which we can somewhat manage + case sina_file: + for(int i = 0; i < adiak_type->num_elements; i++) + { + addFile(name, toString(subvals + i, adiak_type->subtype[0]), record); + } + break; + case sina_scalar: + addDatum(name, toScalarList(subvals, adiak_type), tags, record); + break; + case sina_unknown: + throw std::runtime_error( + "Type must not be unknown for list entries to be added to a Sina " + "record"); + case sina_list: + throw std::runtime_error( + "Lists must not be nested for list entries to be added to a Sina " + "record"); + default: + throw std::runtime_error( + "Type must be set for list entries to be added to a Sina record"); + } + } + } +} + +} // namespace sina +} // namespace axom + +#endif // AXOM_USE_ADIAK diff --git a/src/axom/sina/core/AdiakWriter.hpp b/src/axom/sina/core/AdiakWriter.hpp new file mode 100644 index 0000000000..27ea1d4d50 --- /dev/null +++ b/src/axom/sina/core/AdiakWriter.hpp @@ -0,0 +1,67 @@ +// 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) + +#ifndef SINA_ADIAK_HPP +#define SINA_ADIAK_HPP + +/*! + ****************************************************************************** + * + * \file AdiakWriter.hpp + * + * \brief Header file for the Adiak Sina callback function. + * + ****************************************************************************** + */ + +#include "axom/config.hpp" +#ifdef AXOM_USE_ADIAK + + #include + #include + + #include "axom/sina/core/ConduitUtil.hpp" + #include "axom/sina/core/Record.hpp" + #include "axom/sina/core/Run.hpp" + +extern "C" { + #include "adiak_tool.h" +} + +namespace axom +{ +namespace sina +{ + +/** + * \brief The callback function to pass to Adiak in order to write collected data to a Sina Record. + * + * To register this with Adiak, you should call adiak_register_cb, passing in + * a pointer to the record as the last value. Your code should look something + * like this: + * + * \code + * axom::sina::Record record{ID{"my_id", axom::sina::IDType::Local}, "my_record_type"}; + * axom::sina::adiak_register_cb(1, adiak_category_all, axom::sina::adiakSinaCallback, 0, &record); + * \endcode + * + * \attention Not everything that Sina can capture an be captured through the + * Adiak API. For example, there is currently no support in Adiak to capture + * anything like a CurveSet. As a result, to do that, you must hold on to + * the Record object passed here as the opaque value and manipulate it directly. + **/ +void adiakSinaCallback(const char *name, + adiak_category_t category, + const char *subcategory, + adiak_value_t *value, + adiak_datatype_t *t, + void *opaque_value); + +} // namespace sina +} // namespace axom + +#endif // AXOM_USE_ADIAK + +#endif // SINA_ADIAK_HPP diff --git a/src/axom/sina/core/ConduitUtil.cpp b/src/axom/sina/core/ConduitUtil.cpp new file mode 100644 index 0000000000..afaf2ae95d --- /dev/null +++ b/src/axom/sina/core/ConduitUtil.cpp @@ -0,0 +1,185 @@ +// 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) + +/*! + ****************************************************************************** + * + * \file ConduitUtil.cpp + * + * \brief Implementation file for Sina Conduit utility functions + * + ****************************************************************************** + */ + +#include "axom/sina/core/ConduitUtil.hpp" + +#include +#include +#include +#include +#include + +#include "conduit.hpp" + +namespace axom +{ +namespace sina +{ + +namespace +{ +/** + * Get the given field as string. If it is not a string, an exception is + * thrown with a user-friendly message. + * + * @param field the value of the field + * @param fieldName the name of the field + * @param parentType the name of the parent which contained the field + * @return the avlue of the field + * @throws std::invalid_argument if the field is not a string + */ +std::string getExpectedString(conduit::Node const &field, + std::string const &fieldName, + std::string const &parentType) +{ + if(!field.dtype().is_string()) + { + std::ostringstream message; + message << "The field '" << fieldName << "' for objects of type '" + << parentType << "' must be a string, was '" << field.dtype().name() + << "'"; + throw std::invalid_argument(message.str()); + } + return field.as_string(); +} +} // namespace + +conduit::Node const &getRequiredField(std::string const &fieldName, + conduit::Node const &parent, + std::string const &parentType) +{ + if(!parent.has_child(fieldName)) + { + std::ostringstream message; + message << "The field '" << fieldName + << "' is required for objects of type '" << parentType << "'"; + throw std::invalid_argument(message.str()); + } + return parent.child(fieldName); +} + +std::string getRequiredString(std::string const &fieldName, + conduit::Node const &parent, + std::string const &parentType) +{ + conduit::Node const &field = getRequiredField(fieldName, parent, parentType); + return getExpectedString(field, fieldName, parentType); +} + +std::string getOptionalString(std::string const &fieldName, + conduit::Node const &parent, + std::string const &parentType) +{ + if(!parent.has_child(fieldName) || parent.child(fieldName).dtype().is_empty()) + { + return ""; + } + return getExpectedString(parent.child(fieldName), fieldName, parentType); +} + +double getRequiredDouble(std::string const &fieldName, + conduit::Node const &parent, + std::string const &parentType) +{ + auto &ref = getRequiredField(fieldName, parent, parentType); + if(!ref.dtype().is_number()) + { + std::ostringstream message; + message << "The field '" << fieldName << "' for objects of type '" + << parentType << "' must be a double"; + throw std::invalid_argument(message.str()); + } + return ref.as_double(); +} + +void addStringsToNode(conduit::Node &parent, + std::string const &child_name, + std::vector const &string_values) +{ + // If the child already exists, add_child returns it + conduit::Node &child_node = parent.add_child(child_name); + for(auto &value : string_values) + { + auto &list_entry = child_node.append(); + list_entry.set(value); + } + + // If there were no children, this will be a null node rather than a list. + // We prefer empty lists to null values in our serialized JSON, so force + // this to be a list + if(child_node.number_of_children() == 0) + { + child_node.set_dtype(conduit::DataType::list()); + } +} + +std::vector toDoubleVector(conduit::Node const &node, + std::string const &name) +{ + if(node.dtype().is_list() && node.dtype().number_of_elements() == 0) + { + return std::vector {}; + } + conduit::Node asDoubles; + try + { + node.to_double_array(asDoubles); + } + catch(conduit::Error const &err) + { + std::ostringstream errStream; + errStream << "Error trying to convert node \"" << name + << "\" into a list of doubles" << err.what(); + throw std::invalid_argument(errStream.str()); + } + double const *start = asDoubles.as_double_ptr(); + auto count = static_cast::size_type>( + asDoubles.dtype().number_of_elements()); + return std::vector {start, start + count}; +} + +std::vector toStringVector(conduit::Node const &node, + std::string const &name) +{ + std::vector converted; + if(!node.dtype().is_list()) + { + std::ostringstream errStream; + errStream << "Error trying to convert node \"" << name + << "\" into a list of strings. It is not a list. " + << node.to_json_default(); + throw std::invalid_argument(errStream.str()); + } + for(auto iter = node.children(); iter.has_next();) + { + auto &child = iter.next(); + if(child.dtype().is_string()) + { + converted.emplace_back(child.as_string()); + } + else + { + std::ostringstream errStream; + errStream << "Error trying to convert node \"" << name + << "\" into a list of strings. A value is not a string. " + << node.to_json_default(); + throw std::invalid_argument(errStream.str()); + } + } + return converted; +} + +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/core/ConduitUtil.hpp b/src/axom/sina/core/ConduitUtil.hpp new file mode 100644 index 0000000000..46ee94b7bd --- /dev/null +++ b/src/axom/sina/core/ConduitUtil.hpp @@ -0,0 +1,126 @@ +// 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) + +#ifndef SINA_JSONUTIL_HPP +#define SINA_JSONUTIL_HPP + +/*! + ****************************************************************************** + * + * \file ConduitUtil.hpp + * + * \brief Header file for Sina Conduit utility functions + * + ****************************************************************************** + */ + +#include +#include + +#include "conduit.hpp" + +namespace axom +{ +namespace sina +{ + +/** + * \brief Get a required field from a conduit Node. + * + * \param fieldName the name of the field to get + * \param parent the parent object from which to get the field + * \param parentType a user-friendly name of the type of the parent to use + * in an error message if the field doesn't exist. + * \return the requested field as a Node + * \throws std::invalid_argument if the field does not exist + */ +conduit::Node const &getRequiredField(std::string const &fieldName, + conduit::Node const &parent, + std::string const &parentType); + +/** + * \brief Get the value of a required field from a conduit Node. The field value + * must be a string. + * + * \param fieldName the name of the field to get + * \param parent the parent object from which to get the field + * \param parentType a user-friendly name of the type of the parent to use + * in an error message if the field doesn't exist. + * \return the value of the requested field + * \throws std::invalid_argument if the field does not exist or is not a string + */ +std::string getRequiredString(std::string const &fieldName, + conduit::Node const &parent, + std::string const &parentType); + +/** + * \brief Get the value of a required field from a conduit Node. The field value + * must be a double. + * + * \param fieldName the name of the field to get + * \param parent the parent object from which to get the field + * \param parentType a user-friendly name of the type of the parent to use + * in an error message if the field doesn't exist. + * \return the value of the requested field + * \throws std::invalid_argument if the field does not exist or is not a double + */ +double getRequiredDouble(std::string const &fieldName, + conduit::Node const &parent, + std::string const &parentType); + +/** + * \brief Get the value of an optional field from a conduit Node. The field value + * must be a string if it is present. + * + * \param fieldName the name of the field to get + * \param parent the parent object from which to get the field + * \param parentType a user-friendly name of the type of the parent to use + * in an error message if the field doesn't exist. + * \return the value of the requested field, or an empty string if it + * does not exist + * \throws std::invalid_argument if the field exists but is not a string + */ +std::string getOptionalString(std::string const &fieldName, + conduit::Node const &parent, + std::string const &parentType); + +/** + * \brief Convert the given node to a vector of doubles. + * + * \param node the node to convert + * \param name the name of the node, used in error reporting + * \return the node as a list of doubles + * \throws std::invalid_argument if the node is not a list of doubles + */ +std::vector toDoubleVector(conduit::Node const &node, + std::string const &name); + +/** + * \brief Convert the given node to a vector of strings. + * + * \param node the node to convert + * \param name the name of the node, used in error reporting + * \return the node as a list of strings + * \throws std::invalid_argument if the node is not a list of strings + */ +std::vector toStringVector(conduit::Node const &node, + std::string const &name); + +/** + * \brief Add a vector of strings to a Node. This operation's not natively + * part of Conduit. + * + * \param parent the node to add the strings to + * \param child_name the name of the child (aka the name of the field) + * \param string_values the data values for the field + */ +void addStringsToNode(conduit::Node &parent, + const std::string &child_name, + std::vector const &string_values); + +} // end namespace sina +} // end namespace axom + +#endif //SINA_JSONUTIL_HPP diff --git a/src/axom/sina/core/Curve.cpp b/src/axom/sina/core/Curve.cpp new file mode 100644 index 0000000000..516d560378 --- /dev/null +++ b/src/axom/sina/core/Curve.cpp @@ -0,0 +1,85 @@ +// 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) + +/*! + ****************************************************************************** + * + * \file Curve.cpp + * + * \brief Implementation file for Sina Curve class + * + ****************************************************************************** + */ + +#include "axom/sina/core/Curve.hpp" +#include "axom/sina/core/ConduitUtil.hpp" + +#include + +namespace axom +{ +namespace sina +{ + +namespace +{ +constexpr auto CURVE_TYPE_NAME = "curve"; +constexpr auto VALUES_KEY = "value"; +constexpr auto UNITS_KEY = "units"; +constexpr auto TAGS_KEY = "tags"; +} // namespace + +Curve::Curve(std::string name_, std::vector values_) + : name {std::move(name_)} + , values {std::move(values_)} + , units {} + , tags {} +{ } + +Curve::Curve(std::string name_, double const *values_, std::size_t numValues) + : name {std::move(name_)} + , values {values_, values_ + numValues} + , units {} + , tags {} +{ } + +Curve::Curve(std::string name_, conduit::Node const &curveAsNode) + : name {std::move(name_)} + , values {} + , units {} + , tags {} +{ + auto &valuesAsNode = getRequiredField(VALUES_KEY, curveAsNode, CURVE_TYPE_NAME); + values = toDoubleVector(valuesAsNode, VALUES_KEY); + + units = getOptionalString(UNITS_KEY, curveAsNode, CURVE_TYPE_NAME); + + if(curveAsNode.has_child(TAGS_KEY)) + { + tags = toStringVector(curveAsNode[TAGS_KEY], TAGS_KEY); + } +} + +void Curve::setUnits(std::string units_) { units = std::move(units_); } + +void Curve::setTags(std::vector tags_) { tags = std::move(tags_); } + +conduit::Node Curve::toNode() const +{ + conduit::Node asNode; + asNode[VALUES_KEY] = values; + if(!units.empty()) + { + asNode[UNITS_KEY] = units; + } + if(!tags.empty()) + { + addStringsToNode(asNode, TAGS_KEY, tags); + } + return asNode; +} + +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/core/Curve.hpp b/src/axom/sina/core/Curve.hpp new file mode 100644 index 0000000000..88a3eb597e --- /dev/null +++ b/src/axom/sina/core/Curve.hpp @@ -0,0 +1,119 @@ +// 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) + +#ifndef SINA_CURVE_HPP +#define SINA_CURVE_HPP + +/*! + ****************************************************************************** + * + * \file Curve.hpp + * + * \brief Header file for Sina Curve class + * + ****************************************************************************** + */ + +#include +#include + +#include "conduit.hpp" + +namespace axom +{ +namespace sina +{ + +/** + * A Curve represents a 1-dimensional curve inside a CurveSet. + */ +class Curve +{ +public: + /** + * \brief Create a Curve with the given name and values + * + * \param name the name of the curve + * \param values the curve's values + */ + Curve(std::string name, std::vector values); + + /** + * \brief Create a Curve with the given name and values + * + * \param name the name of the curve + * \param values the curve's values + * \param numValues the number of values. + */ + Curve(std::string name, double const *values, std::size_t numValues); + + /** + * \brief Create a Curve by deserializing a conduit node. + * + * \param name the name of the curve + * \param curveAsNode the serialized version of a curve + */ + Curve(std::string name, conduit::Node const &curveAsNode); + + /** + * \brief Get the curve's name. + * + * \return the curve's name + */ + std::string const &getName() const { return name; } + + /** + * \brief Get the values of the curve. + * + * \return the curve's values + */ + std::vector const &getValues() const { return values; } + + /** + * \brief Set the units of the values. + * + * \param units the value's units + */ + void setUnits(std::string units); + + /** + * \brief Get the units of the values. + * + * \return the value's units + */ + std::string const &getUnits() const { return units; } + + /** + * \brief Set the tags for this curve. + * + * \param tags the curve's tags + */ + void setTags(std::vector tags); + + /** + * \brief Get the tags for this curve. + * + * \return the curve's tags + */ + std::vector const &getTags() const { return tags; } + + /** + * \brief Convert this curve to a Conduit node. + * + * \return a Conduit representation of this curve + */ + conduit::Node toNode() const; + +private: + std::string name; + std::vector values; + std::string units; + std::vector tags; +}; + +} // namespace sina +} // namespace axom + +#endif //SINA_CURVE_HPP diff --git a/src/axom/sina/core/CurveSet.cpp b/src/axom/sina/core/CurveSet.cpp new file mode 100644 index 0000000000..c16afa2b68 --- /dev/null +++ b/src/axom/sina/core/CurveSet.cpp @@ -0,0 +1,133 @@ +// 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) + +/*! + ****************************************************************************** + * + * \file CurveSet.cpp + * + * \brief Implementation file for Sina CurveSet class + * + * \sa Curve.hpp + * + ****************************************************************************** + */ + +#include "axom/sina/core/CurveSet.hpp" + +#include + +#include "axom/sina/core/ConduitUtil.hpp" + +namespace axom +{ +namespace sina +{ + +namespace +{ + +constexpr auto INDEPENDENT_KEY = "independent"; +constexpr auto DEPENDENT_KEY = "dependent"; + +/** + * Add a curve to the given curve map. + * + * @param curve the curve to add + * @param curves the CurveMap to which to add the curve + */ +void addCurve(Curve &&curve, CurveSet::CurveMap &curves) +{ + auto &curveName = curve.getName(); + auto existing = curves.find(curveName); + if(existing == curves.end()) + { + curves.insert(std::make_pair(curveName, curve)); + } + else + { + existing->second = curve; + } +} + +/** + * Extract a CurveMap from the given node. + * + * @param parent the parent node + * @param childNodeName the name of the child node + * @return a CurveMap representing the specified child + */ +CurveSet::CurveMap extractCurveMap(conduit::Node const &parent, + std::string const &childNodeName) +{ + CurveSet::CurveMap curveMap; + if(!parent.has_child(childNodeName)) + { + return curveMap; + } + + auto &mapAsNode = parent.child(childNodeName); + for(auto iter = mapAsNode.children(); iter.has_next();) + { + auto &curveAsNode = iter.next(); + std::string curveName = iter.name(); + Curve curve {curveName, curveAsNode}; + curveMap.insert(std::make_pair(std::move(curveName), std::move(curve))); + } + + return curveMap; +}; + +/** + * Create a Conduit node to represent the given CurveMap. + * + * @param curveMap the CurveMap to convert + * @return the map as a node + */ +conduit::Node createCurveMapNode(CurveSet::CurveMap const &curveMap) +{ + conduit::Node mapNode; + mapNode.set_dtype(conduit::DataType::object()); + for(auto &entry : curveMap) + { + mapNode.add_child(entry.first) = entry.second.toNode(); + } + return mapNode; +} + +} // namespace + +CurveSet::CurveSet(std::string name_) + : name {std::move(name_)} + , independentCurves {} + , dependentCurves {} +{ } + +CurveSet::CurveSet(std::string name_, conduit::Node const &node) + : name {std::move(name_)} + , independentCurves {extractCurveMap(node, INDEPENDENT_KEY)} + , dependentCurves {extractCurveMap(node, DEPENDENT_KEY)} +{ } + +void CurveSet::addIndependentCurve(Curve curve) +{ + addCurve(std::move(curve), independentCurves); +} + +void CurveSet::addDependentCurve(Curve curve) +{ + addCurve(std::move(curve), dependentCurves); +} + +conduit::Node CurveSet::toNode() const +{ + conduit::Node asNode; + asNode[INDEPENDENT_KEY] = createCurveMapNode(independentCurves); + asNode[DEPENDENT_KEY] = createCurveMapNode(dependentCurves); + return asNode; +} + +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/core/CurveSet.hpp b/src/axom/sina/core/CurveSet.hpp new file mode 100644 index 0000000000..d6c546e20b --- /dev/null +++ b/src/axom/sina/core/CurveSet.hpp @@ -0,0 +1,116 @@ +// 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) + +#ifndef SINA_CURVESET_HPP +#define SINA_CURVESET_HPP + +/*! + ****************************************************************************** + * + * \file CurveSet.hpp + * + * \brief Header file for Sina CurveSet class + * + * \sa Curve.hpp + * + ****************************************************************************** + */ + +#include +#include + +#include "conduit.hpp" + +#include "axom/sina/core/Curve.hpp" + +namespace axom +{ +namespace sina +{ + +/** + * \brief A CurveSet represents an entry in a record's "curve_set". + * + * A CurveSet consist of a set of independent and dependent curves. Each curve + * is a list of numbers along with optional units and tags. + * + * \sa Record + * \sa Curve + */ +class CurveSet +{ +public: + /** + * An unordered map of Curve objects. + */ + using CurveMap = std::unordered_map; + + /** + * \brief Create a CurveSet with the given name + * + * \param name the name of the CurveSet + */ + explicit CurveSet(std::string name); + + /** + * \brief Create a CurveSet from the given Conduit node. + * + * \param name the name of the CurveSet + * \param node the Conduit node representing the CurveSet + */ + CurveSet(std::string name, conduit::Node const &node); + + /** + * \brief Get the name of the this CurveSet. + * + * \return the curve set's name + */ + std::string const &getName() const { return name; } + + /** + * \brief Add an independent curve. + * + * \param curve the curve to add + */ + void addIndependentCurve(Curve curve); + + /** + * \brief Add a dependent curve. + * + * \param curve the curve to add + */ + void addDependentCurve(Curve curve); + + /** + * \brief Get a map of all the independent curves. + * + * \return a map of all the independent curves + */ + CurveMap const &getIndependentCurves() const { return independentCurves; } + + /** + * \brief Get a map of all the dependent curves. + * + * \return a map of all the dependent curves + */ + CurveMap const &getDependentCurves() const { return dependentCurves; } + + /** + * \brief Convert his CurveSet to a Conduit node. + * + * \return the Node representation of this CurveSet + */ + conduit::Node toNode() const; + +private: + std::string name; + CurveMap independentCurves; + CurveMap dependentCurves; +}; + +} // namespace sina +} // namespace axom + +#endif //SINA_CURVESET_HPP diff --git a/src/axom/sina/core/DataHolder.cpp b/src/axom/sina/core/DataHolder.cpp new file mode 100644 index 0000000000..f84414a8d9 --- /dev/null +++ b/src/axom/sina/core/DataHolder.cpp @@ -0,0 +1,186 @@ +// 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) + +/*! + ****************************************************************************** + * + * \file DataHolder.cpp + * + * \brief Implementation file for Sina DataHolder class + * + ****************************************************************************** + */ + +#include "axom/sina/core/DataHolder.hpp" + +#include "axom/sina/core/ConduitUtil.hpp" +#include "axom/sina/core/Datum.hpp" + +#include + +namespace +{ + +char const DATA_FIELD[] = "data"; +char const CURVE_SETS_FIELD[] = "curve_sets"; +char const LIBRARY_DATA_FIELD[] = "library_data"; +char const USER_DEFINED_FIELD[] = "user_defined"; + +} // namespace + +namespace axom +{ +namespace sina +{ + +void DataHolder::add(std::string name, Datum datum) +{ + auto existing = data.find(name); + if(existing == data.end()) + { + data.emplace(std::make_pair(std::move(name), datum)); + } + else + { + existing->second = datum; + } +} + +void DataHolder::add(CurveSet curveSet) +{ + auto name = curveSet.getName(); + auto existing = curveSets.find(name); + if(existing == curveSets.end()) + { + curveSets.emplace(name, std::move(curveSet)); + } + else + { + existing->second = std::move(curveSet); + } +} + +std::shared_ptr DataHolder::addLibraryData(std::string const &name) +{ + auto existing = libraryData.find(name); + if(existing == libraryData.end()) + { + libraryData.emplace(name, std::make_shared()); + } + else + { + existing->second = std::make_shared(); + } + return libraryData.at(name); +} + +std::shared_ptr DataHolder::addLibraryData( + std::string const &name, + conduit::Node existingLibraryData) +{ + auto existing = libraryData.find(name); + if(existing == libraryData.end()) + { + libraryData.emplace(name, std::make_shared(existingLibraryData)); + } + else + { + existing->second = std::make_shared(existingLibraryData); + } + return libraryData.at(name); +} + +void DataHolder::setUserDefinedContent(conduit::Node userDefined_) +{ + userDefined = std::move(userDefined_); +} + +conduit::Node DataHolder::toNode() const +{ + conduit::Node asNode; + asNode.set(conduit::DataType::object()); + if(!libraryData.empty()) + { + //Loop through vector of data and append Json + conduit::Node libRef; + for(auto &lib : libraryData) + { + libRef.add_child(lib.first) = lib.second->toNode(); + } + asNode[LIBRARY_DATA_FIELD] = libRef; + } + if(!curveSets.empty()) + { + conduit::Node curveSetsNode; + for(auto &entry : curveSets) + { + curveSetsNode.add_child(entry.first) = entry.second.toNode(); + } + asNode[CURVE_SETS_FIELD] = curveSetsNode; + } + if(!data.empty()) + { + //Loop through vector of data and append Json + conduit::Node datumRef; + for(auto &datum : data) + { + datumRef.add_child(datum.first) = datum.second.toNode(); + } + asNode[DATA_FIELD] = datumRef; + } + if(!userDefined.dtype().is_empty()) + { + asNode[USER_DEFINED_FIELD] = userDefined; + } + return asNode; +} + +DataHolder::DataHolder(conduit::Node const &asNode) +{ + if(asNode.has_child(DATA_FIELD)) + { + auto dataIter = asNode[DATA_FIELD].children(); + //Loop through DATA_FIELD objects and add them to data: + while(dataIter.has_next()) + { + auto &namedDatum = dataIter.next(); + data.emplace(std::make_pair(dataIter.name(), Datum(namedDatum))); + } + } + if(asNode.has_child(CURVE_SETS_FIELD)) + { + auto curveSetsIter = asNode[CURVE_SETS_FIELD].children(); + while(curveSetsIter.has_next()) + { + auto &curveSetNode = curveSetsIter.next(); + std::string name = curveSetsIter.name(); + CurveSet cs {name, curveSetNode}; + curveSets.emplace(std::make_pair(std::move(name), std::move(cs))); + } + } + if(asNode.has_child(LIBRARY_DATA_FIELD)) + { + auto libraryIter = asNode[LIBRARY_DATA_FIELD].children(); + while(libraryIter.has_next()) + { + auto &libraryDataNode = libraryIter.next(); + std::string name = libraryIter.name(); + libraryData.emplace( + std::make_pair(std::move(name), + std::make_shared(libraryDataNode))); + } + } + if(asNode.has_child(USER_DEFINED_FIELD)) + { + userDefined = asNode[USER_DEFINED_FIELD]; + if(!userDefined.dtype().is_object()) + { + throw std::invalid_argument("user_defined must be an object Node"); + } + } +} + +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/core/DataHolder.hpp b/src/axom/sina/core/DataHolder.hpp new file mode 100644 index 0000000000..daad0e00ba --- /dev/null +++ b/src/axom/sina/core/DataHolder.hpp @@ -0,0 +1,205 @@ +// 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) + +#ifndef SINA_DATAHOLDER_HPP +#define SINA_DATAHOLDER_HPP + +/*! + ****************************************************************************** + * + * \file DataHolder.hpp + * + * \brief Header file for Sina DataHolder class + * + ****************************************************************************** + */ + +#include +#include +#include + +#include "conduit.hpp" + +#include "axom/sina/core/Datum.hpp" +#include "axom/sina/core/CurveSet.hpp" + +namespace axom +{ +namespace sina +{ + +/** + * \brief A DataHolder is a basic container for certain types of information. + * + * DataHolders contain curves, libraries, and data (Datum), and represent + * all the information a library can have associated with it. Records expand + * on DataHolders to contain additional info. + * + * \sa Record + * \sa LibraryDataMap + */ +class DataHolder +{ +public: + /** + * An unordered map of Datum objects. + */ + using DatumMap = std::unordered_map; + + /** + * An unordered map of CurveSet objects. + */ + using CurveSetMap = std::unordered_map; + + /** + * An unordered map of shared pointers to DataHolder objects. + */ + using LibraryDataMap = + std::unordered_map>; + + /** + * Construct an empty DataHolder. + */ + DataHolder() = default; + + /** + * Virtual destructor to automatically clean up resources held by an instance of the DataHolder class. + */ + virtual ~DataHolder() = default; + + /** + * Copy constructor that disallows this constructor type. + */ + DataHolder(DataHolder const &) = delete; + + /** + * Disable copy assignment. + */ + DataHolder &operator=(DataHolder const &) = delete; + + /** + * \brief Construct a DataHolder from its conduit Node representation. + * + * \param asNode the DataHolder as a Node + */ + explicit DataHolder(conduit::Node const &asNode); + + /** + * \brief Get the DataHolder's data. + * + * \return the DataHolder's data + */ + DatumMap const &getData() const noexcept { return data; } + + /** + * \brief Add a Datum to this DataHolder. + * + * \param name the key for the Datum to add + * \param datum the Datum to add + */ + void add(std::string name, Datum datum); + + /** + * \brief Add a CurveSet to this DataHolder. + * + * \param curveSet the CurveSet to add + */ + void add(CurveSet curveSet); + + /** + * \brief Get the curve sets associated with this DataHolder. + * + * \return the dataholder's curve sets + */ + CurveSetMap const &getCurveSets() const noexcept { return curveSets; } + + /** + * \brief Add a new library to this DataHolder. + * + * If you try to add a library with a name that already exists, the old + * library will be replaced. + * + * \return a pointer to a new DataHolder for a library + * of the given name. + */ + std::shared_ptr addLibraryData(std::string const &name); + + /** + * \brief Add a new library to this DataHolder with existing library data. + * + * \return a pointer to a new DataHolder for a library of the given name. + */ + std::shared_ptr addLibraryData(std::string const &name, + conduit::Node existingLibraryData); + + /** + * \brief Get all library data associated with this DataHolder. + * + * \return the dataholder's library data + */ + LibraryDataMap const &getLibraryData() const noexcept { return libraryData; } + + /** + * \brief Get a specific library associated with this DataHolder. + * + * \return the dataholder's library data + */ + std::shared_ptr getLibraryData(std::string const &libraryName) + { + return libraryData.at(libraryName); + } + + /** + * \brief Get a specific library associated with this DataHolder. + * + * \return the dataholder's library data + */ + std::shared_ptr const getLibraryData(std::string const &libraryName) const + { + return libraryData.at(libraryName); + } + + /** + * \brief Get the user-defined content of the object. + * + * \return the user-defined content + */ + conduit::Node const &getUserDefinedContent() const noexcept + { + return userDefined; + } + + /** + * \brief Get the user-defined content of the object. + * + * \return the user-defined content + */ + conduit::Node &getUserDefinedContent() noexcept { return userDefined; } + + /** + * \brief Set the user-defined content of the object. + * + * \param userDefined the user-defined content. Must be an object (key/value pairs) + */ + void setUserDefinedContent(conduit::Node userDefined); + + /** + * \brief Convert this DataHolder to its conduit Node representation. + * + * \return the Node representation of this DataHolder. + */ + virtual conduit::Node toNode() const; + +private: + CurveSetMap curveSets; + DatumMap data; + LibraryDataMap libraryData; + conduit::Node userDefined; +}; + +} // namespace sina +} // namespace axom + +#endif //SINA_DATAHOLDER_HPP diff --git a/src/axom/sina/core/Datum.cpp b/src/axom/sina/core/Datum.cpp new file mode 100644 index 0000000000..cf457a3335 --- /dev/null +++ b/src/axom/sina/core/Datum.cpp @@ -0,0 +1,204 @@ +// 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) + +/*! + ****************************************************************************** + * + * \file Datum.cpp + * + * \brief Implementation file for Sina Datum class + * + ****************************************************************************** + */ + +#include "axom/sina/core/Datum.hpp" +#include "conduit.hpp" + +#include +#include +#include + +namespace +{ + +char const VALUE_FIELD[] = "value"; +char const UNITS_FIELD[] = "units"; +char const TAGS_FIELD[] = "tags"; +char const DATA_PARENT_TYPE[] = "data"; + +} // namespace + +namespace axom +{ +namespace sina +{ + +Datum::Datum(const std::string &value_) : stringValue {value_} +{ + //Set type to String, as we know it uses strings + type = ValueType::String; +} + +Datum::Datum(const double &value_) : scalarValue {value_} +{ + //Set type to Scalar, as we know it uses doubles + type = ValueType::Scalar; +} + +Datum::Datum(const std::vector &value_) : stringArrayValue {value_} +{ + //Set type to StringArray, as we know it uses an array of strings + type = ValueType::StringArray; +} + +Datum::Datum(const std::vector &value_) : scalarArrayValue {value_} +{ + //Set type to ScalarArray, as we know it uses an array of doubles + type = ValueType::ScalarArray; +} + +Datum::Datum(conduit::Node const &asNode) +{ + //Need to determine what type of Datum we have: Scalar (double), String, + //or list of one of those two. + conduit::Node valueNode = + getRequiredField(VALUE_FIELD, asNode, DATA_PARENT_TYPE); + if(valueNode.dtype().is_string()) + { + stringValue = valueNode.as_string(); + type = ValueType::String; + } + else if(valueNode.dtype().is_number() && + valueNode.dtype().number_of_elements() == 1) + { + scalarValue = valueNode.to_double(); + type = ValueType::Scalar; + } + // There are two different ways to end up with a "list" of numbers in conduit, but + // only one of them tests True for is_list. This handles the other. + else if(valueNode.dtype().is_number()) + { + type = ValueType::ScalarArray; + // What's passed in could be an array of any numeric type + // We pass a cast copy into captureNode + conduit::Node captureNode; + valueNode.to_float64_array(captureNode); + std::vector array_as_vect( + captureNode.as_double_ptr(), + captureNode.as_double_ptr() + captureNode.dtype().number_of_elements()); + scalarArrayValue = array_as_vect; + } + else if(valueNode.dtype().is_list()) + { + //An empty list is assumed to be an empty list of doubles. + //This only works because this field is immutable! + //If this ever changes, or if Datum's type is used directly to make + //decisions (ex: Sina deciding where to store data), this logic + //should be revisited. + if(valueNode.number_of_children() == 0 || valueNode[0].dtype().is_number()) + { + type = ValueType::ScalarArray; + } + else if(valueNode[0].dtype().is_string()) + { + type = ValueType::StringArray; + } + else + { + std::ostringstream message; + message << "The only valid types for an array '" << VALUE_FIELD + << "' are strings and numbers. Got '" << valueNode.to_json() + << "'"; + throw std::invalid_argument(message.str()); + } + + auto itr = valueNode.children(); + while(itr.has_next()) + { + conduit::Node const &entry = itr.next(); + if(entry.dtype().is_string() && type == ValueType::StringArray) + { + stringArrayValue.emplace_back(entry.as_string()); + } + else if(entry.dtype().is_number() && type == ValueType::ScalarArray) + { + scalarArrayValue.emplace_back(entry.to_double()); + } + else + { + std::ostringstream message; + message + << "If the required field '" << VALUE_FIELD + << "' is an array, it must consist of only strings or only numbers, " + << "but got '" << entry.dtype().name() << "' (" << entry.to_json() + << ")"; + throw std::invalid_argument(message.str()); + } + } + } + else + { + std::ostringstream message; + message + << "The required field '" << VALUE_FIELD + << "' must be a string, double, list of strings, or list of doubles."; + throw std::invalid_argument(message.str()); + } + + //Get the units, if there are any + units = getOptionalString(UNITS_FIELD, asNode, DATA_PARENT_TYPE); + + //Need to grab the tags and add them to a vector of strings + if(asNode.has_child(TAGS_FIELD)) + { + auto tagNodeIter = asNode[TAGS_FIELD].children(); + while(tagNodeIter.has_next()) + { + auto &tag = tagNodeIter.next(); + if(tag.dtype().is_string()) + { + tags.emplace_back(std::string(tag.as_string())); + } + else + { + std::ostringstream message; + message << "The optional field '" << TAGS_FIELD + << "' must be an array of strings. Found '" + << tag.dtype().name() << "' instead."; + throw std::invalid_argument(message.str()); + } + } + } +} + +void Datum::setUnits(const std::string &units_) { units = units_; } + +void Datum::setTags(const std::vector &tags_) { tags = tags_; } + +conduit::Node Datum::toNode() const +{ + conduit::Node asNode; + switch(type) + { + case ValueType::Scalar: + asNode[VALUE_FIELD] = scalarValue; + break; + case ValueType::String: + asNode[VALUE_FIELD] = stringValue; + break; + case ValueType::ScalarArray: + asNode[VALUE_FIELD] = scalarArrayValue; + break; + case ValueType::StringArray: + addStringsToNode(asNode, VALUE_FIELD, stringArrayValue); + break; + } + if(tags.size() > 0) addStringsToNode(asNode, TAGS_FIELD, tags); + if(!units.empty()) asNode[UNITS_FIELD] = units; + return asNode; +}; + +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/core/Datum.hpp b/src/axom/sina/core/Datum.hpp new file mode 100644 index 0000000000..cc23117696 --- /dev/null +++ b/src/axom/sina/core/Datum.hpp @@ -0,0 +1,206 @@ +// 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) + +#ifndef SINA_DATUM_HPP +#define SINA_DATUM_HPP + +/*! + ****************************************************************************** + * + * \file Datum.hpp + * + * \brief Header file for Sina Datum class + * + ****************************************************************************** + */ + +#include +#include + +#include "axom/sina/core/ConduitUtil.hpp" +#include "conduit.hpp" + +namespace axom +{ +namespace sina +{ + +/** + * Represents whether a Datum is a String, Scalar (double), array of Strings, + * or array of Scalars. + */ +enum class ValueType +{ + String, + Scalar, + StringArray, + ScalarArray +}; + +/** + * \brief An object to track a value associated with a Record + * + * A Datum tracks the value and (optionally) tags and/or units of a + * value associated with a Record, e.g. a scalar, a piece of metadata, + * or an input parameter. In the Sina schema, a Datum always + * belongs to a Record or one of Record's inheriting types. + * + * Every Datum must have a value; units and tags are optional. + * + * The value of a Datum may be a string, a double, an array of strings, + * or an array of doubles. + * + * \code + * axom::sina::Datum myDatum{12.34}; + * std::string value = "foobar"; + * axom::sina::Datum myOtherDatum{value}; + * std::vector scalars = {1, 2, 20.0}; + * axom::sina::Datum myArrayDatum{scalars}; + * + * //prints 1, corresponding to Scalar + * std::cout << static_cast::type>(myDatum.getType()) << std::endl; + * + * //prints 0, corresponding to String + * std::cout << static_cast::type>(myOtherDatum.getType()) << std::endl; + * + * //prints 3, corresponding to ScalarArray + * std::cout << static_cast::type>(myArrayDatum.getType()) << std::endl; + * + * myRecord->add(myDatum); + * myOtherDatum.setUnits("km/s"); + * myRecord->add(myOtherDatum); + * std::vector tags = {"input", "core"}; + * myArrayDatum.setTags(tags); + * myRecord->add(myArrayDatum); + * \endcode + */ +class Datum +{ +public: + /** + * \brief Construct a new Datum. + * + * \param value the string value of the datum + */ + Datum(const std::string &value); + + /** + * \brief Construct a new Datum. + * + * \param value the double value of the datum + */ + Datum(const double &value); + + /** + * \brief Construct a new Datum. + * + * \param value the string array value of the datum + */ + Datum(const std::vector &value); + + /** + * \brief Construct a new Datum. + * + * \param value the scalar array value of the datum + */ + Datum(const std::vector &value); + + /** + * \brief Construct a Datum from its Node representation. + * + * \param asNode the Datum as conduit Node + */ + explicit Datum(conduit::Node const &asNode); + + /** + * \brief Get the string value of the Datum. + * + * \return the string value + */ + std::string const &getValue() const noexcept { return stringValue; } + + /** + * \brief Get the scalar value of the Datum. + * + * \return the scalar value + */ + double const &getScalar() const noexcept { return scalarValue; } + + /** + * \brief Get the string array value of the Datum. + * + * \return the string vector value + */ + std::vector const &getStringArray() const noexcept + { + return stringArrayValue; + } + + /** + * \brief Get the scalar array value of the Datum. + * + * \return the scalar vector value + */ + std::vector const &getScalarArray() const noexcept + { + return scalarArrayValue; + } + + /** + * \brief Get the tags of the Datum + * + * \return the tags of the value + */ + std::vector const &getTags() const noexcept { return tags; } + + /** + * \brief Set the tags of the Datum + * + * \param tags the tags of the value + */ + void setTags(const std::vector &tags); + + /** + * \brief Get the units of the Datum + * + * \return the units of the value + */ + std::string const &getUnits() const noexcept { return units; } + + /** + * \brief Set the units of the Datum + * + * \param units the units of the value + */ + void setUnits(const std::string &units); + + /** + * \brief Get the type of the Datum + * + * \return the type of the value + */ + ValueType getType() const noexcept { return type; } + + /** + * \brief Convert this Datum to its conduit Node representation. + * + * \return the Node representation of this Datum. + */ + conduit::Node toNode() const; + +private: + std::string stringValue; + double scalarValue; + std::vector stringArrayValue; + std::vector scalarArrayValue; + std::string units; + std::vector tags; + ValueType type; +}; + +} // namespace sina +} // namespace axom + +#endif //SINA_DATUM_HPP diff --git a/src/axom/sina/core/Document.cpp b/src/axom/sina/core/Document.cpp new file mode 100644 index 0000000000..d53892e477 --- /dev/null +++ b/src/axom/sina/core/Document.cpp @@ -0,0 +1,174 @@ +// 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) + +/*! + ****************************************************************************** + * + * \file Document.cpp + * + * \brief Implementation file for Sina Document class + * + ****************************************************************************** + */ + +#include "axom/sina/core/Document.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace axom +{ +namespace sina +{ + +namespace +{ +char const RECORDS_KEY[] = "records"; +char const RELATIONSHIPS_KEY[] = "relationships"; +char const SAVE_TMP_FILE_EXTENSION[] = ".sina.tmp"; +} // namespace + +void Document::add(std::unique_ptr record) +{ + records.emplace_back(std::move(record)); +} + +void Document::add(Relationship relationship) +{ + relationships.emplace_back(std::move(relationship)); +} + +conduit::Node Document::toNode() const +{ + conduit::Node document(conduit::DataType::object()); + document[RECORDS_KEY] = conduit::Node(conduit::DataType::list()); + document[RELATIONSHIPS_KEY] = conduit::Node(conduit::DataType::list()); + for(auto &record : records) + { + auto &list_entry = document[RECORDS_KEY].append(); + list_entry.set_node(record->toNode()); + } + for(auto &relationship : relationships) + { + auto &list_entry = document[RELATIONSHIPS_KEY].append(); + list_entry = relationship.toNode(); + } + return document; +} + +void Document::createFromNode(conduit::Node const &asNode, + RecordLoader const &recordLoader) +{ + if(asNode.has_child(RECORDS_KEY)) + { + 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)) + { + 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 + { + std::ostringstream message; + message << "The '" << RELATIONSHIPS_KEY + << "' element of a document must be an array"; + throw std::invalid_argument(message.str()); + } + } +} + +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); +} + +std::string Document::toJson(conduit::index_t indent, + conduit::index_t depth, + const std::string &pad, + const std::string &eoe) const +{ + return this->toNode().to_json("json", indent, depth, pad, eoe); +} + +void saveDocument(Document const &document, std::string const &fileName) +{ + // 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) + { + std::string message {"Could not save to '"}; + message += fileName; + message += "'"; + throw std::ios::failure {message}; + } +} + +Document loadDocument(std::string const &path) +{ + return loadDocument(path, createRecordLoaderWithAllKnownTypes()); +} + +Document loadDocument(std::string const &path, RecordLoader const &recordLoader) +{ + 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}; +} + +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/core/Document.hpp b/src/axom/sina/core/Document.hpp new file mode 100644 index 0000000000..e048ea4ef2 --- /dev/null +++ b/src/axom/sina/core/Document.hpp @@ -0,0 +1,224 @@ +// 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) + +#ifndef SINA_DOCUMENT_HPP +#define SINA_DOCUMENT_HPP + +/*! + ****************************************************************************** + * + * \file Document.hpp + * + * \brief Header file for Sina Document class + * + ****************************************************************************** + */ + +#include +#include + +#include "conduit.hpp" + +#include "axom/sina/core/Record.hpp" +#include "axom/sina/core/Relationship.hpp" + +namespace axom +{ +namespace sina +{ + +/** + * \brief An object representing the top-level object of a Sina JSON file + * + * A Document represents the top-level object of a JSON file conforming to the + * Sina schema. When serialized, these documents can be ingested into a + * Sina database and used with the Sina tool. + * + * Documents contain at most two objects: a list of Records and a list of Relationships. A simple, empty document: + * \code{.json} + * { + * "records": [], + * "relationships": [] + * } + * \endcode + * + * The "records" list can contain Record objects and their inheriting types, such as Run (for a full list, please see + * the inheritance diagram in the Record documentation). The "relationships" list can contain Relationship objects. + * + * Documents can be assembled programatically and/or generated from existing JSON. An example of an assembled + * Document is provided on the main page. To load a Document from an existing JSON file: + * \code + * axom::sina::Document myDocument = axom::sina::loadDocument("path/to/infile.json"); + * \endcode + * + * To generate a Document from a JSON string and vice versa: + * \code + * std::string my_json = "{\"records\":[{\"type\":\"run\",\"id\":\"test\"}],\"relationships\":[]}"; + * axom::sina::Document myDocument = axom::sina::Document(my_json, axom::sina::createRecordLoaderWithAllKnownTypes()); + * std::cout << myDocument.toJson() << std::endl;); + * \endcode + * + * You can add further entries to the Document using add(): + * \code + * std::unique_ptr myRun{new axom::sina::Run{someID, "My Sim Code", "1.2.3", "jdoe"}}; + * axom::sina::Relationship myRelationship{someID, "comes before", someOtherID}; + * myDocument.add(myRun); + * myDocument.add(myRelationship); + * \endcode + * + * You can also export your Document to file: + * \code + * axom::sina::saveDocument(myDocument, "path/to/outfile.json") + * \endcode + * + */ +class Document +{ +public: + /** + * A vector of pointers to Record objects. + */ + using RecordList = std::vector>; + + /** + * A vector of Relationship objects. + */ + using RelationshipList = std::vector; + + /** + * Construct an empty Document. + */ + Document() = default; + + /** + * Disable copying Document objects. We must do this since we hold + * pointers to polymorphic objects. + */ + Document(Document const &) = delete; + + /** + * Disabling copy assignment. + */ + Document &operator=(Document const &) = delete; + + /** + * Move constructor which should be handled by the compiler. + */ + Document(Document &&) = default; + + /** + * Move assignment which should be handled by the compiler. + */ + Document &operator=(Document &&) = default; + + /** + * \brief Create a Document from its Conduit Node representation + * + * \param asNode the Document as a Node + * \param recordLoader an RecordLoader to use to load the different + * types of records which may be in the document + */ + Document(conduit::Node const &asNode, RecordLoader const &recordLoader); + + /** + * \brief Create a Document from a JSON string representation + * + * \param asJson the Document as a JSON string + * \param recordLoader an RecordLoader to use to load the different + * types of records which may be in the document + */ + Document(std::string const &asJson, RecordLoader const &recordLoader); + + /** + * \brief Add the given record to this document. + * + * \param record the record to add + */ + void add(std::unique_ptr record); + + /** + * \brief Get the list of records currently in this document. + * + * \return the list of records + */ + RecordList const &getRecords() const noexcept { return records; } + + /** + * \brief Add a relationship to this document + * + * \param relationship the relationship to add + */ + void add(Relationship relationship); + + /** + * \brief Get the list of relationships in this document. + * + * \return the list of relationships + */ + RelationshipList const &getRelationships() const noexcept + { + return relationships; + } + + /** + * \brief Convert this document to a conduit Node. + * + * \return the contents of the document as a Node + */ + conduit::Node toNode() const; + + /** + * \brief Convert this document to a JSON string. + * + * \return the contents of the document as a JSON string + */ + std::string toJson(conduit::index_t indent = 0, + conduit::index_t depth = 0, + const std::string &pad = "", + const std::string &eoe = "") const; + +private: + /** + * Constructor helper method, extracts info from a conduit Node. + */ + void createFromNode(conduit::Node const &asNode, + RecordLoader const &recordLoader); + RecordList records; + RelationshipList relationships; +}; + +/** + * \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 to which to save the file + * \throws std::ios::failure if there are any IO errors + */ +void saveDocument(Document const &document, std::string const &fileName); + +/** + * \brief Load a document from the given path. Only records which this library + * knows about will be able to be loaded. + * + * \param path the file system path from which to load the document + * \return the loaded Document + */ +Document loadDocument(std::string const &path); + +/** + * \brief Load a document from the given 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 + * \return the loaded Document + */ +Document loadDocument(std::string const &path, RecordLoader const &recordLoader); + +} // namespace sina +} // namespace axom + +#endif //SINA_DOCUMENT_HPP diff --git a/src/axom/sina/core/File.cpp b/src/axom/sina/core/File.cpp new file mode 100644 index 0000000000..d624ac4175 --- /dev/null +++ b/src/axom/sina/core/File.cpp @@ -0,0 +1,89 @@ +// 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) + +/*! + ****************************************************************************** + * + * \file File.cpp + * + * \brief Implementation file for Sina File class + * + ****************************************************************************** + */ + +#include "axom/sina/core/File.hpp" +#include "axom/sina/core/ConduitUtil.hpp" + +#include +#include +#include + +#include "conduit.hpp" + +namespace axom +{ +namespace sina +{ + +namespace +{ +char const MIMETYPE_KEY[] = "mimetype"; +char const FILE_TYPE_NAME[] = "File"; +char const TAGS_KEY[] = "tags"; +} // namespace + +File::File(std::string uri_) : uri {std::move(uri_)} { } + +File::File(std::string uri_, conduit::Node const &asNode) + : uri {std::move(uri_)} + , mimeType {getOptionalString(MIMETYPE_KEY, asNode, FILE_TYPE_NAME)} +{ + if(asNode.has_child(TAGS_KEY)) + { + auto tagsIter = asNode[TAGS_KEY].children(); + while(tagsIter.has_next()) + { + auto &tag = tagsIter.next(); + if(tag.dtype().is_string()) + tags.emplace_back(tag.as_string()); + else + { + std::ostringstream message; + message << "The optional field '" << TAGS_KEY + << "' must be an array of strings. Found '" + << tag.dtype().name() << "' instead."; + throw std::invalid_argument(message.str()); + } + } + } +} + +void File::setMimeType(std::string mimeType_) +{ + File::mimeType = std::move(mimeType_); +} + +void File::setTags(std::vector tags_) +{ + File::tags = std::move(tags_); +} + +conduit::Node File::toNode() const +{ + conduit::Node asNode; + if(!mimeType.empty()) + { + asNode[MIMETYPE_KEY] = mimeType; + } + if(tags.size() > 0) + { + std::vector tags_copy(tags); + addStringsToNode(asNode, TAGS_KEY, tags_copy); + } + return asNode; +} + +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/core/File.hpp b/src/axom/sina/core/File.hpp new file mode 100644 index 0000000000..4c463b90dd --- /dev/null +++ b/src/axom/sina/core/File.hpp @@ -0,0 +1,114 @@ +// 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) + +#ifndef SINA_FILE_HPP +#define SINA_FILE_HPP + +/*! + ****************************************************************************** + * + * \file File.hpp + * + * \brief Header file for Sina File class + * + ****************************************************************************** + */ + +#include +#include + +#include "conduit.hpp" + +namespace axom +{ +namespace sina +{ +/** + * \brief An object to help track locations of files in the file system + * + * A File tracks the location (URI) and mimetype of a file on the file system, plus any tags. + * In the Sina schema, a File always belongs to a Record or one of Record's inheriting types. + * + * Every File must have a URI, while mimetype and tags are optional. + * + * \code + * axom::sina::File myFile{"/path/to/file.png"}; + * myFile.setMimeType("image/png"); + * axom::sina::File myOtherFile{"/path/to/other/file.txt"}; + * myOtherFile.setTags({"these","are","tags"}); + * myRecord->add(myFile); + * myRecord->add(myOtherFile); + * \endcode + */ +class File +{ +public: + /** + * \brief Construct a new File. + * + * \param uri the location of the file + */ + explicit File(std::string uri); + + /** + * \brief Construct a new File. + * + * \param uri the uri for a file + * \param asNode the Node representation of the file's additional info + */ + File(std::string uri, conduit::Node const &asNode); + + /** + * \brief Get the File's URI. + * + * \return the URI + */ + std::string const &getUri() const noexcept { return uri; } + + /** + * \brief Get the File's MIME type. + * + * \return the MIME type + */ + std::string const &getMimeType() const noexcept { return mimeType; } + + /** + * \brief Get the File's tags. + * + * \return the tags + */ + std::vector const &getTags() const noexcept { return tags; } + + /** + * \brief Set the File's MIME type. + * + * \param mimeType the MIME type + */ + void setMimeType(std::string mimeType); + + /** + * \brief Set the File's tags. + * + * \param tags the File's tags + */ + void setTags(std::vector tags); + + /** + * \brief Convert this File to its conduit Node representation. + * + * \return the File in its Node representation + */ + conduit::Node toNode() const; + +private: + std::string uri; + std::string mimeType; + std::vector tags; +}; + +} // namespace sina +} // namespace axom + +#endif //SINA_FILE_HPP diff --git a/src/axom/sina/core/ID.cpp b/src/axom/sina/core/ID.cpp new file mode 100644 index 0000000000..8d87e40713 --- /dev/null +++ b/src/axom/sina/core/ID.cpp @@ -0,0 +1,81 @@ +// 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) + +/*! + ****************************************************************************** + * + * \file ID.cpp + * + * \brief Implementation file for Sina ID class + * + ****************************************************************************** + */ + +#include "axom/sina/core/ID.hpp" + +#include +#include + +namespace axom +{ +namespace sina +{ + +ID::ID(std::string id_, IDType type_) : id {std::move(id_)}, type {type_} { } + +namespace internal +{ + +namespace +{ +/** + * Extract an ID from a given JSON object. + * + * @param parentObject the object from which to extract the ID + * @param localName the local variant of the ID field + * @param globalName the global variant of the ID field + * @return the ID from the object + */ +ID extractIDFromObject(conduit::Node const &parentObject, + std::string const &localName, + std::string const &globalName) +{ + if(parentObject.has_child(globalName)) + { + return ID {std::string(parentObject[globalName].as_string()), IDType::Global}; + } + if(parentObject.has_child(localName)) + { + return ID {std::string(parentObject[localName].as_string()), IDType::Local}; + } + std::string message {"Could not find either of the required ID fields '"}; + message += localName + "' or '" + globalName + "'"; + throw std::invalid_argument(message); +} +} // namespace + +IDField::IDField(ID value_, std::string localName_, std::string globalName_) + : value {std::move(value_)} + , localName {std::move(localName_)} + , globalName {std::move(globalName_)} +{ } + +IDField::IDField(conduit::Node const &parentObject, + std::string localName_, + std::string globalName_) + : IDField {extractIDFromObject(parentObject, localName_, globalName_), + std::move(localName_), + std::move(globalName_)} +{ } + +void IDField::addTo(conduit::Node &object) const +{ + auto &key = value.getType() == IDType::Global ? globalName : localName; + object[key] = value.getId(); +} + +} // namespace internal +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/core/ID.hpp b/src/axom/sina/core/ID.hpp new file mode 100644 index 0000000000..d35b057bd3 --- /dev/null +++ b/src/axom/sina/core/ID.hpp @@ -0,0 +1,151 @@ +// 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) + +#ifndef SINA_ID_HPP +#define SINA_ID_HPP + +/*! + ****************************************************************************** + * + * \file ID.hpp + * + * \brief Header file for Sina ID class + * + * The Sina schema allows records to have either a local ID or a global ID. + * When a global ID is specified, that will be used in the database. When a + * local ID is specified, an ID will be automatically generated when inserting + * the record into the database. + * + ****************************************************************************** + */ + +#include + +#include "conduit.hpp" + +namespace axom +{ +namespace sina +{ + +/** + * Represents whether an ID is local or global. + */ +enum class IDType +{ + Local, + Global +}; + +/** + * \brief The ID of a Record + * + * An ID is used to represent the ID of a Record. This class holds both the + * actual ID and whether it is a local or global ID. + */ +class ID +{ +public: + /** + * \brief Create a new ID. + * + * \param id the actual value of the ID + * \param type whether the ID is local or global + */ + ID(std::string id, IDType type); + + /** + * \brief Get the value of the ID. + * + * \return the actual ID + */ + std::string const &getId() const noexcept { return id; } + + /** + * \brief Get the type of the ID. + * + * \return whether the ID is local or global + */ + IDType getType() const noexcept { return type; } + +private: + std::string id; + IDType type; +}; + +namespace internal +{ + +/** + * \brief An object representing a pair of ID fields + * + * IDField instances are used to describe a pair of ID fields in a schema + * object which correspond to global and local names for the field. For + * example, the "id" and "local_id" fields in records, or the + * "subject"/"local_subject" and "object"/"local_object" pairs in + * relationships. + */ +class IDField +{ +public: + /** + * \brief Construct a new IDField. + * + * \param value the value of the ID + * \param localName the name of the local variant of the field + * \param globalName the name of the global variant of the field + */ + IDField(ID value, std::string localName, std::string globalName); + + /** + * \brief Construct an IDField by looking for its values in a conduit Node. + * + * \param parentObject the conduit Node containing the ID field + * \param localName the local name of the field + * \param globalName the global name of the field + */ + IDField(conduit::Node const &parentObject, + std::string localName, + std::string globalName); + + /** + * \brief Get the value of this field. + * + * \return the ID describing the field's value + */ + ID const &getID() const noexcept { return value; } + + /** + * \brief Get the name to use for this field when the ID is local. + * + * \return the name of the local ID field + */ + std::string const &getLocalName() const noexcept { return localName; } + + /** + * \brief Get the name to use for this field when the ID is global. + * + * \return the name of the global ID field + */ + std::string const &getGlobalName() const noexcept { return globalName; } + + /** + * \brief Add this field to the given Node. + * + * \param object the Node to which to add the field + */ + void addTo(conduit::Node &object) const; + +private: + ID value; + std::string localName; + std::string globalName; +}; + +} // namespace internal +} // namespace sina +} // namespace axom + +#endif //SINA_ID_HPP diff --git a/src/axom/sina/core/Record.cpp b/src/axom/sina/core/Record.cpp new file mode 100644 index 0000000000..58fd35c917 --- /dev/null +++ b/src/axom/sina/core/Record.cpp @@ -0,0 +1,138 @@ +// 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) + +/*! + ****************************************************************************** + * + * \file Record.cpp + * + * \brief Implementation file for Sina Record class + * + * \sa DataHolder.hpp + * + ****************************************************************************** + */ + +#include "axom/sina/core/Record.hpp" + +#include +#include + +#include "axom/sina/core/ConduitUtil.hpp" +#include "axom/sina/core/DataHolder.hpp" +#include "axom/sina/core/Run.hpp" + +namespace +{ + +char const LOCAL_ID_FIELD[] = "local_id"; +char const GLOBAL_ID_FIELD[] = "id"; +char const TYPE_FIELD[] = "type"; +char const FILES_FIELD[] = "files"; + +// Used to preserve information when appending "standalone" library data +char const LIBRARY_DATA_ID_DATUM[] = "SINA_librarydata_id"; +char const LIBRARY_DATA_TYPE_DATUM[] = "SINA_librarydata_type"; + +} // namespace + +namespace axom +{ +namespace sina +{ + +Record::Record(ID id_, std::string type_) + : DataHolder {} + , id {std::move(id_), LOCAL_ID_FIELD, GLOBAL_ID_FIELD} + , type {std::move(type_)} +{ } + +conduit::Node Record::toNode() const +{ + conduit::Node asNode = DataHolder::toNode(); + asNode[TYPE_FIELD] = type; + id.addTo(asNode); + // Optional fields + if(!files.empty()) + { + conduit::Node fileRef; + for(auto &file : files) + { + auto &n = fileRef.add_child(file.getUri()); + n.set(file.toNode()); + asNode[FILES_FIELD] = fileRef; + } + } + return asNode; +} + +Record::Record(conduit::Node const &asNode) + : DataHolder {asNode} + , id {asNode, LOCAL_ID_FIELD, GLOBAL_ID_FIELD} + , type {getRequiredString(TYPE_FIELD, asNode, "record")} +{ + if(asNode.has_child(FILES_FIELD)) + { + auto filesIter = asNode[FILES_FIELD].children(); + while(filesIter.has_next()) + { + auto &namedFile = filesIter.next(); + files.insert(File(filesIter.name(), namedFile)); + } + } +} + +void Record::remove(File const &file) { files.erase(file); } + +void Record::add(File file) +{ + files.erase(file); + files.insert(std::move(file)); +} + +void Record::addRecordAsLibraryData(Record const &childRecord, + std::string const &name) +{ + if(!childRecord.files.empty()) + { + for(auto &file : childRecord.files) + { + add(file); + } + } + auto newLibData = addLibraryData(name, childRecord.toNode()); + newLibData->add(LIBRARY_DATA_ID_DATUM, Datum {childRecord.getId().getId()}); + newLibData->add(LIBRARY_DATA_TYPE_DATUM, Datum {childRecord.type}); +} + +void RecordLoader::addTypeLoader(std::string const &type, TypeLoader loader) +{ + typeLoaders[type] = std::move(loader); +} + +std::unique_ptr RecordLoader::load(conduit::Node const &recordAsNode) const +{ + auto loaderIter = typeLoaders.find(recordAsNode[TYPE_FIELD].as_string()); + if(loaderIter != typeLoaders.end()) + { + return loaderIter->second(recordAsNode); + } + return std::make_unique(recordAsNode); +} + +bool RecordLoader::canLoad(std::string const &type) const +{ + return typeLoaders.count(type) > 0; +} + +RecordLoader createRecordLoaderWithAllKnownTypes() +{ + RecordLoader loader; + addRunLoader(loader); + return loader; +} + +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/core/Record.hpp b/src/axom/sina/core/Record.hpp new file mode 100644 index 0000000000..49d4e6b05c --- /dev/null +++ b/src/axom/sina/core/Record.hpp @@ -0,0 +1,240 @@ +// 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) + +#ifndef SINA_RECORD_HPP +#define SINA_RECORD_HPP + +/*! + ****************************************************************************** + * + * \file Record.hpp + * + * \brief Header file for Sina Record class + * + * \sa DataHolder.hpp + * + ****************************************************************************** + */ + +#include +#include +#include +#include +#include +#include + +#include "conduit.hpp" + +#include "axom/sina/core/ID.hpp" +#include "axom/sina/core/DataHolder.hpp" +#include "axom/sina/core/CurveSet.hpp" +#include "axom/sina/core/Datum.hpp" +#include "axom/sina/core/File.hpp" + +namespace axom +{ +namespace sina +{ + +/** + * FileEqualByURI is used to store files in a Record. + */ +struct FileEqualByURI +{ + bool operator()(const File &file1, const File &file2) const + { + return file1.getUri() == file2.getUri(); + } +}; + +/** + * FileHashByURI is used to store files in a Record. Files are stored according + * to the hash of their URI. + */ +struct FileHashByURI +{ + size_t operator()(const File &file) const + { + return std::hash()(file.getUri()); + } +}; + +/** + * \brief An object representing an entry in a Document's Record list + * + * The Record class represents an entry in a Document's Record list. Records represent the data to be stored + * (as opposed to the relationships between data)--natural scopes for Records include things like a single run + * of an application, an msub job, a cluster of runs that has some metadata attached to the cluster (this Record + * might have a "contains" Relationship for all the runs within it), etc. + * + * Each Record must have a type and an id. Each Record can also have a list of + * File objects and a map of Datum objects. + * + * \code + * axom::sina::ID myID{"my_record", axom::sina::IDType::Local}; + * std::unique_ptr myRecord{new axom::sina::Record{myID, "my_type"}}; + * std::vector myTags{"input"}; + * axom::sina::Datum myDatum{12, myTags}; + * myRecord->add("my_scalar",std::move(myDatum)); + * std::cout << myRecord->toNode().to_json() << std::endl; + * \endcode + * + * The output would be: + * \code{.json} + * {"local_id":"my_record","type":"my_type","data":{"my_scalar":{"tags":["input"],"value":12.0}}} + * \endcode + */ +class Record : public DataHolder +{ +public: + /** + * An unordered set of File objects. + */ + using FileSet = std::unordered_set; + + /** + * \brief Construct a new Record. + * + * \param id the ID of the record + * \param type the type of the record + */ + Record(ID id, std::string type); + + /** + * \brief Construct a Record from its conduit Node representation. + * + * \param asNode the Record as a Node + */ + explicit Record(conduit::Node const &asNode); + + /** + * Disable the copy constructor. + */ + Record(Record const &) = delete; + + /** + * Disable copy assignment. + */ + Record &operator=(Record const &) = delete; + + /** + * \brief Get the Record's ID. + * + * \return the ID + */ + ID const &getId() const noexcept { return id.getID(); } + + /** + * \brief Get the Record's type. + * + * \return the Record's type + */ + std::string const &getType() const noexcept { return type; } + + /** + * \brief Remove a File from this record. + * + * \param file the File to remove + */ + void remove(File const &file); + + using DataHolder::add; + /** + * \brief Add a File to this record. + * + * \param file the File to add + */ + void add(File file); + + /** + * \brief Get the files associated with this record. + * + * \return the record's files + */ + FileSet const &getFiles() const noexcept { return files; } + + /** + * \brief Convert this record to its conduit Node representation. + * + * \return the Node representation of this record. + */ + conduit::Node toNode() const override; + + /** + * \brief Add another record to this one as library data. + * + * Useful for libraries that can run in standalone mode; the host + * simply calls this method on the record the library produces. + * Merges file lists. + */ + void addRecordAsLibraryData(Record const &childRecord, std::string const &name); + +private: + internal::IDField id; + std::string type; + FileSet files; +}; + +/** + * \brief An object to convert Conduit Nodes into Records + * + * A RecordLoader is used to convert conduit::Node instances which represent + * Sina Records into instances of their corresponding axom::sina::Record + * subclasses. For convenience, a RecordLoader capable of handling Records of all known + * types can be created using createRecordLoaderWithAllKnownTypes: + * + * \code + * axom::sina::Document myDocument = axom::sina::Document(jObj, axom::sina::createRecordLoaderWithAllKnownTypes()); + * \endcode + */ +class RecordLoader +{ +public: + /** + * A TypeLoader is a function which converts records of a specific type + * to their corresponding sub classes. + */ + using TypeLoader = + std::function(conduit::Node const &)>; + + /** + * \brief Add a function for loading records of the specified type. + * + * \param type the type of records this function can load + * \param loader the function which can load the records + */ + void addTypeLoader(std::string const &type, TypeLoader loader); + + /** + * \brief Load a Record from its conduit Node representation. + * + * \param recordAsNode the Record as a Node + * \return the Record + */ + std::unique_ptr load(conduit::Node const &recordAsNode) const; + + /** + * \brief Check whether this loader can load records of the given type. + * + * \param type the type of the records to check + * \return whether records of the given type can be loaded + */ + bool canLoad(std::string const &type) const; + +private: + std::unordered_map typeLoaders; +}; + +/** + * \brief Create a RecordLoader which can load records of all known types. + * + * \return the newly-created loader + */ +RecordLoader createRecordLoaderWithAllKnownTypes(); + +} // namespace sina +} // namespace axom + +#endif //SINA_RECORD_HPP diff --git a/src/axom/sina/core/Relationship.cpp b/src/axom/sina/core/Relationship.cpp new file mode 100644 index 0000000000..0a49a6de94 --- /dev/null +++ b/src/axom/sina/core/Relationship.cpp @@ -0,0 +1,58 @@ +// 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) + +/*! + ****************************************************************************** + * + * \file Relationship.cpp + * + * \brief Implementation file for Sina Relationship class + * + ****************************************************************************** + */ + +#include "axom/sina/core/Relationship.hpp" + +#include + +#include "axom/sina/core/ConduitUtil.hpp" + +namespace axom +{ +namespace sina +{ + +namespace +{ +char const GLOBAL_SUBJECT_KEY[] = "subject"; +char const LOCAL_SUBJECT_KEY[] = "local_subject"; +char const GLOBAL_OBJECT_KEY[] = "object"; +char const LOCAL_OBJECT_KEY[] = "local_object"; +char const PREDICATE_KEY[] = "predicate"; +} // namespace + +Relationship::Relationship(ID subject_, std::string predicate_, ID object_) + : subject {std::move(subject_), LOCAL_SUBJECT_KEY, GLOBAL_SUBJECT_KEY} + , object {std::move(object_), LOCAL_OBJECT_KEY, GLOBAL_OBJECT_KEY} + , predicate {std::move(predicate_)} +{ } + +Relationship::Relationship(conduit::Node const &asNode) + : subject {asNode, LOCAL_SUBJECT_KEY, GLOBAL_SUBJECT_KEY} + , object {asNode, LOCAL_OBJECT_KEY, GLOBAL_OBJECT_KEY} + , predicate {getRequiredString(PREDICATE_KEY, asNode, "Relationship")} +{ } + +conduit::Node Relationship::toNode() const +{ + conduit::Node relationshipNode; + relationshipNode[PREDICATE_KEY] = predicate; + subject.addTo(relationshipNode); + object.addTo(relationshipNode); + return relationshipNode; +} + +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/core/Relationship.hpp b/src/axom/sina/core/Relationship.hpp new file mode 100644 index 0000000000..ac636f0be9 --- /dev/null +++ b/src/axom/sina/core/Relationship.hpp @@ -0,0 +1,140 @@ +// 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) + +#ifndef SINA_RELATIONSHIP_HPP +#define SINA_RELATIONSHIP_HPP + +/*! + ****************************************************************************** + * + * \file Relationship.hpp + * + * \brief Header file for Sina Relationship class + * + ****************************************************************************** + */ + +#include + +#include "conduit.hpp" + +#include "axom/sina/core/ID.hpp" + +namespace axom +{ +namespace sina +{ + +/** + * \brief An object used to correlate 2 Records + * + * A Relationship consists of three parts: a subject, an object, and a predicate. It describes + * a relationship between two Records (and/or Record inheritors, e.g. Run). + * The subject and object must be IDs referring to valid records, while the predicate may be + * any string. + * + * In describing the connection between objects, a Relationship is read as + * " ". For example, in the relationship + * "Alice knows Bob", "Alice" is the subject, "knows" is the predicate, and + * "Bob" is the object. For further examples: + * + * - Task_22 contains Run_1024 + * - msub_1_1 describes out_j_1_1 + * - Carlos sends an email to Dani + * - local_task_12 runs before local_run_14 + * + * Note that Relationships are described in the active voice. **Avoiding the passive voice + * in predicates is recommended**, as this keeps the "direction" of the relationship constant. + * An example of a passively-voiced Relationship is "Dani is emailed by Carlos". Instead, + * this should be phrased as "Carlos emails Dani". + * + * If assembling Relationships programatically, it may be useful to reference the + * ID documentation. + * + * \code + * axom::sina::ID task22{"Task_22", axom::sina::IDType::Global}; + * axom::sina::ID run1024{"Run_1024", axom::sina::IDType::Global}; + * axom::sina::Relationship myRelationship{task22, "contains", run1024}; + * std::cout << myRelationship.toNode().to_json() << std::endl; + * \endcode + * + * This would output: + * \code{.json} + * {"object":"Run_1024","predicate":"contains","subject":"Task_22"} + * \endcode + * + * As with any other Sina ID, the subject or object may be either local (uniquely refer to one object + * in a Sina file) or global (uniquely refer to one object in a database). Local IDs are replaced with + * global ones upon ingestion; all Relationships referring to that Local ID (as well as the Record possessing + * that ID) will be updated to use the same global ID. + * + * \code + * axom::sina::ID myLocalID{"my_local_run", axom::sina::IDType::Local}; + * std::unique_ptr myRun{new axom::sina::Run{myLocalID, "My Sim Code", "1.2.3", "jdoe"}}; + * axom::sina::Relationship myRelationship{task22, "contains", myLocalID}; + * \endcode + * + * In the above code, "my_local_run" would be replaced by a global ID on ingestion. If this new global ID was, + * for example, "5Aed-BCds-23G1", then "my_local_run" would automatically be replaced by "5Aed-BCds-23G1" in both + * the Record and Relationship entries. + */ +class Relationship +{ +public: + /** + * \brief Create a new relationship. + * + * \param subject the subject of the relationship + * \param predicate the predicate describing the relationship from the + * subject to the object + * \param object the object of the relationship + */ + Relationship(ID subject, std::string predicate, ID object); + + /** + * \brief Create a Relationship object from its representation as a conduit Node. + * + * \param asNode the relationship as a Node + */ + explicit Relationship(conduit::Node const &asNode); + + /** + * \brief Get the subject. + * + * \return the subject + */ + ID const &getSubject() const noexcept { return subject.getID(); } + + /** + * \brief Get the object. + * + * \return the object + */ + ID const &getObject() const noexcept { return object.getID(); } + + /** + * \brief Get the predicate. + * + * \return the predicate + */ + std::string const &getPredicate() const noexcept { return predicate; } + + /** + * \brief Convert this Relationship to its Node representation. + * + * \return this relationship as a conduit Node + */ + conduit::Node toNode() const; + +private: + internal::IDField subject; + internal::IDField object; + std::string predicate; +}; + +} // namespace sina +} // namespace axom + +#endif //SINA_RELATIONSHIP_HPP diff --git a/src/axom/sina/core/Run.cpp b/src/axom/sina/core/Run.cpp new file mode 100644 index 0000000000..061ab5e5cc --- /dev/null +++ b/src/axom/sina/core/Run.cpp @@ -0,0 +1,71 @@ +// 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) + +/*! + ****************************************************************************** + * + * \file Run.cpp + * + * \brief Implementation file for Sina Run class + * + * \sa Record.hpp + * + ****************************************************************************** + */ + +#include "axom/sina/core/Run.hpp" + +#include + +#include "axom/sina/core/ConduitUtil.hpp" + +namespace axom +{ +namespace sina +{ + +namespace +{ +char const RUN_TYPE[] = "run"; +char const APPLICATION_FIELD[] = "application"; +char const VERSION_FIELD[] = "version"; +char const USER_FIELD[] = "user"; +} // namespace + +Run::Run(sina::ID id, + std::string application_, + std::string version_, + std::string user_) + : Record {std::move(id), RUN_TYPE} + , application {std::move(application_)} + , version {std::move(version_)} + , user {std::move(user_)} +{ } + +Run::Run(conduit::Node const &asNode) + : Record(asNode) + , application {getRequiredString(APPLICATION_FIELD, asNode, RUN_TYPE)} + , version {getOptionalString(VERSION_FIELD, asNode, RUN_TYPE)} + , user {getOptionalString(USER_FIELD, asNode, RUN_TYPE)} +{ } + +conduit::Node Run::toNode() const +{ + auto asNode = Record::toNode(); + asNode[APPLICATION_FIELD] = application; + asNode[VERSION_FIELD] = version; + asNode[USER_FIELD] = user; + return asNode; +} + +void addRunLoader(RecordLoader &loader) +{ + loader.addTypeLoader(RUN_TYPE, [](conduit::Node const &value) { + return std::make_unique(value); + }); +} + +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/core/Run.hpp b/src/axom/sina/core/Run.hpp new file mode 100644 index 0000000000..9b7ade1821 --- /dev/null +++ b/src/axom/sina/core/Run.hpp @@ -0,0 +1,109 @@ +// 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) + +#ifndef SINA_RUN_HPP +#define SINA_RUN_HPP + +/*! + ****************************************************************************** + * + * \file Run.hpp + * + * \brief Header file for Sina Run class + * + * \sa Record.hpp + * + ****************************************************************************** + */ + +#include "axom/sina/core/Record.hpp" + +namespace axom +{ +namespace sina +{ + +/** + * \brief A sub-type of Record representing a single run of an applicaiton + * + * A Run is a subtype of Record corresponding to a single run of an application, as + * specified in the Sina schema. A Run has a few additional fields required in addition + * to the id required by a Record (type is automatically set to "run"): + * + * - application: the application/code used to create the Run + * - version: the version of the application used to create the Run + * - user: the username of the person who ran the application that generated this Run + * + * To create a Run: + * \code + * axom::sina::ID run1ID{"run1", axom::sina::IDType::Local}; + * std::unique_ptr run1{new axom::sina::Run{run1ID, "My Sim Code", "1.2.3", "jdoe"}}; + * \endcode + * + */ +class Run : public Record +{ +public: + /** + * \brief Create a new Run. + * + * \param id the run's ID + * \param application the application that was run + * \param version (optional) the version of the application + * \param user (optional) the user who executed the run + */ + Run(ID id, + std::string application, + std::string version = "", + std::string user = ""); + + /** + * \brief Create a Run from its representation as a conduit Node + * + * \param asNode the run as a Node + */ + explicit Run(conduit::Node const &asNode); + + /** + * \brief Get the application that was run. + * + * \return the application's name + */ + std::string const &getApplication() const { return application; } + + /** + * \brief Get the version of the application that was run. + * + * \return the application's version + */ + std::string const &getVersion() const { return version; } + + /** + * \brief Get the name of the user who ran the application. + * + * \return the user's name + */ + std::string const &getUser() const { return user; } + + conduit::Node toNode() const override; + +private: + std::string application; + std::string version; + std::string user; +}; + +/** + * \brief Add a type loader to the given RecordLoader for loading Run instances. + * + * \param loader the RecordLoader to which to add the function for loading + * Run instances. + */ +void addRunLoader(RecordLoader &loader); + +} // namespace sina +} // namespace axom + +#endif //SINA_RUN_HPP diff --git a/src/axom/sina/docs/imgs/ball-bounce-y-axis.png b/src/axom/sina/docs/imgs/ball-bounce-y-axis.png new file mode 100644 index 0000000000..de927a2c51 Binary files /dev/null and b/src/axom/sina/docs/imgs/ball-bounce-y-axis.png differ diff --git a/src/axom/sina/docs/sphinx/core_concepts.rst b/src/axom/sina/docs/sphinx/core_concepts.rst new file mode 100644 index 0000000000..0a9b2e1dfe --- /dev/null +++ b/src/axom/sina/docs/sphinx/core_concepts.rst @@ -0,0 +1,26 @@ +.. ## 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) + +============= +Core Concepts +============= + +Sina provides four main classes: + + - ``Document`` - represents the top-level object of a JSON file conforming to the Sina schema. + - ``Record`` - represents the data to be stored. + - ``Relationship`` - represents a way to define the relationship between two ``Record`` objects. + - ``CurveSet`` - a class to store related independent and dependent ``Curve`` objects. \ + ``Curve`` and ``CurveSet`` objects are just two more types of data that ``Record`` objects can store. + +More details on each class can be found in their respective pages below. + +.. toctree:: + :maxdepth: 2 + + documents + records + relationships + curve_sets \ No newline at end of file diff --git a/src/axom/sina/docs/sphinx/curve_sets.rst b/src/axom/sina/docs/sphinx/curve_sets.rst new file mode 100644 index 0000000000..d43f6cd0e6 --- /dev/null +++ b/src/axom/sina/docs/sphinx/curve_sets.rst @@ -0,0 +1,34 @@ +.. ## 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) + +.. _curvesets-label: + +========== +Curve Sets +========== + +Sina ``CurveSet`` objects act as a way to group related independent and dependent +``Curve`` objects. Each ``Curve`` is a 1-dimensional list of numbers along with +optional units and tags. + +A common example of a ``CurveSet`` could be time series data. Here the independent +curve would be time and the dependent curve(s) would be the data you're measuring +over time. + +We will demonstrate the creation of a ``CurveSet`` using a simple experiment that +measures the 2D position and velocity of a ball while it bounces. In the below script +We will create one function to generate the data for the ball bounce experiment and +another function to assemble the ``CurveSet`` object and add it to our ``Record``: + +.. literalinclude:: ../../examples/sina_curve_set.cpp + :language: cpp + +Once this code is compiled and executed a json file will be created. Here, we'll +use `PyDV `_ to ingest the Sina json +file in order to view the position and velocity of the ball along the y-axis over +time. + +.. image:: ../imgs/ball-bounce-y-axis.png + :alt: The position and velocity of the ball over time along the y-axis diff --git a/src/axom/sina/docs/sphinx/documents.rst b/src/axom/sina/docs/sphinx/documents.rst new file mode 100644 index 0000000000..8cdc6ebfb6 --- /dev/null +++ b/src/axom/sina/docs/sphinx/documents.rst @@ -0,0 +1,128 @@ +.. ## 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) + +.. _documents-label: + +========= +Documents +========= + +Sina ``Document`` objects are a way to represent the top-level object of a +JSON file that conforms to the Sina schema. When serialized, these documents +can be ingested into a Sina database and used with the Sina tool. + +``Document`` objects follow a very basic JSON layout consisting of two entries: +``records`` and ``relationships``. Each of these entries will store a list of their +respective objects. An example of an empty document is shown below: + +.. code:: json + + { + "records": [], + "relationships": [] + } + +The ``records`` list can contain ``Record`` objects and their inheritying types, +such as ``Run`` objects. The ``relationships`` list can contain ``Relationship`` +objects. For more information on these objects, see `Records <./records>`_ +and `Relationships <./relationships>`_. + +-------------------- +Assembling Documents +-------------------- + +``Document`` objects can be assembled programatically. To accomplish this: + +1. Create a new instance of the ``Document`` class +2. Create a ``Record`` +3. Add the instance of the ``Record`` with the ``add`` method + +On the `Sina C++ User Guide <./index>`_ page, you can see an example of this +process. Below we will expand on this example to add a ``Relationship``: + +.. literalinclude:: ../../examples/sina_document_assembly.cpp + :language: cpp + +After executing the above code, the resulting ``MySinaData.json`` file will +look like so: + +.. code:: json + + { + "records": [ + { + "type": "run", + "local_id": "run1", + "application": "My Sim Code", + "version": "1.2.3", + "user": "jdoe" + }, + { + "type": "UQ study", + "local_id": "study1" + } + ], + "relationships": [ + { + "predicate": "contains", + "local_subject": "study1", + "local_object": "run1" + } + ] + } + +------------------------------ +Generating Documents From JSON +------------------------------ + +Alternatively to assembling ``Document`` instances programatically, it is +also possible to generate ``Document`` objects from existing JSON files +or JSON strings. + +Using our same example from the previous section, if we instead had the +``MySinaData.json`` file prior to executing our code, we could generate +the document using Sina's ``loadDocument()`` function: + +.. code:: cpp + + #include "axom/sina.hpp" + + int main (void) { + axom::sina::Document myDocument = axom::sina::loadDocument("MySinaData.json"); + } + +Similarly, if we had JSON in string format we could also load an instance +of the ``Document`` that way: + +.. code:: cpp + + #include "axom/sina.hpp" + + int main (void) { + std::string my_json = "{\"records\":[{\"type\":\"run\",\"id\":\"test\"}],\"relationships\":[]}"; + axom::sina::Document myDocument = axom::sina::Document(my_json, axom::sina::createRecordLoaderWithAllKnownTypes()); + std::cout << myDocument.toJson() << std::endl; + } + +--------------------------------------------------------- +Obtaining Records & Relationships from Existing Documents +--------------------------------------------------------- + +Sina provides an easy way to query for both ``Record`` and ``Relationship`` +objects that are associated with a ``Document``. The ``getRecords()`` and +``getRelationships()`` methods will handle this respectively. + +Below is an example showcasing their usage: + +.. literalinclude:: ../../examples/sina_query_records_relationships.cpp + :language: cpp + +Running this will show that both records and the one relationship were +properly queried: + +.. code:: bash + + Number of Records: 2 + Number of Relationships: 1 diff --git a/src/axom/sina/docs/sphinx/index.rst b/src/axom/sina/docs/sphinx/index.rst new file mode 100644 index 0000000000..a00c03b2a2 --- /dev/null +++ b/src/axom/sina/docs/sphinx/index.rst @@ -0,0 +1,45 @@ +.. ## 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) + +=================== +Sina C++ User Guide +=================== + +The Sina ([S]imulation [In]sight and [A]nalysis) C++ library can read and write +JSON files in the Sina schema. It can be used by simulation applications to summarize +run data to be ingested into a database using the Sina tool suite. + +The top-level object in the Sina schema is the :doc:`Document `. +It contains lists of :doc:`Record ` and :doc:`Relationship ` +objects. The example below shows the basics. For more details, see the +:doc:`Tutorial `. + +.. literalinclude:: ../../examples/sina_basic.cpp + :language: cpp + +After running the above, the file "MySinaData.json" will contain the +following: + +.. code:: json + + { + "records": [ + { + "application": "My Sim Code", + "local_id": "run1", + "type": "run", + "user": "jdoe", + "version": "1.2.3" + } + ], + "relationships": [] + } + +.. toctree:: + :caption: Contents + :maxdepth: 2 + + tutorial + core_concepts diff --git a/src/axom/sina/docs/sphinx/records.rst b/src/axom/sina/docs/sphinx/records.rst new file mode 100644 index 0000000000..c0bf40c1af --- /dev/null +++ b/src/axom/sina/docs/sphinx/records.rst @@ -0,0 +1,257 @@ +.. ## 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) + +.. _records-label: + +======= +Records +======= + +Sina ``Record`` objects are used to represent the data to be stored for your +study. Some examples for the natural scope of ``Record`` objects include things +like: + + - a single run of an application + - a `Maestro `_ step + - a cluster of runs that has some metadata attached to the cluster (this + ``Record`` might have a "contains" :doc:`Relationship ` for all + the runs within it) + +Each ``Record`` must have an ID and a type that you define. Additionally, Each +``Record`` can have a list of ``File`` objects and a list of ``Datum`` objects. + +.. contents:: Topics Covered in this Page + :depth: 2 + :local: + +------ +Datums +------ + +Sina ``Datum`` objects help track the value and (optionally) tags and/or units of a +value associated with a ``Record``. In the Sina schema, a ``Datum`` always belongs +to a ``Record`` or one of ``Record``'s inheriting types. + +Some examples of potential ``Datum`` values would be: + + - a scalar + - a piece of metadata + - an input parameter + +The value of a ``Datum`` may be a string, a double, an array of strings, or an array +of doubles. + +Below showcases an example of creating an instance of ``Datum`` with an array of +strings and adding it to a ``Record``: + +.. literalinclude:: ../../examples/sina_create_datum.cpp + :language: cpp + +Once executed, this code will output: + +.. code:: json + + { + "data": + { + "my_scalar": + { + "value": + [ + "input" + ] + } + }, + "type": "my_type", + "local_id": "my_record" + } + +.. _datum-type-label: + ++++++++++++++++++++ +Checking Datum Type ++++++++++++++++++++ + +It's possible to check the type of a ``Datum`` with the ``getType()`` method. Types +are tracked in an enumeration called ``ValueType``. The enumeration is as follows: + + - 0: string + - 1: scalar + - 2: array of strings + - 3: array of scalars + +Below is an example of this in action: + +.. literalinclude:: ../../examples/sina_check_datum_type.cpp + :language: cpp + +++++++++++++++++++++++ +Setting Units and Tags +++++++++++++++++++++++ + +For certain ``Datum`` instances it may be helpful to assign them units and/or tags. +This can be accomplished with the ``setUnits()`` and ``setTags()`` methods respectively. + +Below is an example of this functionality: + +.. literalinclude:: ../../examples/sina_set_datum_units_tags.cpp + :language: cpp + ++++++++++++++++++++++++++++++++++++++ +Viewing Datum From an existing Record ++++++++++++++++++++++++++++++++++++++ + +Sometimes it's necessary to obtain the current ``Datum`` instances from an existing +``Record``. To do this, you can utilize the ``Record`` object's ``getData`` method. +This method will return an unordered map of ``Datum`` instances. + +Below is an example of this process: + +.. literalinclude:: ../../examples/sina_view_datum_types.cpp + :language: cpp + +Executing this code will print out: + +.. code:: bash + + datum1 is type: 1 + datum2 is type: 0 + datum3 is type: 3 + +Which, we know from `Checking Datum Type <#datum-type-label>`_, signifies that +datum1 is a scalar, datum2 is a string, and datum3 is an array of scalars. + +Using this knowledge we can modify our code to show us the current datum values: + +.. literalinclude:: ../../examples/sina_view_datum_values.cpp + :language: cpp + +This will provide the following output: + +.. code:: bash + + datum1: 12.34 + datum2: foobar + datum3: 1 2 20 + +----- +Files +----- + +Sina ``File`` objects help track the location (URI) and mimetype of a file on the +file system, plus any tags. In the Sina schema, a ``File`` always belongs to a ``Record`` +or one of ``Record``'s inheriting types. + +Every ``File`` must have a URI, while mimetype and tags are optional. + +Below is an example showcasing how to create a file and add it to a record: + +.. literalinclude:: ../../examples/sina_file_object_creation.cpp + :language: cpp + +This code will produce the following output: + +.. code:: json + + { + "type": "my_type", + "local_id": "my_record", + "files": + { + "/path/to/other/file.txt": + { + "tags": + [ + "these", + "are", + "tags" + ] + }, + "/path/to/file.png": + { + "mimetype": "image/png" + } + } + } + +Similarly, files can be removed from a ``Record`` with the ``remove()`` method: + +.. literalinclude:: ../../examples/sina_file_object_removal.cpp + :language: cpp + +As we see from the output, the contents of ``myFile`` are no longer in the +``Record`` instance: + +.. code:: json + + { + "type": "my_type", + "local_id": "my_record", + "files": + { + "/path/to/other/file.txt": + { + "tags": + [ + "these", + "are", + "tags" + ] + } + } + } + ++++++++++++++++++++++++++++++++++++++ +Viewing Files From an Existing Record ++++++++++++++++++++++++++++++++++++++ + +Sometimes it's necessary to view the current ``File`` instances that are stored in +a ``Record``. This can be accomplished by using the ``Record`` object's ``getFiles()`` +method which returns an unordered map of ``File`` instances. + +Below is an expansion of the previous example where we query the ``Record`` instance +for files: + +.. literalinclude:: ../../examples/sina_query_record_for_files.cpp + :language: cpp + +The above code will output: + +.. code:: bash + + File with URI '/path/to/file.png' has mimetype 'image/png' and tags '' + File with URI '/path/to/other/file.txt' has mimetype '' and tags 'these are tags ' + +---- +Runs +---- + +Sina ``Run`` objects are a subtype of Sina ``Record`` objects corresponding to +a single run of an application as specified in the Sina schema. Similar to ``Record`` +objects, ``Run`` objects also require an ID; however, they do *not* require a type as +it will automatically be set to "run". In addition to IDs, there are a few other +required fields: + + - application: the application/code used to create the ``Run`` + - version: the version of the application used to create the ``Run`` + - user: the username of the person who ran the application that generated this ``Run`` + +The below code shows an example of how a ``Run`` can be created: + +.. code:: cpp + + #include "axom/sina.hpp" + + int main(void) { + // Create the ID first + axom::sina::ID run1ID{"run1", axom::sina::IDType::Local}; + + // Create a run with: + // ID: run1ID + // application: "My Sim Code" + // version: "1.2.3" + // user: "jdoe" + std::unique_ptr run1{new sina::Run{run1ID, "My Sim Code", "1.2.3", "jdoe"}}; + } diff --git a/src/axom/sina/docs/sphinx/relationships.rst b/src/axom/sina/docs/sphinx/relationships.rst new file mode 100644 index 0000000000..6d497f39b6 --- /dev/null +++ b/src/axom/sina/docs/sphinx/relationships.rst @@ -0,0 +1,62 @@ +.. ## 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) + +.. _relationships-label: + +============= +Relationships +============= + +Sina ``Relationship`` objects are used as a way to describe the correlation between +two ``Record`` instances (and/or ``Record`` inheritors, e.g. ``Run``). A ``Relationship`` +consists of three parts: a subject, an object, and a predicate. The subject and object +must be IDs referring to valid instances of ``Records``, while the predicate may be +any string. + +In describing the connection between objects, a ``Relationship`` is read as " + ". For example, in the relationship "Alice knows Bob", "Alice" is +the subject, "knows" is the predicate, and "Bob" is the object. Below are some additional +examples: + + - Task_22 contains Run_1024 + - msub_1_1 describes out_j_1_1 + - Carlos sends an email to Dani + - local_task_12 runs before local_run_14 + +A ``Relationship`` should be described in the active voice. **Using active voice in predicates +is recommended** to maintain a clear direction in the relationship. For example, instead of the +passive construction "Dani is emailed by Carlos," use the active form "Carlos emails Dani." + +Below is an example showcasing how to construct a ``Relationship`` programmatically. Here, +we assemble a ``Relationship`` showing that "Task_22 contains Run_1024": + +.. literalinclude:: ../../examples/sina_relationship_assembly.cpp + :language: cpp + +If executed, the above code will output: + +.. code:: json + + { + "predicate": "contains", + "subject": "Task_22", + "object": "Run_1024" + } + +As with any other Sina ID, the subject or object may be either local (uniquely refer to +one object in a Sina file) or global (uniquely refer to one object in a database). Local +IDs are replaced with global ones upon ingestion; all ``Relationship`` instances referring +to that local ID (as well as the ``Record`` possessing that ID) will be updated to use the +same global ID. + +Let's add on to our previous example to demonstrate this: + +.. literalinclude:: ../../examples/sina_local_id_relationship.cpp + :language: cpp + +In the above code, the "my_local_run" ID would be replaced by a global ID on ingestion. +If this new global ID was, for example, "5Aed-BCds-23G1", then "my_local_run" would +automatically be replaced by "5Aed-BCds-23G1" in both the ``Record`` and ``Relationship`` +entries. diff --git a/src/axom/sina/docs/sphinx/tutorial.rst b/src/axom/sina/docs/sphinx/tutorial.rst new file mode 100644 index 0000000000..70131b748d --- /dev/null +++ b/src/axom/sina/docs/sphinx/tutorial.rst @@ -0,0 +1,186 @@ +.. ## 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) + +.. _tutorial-label: + +======== +Tutorial +======== + +This short tutorial walks you through the basic usage of the Sina library. +For more in-depth details, see the documentation for the individual classes, +such as Record, Relationship, and Document. + +.. contents:: Tutorial Contents + :depth: 2 + +------------------------------ +Creating Documents and Records +------------------------------ + +The basic working units in Sina are the Document, Record, and Relationship. +A Document is a collection of Records and Relationships. A Record contains +information about a particular entity, such as the run of an application, +or a description of a uncertainty quantification (UQ) study. A Relationship +describes how two records relate to each user (e.g. UQ studies contain runs). + +This first example shows how to create a record: + +.. literalinclude:: ../../examples/sina_tutorial.cpp + :language: cpp + :start-after: //! [begin create record] + :end-before: //! [end create record] + +The record has an ID "some_record_id", which is unique to the enclosing +document (it will be replaced by a global ID upon ingestion). The only +required field for records is their type, which is "my_record_type" in this +case. Once created, a record can be added to a Document. + +We can create Runs. Runs are special types of records. They have the required +fields of application ("My Sim Code"), version ("1.2.3"), and user ("jdoe"). +The type is automatically set to "run". + +.. literalinclude:: ../../examples/sina_tutorial.cpp + :language: cpp + :start-after: //! [begin create run] + :end-before: //! [end create run] + +----------- +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. + +.. literalinclude:: ../../examples/sina_tutorial.cpp + :language: cpp + :start-after: //! [begin adding data] + :end-before: //! [end adding data] + +----------------- +Adding Curve Sets +----------------- + +While you can add data items that are vectors of numbers, sometimes you want +to express relationships between them. For example, you may want to express +the fact that a timeplot captures the fact that there is an independent +variable (e.g. "time"), and possibly multiple dependent variables (e.g. +"temperature" and "energy"). + +.. literalinclude:: ../../examples/sina_tutorial.cpp + :language: cpp + :start-after: //! [begin curve sets] + :end-before: //! [end curve sets] + +------------ +Adding Files +------------ + +It is also useful to add to a record a set of files that it relates to. +For example your application generated some external data, or you want to +point to a license file. + +Conversely, at times it may be necessary to remove a file from the record's file list. +For example if the file was deleted or renamed. + +.. literalinclude:: ../../examples/sina_tutorial.cpp + :language: cpp + :start-after: //! [begin file add_and_remove] + :end-before: //! [end file add_and_remove] + +----------------------------- +Relationships Between Records +----------------------------- + +Relationships between objects can be captured via the Relationship class. +This relates two records via a user-defined predicate. In the example below, +a new relashionship is created between two records: a UQ study and a run. The +study is said to "contain" the run. As a best practice, predicates should +be active verbs, such as "contains" in "the study contains the run", rather +than "is part of", as in "the run is part of the study". + +.. literalinclude:: ../../examples/sina_tutorial.cpp + :language: cpp + :start-after: //! [begin relationships] + :end-before: //! [end relationships] + +--------------------- +Library-Specific Data +--------------------- + +Oftentimes, simulation codes are composed of multiple libraries. If those +offer a capability to collect data in a Sina document, you can leverage that +to expose this additional data in your records. + +For example, suppose you are using libraries named ``foo`` and ``bar``. +library ``foo`` defines ``foo_collectData()`` like this: + +.. literalinclude:: ../../examples/sina_tutorial.cpp + :language: cpp + :start-after: //! [begin library data foo] + :end-before: //! [end library data foo] + +Library ``bar`` defines ``bar_gatherData()`` like this: + +.. literalinclude:: ../../examples/sina_tutorial.cpp + :language: cpp + :start-after: //! [begin library data bar] + :end-before: //! [end library data bar] + +In your host application, you can define sections for ``foo`` and ``bar`` +to add their own data. + +.. literalinclude:: ../../examples/sina_tutorial.cpp + :language: cpp + :start-after: //! [begin library data host] + :end-before: //! [end library data host] + +In the example above, once the record is ingested into a Sina datastore, +users will be able to search for "temperature" (value = 450), +"foo/temperature" (value = 500), and "bar/temperature" (value = 400). + +---------------- +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. + +.. literalinclude:: ../../examples/sina_tutorial.cpp + :language: cpp + :start-after: //! [begin io write] + :end-before: //! [end io write] + +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. + +.. literalinclude:: ../../examples/sina_tutorial.cpp + :language: cpp + :start-after: //! [begin io read] + :end-before: //! [end io read] + +--------------------------------- +Non-Conforming, User-Defined Data +--------------------------------- + +While the Sina format is capable of expressing and indexing lots of different +types of data, there may be special cases it doesn't cover well. If you want +to add extra data to a record but don't care if it doesn't get indexed, you +can add it to the "user_defined" section of records (or libraries of +a record). This is a JSON object that will be ignored by Sina for +processing purposes, but will be brought back with your record if you +retrieve it from a database. + +Sina uses `Conduit `_ to +convert to and from JSON. The user-defined section is exposed as a +`Conduit Node `_. + +.. literalinclude:: ../../examples/sina_tutorial.cpp + :language: cpp + :start-after: //! [begin user defined] + :end-before: //! [end user defined] diff --git a/src/axom/sina/doxygen_mainpage.md b/src/axom/sina/doxygen_mainpage.md new file mode 100644 index 0000000000..f45fac1e0f --- /dev/null +++ b/src/axom/sina/doxygen_mainpage.md @@ -0,0 +1,14 @@ +Sina {#sinatop} +========= + +[Sina](@ref axom::sina) provides an easy way to collect data directly within codes and output them to a common file format. This is accomplished in an object oriented manner through the following classes: + +- [Curve](@ref axom::sina::Curve): represents a 1D curve +- [CurveSet](@ref axom::sina::CurveSet): represents an entry in a record's "curve_set" +- [DataHolder](@ref axom::sina::DataHolder): a basic container for certain types of information +- [Datum](@ref axom::sina::Datum): tracks the value and (optionally) tags and/or units of a value associated with a Record +- [Document](@ref axom::sina::Document): represents the top-lvevl object of a JSON file conforming to the Sina schema +- [File](@ref axom::sina::File): tracks the location (URI) and mimetype of a file on the file system, plus any tags +- [Record](@ref axom::sina::Record): entry in a Document's Record list +- [Relationship](@ref axom::sina::Relationship): represents correlations between records; consists of three parts: a subject, an object, and a predicate +- [Run](@ref axom::sina::Run): a subtype of Record corresponding to a single run of an application, as specified in the Sina schema \ No newline at end of file diff --git a/src/axom/sina/examples/CMakeLists.txt b/src/axom/sina/examples/CMakeLists.txt new file mode 100644 index 0000000000..2f6179f950 --- /dev/null +++ b/src/axom/sina/examples/CMakeLists.txt @@ -0,0 +1,53 @@ +# 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) +#------------------------------------------------------------------------------ +# Sina examples +#------------------------------------------------------------------------------ + +#------------------------------------------------------------------------------ +# List of single source file examples +#------------------------------------------------------------------------------ +set(sina_example_sources + sina_basic.cpp + sina_check_datum_type.cpp + sina_create_datum.cpp + sina_curve_set.cpp + sina_document_assembly.cpp + sina_file_object_creation.cpp + sina_file_object_removal.cpp + sina_local_id_relationship.cpp + sina_query_record_for_files.cpp + sina_query_records_relationships.cpp + sina_relationship_assembly.cpp + sina_set_datum_units_tags.cpp + sina_tutorial.cpp + sina_view_datum_types.cpp + sina_view_datum_values.cpp +) + +set(sina_example_depends sina conduit::conduit) + +if (ENABLE_FORTRAN) + blt_list_append( TO sina_example_sources ELEMENTS sina_fortran.f90) +endif() + +#------------------------------------------------------------------------------ +# Add targets for Sina examples +#------------------------------------------------------------------------------ +foreach(src ${sina_example_sources}) + get_filename_component(exe_name ${src} NAME_WE) + axom_add_executable( + NAME ${exe_name}_ex + SOURCES ${src} + OUTPUT_DIR ${EXAMPLE_OUTPUT_DIRECTORY} + DEPENDS_ON ${sina_example_depends} + FOLDER axom/sina/examples + ) + + # 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() +endforeach() diff --git a/src/axom/sina/examples/sina_basic.cpp b/src/axom/sina/examples/sina_basic.cpp new file mode 100644 index 0000000000..2d59f4429d --- /dev/null +++ b/src/axom/sina/examples/sina_basic.cpp @@ -0,0 +1,21 @@ +// 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) + +#include "axom/sina.hpp" + +int main(void) +{ + // Create a new document + axom::sina::Document document; + // Create a run of "My Sim Code" version "1.2.3", which was run by "jdoe". + // The run has an ID of "run1", which has to be unique to this file. + axom::sina::ID runID {"run1", axom::sina::IDType::Local}; + std::unique_ptr run { + new axom::sina::Run {runID, "My Sim Code", "1.2.3", "jdoe"}}; + // Add the run to the document + document.add(std::move(run)); + // Save the document directly to a file. + axom::sina::saveDocument(document, "MySinaData.json"); +} \ No newline at end of file diff --git a/src/axom/sina/examples/sina_check_datum_type.cpp b/src/axom/sina/examples/sina_check_datum_type.cpp new file mode 100644 index 0000000000..4efb97decc --- /dev/null +++ b/src/axom/sina/examples/sina_check_datum_type.cpp @@ -0,0 +1,31 @@ +// 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) + +#include "axom/sina.hpp" + +int main(void) +{ + // Define 3 different datums + axom::sina::Datum myDatum {12.34}; + std::string value = "foobar"; + axom::sina::Datum myOtherDatum {value}; + std::vector scalars = {1, 2, 20.0}; + axom::sina::Datum myArrayDatum {scalars}; + + // Prints 0, corresponding to string + std::cout << static_cast::type>( + myDatum.getType()) + << std::endl; + + // Prints 1, corresponding to scalar + std::cout << static_cast::type>( + myOtherDatum.getType()) + << std::endl; + + // Prints 3, corresponding to scalar array + std::cout << static_cast::type>( + myArrayDatum.getType()) + << std::endl; +} \ No newline at end of file diff --git a/src/axom/sina/examples/sina_create_datum.cpp b/src/axom/sina/examples/sina_create_datum.cpp new file mode 100644 index 0000000000..8bd32bb10f --- /dev/null +++ b/src/axom/sina/examples/sina_create_datum.cpp @@ -0,0 +1,22 @@ +// 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) + +#include "axom/sina.hpp" + +int main(void) +{ + // Create the record + axom::sina::ID myID {"my_record", axom::sina::IDType::Local}; + std::unique_ptr myRecord { + new axom::sina::Record {myID, "my_type"}}; + + // Create the datum with an array of strings + std::vector myTags {"input"}; + axom::sina::Datum myDatum {myTags}; + + // Add the datum to the record + myRecord->add("my_scalar", std::move(myDatum)); + std::cout << myRecord->toNode().to_json() << std::endl; +} \ No newline at end of file diff --git a/src/axom/sina/examples/sina_curve_set.cpp b/src/axom/sina/examples/sina_curve_set.cpp new file mode 100644 index 0000000000..430b7551ec --- /dev/null +++ b/src/axom/sina/examples/sina_curve_set.cpp @@ -0,0 +1,123 @@ +// 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) + +#include "axom/sina.hpp" + +#include +#include +#include + +using namespace std; + +struct BounceData +{ + vector time; + vector xPosition; + vector yPosition; + vector xVelocity; + vector yVelocity; +}; + +BounceData generateBounceData(double initialY, + double initialXVelocity, + double coefficientOfRestitution, + double timeStep, + double simulationTime, + double airResistanceCoefficient) +{ + BounceData data; + double time = 0.0; + double yPosition = initialY; + double xPosition = 0.0; + double xVelocity = initialXVelocity; + double yVelocity = 0.0; + + while(time < simulationTime) + { + // Update position and velocity based on time step + xPosition += xVelocity * timeStep; + yVelocity += -9.81 * timeStep; // Acceleration due to gravity + + // Apply air resistance on x-velocity + xVelocity -= airResistanceCoefficient * xVelocity * timeStep; + + // Update height (y position) + yPosition += yVelocity * timeStep; + + // Check for bounce (when y position becomes negative) + if(yPosition < 0) + { + yPosition = 0; // Set y position to 0 at the ground + yVelocity *= + -coefficientOfRestitution; // Reverse y velocity with energy loss + } + + // Add data points for this time step + data.time.push_back(time); + data.xPosition.push_back(xPosition); + data.yPosition.push_back(yPosition); + data.xVelocity.push_back(xVelocity); + data.yVelocity.push_back(yVelocity); + + time += timeStep; + } + + return data; +} + +void addCurveSet(axom::sina::Record &record, BounceData bounceData, string curveName) +{ + // Create the curve set object + axom::sina::CurveSet bounceCurveSet {curveName}; + + // Add the independent variable + axom::sina::Curve timeCurve {"time", bounceData.time}; + timeCurve.setUnits("seconds"); + bounceCurveSet.addIndependentCurve(timeCurve); + + // Add the dependent variables + bounceCurveSet.addDependentCurve( + axom::sina::Curve {"x_position", bounceData.xPosition}); + bounceCurveSet.addDependentCurve( + axom::sina::Curve {"y_position", bounceData.yPosition}); + axom::sina::Curve xVelCurve {"x_velocity", bounceData.xVelocity}; + xVelCurve.setUnits("m/s"); + bounceCurveSet.addDependentCurve(xVelCurve); + axom::sina::Curve yVelCurve {"y_velocity", bounceData.yVelocity}; + yVelCurve.setUnits("m/s"); + bounceCurveSet.addDependentCurve(yVelCurve); + + // Add the curve set to the record + record.add(bounceCurveSet); +} + +int main() +{ + double initialY = 10.0; + double initialXVelocity = 2.0; + double coefficientOfRestitution = 0.7; + double timeStep = 0.2; + double simulationTime = 5.0; + double airResistanceCoefficient = 0.01; + + BounceData bounceData = generateBounceData(initialY, + initialXVelocity, + coefficientOfRestitution, + timeStep, + simulationTime, + airResistanceCoefficient); + + axom::sina::Document doc; + + axom::sina::ID id {"ball_bounce_run", axom::sina::IDType::Global}; + unique_ptr study { + new axom::sina::Record {id, "ball bounce study"}}; + + addCurveSet(*study, bounceData, "ball_bounce"); + doc.add(move(study)); + axom::sina::saveDocument(doc, "ball_bounce.json"); + + return 0; +} \ 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 new file mode 100644 index 0000000000..428c076767 --- /dev/null +++ b/src/axom/sina/examples/sina_document_assembly.cpp @@ -0,0 +1,36 @@ +// 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) + +#include "axom/sina.hpp" + +int main(void) +{ + // Create a new document + axom::sina::Document document; + + // Create a record of this specific study + // This study has an ID of "study1", which has to be unique to this file + axom::sina::ID studyID {"study1", axom::sina::IDType::Local}; + std::unique_ptr study { + new axom::sina::Record {studyID, "UQ study"}}; + + // Create a run of "My Sim Code" version "1.2.3", which was run by "jdoe". + // The run has an ID of "run1", which has to be unique to this file. + axom::sina::ID runID {"run1", axom::sina::IDType::Local}; + std::unique_ptr run { + new axom::sina::Run {runID, "My Sim Code", "1.2.3", "jdoe"}}; + + // Create a relationship between the study and the run + // Here we're saying that the study contains the run + axom::sina::Relationship relationship {studyID, "contains", runID}; + + // Add the run, study record, and relationship to the document + document.add(std::move(run)); + document.add(std::move(study)); + document.add(relationship); + + // Save the document directly to a file. + axom::sina::saveDocument(document, "MySinaData.json"); +} \ No newline at end of file diff --git a/src/axom/sina/examples/sina_file_object_creation.cpp b/src/axom/sina/examples/sina_file_object_creation.cpp new file mode 100644 index 0000000000..4d9cdaeaad --- /dev/null +++ b/src/axom/sina/examples/sina_file_object_creation.cpp @@ -0,0 +1,26 @@ +// 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) + +#include "axom/sina.hpp" + +int main(void) +{ + // Create 2 different files + axom::sina::File myFile {"/path/to/file.png"}; + myFile.setMimeType("image/png"); + axom::sina::File myOtherFile {"/path/to/other/file.txt"}; + myOtherFile.setTags({"these", "are", "tags"}); + + // Create a record to store the files + axom::sina::ID myID {"my_record", axom::sina::IDType::Local}; + std::unique_ptr myRecord { + new axom::sina::Record {myID, "my_type"}}; + + // Add the files to the record + myRecord->add(myFile); + myRecord->add(myOtherFile); + + std::cout << myRecord->toNode().to_json() << std::endl; +} \ No newline at end of file diff --git a/src/axom/sina/examples/sina_file_object_removal.cpp b/src/axom/sina/examples/sina_file_object_removal.cpp new file mode 100644 index 0000000000..83c7bd3a7e --- /dev/null +++ b/src/axom/sina/examples/sina_file_object_removal.cpp @@ -0,0 +1,29 @@ +// 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) + +#include "axom/sina.hpp" + +int main(void) +{ + // Create 2 different files + axom::sina::File myFile {"/path/to/file.png"}; + myFile.setMimeType("image/png"); + axom::sina::File myOtherFile {"/path/to/other/file.txt"}; + myOtherFile.setTags({"these", "are", "tags"}); + + // Create a record to store the files + axom::sina::ID myID {"my_record", axom::sina::IDType::Local}; + std::unique_ptr myRecord { + new axom::sina::Record {myID, "my_type"}}; + + // Add the files to the record + myRecord->add(myFile); + myRecord->add(myOtherFile); + + // Remove a file from the record + myRecord->remove(myFile); + + std::cout << myRecord->toNode().to_json() << std::endl; +} \ No newline at end of file diff --git a/src/axom/sina/examples/sina_fortran.f90 b/src/axom/sina/examples/sina_fortran.f90 new file mode 100644 index 0000000000..8dd2a5e0d4 --- /dev/null +++ b/src/axom/sina/examples/sina_fortran.f90 @@ -0,0 +1,162 @@ +program example + use sina_functions + implicit none + + ! data types + integer (KIND=4) :: int_val + integer (KIND=8) :: long_val + real :: real_val + double precision :: double_val + character :: char_val + logical :: is_val + integer :: i + logical :: independent + + ! 1D real Array + real, dimension(20) :: real_arr + double precision, dimension(20) :: double_arr + + + ! Strings + character(:), allocatable :: fle_nme + character(:), allocatable :: ofle_nme + character(17) :: wrk_dir + character(29) :: full_path + character(36) :: ofull_path + character(:), allocatable :: rec_id + character(:), allocatable :: mime_type + character(:), allocatable :: tag + character(:), allocatable :: units + character(20) :: json_fn + character(15) :: name + character(25) :: curve + + ! 1D integer Array + integer, dimension(20) :: int_arr + integer (kind=8), dimension(20) :: long_arr + + int_val = 10 + long_val = 1000000000 + real_val = 1.234567 + double_val = 1./1.2345678901234567890123456789 + char_val = 'A' + is_val = .false. + + do i = 1, 20 + real_arr(i) = i + double_arr(i) = i*2. + int_arr(i) = i*3 + long_arr(i) = i*4 + end do + + rec_id = make_cstring('my_rec_id') + fle_nme = 'my_file.txt' + ofle_nme = 'my_other_file.txt' + wrk_dir = '/path/to/my/file/' + full_path = make_cstring(wrk_dir//''//fle_nme) + ofull_path = make_cstring(wrk_dir//''//ofle_nme) + json_fn = make_cstring('sina_dump.json') + + + mime_type = make_cstring('') + units = make_cstring('') + tag = make_cstring('') + + print *,rec_id + + ! ========== USAGE ========== + + ! create sina record and document + print *,'Creating the document' + call create_document_and_record(rec_id) + + ! add file to sina record + print *,'Adding a file to the Sina record' + call sina_add_file(full_path, mime_type) + mime_type = make_cstring('png') + print *,'Adding another file (PNG) to the Sina record' + call sina_add_file(ofull_path, mime_type) + print *, "Adding int", int_val + name = make_cstring('int') + call sina_add(name, int_val, units, tag) + print *, "Adding logical" + name = make_cstring('logical') + call sina_add(name, is_val, units, tag) + print *, "Adding long" + name = make_cstring('long') + call sina_add(name, long_val, units, tag) + print *, "Adding real" + name = make_cstring('real') + call sina_add(name, real_val, units, tag) + print *, "Adding double" + name = make_cstring('double') + call sina_add(name, double_val, units, tag) + print *, "Adding char" + name = make_cstring('char') + call sina_add(name, trim(char_val)//char(0), units, tag) + units = make_cstring("kg") + print *, "Adding int", int_val + name = make_cstring('u_int') + call sina_add(name, int_val, units, tag) + print *, "Adding logical" + name = make_cstring('u_logical') + is_val = .true. + call sina_add(name, is_val, units, tag) + print *, "Adding long" + name = make_cstring('u_long') + call sina_add(name, long_val, units, tag) + print *, "Adding real" + name = make_cstring('u_real') + call sina_add(name, real_val, units, tag) + print *, "Adding double" + name = make_cstring('u_double') + call sina_add(name, double_val, units, tag) + + print *, "Adding double with tag" + name = make_cstring('u_double_w_tag') + tag = make_cstring('new_fancy_tag') + call sina_add(name, double_val, units, tag) + + print *, "Adding char" + name = make_cstring('u_char') + call sina_add(name, trim(char_val)//char(0), units, tag) + + deallocate(tag) + + name = make_cstring('my_curveset') + call sina_add_curveset(name) + + curve = make_cstring('my_indep_curve_double') + independent = .TRUE. + call sina_add_curve(name, curve, double_arr, size(double_arr), independent) + curve = make_cstring('my_indep_curve_real') + call sina_add_curve(name, curve, real_arr, size(real_arr), independent) + curve = make_cstring('my_indep_curve_int') + call sina_add_curve(name, curve, int_arr, size(int_arr), independent) + curve = make_cstring('my_indep_curve_long') + call sina_add_curve(name, curve, long_arr, size(long_arr), independent) + curve = make_cstring('my_dep_curve_double') + independent = .false. + call sina_add_curve(name, curve, double_arr, size(double_arr), independent) + curve = make_cstring('my_dep_curve_double_2') + call sina_add_curve(name, curve, double_arr, size(double_arr), independent) + curve = make_cstring('my_dep_curve_real') + call sina_add_curve(name, curve, real_arr, size(real_arr), independent) + curve = make_cstring('my_dep_curve_int') + call sina_add_curve(name, curve, int_arr, size(int_arr), independent) + curve = make_cstring('my_dep_curve_long') + call sina_add_curve(name, curve, long_arr, size(long_arr), independent) + ! write out the Sina Document + print *,'Writing out the Sina Document' + call write_sina_document(json_fn) + + +contains + function make_cstring(string) result(cstring) + character(len=*), intent(in) :: string + character(len=:), allocatable :: cstring + cstring = trim(string) // char(0) + end function make_cstring + + +end program example diff --git a/src/axom/sina/examples/sina_local_id_relationship.cpp b/src/axom/sina/examples/sina_local_id_relationship.cpp new file mode 100644 index 0000000000..5395c5bfb9 --- /dev/null +++ b/src/axom/sina/examples/sina_local_id_relationship.cpp @@ -0,0 +1,23 @@ +// 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) + +#include "axom/sina.hpp" + +int main(void) +{ + // Create IDs for Task 22 and Run 1024 + axom::sina::ID task22 {"Task_22", axom::sina::IDType::Global}; + axom::sina::ID run1024 {"Run_1024", axom::sina::IDType::Global}; + + // Create the relationship and print it out + axom::sina::Relationship myRelationship {task22, "contains", run1024}; + std::cout << myRelationship.toNode().to_json() << std::endl; + + // Create a new ID with local scope and use it to create a Record and Relationship + axom::sina::ID myLocalID {"my_local_run", axom::sina::IDType::Local}; + std::unique_ptr myRun { + new axom::sina::Run {myLocalID, "My Sim Code", "1.2.3", "jdoe"}}; + axom::sina::Relationship myLocalRelationship {task22, "containts", myLocalID}; +} \ No newline at end of file diff --git a/src/axom/sina/examples/sina_query_record_for_files.cpp b/src/axom/sina/examples/sina_query_record_for_files.cpp new file mode 100644 index 0000000000..2a63b0c551 --- /dev/null +++ b/src/axom/sina/examples/sina_query_record_for_files.cpp @@ -0,0 +1,37 @@ +// 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) + +#include "axom/sina.hpp" + +int main(void) +{ + // Create 2 different files + axom::sina::File myFile {"/path/to/file.png"}; + myFile.setMimeType("image/png"); + axom::sina::File myOtherFile {"/path/to/other/file.txt"}; + myOtherFile.setTags({"these", "are", "tags"}); + + // Create a record to store the files + axom::sina::ID myID {"my_record", axom::sina::IDType::Local}; + std::unique_ptr myRecord { + new axom::sina::Record {myID, "my_type"}}; + + // Add the files to the record + myRecord->add(myFile); + myRecord->add(myOtherFile); + + // Query the record for files + auto& files = myRecord->getFiles(); + for(const auto& file : files) + { + std::cout << "File with URI '" << file.getUri() << "' has mimetype '" + << file.getMimeType() << "' and tags '"; + for(const auto& tag : file.getTags()) + { + std::cout << tag << " "; + } + std::cout << "'" << std::endl; + } +} \ No newline at end of file diff --git a/src/axom/sina/examples/sina_query_records_relationships.cpp b/src/axom/sina/examples/sina_query_records_relationships.cpp new file mode 100644 index 0000000000..f76661437f --- /dev/null +++ b/src/axom/sina/examples/sina_query_records_relationships.cpp @@ -0,0 +1,40 @@ +// 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) + +#include "axom/sina.hpp" + +int main(void) +{ + // Create a new document + axom::sina::Document document; + + // Create a record of this specific study + // This study has an ID of "study1", which has to be unique to this file + axom::sina::ID studyID {"study1", axom::sina::IDType::Local}; + std::unique_ptr study { + new axom::sina::Record {studyID, "UQ study"}}; + + // Create a run of "My Sim Code" version "1.2.3", which was run by "jdoe". + // The run has an ID of "run1", which has to be unique to this file. + axom::sina::ID runID {"run1", axom::sina::IDType::Local}; + std::unique_ptr run { + new axom::sina::Run {runID, "My Sim Code", "1.2.3", "jdoe"}}; + + // Create a relationship between the study and the run + // Here we're saying that the study contains the run + axom::sina::Relationship relationship {studyID, "contains", runID}; + + // Add the run, study record, and relationship to the document + document.add(std::move(run)); + document.add(std::move(study)); + document.add(relationship); + + // Query for a list of records and relationships + auto &records = document.getRecords(); + auto &relationships = document.getRelationships(); + + std::cout << "Number of Records: " << records.size() << std::endl; + std::cout << "Number of Relationships: " << relationships.size() << std::endl; +} \ No newline at end of file diff --git a/src/axom/sina/examples/sina_relationship_assembly.cpp b/src/axom/sina/examples/sina_relationship_assembly.cpp new file mode 100644 index 0000000000..62048881f4 --- /dev/null +++ b/src/axom/sina/examples/sina_relationship_assembly.cpp @@ -0,0 +1,17 @@ +// 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) + +#include "axom/sina.hpp" + +int main(void) +{ + // Create IDs for both Task 22 and Run 1024 + axom::sina::ID task22 {"Task_22", axom::sina::IDType::Global}; + axom::sina::ID run1024 {"Run_1024", axom::sina::IDType::Global}; + + // Create the relationship and print it out + axom::sina::Relationship myRelationship {task22, "contains", run1024}; + std::cout << myRelationship.toNode().to_json() << std::endl; +} diff --git a/src/axom/sina/examples/sina_set_datum_units_tags.cpp b/src/axom/sina/examples/sina_set_datum_units_tags.cpp new file mode 100644 index 0000000000..db8723f985 --- /dev/null +++ b/src/axom/sina/examples/sina_set_datum_units_tags.cpp @@ -0,0 +1,19 @@ +// 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) + +#include "axom/sina.hpp" + +int main(void) +{ + // Define 2 different datums + axom::sina::Datum myDatum {12.34}; + std::vector scalars = {1, 2, 20.0}; + axom::sina::Datum myArrayDatum {scalars}; + + // Set the units for one datum and the tags for the other + myDatum.setUnits("km/s"); + std::vector tags = {"input", "core"}; + myArrayDatum.setTags(tags); +} \ No newline at end of file diff --git a/src/axom/sina/examples/sina_tutorial.cpp b/src/axom/sina/examples/sina_tutorial.cpp new file mode 100644 index 0000000000..13445f0fe9 --- /dev/null +++ b/src/axom/sina/examples/sina_tutorial.cpp @@ -0,0 +1,178 @@ +// 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) + +#include "axom/sina.hpp" + +#include +#include + +namespace +{ + +//! [begin create record] +void createRecord() +{ + axom::sina::ID id {"some_record_id", axom::sina::IDType::Local}; + std::unique_ptr record { + new axom::sina::Record {id, "my_record_type"}}; + + // Add the record to a document + axom::sina::Document doc; + doc.add(std::move(record)); +} +//! [end create record] + +//! [begin create run] +void createRun() +{ + axom::sina::ID id {"some_run_id", axom::sina::IDType::Local}; + std::unique_ptr run { + new axom::sina::Run {id, "My Sim Code", "1.2.3", "jdoe"}}; + + // Add the record to a document + axom::sina::Document doc; + doc.add(std::move(run)); +} +//! [end create run] + +//! [begin adding data] +void addData(axom::sina::Record &record) +{ + // Add a scalar named "my_scalar" with the value 123.456 + record.add("my_scalar", axom::sina::Datum {123.456}); + + // Add a string named "my_string" with the value "abc" + record.add("my_string", axom::sina::Datum {"abc"}); + + // Add a list of scalars named "my_scalar_list" + std::vector scalarList = {1.2, -3.4, 5.6}; + record.add("my_scalar_list", axom::sina::Datum {scalarList}); + + // Add a list of strings named "my_string_list" + std::vector stringList = {"hi", "hello", "howdy"}; + record.add("my_string_list", axom::sina::Datum {stringList}); +} +//! [end adding data] + +//! [begin curve sets] +void addCurveSets(axom::sina::Record &record) +{ + axom::sina::CurveSet timePlots {"time_plots"}; + + // Add the independent variable + timePlots.addIndependentCurve(axom::sina::Curve {"time", {0.0, 0.1, 0.25, 0.3}}); + + // Add some dependent variables. + // The length of each must be the same as the length of the independent. + timePlots.addDependentCurve( + axom::sina::Curve {"temperature", {300.0, 310.0, 350.0, 400.0}}); + + timePlots.addDependentCurve( + axom::sina::Curve {"energy", {0.0, 10.0, 20.0, 30.0}}); + + // Associate the curve sets with the record + record.add(timePlots); +} +//! [end curve sets] + +//! [begin file add_and_remove] +void addAndRemoveFileToRecord(axom::sina::Record &run) +{ + axom::sina::File my_file {"some/path.txt"}; + // Adds the file to the record's file list + run.add(my_file); + // Removes the file from the record's file list + run.remove(my_file); +} +//! [end file add_and_remove] + +//! [begin relationships] +void associateRunToStudy(axom::sina::Document &doc, + axom::sina::Record const &uqStudy, + axom::sina::Record const &run) +{ + doc.add(axom::sina::Relationship {uqStudy.getId(), "contains", run.getId()}); +} +//! [end relationships] + +//! [begin library data foo] +void foo_collectData(axom::sina::DataHolder &fooData) +{ + fooData.add("temperature", axom::sina::Datum {500}); + fooData.add("energy", axom::sina::Datum {1.2e10}); +} +//! [end library data foo] + +//! [begin library data bar] +void bar_gatherData(axom::sina::DataHolder &barData) +{ + barData.add("temperature", axom::sina::Datum {400}); + barData.add("mass", axom::sina::Datum {15}); +} +//! [end library data bar] + +//! [begin library data host] +void gatherAllData(axom::sina::Record &record) +{ + auto fooData = record.addLibraryData("foo"); + auto barData = record.addLibraryData("bar"); + + foo_collectData(*fooData); + bar_gatherData(*barData); + + record.add("temperature", axom::sina::Datum {450}); +} +//! [end library data host] + +//! [begin io write] +void save(axom::sina::Document const &doc) +{ + axom::sina::saveDocument(doc, "my_output.json"); +} +//! [end io write] + +//! [begin io read] +void load() +{ + axom::sina::Document doc = axom::sina::loadDocument("my_output.json"); +} +//! [end io read] + +//! [begin user defined] +void addUserDefined(axom::sina::Record &record) +{ + conduit::Node &userDefined = record.getUserDefinedContent(); + userDefined["var_1"] = "a"; + userDefined["var_2"] = "b"; + + conduit::Node subNode; + subNode["sub_1"] = 10; + subNode["sub_2"] = 20; + userDefined["sub_structure"] = subNode; +} +//! [end user defined] + +} // namespace + +int main() +{ + // Call everything to keep the compiler from complaining about unused functions + axom::sina::Record run { + axom::sina::ID {"my_record", axom::sina::IDType::Global}, + "my_record_type"}; + axom::sina::Record study {axom::sina::ID {"my_run", axom::sina::IDType::Global}, + "UQ study"}; + axom::sina::Document doc; + addData(run); + createRecord(); + createRun(); + associateRunToStudy(doc, study, run); + gatherAllData(run); + addCurveSets(run); + addAndRemoveFileToRecord(run); + addUserDefined(run); + save(doc); + load(); +} diff --git a/src/axom/sina/examples/sina_view_datum_types.cpp b/src/axom/sina/examples/sina_view_datum_types.cpp new file mode 100644 index 0000000000..01d50c5150 --- /dev/null +++ b/src/axom/sina/examples/sina_view_datum_types.cpp @@ -0,0 +1,38 @@ +// 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) + +#include "axom/sina.hpp" + +int main(void) +{ + // Define 3 different datums + axom::sina::Datum myDatum {12.34}; + std::string value = "foobar"; + axom::sina::Datum myOtherDatum {value}; + std::vector scalars = {1, 2, 20.0}; + axom::sina::Datum myArrayDatum {scalars}; + + // Create a record to store the datum + axom::sina::ID myID {"my_record", axom::sina::IDType::Local}; + std::unique_ptr myRecord { + new axom::sina::Record {myID, "my_type"}}; + + // Add the datum instances to the record + myRecord->add("datum1", std::move(myDatum)); + myRecord->add("datum2", std::move(myOtherDatum)); + myRecord->add("datum3", std::move(myArrayDatum)); + + // Query the datum from the record + auto& data = myRecord->getData(); + + // Print the keys and type of datum + for(const auto& pair : data) + { + std::cout << pair.first << " is type: " + << static_cast::type>( + pair.second.getType()) + << std::endl; + } +} \ No newline at end of file diff --git a/src/axom/sina/examples/sina_view_datum_values.cpp b/src/axom/sina/examples/sina_view_datum_values.cpp new file mode 100644 index 0000000000..019abf9b85 --- /dev/null +++ b/src/axom/sina/examples/sina_view_datum_values.cpp @@ -0,0 +1,39 @@ +// 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) + +#include "axom/sina.hpp" + +int main(void) +{ + // Define 3 different datums + axom::sina::Datum myDatum {12.34}; + std::string value = "foobar"; + axom::sina::Datum myOtherDatum {value}; + std::vector scalars = {1, 2, 20.0}; + axom::sina::Datum myArrayDatum {scalars}; + + // Create a record to store the datum + axom::sina::ID myID {"my_record", axom::sina::IDType::Local}; + std::unique_ptr myRecord { + new axom::sina::Record {myID, "my_type"}}; + + // Add the datum instances to the record + myRecord->add("datum1", std::move(myDatum)); + myRecord->add("datum2", std::move(myOtherDatum)); + myRecord->add("datum3", std::move(myArrayDatum)); + + // Query the datum + auto& data = myRecord->getData(); + + // Print the datum values + std::cout << "datum1: " << data.at("datum1").getScalar() << std::endl; + std::cout << "datum2: " << data.at("datum2").getValue() << std::endl; + std::cout << "datum3: "; + for(const auto& value : data.at("datum3").getScalarArray()) + { + std::cout << value << " "; + } + std::cout << std::endl; +} \ No newline at end of file diff --git a/src/axom/sina/interface/sina_fortran_interface.cpp b/src/axom/sina/interface/sina_fortran_interface.cpp new file mode 100644 index 0000000000..0f3c55fcec --- /dev/null +++ b/src/axom/sina/interface/sina_fortran_interface.cpp @@ -0,0 +1,409 @@ +// 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) + +#include + +#include "axom/sina/interface/sina_fortran_interface.h" + +axom::sina::Document *sina_document; + +extern "C" char *Get_File_Extension(char *input_fn) +{ + char *ext = strrchr(input_fn, '.'); + if(!ext) + { + return (new char[1] {'\0'}); + } + return (ext + 1); +} + +extern "C" void create_document_and_record_(char *recID) +{ + sina_document = new axom::sina::Document; + // Create a record of "My Sim Code" version "1.2.3", which was run by "jdoe". + // The run has an ID of "run1", which has to be unique to this file. + axom::sina::ID id {recID, axom::sina::IDType::Global}; + std::unique_ptr myRecord { + new axom::sina::Record {id, "my_type"}}; + sina_document->add(std::move(myRecord)); +} + +extern "C" axom::sina::Record *Sina_Get_Record() +{ + if(sina_document) + { + axom::sina::Document::RecordList const &allRecords = + sina_document->getRecords(); + if(allRecords.size()) + { + std::unique_ptr const &myRecord = allRecords.front(); + return myRecord.get(); + } + } + return nullptr; +} + +extern "C" void sina_add_logical_(char *key, bool *value, char *units, char *tags) +{ + if(sina_document) + { + axom::sina::Record *sina_record = Sina_Get_Record(); + std::string key_name = std::string(key); + if(sina_record) + { + axom::sina::Datum datum {static_cast(*value)}; + if(units) + { + std::string key_units = std::string(units); + if(key_units != "") + { + datum.setUnits(key_units); + } + } + if(tags) + { + std::vector tagVector = {tags}; + if(tagVector.front() != "") + { + datum.setTags(tagVector); + } + } + sina_record->add(key_name, datum); + } + } +} + +extern "C" void sina_add_long_(char *key, + long long int *value, + char *units, + char *tags) +{ + if(sina_document) + { + axom::sina::Record *sina_record = Sina_Get_Record(); + std::string key_name = std::string(key); + if(sina_record) + { + axom::sina::Datum datum {static_cast(*value)}; + if(units) + { + std::string key_units = std::string(units); + if(key_units != "") + { + datum.setUnits(key_units); + } + } + if(tags) + { + std::vector tagVector = {tags}; + if(tagVector.front() != "") + { + datum.setTags(tagVector); + } + } + sina_record->add(key_name, datum); + } + } +} + +extern "C" void sina_add_int_(char *key, int *value, char *units, char *tags) +{ + if(sina_document) + { + axom::sina::Record *sina_record = Sina_Get_Record(); + std::string key_name = std::string(key); + if(sina_record) + { + axom::sina::Datum datum {static_cast(*value)}; + if(units) + { + std::string key_units = std::string(units); + if(key_units != "") + { + datum.setUnits(key_units); + } + } + if(tags) + { + std::vector tagVector = {tags}; + if(tagVector.front() != "") + { + datum.setTags(tagVector); + } + } + sina_record->add(key_name, datum); + } + } +} + +extern "C" void sina_add_double_(char *key, double *value, char *units, char *tags) +{ + if(sina_document) + { + axom::sina::Record *sina_record = Sina_Get_Record(); + std::string key_name = std::string(key); + if(sina_record) + { + axom::sina::Datum datum {*value}; + if(units) + { + std::string key_units = std::string(units); + if(key_units != "") + { + datum.setUnits(key_units); + } + } + if(tags) + { + std::vector tagVector = {tags}; + if(tagVector.front() != "") + { + datum.setTags(tagVector); + } + } + sina_record->add(key_name, datum); + } + } +} + +extern "C" void sina_add_float_(char *key, float *value, char *units, char *tags) +{ + if(sina_document) + { + axom::sina::Record *sina_record = Sina_Get_Record(); + std::string key_name = std::string(key); + if(sina_record) + { + axom::sina::Datum datum {*value}; + if(units) + { + std::string key_units = std::string(units); + if(key_units != "") + { + datum.setUnits(key_units); + } + } + if(tags) + { + std::vector tagVector = {tags}; + if(tagVector.front() != "") + { + datum.setTags(tagVector); + } + } + sina_record->add(key_name, datum); + } + } +} + +extern "C" void sina_add_string_(char *key, char *value, char *units, char *tags) +{ + if(sina_document) + { + axom::sina::Record *sina_record = Sina_Get_Record(); + std::string key_name = std::string(key); + std::string key_value = std::string(value); + std::string key_units = std::string(units); + if(sina_record) + { + axom::sina::Datum datum {key_value}; + if(units) + { + std::string key_units = std::string(units); + if(key_units != "") + { + datum.setUnits(key_units); + } + } + if(tags) + { + std::vector tagVector = {tags}; + if(tagVector.front() != "") + { + datum.setTags(tagVector); + } + } + sina_record->add(key_name, datum); + } + } +} + +extern "C" void sina_add_file_(char *filename, char *mime_type) +{ + std::string used_mime_type = ""; + if(sina_document) + { + axom::sina::Record *sina_record = Sina_Get_Record(); + if(mime_type) + { + used_mime_type = std::string(mime_type); + } + axom::sina::File my_file {filename}; + if(used_mime_type != "") + { + my_file.setMimeType(used_mime_type); + } + else + { + used_mime_type = Get_File_Extension(filename); + my_file.setMimeType(used_mime_type); + } + + if(sina_record) + { + sina_record->add(my_file); + } + } +} + +extern "C" void write_sina_document_(char *input_fn) +{ + std::string filename(input_fn); + // Save everything + if(sina_document) + { + axom::sina::saveDocument(*sina_document, filename.c_str()); + } +} + +extern "C" void sina_add_curveset_(char *name) +{ + if(sina_document) + { + axom::sina::Record *sina_record = Sina_Get_Record(); + if(sina_record) + { + axom::sina::CurveSet cs {name}; + sina_record->add(cs); + } + } +} + +extern "C" void sina_add_curve_long_(char *curveset_name, + char *curve_name, + long long int *values, + int *n, + bool *independent) +{ + if(sina_document) + { + axom::sina::Record *sina_record = Sina_Get_Record(); + if(sina_record) + { + double y[*n]; + for(int i = 0; i < *n; i++) + { + y[i] = values[i]; + } + axom::sina::Curve curve {curve_name, y, static_cast(*n)}; + + auto &curvesets = sina_record->getCurveSets(); + axom::sina::CurveSet cs = curvesets.at(curveset_name); + if(*independent) + { + cs.addIndependentCurve(curve); + } + else + { + cs.addDependentCurve(curve); + } + sina_record->add(cs); + } + } +} + +extern "C" void sina_add_curve_int_(char *curveset_name, + char *curve_name, + int *values, + int *n, + bool *independent) +{ + if(sina_document) + { + axom::sina::Record *sina_record = Sina_Get_Record(); + if(sina_record) + { + double y[*n]; + for(int i = 0; i < *n; i++) + { + y[i] = values[i]; + } + axom::sina::Curve curve {curve_name, y, static_cast(*n)}; + + auto &curvesets = sina_record->getCurveSets(); + axom::sina::CurveSet cs = curvesets.at(curveset_name); + if(*independent) + { + cs.addIndependentCurve(curve); + } + else + { + cs.addDependentCurve(curve); + } + sina_record->add(cs); + } + } +} + +extern "C" void sina_add_curve_float_(char *curveset_name, + char *curve_name, + float *values, + int *n, + bool *independent) +{ + if(sina_document) + { + axom::sina::Record *sina_record = Sina_Get_Record(); + if(sina_record) + { + double y[*n]; + for(int i = 0; i < *n; i++) + { + y[i] = values[i]; + } + axom::sina::Curve curve {curve_name, y, static_cast(*n)}; + + auto &curvesets = sina_record->getCurveSets(); + axom::sina::CurveSet cs = curvesets.at(curveset_name); + if(*independent) + { + cs.addIndependentCurve(curve); + } + else + { + cs.addDependentCurve(curve); + } + sina_record->add(cs); + } + } +} + +extern "C" void sina_add_curve_double_(char *curveset_name, + char *curve_name, + double *values, + int *n, + bool *independent) +{ + if(sina_document) + { + axom::sina::Record *sina_record = Sina_Get_Record(); + if(sina_record) + { + axom::sina::Curve curve {curve_name, values, static_cast(*n)}; + + auto &curvesets = sina_record->getCurveSets(); + axom::sina::CurveSet cs = curvesets.at(curveset_name); + if(*independent) + { + cs.addIndependentCurve(curve); + } + else + { + cs.addDependentCurve(curve); + } + sina_record->add(cs); + } + } +} diff --git a/src/axom/sina/interface/sina_fortran_interface.f90 b/src/axom/sina/interface/sina_fortran_interface.f90 new file mode 100644 index 0000000000..56fe05ebe6 --- /dev/null +++ b/src/axom/sina/interface/sina_fortran_interface.f90 @@ -0,0 +1,118 @@ +module sina_functions + + interface + + subroutine create_document_and_record(id) + character(*) id + end subroutine create_document_and_record + + end interface + + interface + + subroutine sina_add_file(file_nm, mime_type) + character(*) file_nm + character(*) mime_type + 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) + character(*) key + integer (KIND=8) value + character(*) units + character(*) tags + end subroutine sina_add_long + + subroutine sina_add_logical(key, value, units, tags) + character(*) key + logical value + character(*) units + character(*) tags + end subroutine sina_add_logical + + subroutine sina_add_int(key, value, units, tags) + character(*) key + integer value + character(*) units + character(*) tags + end subroutine sina_add_int + + subroutine sina_add_double(key, value, units, tags) + character(*) key + double precision value + character(*) units + character(*) tags + end subroutine sina_add_double + + subroutine sina_add_float(key, value, units, tags) + character(*) key + real value + character(*) units + character(*) tags + end subroutine sina_add_float + + subroutine sina_add_string(key, value, units, tags) + character(*) key + character(*) value + character(*) units + character(*) tags + end subroutine sina_add_string + + end interface + + interface sina_add_curveset + + subroutine sina_add_curveset(name) + character(*) name + end subroutine sina_add_curveset + + end interface + + interface sina_add_curve + + subroutine sina_add_curve_double(name, curve, values, n, independent) + character(*) name + character(*) curve + double precision values(n) + integer n + logical independent + end subroutine sina_add_curve_double + + subroutine sina_add_curve_float(name, curve, values, n, independent) + character(*) name + character(*) curve + real values(n) + integer n + logical independent + end subroutine sina_add_curve_float + + subroutine sina_add_curve_int(name, curve, values, n, independent) + character(*) name + character(*) curve + integer (KIND=4), dimension(n) :: values + integer n + logical independent + end subroutine sina_add_curve_int + + subroutine sina_add_curve_long(name, curve, values, n, independent) + character(*) name + character(*) curve + integer (KIND=8), dimension(n) :: values + integer n + logical independent + end subroutine sina_add_curve_long + + end interface + +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 new file mode 100644 index 0000000000..ed004c81be --- /dev/null +++ b/src/axom/sina/interface/sina_fortran_interface.h @@ -0,0 +1,27 @@ +// 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) + +#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 *); +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 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 *); +extern "C" void sina_add_double_(char *, double *, char *, char *); +extern "C" void sina_add_logical_(char *, bool *, char *, char *); +extern "C" void sina_add_string_(char *, char *, char *, char *); +extern "C" void sina_add_curveset_(char *); +extern "C" void sina_add_curve_double_(char *, char *, double *, int *, bool *); +extern "C" void sina_add_curve_float_(char *, char *, float *, int *, bool *); +extern "C" void sina_add_curve_int_(char *, char *, int *, int *, bool *); +extern "C" void sina_add_curve_long_(char *, char *, long long int *, int *, bool *); diff --git a/src/axom/sina/interface/sina_schema.json b/src/axom/sina/interface/sina_schema.json new file mode 100644 index 0000000000..eff79e533b --- /dev/null +++ b/src/axom/sina/interface/sina_schema.json @@ -0,0 +1,255 @@ +{ + "$id" : "https://llnl.gov/sina.schema.json", + "$schema" : "http://json-schema.org/draft-07/schema#", + "title" : "Sina Schema", + "description" : "Sina schema for simulation data", + "type" : "object", + "definitions" : { + "userDefDict" : { + "description": "Dictionary of additional misc. values not belonging elsewhere.", + "type": "object" + }, + "libraryDataDict" : { + "description": "Dictionary of libraries and associated data", + "type": "object", + "additionalProperties": {"$ref": "#/definitions/libraryType" } + }, + "libraryType" : { + "description": "Library with associated data", + "type": "object", + "properties": { + "data": {"$ref": "#/definitions/dataDict" }, + "curve_sets": {"$ref": "#/definitions/curveSetDict" }, + "library_data": {"$ref": "#/definitions/libraryDataDict" } + } + }, + "fileDict" : { + "description": "Dictionary of files associated with Record", + "type": "object", + "additionalProperties": {"$ref": "#/definitions/fileType" } + }, + "fileType" : { + "description": "User-defined file values", + "type": "object", + "properties": { + "mimetype": { "type": "string" }, + "tags": { "$ref": "#/definitions/tagArray" } + } + }, + "stringDataArray" : { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": false + }, + "scalarDataArray" : { + "type": "array", + "items": { "type": "number" }, + "uniqueItems": false + }, + "objectType" : { + "description": "Object being acted upon by the subject record", + "oneOf": [ + { + "properties": { + "object": { + "description": "Global id of the object record", + "type": "string" + } + }, + "required": [ "object" ] + }, { + "properties": { + "local_object": { + "description": "Local id of the object record", + "type": "string" + } + }, + "required": [ "local_object" ] + } + ] + }, + "record" : { + "description": "A component of application execution", + "allOf": [ + { "$ref": "#/definitions/recordType" }, + { "$ref": "#/definitions/recordIdType" }, + { "$ref": "#/definitions/recordData" } + ] + }, + "recordData" : { + "description": "Optional, indexed simulation data", + "properties": { + "files": { "$ref": "#/definitions/fileDict" }, + "data": { "$ref": "#/definitions/dataDict" }, + "library_data": { "$ref": "#/definitions/libraryDataDict" }, + "curve_sets": { "$ref": "#/definitions/curveSetDict" }, + "user_defined": { "$ref": "#/definitions/userDefDict"} + } + }, + "recordIdType" : { + "oneOf": [ + { + "properties": { + "id": { + "description": "Unique identifier", + "type": "string" + } + }, + "required": [ "id" ] + }, { + "properties": { + "local_id": { + "description": "Unique, auto-assigned identifier", + "type": "string" + } + }, + "required": [ "local_id" ] + } + ] + }, + "recordType" : { + "properties": { + "type": { + "description": "The type of record", + "type": "string" + } + }, + "required": [ "type" ] + }, + "relationship" : { + "description": "Relationship between two records", + "allOf": [ + { "$ref": "#/definitions/subjectType" }, + { + "properties": { + "predicate": { "type": "string" } + }, + "required": [ "predicate" ] + }, + { "$ref": "#/definitions/objectType" } + ] + }, + "run" : { + "description": "An individual simulation run", + "allOf": [ + { "$ref": "#/definitions/recordIdType" }, + { + "properties": { + "type": { "enum": [ "run" ] }, + "user": { "type": "string" }, + "application": { "type": "string" }, + "version": { "type": "string" } + }, + "required": [ "type", "application" ] + }, + { "$ref": "#/definitions/recordData" } + ], + "additionalProperties": false + }, + "tagArray" : { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true + }, + "subjectType" : { + "description": "Record acting on the object record", + "oneOf": [ + { + "properties": { + "subject": { + "description": "Global id of the subject record", + "type": "string" + } + }, + "required": [ "subject" ] + }, { + "properties": { + "local_subject": { + "description": "Local id of the subject record", + "type": "string" + } + }, + "required": [ "local_subject" ] + } + ] + }, + "dataDict" : { + "description": "Dictionary of data values", + "type": "object", + "additionalProperties": {"$ref": "#/definitions/dataType" } + }, + "curveSetDict" : { + "description": "Dictionary describing a set of curves", + "type": "object", + "additionalProperties": {"$ref": "#/definitions/curveSetType" } + }, + "dataType" : { + "description": "User-defined data values", + "type": "object", + "properties": { + "value": { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "$ref": "#/definitions/scalarDataArray" }, + { "$ref": "#/definitions/stringDataArray" } + ] + }, + "units": { "type": "string" }, + "tags": { "$ref": "#/definitions/tagArray" } + }, + "required" : [ "value" ] + }, + "curveSetType" : { + "description": "User-defined associations of curves", + "type": "object", + "properties": { + "independent": { + "type": "object", + "additionalProperties": { "$ref": "#/definitions/curveEntryType" } + }, + "dependent": { + "type": "object", + "additionalProperties": { "$ref": "#/definitions/curveEntryType" } + }, + "dependent_order": { "$ref": "#/definitions/stringDataArray"}, + "independent_order": { "$ref": "#/definitions/stringDataArray"}, + "tags": { "$ref": "#/definitions/tagArray" } + }, + "required" : [ "independent", "dependent" ] + }, + "curveEntryType" : { + "description": "Data for a single entry in a curve set", + "type": "object", + "properties": { + "value": { "$ref": "#/definitions/scalarDataArray" }, + "units": { "type": "string" }, + "tags": { "$ref": "#/definitions/tagArray" } + }, + "required" : [ "value" ] + } + }, + + "properties" : { + "records" : { + "description" : "Simulation metadata (e.g., runs, invocations)", + "type" : "array", + "minItems" : 1, + "items": { + "oneOf": [ + { "$ref": "#/definitions/record" }, + { "$ref": "#/definitions/run" } + ] + }, + "uniqueItems" : true + }, + "relationships" : { + "description" : "Associations between records", + "type" : "array", + "minItems" : 0, + "items": { "$ref": "#/definitions/relationship" }, + "uniqueItems" : true + } + }, + "required": [ "records" ] +} diff --git a/src/axom/sina/tests/CMakeLists.txt b/src/axom/sina/tests/CMakeLists.txt new file mode 100644 index 0000000000..f8a7e3f019 --- /dev/null +++ b/src/axom/sina/tests/CMakeLists.txt @@ -0,0 +1,119 @@ +# 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) +#------------------------------------------------------------------------------ +# Sina unit tests +#------------------------------------------------------------------------------ + + +#------------------------------------------------------------------------------ +# Add gtest C++ tests +#------------------------------------------------------------------------------ +set(gtest_sina_tests + sina_File.cpp + ) + +set(sina_gtests_depends_on gtest sina conduit::conduit) + +foreach(test ${gtest_sina_tests}) + get_filename_component( test_name ${test} NAME_WE ) + axom_add_executable(NAME ${test_name}_test + SOURCES ${test} + OUTPUT_DIR ${TEST_OUTPUT_DIRECTORY} + DEPENDS_ON ${sina_gtests_depends_on} + FOLDER axom/sina/tests + ) + + axom_add_test( NAME ${test_name} + COMMAND ${test_name}_test + ) +endforeach() + + +if (ENABLE_GMOCK) + + #------------------------------------------------------------------------------ + # Create test utilities library for Sina + #------------------------------------------------------------------------------ + + # Define Sina test utility sources and headers + set(sina_test_utils_sources + SinaMatchers.cpp + TestRecord.cpp + ) + set(sina_test_utils_headers + SinaMatchers.hpp + TestRecord.hpp + ) + + set(sina_test_utils_depends_on gmock ${sina_gtests_depends_on}) + + # Create a library for the test utilities so they can be used + axom_add_library( + NAME sina_test_utils + SOURCES ${sina_test_utils_sources} + HEADERS ${sina_test_utils_headers} + DEPENDS_ON ${sina_test_utils_depends_on} + FOLDER axom/sina/tests) + + #------------------------------------------------------------------------------ + # Add gmock C++ tests + #------------------------------------------------------------------------------ + + set(gmock_sina_tests + sina_ConduitUtil.cpp + sina_Curve.cpp + sina_CurveSet.cpp + sina_DataHolder.cpp + sina_Datum.cpp + sina_Document.cpp + sina_ID.cpp + sina_Record.cpp + sina_Relationship.cpp + sina_Run.cpp + ) + + set(sina_gmock_depends_on sina_test_utils) + + # Add tests using Adiak if necessary and Adiak dependency + blt_list_append( TO gmock_sina_tests ELEMENTS sina_AdiakWriter.cpp IF AXOM_USE_ADIAK) + blt_list_append( TO sina_gmock_depends_on ELEMENTS adiak::adiak IF AXOM_USE_ADIAK ) + + foreach(test ${gmock_sina_tests}) + get_filename_component( test_name ${test} NAME_WE ) + axom_add_executable(NAME ${test_name}_test + SOURCES ${test} + OUTPUT_DIR ${TEST_OUTPUT_DIRECTORY} + DEPENDS_ON ${sina_gmock_depends_on} + FOLDER axom/sina/tests + ) + + axom_add_test( NAME ${test_name} + COMMAND ${test_name}_test + ) + endforeach() +endif() + +#------------------------------------------------------------------------------ +# Add fortran integration test +#------------------------------------------------------------------------------ +if (ENABLE_FORTRAN) + find_package(Python REQUIRED COMPONENTS Interpreter) + if (Python_FOUND) + configure_file( + ${CMAKE_SOURCE_DIR}/axom/sina/tests/test_fortran_integration.py + ${TEST_OUTPUT_DIRECTORY}/test_fortran_integration.py + COPYONLY + ) + configure_file( + ${CMAKE_SOURCE_DIR}/axom/sina/interface/sina_schema.json + ${TEST_OUTPUT_DIRECTORY}/sina_schema.json + COPYONLY + ) + + axom_add_test( NAME sina_fortran_integration_test + COMMAND ${Python_EXECUTABLE} ${TEST_OUTPUT_DIRECTORY}/test_fortran_integration.py -bd ${PROJECT_BINARY_DIR} + ) + endif() +endif() diff --git a/src/axom/sina/tests/SinaMatchers.cpp b/src/axom/sina/tests/SinaMatchers.cpp new file mode 100644 index 0000000000..6538091c4c --- /dev/null +++ b/src/axom/sina/tests/SinaMatchers.cpp @@ -0,0 +1,56 @@ +// Copyright (c) 2017-2024, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#include "axom/sina/tests/SinaMatchers.hpp" +#include + +namespace axom +{ +namespace sina +{ +namespace testing +{ + +conduit::Node parseJsonValue(const std::string& valueAsString) +{ + // If we just try to do node.parse(valueAsString, "json"), then passing + // in strings does not work. We need to create a document with a key + // so that valueAsString can be parsed as a value. + conduit::Node node; + std::string fullContents = "{\"TEST_KEY\": "; + fullContents += valueAsString; + fullContents += "}"; + node.parse(fullContents, "json"); + return node.child("TEST_KEY"); +} + +// Define the MatchesJson class +MatchesJson::MatchesJson(const std::string& expectedJsonString) + : expectedJsonString_(expectedJsonString) +{ } + +bool MatchesJson::MatchAndExplain(const conduit::Node& node, + ::testing::MatchResultListener* listener) const +{ + conduit::Node expected = parseJsonValue(expectedJsonString_); + *listener << "Given node is " << node.to_json_default(); + conduit::Node diff; + bool differ = expected.diff(node, diff); + return !differ; +} + +void MatchesJson::DescribeTo(std::ostream* os) const +{ + *os << "matches JSON: " << expectedJsonString_; +} + +void MatchesJson::DescribeNegationTo(std::ostream* os) const +{ + *os << "does not match JSON: " << expectedJsonString_; +} + +} // namespace testing +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/tests/SinaMatchers.hpp b/src/axom/sina/tests/SinaMatchers.hpp new file mode 100644 index 0000000000..289019289c --- /dev/null +++ b/src/axom/sina/tests/SinaMatchers.hpp @@ -0,0 +1,53 @@ +// Copyright (c) 2017-2024, Lawrence Livermore National Security, LLC and +// other Axom Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#ifndef AXOM_SINAMATCHERS_HPP +#define AXOM_SINAMATCHERS_HPP + +#include +#include "conduit.hpp" + +// Note: LLNL's blueos complained about using GMock's MATCHER_P macros, as originally implemented. +// This required explicitly generating equivalent classes for the Matcher functionality. + +namespace axom +{ +namespace sina +{ +namespace testing +{ + +// Function to parse JSON value +conduit::Node parseJsonValue(const std::string& valueAsString); + +// Matcher class +class MatchesJson +{ +public: + explicit MatchesJson(const std::string& expectedJsonString); + + bool MatchAndExplain(const conduit::Node& node, + ::testing::MatchResultListener* listener) const; + + void DescribeTo(std::ostream* os) const; + + void DescribeNegationTo(std::ostream* os) const; + +private: + const std::string expectedJsonString_; +}; + +// Helper function to create the matcher +inline ::testing::PolymorphicMatcher MatchesJsonMatcher( + const std::string& expectedJsonString) +{ + return ::testing::MakePolymorphicMatcher(MatchesJson(expectedJsonString)); +} + +} // namespace testing +} // namespace sina +} // namespace axom + +#endif //AXOM_SINAMATCHERS_HPP \ No newline at end of file diff --git a/src/axom/sina/tests/TestRecord.cpp b/src/axom/sina/tests/TestRecord.cpp new file mode 100644 index 0000000000..d333f019f9 --- /dev/null +++ b/src/axom/sina/tests/TestRecord.cpp @@ -0,0 +1,29 @@ +// 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) + +#include "axom/sina/tests/TestRecord.hpp" + +namespace axom +{ +namespace sina +{ +namespace testing +{ + +template <> +TestRecord::TestRecord(conduit::Node const &asNode) + : Record {asNode} + , value {getRequiredString(TEST_RECORD_VALUE_KEY, asNode, "TestRecord")} +{ } + +template <> +TestRecord::TestRecord(conduit::Node const &asNode) + : Record {asNode} + , value {getRequiredField(TEST_RECORD_VALUE_KEY, asNode, "TestRecord").as_int()} +{ } + +} // namespace testing +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/tests/TestRecord.hpp b/src/axom/sina/tests/TestRecord.hpp new file mode 100644 index 0000000000..b665cd4b57 --- /dev/null +++ b/src/axom/sina/tests/TestRecord.hpp @@ -0,0 +1,86 @@ +// 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) + +#ifndef SINA_TESTRECORD_HPP +#define SINA_TESTRECORD_HPP + +#include "axom/sina/core/ConduitUtil.hpp" +#include "axom/sina/core/Record.hpp" + +namespace axom +{ +namespace sina +{ +namespace testing +{ + +char constexpr TEST_RECORD_VALUE_KEY[] = "testKey"; + +/** + * A TestRecord is a class template that's a subclass of Record and simply + * stores a value of a specified type. + * + * @tparam T the type of the value to store + */ +template +class TestRecord : public Record +{ +public: + /** + * Create a new TestRecord. + * + * @param id the ID of the record. It is always a global ID. + * @param type the type of the record + * @param value the value of the record + */ + TestRecord(std::string id, std::string type, T value); + + /** + * Create a new TestRecord from its conduit Node representation. + * + * NOTE: This needs to be implemented explicitly for each type of value + * + * @param asValue the record in its Node representation + */ + explicit TestRecord(conduit::Node const &asValue); + + /** + * Get the record's value. + * + * @return the record's value + */ + const T &getValue() const noexcept { return value; } + + conduit::Node toNode() const override; + +private: + T value; +}; + +template +TestRecord::TestRecord(std::string id, std::string type, T value_) + : Record {ID {std::move(id), IDType::Global}, std::move(type)} + , value {std::move(value_)} +{ } + +template <> +TestRecord::TestRecord(conduit::Node const &asNode); + +template <> +TestRecord::TestRecord(conduit::Node const &asJson); + +template +conduit::Node TestRecord::toNode() const +{ + auto asJson = Record::toNode(); + asJson[TEST_RECORD_VALUE_KEY] = value; + return asJson; +} + +} // namespace testing +} // namespace sina +} // namespace axom + +#endif //SINA_TESTRECORD_HPP diff --git a/src/axom/sina/tests/sina_AdiakWriter.cpp b/src/axom/sina/tests/sina_AdiakWriter.cpp new file mode 100644 index 0000000000..bb1f4617d8 --- /dev/null +++ b/src/axom/sina/tests/sina_AdiakWriter.cpp @@ -0,0 +1,192 @@ +// 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) + +#include "axom/sina/core/AdiakWriter.hpp" + +#ifdef AXOM_USE_ADIAK + + #include + #include + #include + + #include "gtest/gtest.h" + #include "gmock/gmock.h" + + #include "adiak.hpp" +extern "C" { + #include "adiak_tool.h" + #include "adiak.h" +} + + #include "axom/sina/core/Datum.hpp" + #include "axom/sina/core/ID.hpp" + #include "axom/sina/core/Run.hpp" + +namespace axom +{ +namespace sina +{ +namespace testing +{ +namespace +{ + +using ::testing::DoubleEq; +using ::testing::ElementsAre; +using ::testing::HasSubstr; + +char const EXPECTED_DATA_KEY[] = "data"; +char const EXPECTED_FILES_KEY[] = "files"; + +class AdiakWriterTest : public ::testing::Test +{ +protected: + static void SetUpTestCase() + { + adiak::init(nullptr); + adiak_register_cb(1, + adiak_category_all, + AdiakWriterTest::callbackWrapper, + 0, + ¤t_test); + } + + void SetUp() override { current_test = this; } + + static void callbackWrapper(const char *name, + adiak_category_t category, + const char *subcategory, + adiak_value_t *val, + adiak_datatype_t *adiak_type, + void *adiakwriter) + { + auto test = static_cast(adiakwriter); + adiakSinaCallback(name, + category, + subcategory, + val, + adiak_type, + &((*test)->record)); + } + + axom::sina::Record record { + axom::sina::ID {"test_run", axom::sina::IDType::Local}, + "test_type"}; + static AdiakWriterTest *current_test; +}; + +AdiakWriterTest *AdiakWriterTest::current_test; + +TEST_F(AdiakWriterTest, basic_assignment) +{ + //adiak::init(nullptr); + //adiak_register_cb(1, adiak_category_all, sina::adiakSinaCallback, 0, callback_record_ptr); + std::string name1 = "name1"; + std::string value1 = "value1"; + std::vector tags1 = {"string"}; + std::string name2 = "name2"; + int value2 = 2; + std::vector tags2 = {"int"}; + auto result1 = adiak::value(name1, value1); + auto result2 = adiak::value(name2, value2); + EXPECT_TRUE(result1 && result2); + auto asNode = record.toNode(); + EXPECT_EQ(value1, asNode[EXPECTED_DATA_KEY][name1]["value"].as_string()); + EXPECT_EQ(value2, int(asNode[EXPECTED_DATA_KEY][name2]["value"].as_double())); + EXPECT_EQ(tags1[0], asNode[EXPECTED_DATA_KEY][name1]["tags"][0].as_string()); + EXPECT_EQ(tags2[0], asNode[EXPECTED_DATA_KEY][name2]["tags"][0].as_string()); +} + +TEST_F(AdiakWriterTest, scalar_types) +{ + std::string name1 = "my_long"; + long value1 = 0; + std::string name2 = "my_double"; + double value2 = 3.14; + auto result1 = adiak::value(name1, value1); + auto result2 = adiak::value(name2, value2); + EXPECT_TRUE(result1 && result2); + auto asNode = record.toNode(); + EXPECT_EQ(value1, asNode[EXPECTED_DATA_KEY][name1]["value"].as_float64()); + EXPECT_EQ(value2, asNode[EXPECTED_DATA_KEY][name2]["value"].as_double()); +} + +// No extra test for string_types (besides date) as they're handled identically +TEST_F(AdiakWriterTest, date_type) +{ + std::string name1 = "my_date"; + auto result = adiak::value(name1, adiak::date(1568397849)); + EXPECT_TRUE(result); + auto toNode = record.toNode(); + EXPECT_EQ("Fri, 13 Sep 2019 11:04:09 -0700", + toNode[EXPECTED_DATA_KEY][name1]["value"].as_string()); +} + +TEST_F(AdiakWriterTest, list_types) +{ + std::string name1 = "my_scalar_list"; + std::vector value1 {4.5, 0, 5.12, 42}; + std::string name2 = "my_string_list"; + std::set value2 {"spam", "egg and bacon", "egg and spam"}; + auto result1 = adiak::value(name1, value1); + auto result2 = adiak::value(name2, value2); + EXPECT_TRUE(result1 && result2); + auto asNode = record.toNode(); + auto doub_array = asNode[EXPECTED_DATA_KEY][name1]["value"].as_double_ptr(); + std::vector scal_child_vals( + doub_array, + doub_array + + asNode[EXPECTED_DATA_KEY][name1]["value"].dtype().number_of_elements()); + EXPECT_EQ(value1, scal_child_vals); + std::set node_vals; + auto val_itr = asNode[EXPECTED_DATA_KEY][name2]["value"].children(); + while(val_itr.has_next()) node_vals.insert(val_itr.next().as_string()); + EXPECT_EQ(value2, node_vals); +} + +TEST_F(AdiakWriterTest, files) +{ + std::string name1 = "my_bash"; + std::string value1 = "/bin/bash"; + std::string name2 = "my_cat_pics"; + std::string value2 = "~/pictures/neighbor_cat.png"; + std::vector tags2 {name2}; + auto result1 = adiak::value(name1, adiak::path(value1)); + auto result2 = adiak::value(name2, adiak::path(value2)); + EXPECT_TRUE(result1 && result2); + auto asNode = record.toNode(); + EXPECT_FALSE(asNode[EXPECTED_FILES_KEY].child(value1).dtype().is_empty()); + EXPECT_EQ( + 1, + asNode[EXPECTED_FILES_KEY].child(value2)["tags"].number_of_children()); + EXPECT_EQ(tags2[0], + asNode[EXPECTED_FILES_KEY].child(value2)["tags"][0].as_string()); +} + +TEST_F(AdiakWriterTest, files_list) +{ + std::string fileListName = "my_gecko_pics"; + std::string fileListVal1 = "~/pictures/spike.png"; + std::string fileListVal2 = "~/pictures/sandy.png"; + std::vector fileListAdiak {adiak::path(fileListVal1), + adiak::path(fileListVal2)}; + std::vector tags = {"string"}; + EXPECT_TRUE(adiak::value(fileListName, fileListAdiak)); + auto asNode = record.toNode(); + EXPECT_FALSE(asNode[EXPECTED_FILES_KEY].child(fileListVal1).dtype().is_empty()); + EXPECT_EQ( + 1, + asNode[EXPECTED_FILES_KEY].child(fileListVal2)["tags"].number_of_children()); + EXPECT_EQ( + fileListName, + asNode[EXPECTED_FILES_KEY].child(fileListVal2)["tags"][0].as_string()); +} + +} // namespace +} // namespace testing +} // namespace sina +} // namespace axom + +#endif // AXOM_USE_ADIAK diff --git a/src/axom/sina/tests/sina_ConduitUtil.cpp b/src/axom/sina/tests/sina_ConduitUtil.cpp new file mode 100644 index 0000000000..4743bcb278 --- /dev/null +++ b/src/axom/sina/tests/sina_ConduitUtil.cpp @@ -0,0 +1,267 @@ +// 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) + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "axom/sina/core/ConduitUtil.hpp" +#include "axom/sina/tests/SinaMatchers.hpp" + +namespace axom +{ +namespace sina +{ +namespace testing +{ +namespace +{ + +using ::testing::ContainerEq; +using ::testing::DoubleEq; +using ::testing::ElementsAre; +using ::testing::HasSubstr; + +TEST(ConduitUtil, getRequiredField_present) +{ + conduit::Node parent; + parent["fieldName"] = "field value"; + auto &field = getRequiredField("fieldName", parent, "parent name"); + EXPECT_TRUE(field.dtype().is_string()); + EXPECT_EQ("field value", field.as_string()); +} + +TEST(ConduitUtil, getRequiredField_missing) +{ + conduit::Node parent; + try + { + auto &field = getRequiredField("fieldName", parent, "parent name"); + FAIL() << "Should not have found field, but got " << field.name(); + } + catch(std::invalid_argument const &expected) + { + EXPECT_THAT(expected.what(), HasSubstr("fieldName")); + EXPECT_THAT(expected.what(), HasSubstr("parent name")); + } +} + +TEST(ConduitUtil, getRequiredField_slashes) +{ + conduit::Node parent; + // Conduit by default parses /, creating parent["some"]["name"] + parent["some/name"] = 24; + // This is how we provide a literal name with slashes + parent.add_child("some/name") = 42; + EXPECT_EQ(42, getRequiredField("some/name", parent, "parent name").to_int64()); +} + +TEST(ConduitUtil, getRequiredString_valid) +{ + conduit::Node parent; + parent["fieldName"] = "field value"; + EXPECT_EQ("field value", + getRequiredString("fieldName", parent, "parent name")); +} + +TEST(ConduitUtil, getRequiredString_missing) +{ + conduit::Node parent; + try + { + auto value = getRequiredString("fieldName", parent, "parent name"); + FAIL() << "Should not have found string, but got " << value; + } + catch(std::invalid_argument const &expected) + { + EXPECT_THAT(expected.what(), HasSubstr("fieldName")); + EXPECT_THAT(expected.what(), HasSubstr("parent name")); + } +} + +TEST(ConduitUtil, getRequiredString_wrongType) +{ + conduit::Node parent; + parent["fieldName"] = 123; + try + { + auto value = getRequiredString("fieldName", parent, "parent name"); + FAIL() << "Should not have found string, but got " << value; + } + catch(std::invalid_argument const &expected) + { + EXPECT_THAT(expected.what(), HasSubstr("fieldName")); + EXPECT_THAT(expected.what(), HasSubstr("parent name")); + EXPECT_THAT(expected.what(), HasSubstr("string")); + } +} + +TEST(ConduitUtil, getRequiredString_slashes) +{ + conduit::Node parent; + parent["some/name"] = "undesired value"; + parent.add_child("some/name") = "desired value"; + EXPECT_EQ("desired value", + getRequiredString("some/name", parent, "parent name")); +} + +TEST(ConduitUtil, getRequiredDouble_valid) +{ + conduit::Node parent; + parent["fieldName"] = 3.14; + EXPECT_THAT(3.14, + DoubleEq(getRequiredDouble("fieldName", parent, "parent name"))); +} + +TEST(ConduitUtil, getRequiredDouble_missing) +{ + conduit::Node parent; + try + { + auto value = getRequiredDouble("fieldName", parent, "parent name"); + FAIL() << "Should not have found double, but got " << value; + } + catch(std::invalid_argument const &expected) + { + EXPECT_THAT(expected.what(), HasSubstr("fieldName")); + EXPECT_THAT(expected.what(), HasSubstr("parent name")); + } +} + +TEST(ConduitUtil, getRequiredDouble_wrongType) +{ + conduit::Node parent; + parent["fieldName"] = "field value"; + try + { + auto value = getRequiredDouble("fieldName", parent, "parent name"); + FAIL() << "Should not have found double, but got " << value; + } + catch(std::invalid_argument const &expected) + { + EXPECT_THAT(expected.what(), HasSubstr("fieldName")); + EXPECT_THAT(expected.what(), HasSubstr("parent name")); + EXPECT_THAT(expected.what(), HasSubstr("double")); + } +} + +TEST(ConduitUtil, getOptionalString_valid) +{ + conduit::Node parent; + parent["fieldName"] = "the value"; + EXPECT_EQ("the value", getOptionalString("fieldName", parent, "parent name")); +} + +TEST(ConduitUtil, getOptionalString_missing) +{ + conduit::Node parent; + EXPECT_EQ("", getOptionalString("fieldName", parent, "parent name")); +} + +TEST(ConduitUtil, getOptionalString_explicitNullValue) +{ + conduit::Node parent; + parent["fieldName"]; + EXPECT_EQ("", getOptionalString("fieldName", parent, "parent name")); +} + +TEST(ConduitUtil, getOptionalString_wrongType) +{ + conduit::Node parent; + parent["fieldName"] = 123; + try + { + auto value = getOptionalString("fieldName", parent, "parent name"); + FAIL() << "Should not have found string, but got " << value; + } + catch(std::invalid_argument const &expected) + { + EXPECT_THAT(expected.what(), HasSubstr("fieldName")); + EXPECT_THAT(expected.what(), HasSubstr("parent name")); + EXPECT_THAT(expected.what(), HasSubstr("string")); + } +} + +TEST(ConduitUtil, getOptionalField_slashes) +{ + conduit::Node parent; + parent["some/name"] = "undesired value"; + EXPECT_EQ("", getOptionalString("some/name", parent, "parent name")); +} + +TEST(ConduitUtil, toDoubleVector_empty) +{ + conduit::Node emptyList = parseJsonValue("[]"); + EXPECT_EQ(std::vector {}, toDoubleVector(emptyList, "testNode")); +} + +TEST(ConduitUtil, toDoubleVector_validValues) +{ + conduit::Node nonEmptyList = parseJsonValue("[1, 2.0, 3, 4]"); + EXPECT_THAT(toDoubleVector(nonEmptyList, "testNode"), + ElementsAre(1.0, 2.0, 3.0, 4.0)); +} + +TEST(ConduitUtil, toDoubleVector_NotList) +{ + conduit::Node notList = parseJsonValue("\"this is not a list of doubles\""); + try + { + toDoubleVector(notList, "someName"); + FAIL() << "Should have thrown an exception"; + } + catch(std::invalid_argument const &ex) + { + EXPECT_THAT(ex.what(), HasSubstr("someName")); + } +} + +TEST(ConduitUtil, toStringVector_empty) +{ + conduit::Node emptyList = parseJsonValue("[]"); + EXPECT_THAT(toStringVector(emptyList, "testNode"), + ContainerEq(std::vector {})); +} + +TEST(ConduitUtil, toStringVector_validValues) +{ + conduit::Node nonEmptyList = parseJsonValue(R"(["s1", "s2", "s3"])"); + EXPECT_THAT(toStringVector(nonEmptyList, "testNode"), + ElementsAre("s1", "s2", "s3")); +} + +TEST(ConduitUtil, toStringVector_NotList) +{ + conduit::Node notList = parseJsonValue("\"this is not a list of doubles\""); + try + { + toStringVector(notList, "someName"); + FAIL() << "Should have thrown an exception."; + } + catch(std::invalid_argument const &ex) + { + EXPECT_THAT(ex.what(), HasSubstr("someName")); + } +} + +TEST(ConduitUtil, toStringVector_NotListOfStrings) +{ + conduit::Node notList = parseJsonValue(R"([1, 2, "a string"])"); + try + { + toStringVector(notList, "someName"); + FAIL() << "Should have thrown an exception."; + } + catch(std::invalid_argument const &ex) + { + EXPECT_THAT(ex.what(), HasSubstr("someName")); + } +} + +} // namespace +} // namespace testing +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/tests/sina_Curve.cpp b/src/axom/sina/tests/sina_Curve.cpp new file mode 100644 index 0000000000..54d5d7b078 --- /dev/null +++ b/src/axom/sina/tests/sina_Curve.cpp @@ -0,0 +1,125 @@ +// 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) + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "axom/sina/core/Curve.hpp" +#include "axom/sina/core/ConduitUtil.hpp" +#include "axom/sina/tests/SinaMatchers.hpp" + +namespace axom +{ +namespace sina +{ +namespace testing +{ +namespace +{ + +using ::testing::ContainerEq; +using ::testing::ElementsAre; + +TEST(Curve, createFromVector) +{ + std::vector values {1, 2, 3, 4, 5, 6}; + Curve const curve {"theName", values}; + EXPECT_EQ("theName", curve.getName()); + EXPECT_THAT(curve.getValues(), ElementsAre(1, 2, 3, 4, 5, 6)); + EXPECT_EQ("", curve.getUnits()); + EXPECT_THAT(curve.getTags(), ElementsAre()); +} + +TEST(Curve, createFromPointer) +{ + double values[] {1, 2, 3, 4, 5, 6}; + Curve const curve {"theName", values, sizeof(values) / sizeof(double)}; + EXPECT_EQ("theName", curve.getName()); + EXPECT_THAT(curve.getValues(), ElementsAre(1, 2, 3, 4, 5, 6)); + EXPECT_EQ("", curve.getUnits()); + EXPECT_THAT(curve.getTags(), ElementsAre()); +} + +TEST(Curve, createFromInitializerList) +{ + Curve const curve {"theName", {1, 2, 3, 4, 5, 6}}; + EXPECT_EQ("theName", curve.getName()); + EXPECT_THAT(curve.getValues(), ElementsAre(1, 2, 3, 4, 5, 6)); + EXPECT_EQ("", curve.getUnits()); + EXPECT_THAT(curve.getTags(), ElementsAre()); +} + +TEST(Curve, setUnits) +{ + Curve curve {"theName", {1, 2, 3}}; + EXPECT_EQ("", curve.getUnits()); + curve.setUnits("cm"); + EXPECT_EQ("cm", curve.getUnits()); +} + +TEST(Curve, setTags) +{ + Curve curve {"theName", {1, 2, 3}}; + EXPECT_THAT(curve.getTags(), ElementsAre()); + curve.setTags({"t1", "t2", "t3"}); + EXPECT_THAT(curve.getTags(), ElementsAre("t1", "t2", "t3")); +} + +TEST(Curve, createFromNode_requiredOnly) +{ + conduit::Node curveAsNode = parseJsonValue(R"( + { + "value": [1.0, 2.0, 3.0] + } + )"); + Curve curve {"theName", curveAsNode}; + EXPECT_EQ("theName", curve.getName()); + EXPECT_THAT(curve.getValues(), ElementsAre(1, 2, 3)); + EXPECT_EQ("", curve.getUnits()); + EXPECT_THAT(curve.getTags(), ElementsAre()); +} + +TEST(Curve, createFromNode_optionalFields) +{ + conduit::Node curveAsNode = parseJsonValue(R"( + { + "value": [1.0, 2.0, 3.0], + "units": "cm", + "tags": ["t1", "t2", "t3"] + } + )"); + Curve curve {"theName", curveAsNode}; + EXPECT_EQ("theName", curve.getName()); + EXPECT_THAT(curve.getValues(), ElementsAre(1, 2, 3)); + EXPECT_EQ("cm", curve.getUnits()); + EXPECT_THAT(curve.getTags(), ElementsAre("t1", "t2", "t3")); +} + +TEST(Curve, toNode_requiredOnly) +{ + Curve const curve {"theName", {1, 2, 3, 4}}; + std::string expected = (R"({ + "value": [1.0, 2.0, 3.0, 4.0] + })"); + EXPECT_THAT(curve.toNode(), MatchesJsonMatcher(expected)); +} + +TEST(Curve, toNode_optionalFields) +{ + Curve curve {"theName", {1, 2, 3, 4}}; + curve.setUnits("cm"); + curve.setTags({"t1", "t2", "t3"}); + std::string expected = R"({ + "value": [1.0, 2.0, 3.0, 4.0], + "units": "cm", + "tags": ["t1", "t2", "t3"] + })"; + EXPECT_THAT(curve.toNode(), MatchesJsonMatcher(expected)); +} + +} // namespace +} // namespace testing +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/tests/sina_CurveSet.cpp b/src/axom/sina/tests/sina_CurveSet.cpp new file mode 100644 index 0000000000..7bd8d941d6 --- /dev/null +++ b/src/axom/sina/tests/sina_CurveSet.cpp @@ -0,0 +1,216 @@ +// 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) + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "axom/sina/core/CurveSet.hpp" +#include "axom/sina/tests/SinaMatchers.hpp" + +#include +#include + +namespace axom +{ +namespace sina +{ + +// NOTE: We need an operator== for the tests. For it to be able to be found +// by different matchers, it needs to be in the same namespace as Curve. +// If we need up needing it in another test, we'll have to move it to another +// file. +// +// NOTE: Since this isn't in an unnamed namespace, we need a forward +// declaration to satisfy strict compiler warnings. +bool operator==(Curve const &lhs, Curve const &rhs); + +/** + * Compare two curves for equality. All fields must be equal, including the + * doubles in the lists of values. This is not suitable for checking any + * calculated values. + * + * @param lhs the left-hand-side operand + * @param rhs the right-hand-side operand + * @return whether the curves are equal + */ +bool operator==(Curve const &lhs, Curve const &rhs) +{ + bool r = lhs.getName() == rhs.getName() && lhs.getUnits() == rhs.getUnits() && + lhs.getTags() == rhs.getTags() && lhs.getValues() == rhs.getValues(); + return r; +} + +namespace testing +{ +namespace +{ + +using ::testing::ContainerEq; +using ::testing::ElementsAre; + +TEST(CurveSet, initialState) +{ + CurveSet const cs {"theName"}; + ASSERT_EQ("theName", cs.getName()); + ASSERT_TRUE(cs.getIndependentCurves().empty()); + ASSERT_TRUE(cs.getDependentCurves().empty()); + ASSERT_NE(&cs.getIndependentCurves(), &cs.getDependentCurves()); +} + +TEST(CurveSet, addIndependentCurves) +{ + CurveSet cs {"testSet"}; + std::unordered_map expectedCurves; + + Curve i1 {"i1", {1, 2, 3}}; + cs.addIndependentCurve(i1); + expectedCurves.insert(std::make_pair(i1.getName(), i1)); + EXPECT_THAT(cs.getIndependentCurves(), ContainerEq(expectedCurves)); + + Curve i2 {"i2", {4, 5, 6}}; + cs.addIndependentCurve(i2); + expectedCurves.insert(std::make_pair(i2.getName(), i2)); + EXPECT_THAT(cs.getIndependentCurves(), ContainerEq(expectedCurves)); +} + +TEST(CurveSet, addIndependentCurves_replaceExisting) +{ + CurveSet cs {"testSet"}; + + Curve i1 {"theName", {1, 2, 3}}; + cs.addIndependentCurve(i1); + EXPECT_THAT(cs.getIndependentCurves().at("theName").getValues(), + ElementsAre(1, 2, 3)); + + Curve i2 {"theName", {4, 5, 6}}; + cs.addIndependentCurve(i2); + EXPECT_THAT(cs.getIndependentCurves().at("theName").getValues(), + ElementsAre(4, 5, 6)); +} + +TEST(CurveSet, addDpendentCurves) +{ + CurveSet cs {"testSet"}; + std::unordered_map expectedCurves; + + Curve i1 {"i1", {1, 2, 3}}; + cs.addDependentCurve(i1); + expectedCurves.insert(std::make_pair(i1.getName(), i1)); + EXPECT_THAT(cs.getDependentCurves(), ContainerEq(expectedCurves)); + + Curve i2 {"i2", {4, 5, 6}}; + cs.addDependentCurve(i2); + expectedCurves.insert(std::make_pair(i2.getName(), i2)); + EXPECT_THAT(cs.getDependentCurves(), ContainerEq(expectedCurves)); +} + +TEST(CurveSet, addDependentCurves_replaceExisting) +{ + CurveSet cs {"testSet"}; + + Curve d1 {"theName", {1, 2, 3}}; + cs.addDependentCurve(d1); + EXPECT_THAT(cs.getDependentCurves().at("theName").getValues(), + ElementsAre(1, 2, 3)); + + Curve d2 {"theName", {4, 5, 6}}; + cs.addDependentCurve(d2); + EXPECT_THAT(cs.getDependentCurves().at("theName").getValues(), + ElementsAre(4, 5, 6)); +} + +TEST(CurveSet, createFromNode_empty) +{ + conduit::Node curveSetAsNode = parseJsonValue(R"({})"); + CurveSet curveSet {"theName", curveSetAsNode}; + EXPECT_EQ("theName", curveSet.getName()); + std::unordered_map emptyMap; + EXPECT_THAT(curveSet.getDependentCurves(), ContainerEq(emptyMap)); + EXPECT_THAT(curveSet.getIndependentCurves(), ContainerEq(emptyMap)); +} + +TEST(CurveSet, createFromNode_emptySets) +{ + conduit::Node curveSetAsNode = parseJsonValue(R"({ + "dependent": {}, + "independent": {} + })"); + CurveSet curveSet {"theName", curveSetAsNode}; + EXPECT_EQ("theName", curveSet.getName()); + std::unordered_map emptyMap; + EXPECT_THAT(curveSet.getDependentCurves(), ContainerEq(emptyMap)); + EXPECT_THAT(curveSet.getIndependentCurves(), ContainerEq(emptyMap)); +} + +TEST(CurveSet, createFromNode_curveSetsDefined) +{ + conduit::Node curveSetAsNode = parseJsonValue(R"({ + "independent": { + "indep1": { "value": [10, 20, 30]}, + "indep2/with/slash": { "value": [40, 50, 60]} + }, + "dependent": { + "dep1": { "value": [1, 2, 3]}, + "dep2/with/slash": { "value": [4, 5, 6]} + } + })"); + CurveSet curveSet {"theName", curveSetAsNode}; + EXPECT_EQ("theName", curveSet.getName()); + + std::unordered_map expectedDependents { + {"dep1", Curve {"dep1", {1, 2, 3}}}, + {"dep2/with/slash", Curve {"dep2/with/slash", {4, 5, 6}}}, + }; + EXPECT_THAT(curveSet.getDependentCurves(), ContainerEq(expectedDependents)); + + std::unordered_map expectedIndependents { + {"indep1", Curve {"indep1", {10, 20, 30}}}, + {"indep2/with/slash", Curve {"indep2/with/slash", {40, 50, 60}}}, + }; + EXPECT_THAT(curveSet.getIndependentCurves(), ContainerEq(expectedIndependents)); +} + +TEST(CurveSet, toNode_empty) +{ + CurveSet curveSet {"theName"}; + std::string expected = R"({ + "independent": {}, + "dependent": {} + })"; + EXPECT_THAT(curveSet.toNode(), MatchesJsonMatcher(expected)); +} + +TEST(CurveSet, toNode_withCurves) +{ + CurveSet curveSet {"theName"}; + curveSet.addIndependentCurve(Curve {"i1", {1, 2, 3}}); + curveSet.addIndependentCurve(Curve {"i2/with/slash", {4, 5, 6}}); + curveSet.addDependentCurve(Curve {"d1", {10, 20, 30}}); + curveSet.addDependentCurve(Curve {"d2/with/slash", {40, 50, 60}}); + std::string expected = R"({ + "independent": { + "i1": { + "value": [1.0, 2.0, 3.0] + }, + "i2/with/slash": { + "value": [4.0, 5.0, 6.0] + } + }, + "dependent": { + "d1": { + "value": [10.0, 20.0, 30.0] + }, + "d2/with/slash": { + "value": [40.0, 50.0, 60.0] + } + } + })"; + EXPECT_THAT(curveSet.toNode(), MatchesJsonMatcher(expected)); +} + +} // namespace +} // namespace testing +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/tests/sina_DataHolder.cpp b/src/axom/sina/tests/sina_DataHolder.cpp new file mode 100644 index 0000000000..0a4869ade5 --- /dev/null +++ b/src/axom/sina/tests/sina_DataHolder.cpp @@ -0,0 +1,321 @@ +// 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) + +#include +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "axom/sina/core/DataHolder.hpp" +#include "axom/sina/tests/SinaMatchers.hpp" + +namespace axom +{ +namespace sina +{ +namespace testing +{ +namespace +{ + +using ::testing::Contains; +using ::testing::DoubleEq; +using ::testing::ElementsAre; +using ::testing::HasSubstr; +using ::testing::Key; +using ::testing::Not; + +char const EXPECTED_DATA_KEY[] = "data"; +char const EXPECTED_CURVE_SETS_KEY[] = "curve_sets"; +char const EXPECTED_LIBRARY_DATA_KEY[] = "library_data"; +char const EXPECTED_USER_DEFINED_KEY[] = "user_defined"; + +TEST(DataHolder, add_data_existing_key) +{ + DataHolder dh {}; + dh.add("key1", Datum {"val1"}); + EXPECT_EQ("val1", dh.getData().at("key1").getValue()); + dh.add("key1", Datum {"val2"}); + EXPECT_EQ("val2", dh.getData().at("key1").getValue()); +} + +TEST(DataHolder, add_curve_set_existing_key) +{ + DataHolder dh {}; + CurveSet cs1 {"cs1"}; + cs1.addDependentCurve(Curve {"original", {1, 2, 3}}); + dh.add(cs1); + + auto &csAfterFirstInsert = dh.getCurveSets(); + ASSERT_THAT(csAfterFirstInsert, Contains(Key("cs1"))); + EXPECT_THAT(csAfterFirstInsert.at("cs1").getDependentCurves(), + Contains(Key("original"))); + + CurveSet cs2 {"cs1"}; + cs2.addDependentCurve(Curve {"new", {1, 2, 3}}); + dh.add(cs2); + + auto &csAfterSecondInsert = dh.getCurveSets(); + ASSERT_THAT(csAfterSecondInsert, Contains(Key("cs1"))); + EXPECT_THAT(csAfterSecondInsert.at("cs1").getDependentCurves(), + Not(Contains(Key("original")))); + EXPECT_THAT(csAfterSecondInsert.at("cs1").getDependentCurves(), + Contains(Key("new"))); +} + +TEST(DataHolder, create_fromNode_userDefined) +{ + conduit::Node originalNode; + originalNode[EXPECTED_USER_DEFINED_KEY]["k1"] = "v1"; + originalNode[EXPECTED_USER_DEFINED_KEY]["k2"] = 123; + std::vector k3_vals {1, 2, 3}; + originalNode[EXPECTED_USER_DEFINED_KEY]["k3"] = k3_vals; + + DataHolder holder {originalNode}; + auto const &userDefined = holder.getUserDefinedContent(); + EXPECT_EQ("v1", userDefined["k1"].as_string()); + EXPECT_EQ(123, userDefined["k2"].as_int()); + auto int_array = userDefined["k3"].as_int_ptr(); + std::vector udef_ints( + int_array, + int_array + userDefined["k3"].dtype().number_of_elements()); + EXPECT_THAT(udef_ints, ElementsAre(1, 2, 3)); +} + +TEST(DataHolder, create_fromNode_userDefined_not_object) +{ + conduit::Node originalNode; + originalNode[EXPECTED_USER_DEFINED_KEY] = "not an object"; + EXPECT_THROW(DataHolder {originalNode}, std::invalid_argument); +} + +TEST(DataHolder, getUserDefined_initialConst) +{ + DataHolder const holder; + conduit::Node const &userDefined = holder.getUserDefinedContent(); + EXPECT_TRUE(userDefined.dtype().is_empty()); +} + +TEST(DataHolder, getUserDefined_initialNonConst) +{ + DataHolder holder; + conduit::Node &initialUserDefined = holder.getUserDefinedContent(); + EXPECT_TRUE(initialUserDefined.dtype().is_empty()); + initialUserDefined["foo"] = 123; + EXPECT_EQ(123, holder.getUserDefinedContent()["foo"].as_int()); +} + +TEST(DataHolder, add_new_library) +{ + DataHolder dh {}; + auto outer = dh.addLibraryData("outer"); + auto &libDataAfterFirstInsert = dh.getLibraryData(); + ASSERT_THAT(libDataAfterFirstInsert, Contains(Key("outer"))); + dh.addLibraryData("other_outer"); + auto &libDataAfterSecondInsert = dh.getLibraryData(); + ASSERT_THAT(libDataAfterSecondInsert, Contains(Key("outer"))); + ASSERT_THAT(libDataAfterSecondInsert, Contains(Key("other_outer"))); + outer->addLibraryData("inner"); + auto &libDataAfterThirdInsert = dh.getLibraryData(); + ASSERT_THAT(libDataAfterThirdInsert.at("outer")->getLibraryData(), + Contains(Key("inner"))); + ASSERT_THAT(libDataAfterThirdInsert.at("other_outer")->getLibraryData(), + Not(Contains(Key("inner")))); +} + +TEST(DataHolder, add_library_existing_key) +{ + std::string libName = "outer"; + DataHolder dh {}; + auto outer = dh.addLibraryData(libName); + outer->add("key1", Datum {"val1"}); + ASSERT_THAT(dh.getLibraryData(libName)->getData(), Contains(Key("key1"))); + dh.addLibraryData(libName); + ASSERT_THAT(dh.getLibraryData(libName)->getData(), Not(Contains(Key("key1")))); +} + +TEST(DataHolder, create_fromNode_data) +{ + conduit::Node originalNode; + originalNode[EXPECTED_DATA_KEY]; + + std::string name1 = "datum name 1"; + std::string name2 = "datum name 2/with/slash"; + + conduit::Node name1_node; + name1_node["value"] = "value 1"; + originalNode[EXPECTED_DATA_KEY][name1] = name1_node; + conduit::Node name2_node; + name2_node["value"] = 2.22; + name2_node["units"] = "g/L"; + addStringsToNode(name2_node, "tags", {"tag1", "tag2"}); + name2_node["value"] = 2.22; + originalNode[EXPECTED_DATA_KEY].add_child(name2) = name2_node; + DataHolder dh {originalNode}; + auto &data = dh.getData(); + ASSERT_EQ(2u, data.size()); + EXPECT_EQ("value 1", data.at(name1).getValue()); + EXPECT_THAT(2.22, DoubleEq(data.at(name2).getScalar())); + EXPECT_EQ("g/L", data.at(name2).getUnits()); + EXPECT_EQ("tag1", data.at(name2).getTags()[0]); + EXPECT_EQ("tag2", data.at(name2).getTags()[1]); +} + +TEST(DataHolder, create_fromNode_curveSets) +{ + conduit::Node dataHolderAsNode = parseJsonValue(R"({ + "curve_sets": { + "cs1": { + "independent": { + "i1": { "value": [1, 2, 3]} + }, + "dependent": { + "d1": { "value": [4, 5, 6]} + } + } + } + })"); + DataHolder dh {dataHolderAsNode}; + auto &curveSets = dh.getCurveSets(); + ASSERT_THAT(curveSets, Contains(Key("cs1"))); +} + +TEST(DataHolder, create_fromNode_libraryData) +{ + conduit::Node dataHolderAsNode = parseJsonValue(R"({ + "library_data": { + "outer_lib": { + "library_data": { + "inner_lib": { "data": {"i2": { "value": "good morning!"}}} + } + } + } + })"); + DataHolder dh {dataHolderAsNode}; + auto &fullLibData = dh.getLibraryData(); + ASSERT_THAT(fullLibData, Contains(Key("outer_lib"))); + auto outerLibData = fullLibData.at("outer_lib")->getLibraryData(); + ASSERT_THAT(outerLibData, Contains(Key("inner_lib"))); + auto &innerData = outerLibData.at("inner_lib")->getData(); + EXPECT_EQ("good morning!", innerData.at("i2").getValue()); +} + +TEST(DataHolder, toNode_default_values) +{ + DataHolder dh {}; + auto asNode = dh.toNode(); + EXPECT_TRUE(asNode.dtype().is_object()); + // We want to be sure that unset optional fields aren't present + EXPECT_FALSE(asNode.has_child(EXPECTED_DATA_KEY)); + EXPECT_FALSE(asNode.has_child(EXPECTED_CURVE_SETS_KEY)); + EXPECT_FALSE(asNode.has_child(EXPECTED_LIBRARY_DATA_KEY)); +} + +TEST(DataHolder, toNode_data) +{ + DataHolder dh {}; + std::string name1 = "name1"; + std::string value1 = "value1"; + Datum datum1 = Datum {value1}; + datum1.setUnits("some units"); + datum1.setTags({"tag1"}); + dh.add(name1, datum1); + std::string name2 = "name2"; + dh.add(name2, Datum {2.}); + auto asNode = dh.toNode(); + ASSERT_EQ(2u, asNode[EXPECTED_DATA_KEY].number_of_children()); + EXPECT_EQ("value1", asNode[EXPECTED_DATA_KEY][name1]["value"].as_string()); + EXPECT_EQ("some units", asNode[EXPECTED_DATA_KEY][name1]["units"].as_string()); + EXPECT_EQ("tag1", asNode[EXPECTED_DATA_KEY][name1]["tags"][0].as_string()); + + EXPECT_THAT(asNode[EXPECTED_DATA_KEY][name2]["value"].as_double(), + DoubleEq(2.)); + EXPECT_TRUE(asNode[EXPECTED_DATA_KEY][name2]["units"].dtype().is_empty()); + EXPECT_TRUE(asNode[EXPECTED_DATA_KEY][name2]["tags"].dtype().is_empty()); +} + +TEST(DataHolder, toNode_dataWithSlashes) +{ + DataHolder dh {}; + std::string name = "name/with/slashes"; + std::string value = "the value"; + Datum datum = Datum {value}; + dh.add(name, datum); + auto asNode = dh.toNode(); + ASSERT_EQ(1u, asNode[EXPECTED_DATA_KEY].number_of_children()); + EXPECT_EQ("the value", + asNode[EXPECTED_DATA_KEY].child(name)["value"].as_string()); +} + +TEST(DataHolder, toNode_curveSets) +{ + DataHolder dh {}; + CurveSet cs {"myCurveSet/with/slash"}; + cs.addIndependentCurve(Curve {"myCurve", {1, 2, 3}}); + dh.add(cs); + std::string expected = R"({ + "curve_sets": { + "myCurveSet/with/slash": { + "independent": { + "myCurve": { + "value": [1.0, 2.0, 3.0] + } + }, + "dependent": {} + } + } + })"; + EXPECT_THAT(dh.toNode(), MatchesJsonMatcher(expected)); +} + +TEST(DataHolder, toNode_libraryData) +{ + DataHolder dh {}; + auto outer = dh.addLibraryData("outer"); + outer->add("scal", Datum {"goodbye!"}); + auto inner = outer->addLibraryData("inner"); + inner->add("str", Datum {"hello!"}); + std::string expected = R"({ + "library_data": { + "outer": { + "library_data": { + "inner": { + "data": {"str": {"value": "hello!"}} + } + }, + "data": {"scal": {"value": "goodbye!"}} + } + } + })"; + EXPECT_THAT(dh.toNode(), MatchesJsonMatcher(expected)); +} + +TEST(DataHolder, toNode_userDefined) +{ + DataHolder holder; + conduit::Node userDef; + userDef["k1"] = "v1"; + userDef["k2"] = 123; + std::vector int_vals {1, 2, 3}; + userDef["k3"] = int_vals; + holder.setUserDefinedContent(userDef); + + auto asNode = holder.toNode(); + + auto userDefined = asNode[EXPECTED_USER_DEFINED_KEY]; + EXPECT_EQ("v1", userDefined["k1"].as_string()); + EXPECT_EQ(123, userDefined["k2"].as_int()); + auto int_array = userDefined["k3"].as_int_ptr(); + std::vector udef_ints( + int_array, + int_array + userDefined["k3"].dtype().number_of_elements()); + EXPECT_THAT(udef_ints, ElementsAre(1, 2, 3)); +} + +} // namespace +} // namespace testing +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/tests/sina_Datum.cpp b/src/axom/sina/tests/sina_Datum.cpp new file mode 100644 index 0000000000..5126ca8fe3 --- /dev/null +++ b/src/axom/sina/tests/sina_Datum.cpp @@ -0,0 +1,198 @@ +// 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) + +#include +#include +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "axom/sina/core/Datum.hpp" +#include "axom/sina/core/ConduitUtil.hpp" + +namespace axom +{ +namespace sina +{ +namespace testing +{ +namespace +{ + +using ::testing::DoubleEq; +using ::testing::ElementsAre; +using ::testing::HasSubstr; + +TEST(Datum, create) +{ + std::vector tags = {"tag1", "tag2"}; + std::string value = "value"; + std::vector val_list = {"val1", "val2"}; + std::vector scal_list = {100, 2.0}; + Datum datum1 {value}; + datum1.setUnits("some units"); + datum1.setTags(tags); + Datum datum2 {3.14}; + Datum datum3 {val_list}; + Datum datum4 {scal_list}; + + EXPECT_EQ(ValueType::String, datum1.getType()); + EXPECT_EQ("value", datum1.getValue()); + EXPECT_EQ("some units", datum1.getUnits()); + EXPECT_EQ(tags, datum1.getTags()); + + EXPECT_EQ(ValueType::Scalar, datum2.getType()); + EXPECT_THAT(datum2.getScalar(), DoubleEq(3.14)); + + EXPECT_EQ(ValueType::StringArray, datum3.getType()); + EXPECT_EQ(val_list, datum3.getStringArray()); + + EXPECT_EQ(ValueType::ScalarArray, datum4.getType()); + EXPECT_EQ(scal_list, datum4.getScalarArray()); +} + +TEST(Datum, createFromNode) +{ + conduit::Node val_with_tags; + conduit::Node scalar_with_units; + conduit::Node val_list_node; + conduit::Node scal_list_node; + conduit::Node empty_list_node; + conduit::Node int_array_node; + conduit::Node char_array_node; + + std::vector tags = {"hello", "world"}; + std::vector val_list = {"val1", "val2"}; + std::vector scal_list = {100, 2.0}; + // Conduit has some special treatment of arrays + int int_array[] = {-2, 2, 4, 8}; + std::vector array_equiv = {-2, 2, 4, 8}; + char coerced_to_string[] = {'a', 'b', 'c', '\0'}; + //Empty lists are valid + conduit::Node empty_list(conduit::DataType::list()); + + val_with_tags["value"] = "the value"; + addStringsToNode(val_with_tags, "tags", tags); + scalar_with_units["units"] = "some units"; + scalar_with_units["value"] = 3.14; + addStringsToNode(val_list_node, "value", val_list); + scal_list_node["value"] = scal_list; + empty_list_node["value"] = empty_list; + int_array_node["value"].set(int_array, 4); + char_array_node["value"] = coerced_to_string; + + Datum datum1 {val_with_tags}; + Datum datum2 {scalar_with_units}; + Datum datum3 {val_list_node}; + Datum datum4 {scal_list_node}; + Datum datum5 {empty_list_node}; + Datum datum6 {int_array_node}; + Datum datum7 {char_array_node}; + + EXPECT_EQ("the value", datum1.getValue()); + EXPECT_EQ(tags, datum1.getTags()); + EXPECT_THAT(3.14, DoubleEq(datum2.getScalar())); + EXPECT_EQ("some units", datum2.getUnits()); + EXPECT_EQ(val_list, datum3.getStringArray()); + EXPECT_EQ(scal_list, datum4.getScalarArray()); + EXPECT_EQ(ValueType::ScalarArray, datum5.getType()); + EXPECT_EQ(array_equiv, datum6.getScalarArray()); + EXPECT_EQ("abc", datum7.getValue()); +} + +TEST(Datum, setUnits) +{ + std::string value = "value"; + Datum datum1 {value}; + datum1.setUnits("new units"); + EXPECT_EQ("new units", datum1.getUnits()); +} + +TEST(Datum, setTags) +{ + std::string value = "value"; + Datum datum1 {value}; + datum1.setTags({"new_tag"}); + EXPECT_EQ("new_tag", datum1.getTags()[0]); +} + +TEST(Datum, createFromJson_missingKeys) +{ + conduit::Node object1; + try + { + Datum datum1 {object1}; + FAIL() << "Should have gotten a value error"; + } + catch(std::invalid_argument const &expected) + { + EXPECT_THAT(expected.what(), HasSubstr("value")); + } +} + +TEST(Datum, createFromJson_badListValue) +{ + conduit::Node object1; + auto &mixed_scal = object1["value"].append(); + mixed_scal.set(1.0); + auto &mixed_val = object1["value"].append(); + mixed_val.set("two"); + try + { + Datum datum1 {object1}; + FAIL() << "Should have gotten a value error"; + } + catch(std::invalid_argument const &expected) + { + std::string warning = "it must consist of only strings or only numbers"; + EXPECT_THAT(expected.what(), HasSubstr(warning)); + } +} + +TEST(Datum, toJson) +{ + std::vector tags = {"list", "of", "tags"}; + std::string value = "Datum value"; + std::vector scal_list = {-14, 22, 9}; + std::vector val_list = {"east", "west"}; + Datum datum1 {value}; + datum1.setTags(tags); + Datum datum2 {3.14}; + datum2.setUnits("Datum units"); + Datum datum3 {scal_list}; + Datum datum4 {val_list}; + conduit::Node datumRef1 = datum1.toNode(); + conduit::Node datumRef2 = datum2.toNode(); + conduit::Node datumRef3 = datum3.toNode(); + conduit::Node datumRef4 = datum4.toNode(); + EXPECT_EQ("Datum value", datumRef1["value"].as_string()); + std::vector node_tags; + auto tags_itr = datumRef1["tags"].children(); + while(tags_itr.has_next()) + node_tags.emplace_back(tags_itr.next().as_string()); + EXPECT_EQ(tags, node_tags); + + EXPECT_EQ("Datum units", datumRef2["units"].as_string()); + EXPECT_THAT(3.14, DoubleEq(datumRef2["value"].value())); + + // Conduit will pack vectors of numbers into arrays, but + // strings can only live as lists of Nodes + auto doub_array = datumRef3["value"].as_double_ptr(); + std::vector scal_child_vals( + doub_array, + doub_array + datumRef3["value"].dtype().number_of_elements()); + std::vector str_child_vals; + auto str_itr = datumRef4["value"].children(); + while(str_itr.has_next()) + str_child_vals.emplace_back(str_itr.next().as_string()); + EXPECT_EQ(scal_list, scal_child_vals); + EXPECT_EQ(val_list, str_child_vals); +} + +} // namespace +} // namespace testing +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/tests/sina_Document.cpp b/src/axom/sina/tests/sina_Document.cpp new file mode 100644 index 0000000000..d44565d46d --- /dev/null +++ b/src/axom/sina/tests/sina_Document.cpp @@ -0,0 +1,373 @@ +// 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) + +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "axom/sina/core/Document.hpp" +#include "axom/sina/core/Run.hpp" + +#include "axom/sina/tests/TestRecord.hpp" + +namespace axom +{ +namespace sina +{ +namespace testing +{ +namespace +{ + +using ::testing::ElementsAre; +using ::testing::HasSubstr; + +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; + RecordLoader loader; + Document document {documentAsNode, loader}; + EXPECT_EQ(0u, document.getRecords().size()); + EXPECT_EQ(0u, document.getRelationships().size()); +} + +TEST(Document, create_fromNode_wrongRecordsType) +{ + conduit::Node recordsAsNodes; + recordsAsNodes[EXPECTED_RECORDS_KEY] = 123; + RecordLoader loader; + try + { + Document document {recordsAsNodes, loader}; + FAIL() << "Should not have been able to parse records. Have " + << document.getRecords().size(); + } + catch(std::invalid_argument const &expected) + { + EXPECT_THAT(expected.what(), HasSubstr(EXPECTED_RECORDS_KEY)); + } +} + +TEST(Document, create_fromNode_withRecords) +{ + conduit::Node recordAsNode; + recordAsNode["type"] = "IntTestRecord"; + recordAsNode["id"] = "the ID"; + recordAsNode[TEST_RECORD_VALUE_KEY] = 123; + + conduit::Node recordsAsNodes; + recordsAsNodes.append().set(recordAsNode); + + conduit::Node documentAsNode; + documentAsNode[EXPECTED_RECORDS_KEY] = recordsAsNodes; + + RecordLoader loader; + loader.addTypeLoader("IntTestRecord", [](conduit::Node const &asNode) { + return std::make_unique>(asNode); + }); + + Document document {documentAsNode, loader}; + auto &records = document.getRecords(); + ASSERT_EQ(1u, records.size()); + auto testRecord = dynamic_cast const *>(records[0].get()); + ASSERT_NE(nullptr, testRecord); + ASSERT_EQ(123, testRecord->getValue()); +} + +TEST(Document, create_fromNode_withRelationships) +{ + conduit::Node relationshipAsNode; + relationshipAsNode["subject"] = "the subject"; + relationshipAsNode["object"] = "the object"; + relationshipAsNode["predicate"] = "is related to"; + + conduit::Node relationshipsAsNodes; + relationshipsAsNodes.append().set(relationshipAsNode); + + conduit::Node documentAsNode; + documentAsNode[EXPECTED_RELATIONSHIPS_KEY] = relationshipsAsNodes; + + Document document {documentAsNode, RecordLoader {}}; + auto &relationships = document.getRelationships(); + ASSERT_EQ(1u, relationships.size()); + EXPECT_EQ("the subject", relationships[0].getSubject().getId()); + EXPECT_EQ(IDType::Global, relationships[0].getSubject().getType()); + EXPECT_EQ("the object", relationships[0].getObject().getId()); + EXPECT_EQ(IDType::Global, relationships[0].getObject().getType()); + 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 = + "{\"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) +{ + 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 &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 + // relationships as empty arrays. + Document const document; + conduit::Node asNode = document.toNode(); + EXPECT_TRUE(asNode[EXPECTED_RECORDS_KEY].dtype().is_list()); + EXPECT_EQ(0, asNode[EXPECTED_RECORDS_KEY].number_of_children()); + EXPECT_TRUE(asNode[EXPECTED_RELATIONSHIPS_KEY].dtype().is_list()); + EXPECT_EQ(0, asNode[EXPECTED_RELATIONSHIPS_KEY].number_of_children()); +} + +TEST(Document, toNode_records) +{ + Document document; + std::string expectedIds[] = {"id 1", "id 2", "id 3"}; + std::string expectedValues[] = {"value 1", "value 2", "value 3"}; + + auto numRecords = sizeof(expectedIds) / sizeof(expectedIds[0]); + for(std::size_t i = 0; i < numRecords; ++i) + { + document.add(std::make_unique>(expectedIds[i], + TEST_RECORD_TYPE, + expectedValues[i])); + } + + auto asNode = document.toNode(); + + auto record_nodes = asNode[EXPECTED_RECORDS_KEY]; + ASSERT_EQ(numRecords, record_nodes.number_of_children()); + for(auto i = 0; i < record_nodes.number_of_children(); ++i) + { + auto &actualNode = record_nodes[i]; + EXPECT_EQ(expectedIds[i], actualNode["id"].as_string()); + EXPECT_EQ(TEST_RECORD_TYPE, actualNode["type"].as_string()); + EXPECT_EQ(expectedValues[i], actualNode[TEST_RECORD_VALUE_KEY].as_string()); + } +} + +TEST(Document, toNode_relationships) +{ + Document document; + std::string expectedSubjects[] = {"subject 1", "subject 2"}; + std::string expectedObjects[] = {"object 1", "object 2"}; + std::string expectedPredicates[] = {"predicate 1", "predicate 2"}; + + auto numRecords = sizeof(expectedSubjects) / sizeof(expectedSubjects[0]); + for(unsigned long i = 0; i < numRecords; ++i) + { + document.add(Relationship { + ID {expectedSubjects[i], IDType::Global}, + expectedPredicates[i], + ID {expectedObjects[i], IDType::Global}, + }); + } + + auto asNode = document.toNode(); + + auto relationship_nodes = asNode[EXPECTED_RELATIONSHIPS_KEY]; + ASSERT_EQ(numRecords, relationship_nodes.number_of_children()); + for(auto i = 0; i < relationship_nodes.number_of_children(); ++i) + { + auto &actualRelationship = relationship_nodes[i]; + EXPECT_EQ(expectedSubjects[i], actualRelationship["subject"].as_string()); + EXPECT_EQ(expectedObjects[i], actualRelationship["object"].as_string()); + EXPECT_EQ(expectedPredicates[i], actualRelationship["predicate"].as_string()); + } +} + +/** + * Instances of this class acquire a temporary file name when created + * and delete the file when destructed. + * + * NOTE: This class uses unsafe methods and should only be used for testing + * purposes. DO NOT move it to the main code!!!! + */ +class NamedTempFile +{ +public: + NamedTempFile(); + + // As a resource-holding class, we don't want this to be copyable + // (or movable since there is no reason to return it from a function) + NamedTempFile(NamedTempFile const &) = delete; + + NamedTempFile(NamedTempFile &&) = delete; + + NamedTempFile &operator=(NamedTempFile const &) = delete; + + NamedTempFile &operator=(NamedTempFile &&) = delete; + + ~NamedTempFile(); + + std::string const &getName() const { return fileName; } + +private: + std::string fileName; +}; + +NamedTempFile::NamedTempFile() +{ + std::vector tmpFileName; + tmpFileName.resize(L_tmpnam); + // tmpnam is not the best way to do this, but it is standard and this is + // only a test. + if(!std::tmpnam(tmpFileName.data())) + { + throw std::ios::failure {"Could not get temporary file"}; + } + fileName = tmpFileName.data(); +} + +NamedTempFile::~NamedTempFile() { std::remove(fileName.data()); } + +TEST(Document, saveDocument) +{ + 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()); + + conduit::Node readContents; + { + std::ifstream fin {tmpFile.getName()}; + std::stringstream f_buf; + f_buf << fin.rdbuf(); + readContents.parse(f_buf.str(), "json"); + } + + 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; + auto originalRecord = std::make_unique("the ID", "my type", 123); + Document originalDocument; + originalDocument.add(std::move(originalRecord)); + + NamedTempFile file; + { + std::ofstream fout {file.getName()}; + fout << originalDocument.toNode().to_json(); + } + + RecordLoader loader; + loader.addTypeLoader("my type", [](conduit::Node const &asNode) { + return std::make_unique( + getRequiredString("id", asNode, "Test type"), + getRequiredString("type", asNode, "Test type"), + static_cast( + getRequiredField(TEST_RECORD_VALUE_KEY, asNode, "Test type").as_int64())); + }); + Document loadedDocument = loadDocument(file.getName(), loader); + ASSERT_EQ(1u, loadedDocument.getRecords().size()); + auto loadedRecord = + dynamic_cast(loadedDocument.getRecords()[0].get()); + ASSERT_NE(nullptr, loadedRecord); + EXPECT_EQ(123, loadedRecord->getValue()); +} + +TEST(Document, load_defaultRecordLoaders) +{ + auto originalRun = + std::make_unique(ID {"the ID", IDType::Global}, + "the app", + "1.2.3", + "jdoe"); + Document originalDocument; + originalDocument.add(std::move(originalRun)); + + NamedTempFile file; + { + std::ofstream fout {file.getName()}; + fout << originalDocument.toNode().to_json(); + } + + Document loadedDocument = loadDocument(file.getName()); + ASSERT_EQ(1u, loadedDocument.getRecords().size()); + auto loadedRun = + dynamic_cast(loadedDocument.getRecords()[0].get()); + EXPECT_NE(nullptr, loadedRun); +} + +} // namespace +} // namespace testing +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/tests/sina_File.cpp b/src/axom/sina/tests/sina_File.cpp new file mode 100644 index 0000000000..5481ea067b --- /dev/null +++ b/src/axom/sina/tests/sina_File.cpp @@ -0,0 +1,93 @@ +// 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) + +#include "gtest/gtest.h" + +#include "axom/sina/core/File.hpp" +#include "axom/sina/core/ConduitUtil.hpp" + +namespace axom +{ +namespace sina +{ +namespace testing +{ +namespace +{ + +char const EXPECTED_MIMETYPE_KEY[] = "mimetype"; +char const EXPECTED_TAGS_KEY[] = "tags"; + +TEST(File, construct_differentType) +{ + File f1 {"from literal"}; + File f2 {std::string {"from std::string"}}; + EXPECT_EQ("from literal", f1.getUri()); + EXPECT_EQ("from std::string", f2.getUri()); +} + +TEST(File, setMimeType) +{ + File file {"the URI"}; + file.setMimeType("mime"); + EXPECT_EQ("the URI", file.getUri()); + EXPECT_EQ("mime", file.getMimeType()); +} + +TEST(File, setTags) +{ + std::vector tags = {"these", "are", "tags"}; + File file {"the URI"}; + file.setTags(tags); + EXPECT_EQ("the URI", file.getUri()); + EXPECT_EQ(tags, file.getTags()); +} + +TEST(File, create_fromNode_basic) +{ + std::string uri = "the URI"; + conduit::Node basic_file(conduit::DataType::object()); + File file {uri, basic_file}; + EXPECT_EQ(uri, file.getUri()); + EXPECT_EQ("", file.getMimeType()); + EXPECT_EQ(0, file.getTags().size()); +} + +TEST(File, create_fromNode_complete) +{ + std::string uri = "another/uri.txt"; + std::vector tags = {"tags", "are", "fun"}; + conduit::Node full_file(conduit::DataType::object()); + full_file[EXPECTED_MIMETYPE_KEY] = "the mime type"; + addStringsToNode(full_file, EXPECTED_TAGS_KEY, tags); + File file {uri, full_file}; + EXPECT_EQ(uri, file.getUri()); + EXPECT_EQ("the mime type", file.getMimeType()); + EXPECT_EQ(tags, file.getTags()); +} + +TEST(File, toNode_basic) +{ + File file {"the URI"}; + auto asNode = file.toNode(); + EXPECT_FALSE(asNode.has_child(EXPECTED_MIMETYPE_KEY)); + EXPECT_FALSE(asNode.has_child(EXPECTED_TAGS_KEY)); +} + +TEST(File, toNode_complete) +{ + std::vector tags = {"these", "are", "tags"}; + File file {"the URI"}; + file.setMimeType("the mime type"); + file.setTags(tags); + auto asNode = file.toNode(); + EXPECT_EQ("the mime type", asNode[EXPECTED_MIMETYPE_KEY].as_string()); + EXPECT_EQ(tags, file.getTags()); +} + +} // namespace +} // namespace testing +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/tests/sina_ID.cpp b/src/axom/sina/tests/sina_ID.cpp new file mode 100644 index 0000000000..c4b10353bb --- /dev/null +++ b/src/axom/sina/tests/sina_ID.cpp @@ -0,0 +1,110 @@ +// 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) + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "conduit.hpp" + +#include "axom/sina/core/ID.hpp" + +namespace axom +{ +namespace sina +{ +namespace testing +{ +namespace +{ + +using ::testing::HasSubstr; + +TEST(ID, create) +{ + ID id1 {"the name", IDType::Local}; + ID id2 {"another name", IDType::Global}; + + EXPECT_EQ("the name", id1.getId()); + EXPECT_EQ("another name", id2.getId()); + + EXPECT_EQ(IDType::Local, id1.getType()); + EXPECT_EQ(IDType::Global, id2.getType()); +} + +TEST(IDField, create) +{ + ID id {"the id", IDType::Global}; + internal::IDField field {id, "local name", "global name"}; + EXPECT_EQ("the id", field.getID().getId()); + EXPECT_EQ(IDType::Global, field.getID().getType()); + EXPECT_EQ("local name", field.getLocalName()); + EXPECT_EQ("global name", field.getGlobalName()); +} + +TEST(IDField, createFromNode_local) +{ + conduit::Node object; + object["local id key"] = "the id"; + internal::IDField field {object, "local id key", "global id key"}; + EXPECT_EQ("the id", field.getID().getId()); + EXPECT_EQ(IDType::Local, field.getID().getType()); + EXPECT_EQ("local id key", field.getLocalName()); + EXPECT_EQ("global id key", field.getGlobalName()); +} + +TEST(IDField, createFromNode_global) +{ + conduit::Node object; + object["local id key"] = "local id"; + object["global id key"] = "global id"; + + internal::IDField field {object, "local id key", "global id key"}; + EXPECT_EQ("global id", field.getID().getId()); + EXPECT_EQ(IDType::Global, field.getID().getType()); + EXPECT_EQ("local id key", field.getLocalName()); + EXPECT_EQ("global id key", field.getGlobalName()); +} + +TEST(IDField, createFromNode_missingKeys) +{ + conduit::Node object(conduit::DataType::object()); + try + { + internal::IDField field {object, "local id key", "global id key"}; + FAIL() << "Should have gotten a value error"; + } + catch(std::invalid_argument const &expected) + { + EXPECT_THAT(expected.what(), HasSubstr("local id key")); + EXPECT_THAT(expected.what(), HasSubstr("global id key")); + } +} + +TEST(IDField, toNode_local) +{ + ID id {"the id", IDType::Local}; + internal::IDField field {id, "local name", "global name"}; + conduit::Node value; + field.addTo(value); + EXPECT_EQ("the id", value["local name"].as_string()); + EXPECT_FALSE(value.has_child("global name")); +} + +TEST(IDField, toNode_global) +{ + ID id {"the id", IDType::Global}; + internal::IDField field {id, "local name", "global name"}; + conduit::Node value; + field.addTo(value); + EXPECT_EQ("the id", value["global name"].as_string()); + EXPECT_FALSE(value.has_child("local name")); +} + +} // namespace +} // namespace testing +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/tests/sina_Record.cpp b/src/axom/sina/tests/sina_Record.cpp new file mode 100644 index 0000000000..0bdb6746d0 --- /dev/null +++ b/src/axom/sina/tests/sina_Record.cpp @@ -0,0 +1,497 @@ +// 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) + +#include +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "axom/sina/core/Record.hpp" + +#include "axom/sina/tests/SinaMatchers.hpp" +#include "axom/sina/tests/TestRecord.hpp" + +namespace axom +{ +namespace sina +{ +namespace testing +{ +namespace +{ + +using ::testing::Contains; +using ::testing::DoubleEq; +using ::testing::ElementsAre; +using ::testing::HasSubstr; +using ::testing::Key; +using ::testing::Not; + +char const EXPECTED_TYPE_KEY[] = "type"; +char const EXPECTED_LOCAL_ID_KEY[] = "local_id"; +char const EXPECTED_GLOBAL_ID_KEY[] = "id"; +char const EXPECTED_DATA_KEY[] = "data"; +char const EXPECTED_LIBRARY_DATA_KEY[] = "library_data"; +char const EXPECTED_FILES_KEY[] = "files"; +char const EXPECTED_USER_DEFINED_KEY[] = "user_defined"; +char const LIBRARY_DATA_ID_DATUM[] = "SINA_librarydata_id"; +char const LIBRARY_DATA_TYPE_DATUM[] = "SINA_librarydata_type"; + +TEST(Record, create_typeMissing) +{ + conduit::Node originalNode; + originalNode[EXPECTED_LOCAL_ID_KEY] = "the ID"; + try + { + Record record {originalNode}; + FAIL() << "Should have failed due to missing type"; + } + catch(std::invalid_argument const &expected) + { + EXPECT_THAT(expected.what(), HasSubstr(EXPECTED_TYPE_KEY)); + } +} + +TEST(Record, add_data_existing_key) +{ + Record record {ID {"the id", IDType::Local}, "test_record"}; + record.add("key1", Datum {"val1"}); + EXPECT_EQ("val1", record.getData().at("key1").getValue()); + record.add("key1", Datum {"val2"}); + EXPECT_EQ("val2", record.getData().at("key1").getValue()); +} + +TEST(Record, add_curve_set_existing_key) +{ + Record record {ID {"the id", IDType::Local}, "test_record"}; + + CurveSet cs1 {"cs1"}; + cs1.addDependentCurve(Curve {"original", {1, 2, 3}}); + record.add(cs1); + + auto &csAfterFirstInsert = record.getCurveSets(); + ASSERT_THAT(csAfterFirstInsert, Contains(Key("cs1"))); + EXPECT_THAT(csAfterFirstInsert.at("cs1").getDependentCurves(), + Contains(Key("original"))); + + CurveSet cs2 {"cs1"}; + cs2.addDependentCurve(Curve {"new", {1, 2, 3}}); + record.add(cs2); + + auto &csAfterSecondInsert = record.getCurveSets(); + ASSERT_THAT(csAfterSecondInsert, Contains(Key("cs1"))); + EXPECT_THAT(csAfterSecondInsert.at("cs1").getDependentCurves(), + Not(Contains(Key("original")))); + EXPECT_THAT(csAfterSecondInsert.at("cs1").getDependentCurves(), + Contains(Key("new"))); +} + +TEST(Record, remove_file) +{ + Record record {ID {"the id", IDType::Local}, "test_record"}; + std::string path = "the/path.txt"; + + File original {path}; + original.setMimeType("txt"); + record.add(original); + EXPECT_EQ(1u, record.getFiles().size()); + EXPECT_EQ("txt", record.getFiles().find(File {path})->getMimeType()); + + record.remove(original); + EXPECT_EQ(0u, record.getFiles().size()); +} + +TEST(Record, add_file_existing_key) +{ + Record record {ID {"the id", IDType::Local}, "test_record"}; + std::string path = "the/path.txt"; + + File original {path}; + original.setMimeType("txt"); + record.add(original); + EXPECT_EQ(1u, record.getFiles().size()); + EXPECT_EQ("txt", record.getFiles().find(File {path})->getMimeType()); + + File replacement {path}; + replacement.setMimeType("image"); + record.add(replacement); + EXPECT_EQ(1u, record.getFiles().size()); + EXPECT_EQ("image", record.getFiles().find(File {path})->getMimeType()); +} + +TEST(Record, add_child_record_as_library_data) +{ + Record parentRecord {ID {"parent id", IDType::Local}, "test_record_parent"}; + Record childRecord {ID {"child id", IDType::Local}, "test_record_child"}; + parentRecord.addRecordAsLibraryData(childRecord, "child"); + auto &parentLibData = parentRecord.getLibraryData(); + ASSERT_THAT(parentLibData, Contains(Key("child"))); + auto &childLibContents = parentLibData.at("child")->getData(); + ASSERT_THAT(childLibContents, Contains(Key(LIBRARY_DATA_ID_DATUM))); + EXPECT_EQ("child id", childLibContents.at(LIBRARY_DATA_ID_DATUM).getValue()); + ASSERT_THAT(childLibContents, Contains(Key(LIBRARY_DATA_TYPE_DATUM))); + EXPECT_EQ("test_record_child", + childLibContents.at(LIBRARY_DATA_TYPE_DATUM).getValue()); +} + +TEST(Record, add_child_record_as_library_data_with_data) +{ + Record parentRecord {ID {"parent id", IDType::Local}, "test_record_parent"}; + Record childRecord {ID {"child id", IDType::Local}, "test_record_child"}; + childRecord.add("key1", Datum {"val1"}); + parentRecord.addRecordAsLibraryData(childRecord, "child"); + auto &childLibContents = parentRecord.getLibraryData().at("child")->getData(); + ASSERT_THAT(childLibContents, Contains(Key("key1"))); + EXPECT_EQ("val1", childLibContents.at("key1").getValue()); +} + +TEST(Record, add_child_record_as_library_data_with_files) +{ + Record parentRecord {ID {"parent id", IDType::Local}, "test_record_parent"}; + Record childRecord {ID {"child id", IDType::Local}, "test_record_child"}; + std::string path = "the/path.txt"; + File childFile {path}; + childFile.setMimeType("txt"); + childRecord.add(childFile); + parentRecord.addRecordAsLibraryData(childRecord, "child"); + EXPECT_EQ(1u, parentRecord.getFiles().size()); + EXPECT_EQ("txt", parentRecord.getFiles().find(File {path})->getMimeType()); +} + +TEST(Record, create_localId_fromNode) +{ + conduit::Node originalNode; + originalNode[EXPECTED_LOCAL_ID_KEY] = "the ID"; + originalNode[EXPECTED_TYPE_KEY] = "my type"; + Record record {originalNode}; + EXPECT_EQ("my type", record.getType()); + EXPECT_EQ("the ID", record.getId().getId()); + EXPECT_EQ(IDType::Local, record.getId().getType()); +} + +TEST(Record, create_globalId_fromNode) +{ + conduit::Node originalNode; + originalNode[EXPECTED_GLOBAL_ID_KEY] = "the ID"; + originalNode[EXPECTED_TYPE_KEY] = "my type"; + Record record {originalNode}; + EXPECT_EQ("my type", record.getType()); + EXPECT_EQ("the ID", record.getId().getId()); + EXPECT_EQ(IDType::Global, record.getId().getType()); +} + +TEST(Record, create_globalId_withContent) +{ + conduit::Node originalNode; + originalNode[EXPECTED_GLOBAL_ID_KEY] = "the ID"; + originalNode[EXPECTED_TYPE_KEY] = "my type"; + originalNode[EXPECTED_DATA_KEY]; + originalNode[EXPECTED_LIBRARY_DATA_KEY]; + + std::string name1 = "datum name 1"; + std::string name2 = "datum name 2/with/slash"; + + conduit::Node name1_node; + name1_node["value"] = "value 1"; + originalNode[EXPECTED_DATA_KEY][name1] = name1_node; + + conduit::Node name2_node; + name2_node["value"] = 2.22; + name2_node["units"] = "g/L"; + addStringsToNode(name2_node, "tags", {"tag1", "tag2"}); + name2_node["value"] = 2.22; + originalNode[EXPECTED_DATA_KEY].add_child(name2) = name2_node; + + std::string libName = "my_lib"; + conduit::Node libNode; + std::string name3 = "datum name 3"; + conduit::Node name3_node; + name3_node["value"] = "value 3"; + libNode[EXPECTED_DATA_KEY]; + libNode[EXPECTED_DATA_KEY][name3] = name3_node; + originalNode[EXPECTED_LIBRARY_DATA_KEY][libName] = libNode; + + Record record {originalNode}; + auto &data = record.getData(); + ASSERT_EQ(2u, data.size()); + EXPECT_EQ("value 1", data.at(name1).getValue()); + EXPECT_THAT(2.22, DoubleEq(data.at(name2).getScalar())); + EXPECT_EQ("g/L", data.at(name2).getUnits()); + EXPECT_EQ("tag1", data.at(name2).getTags()[0]); + EXPECT_EQ("tag2", data.at(name2).getTags()[1]); + + auto &libdata = record.getLibraryData(); + EXPECT_THAT(libdata, Contains(Key(libName))); + EXPECT_EQ("value 3", libdata.at(libName)->getData().at(name3).getValue()); +} + +TEST(Record, create_globalId_files) +{ + conduit::Node originalNode; + originalNode[EXPECTED_GLOBAL_ID_KEY] = "the ID"; + originalNode[EXPECTED_TYPE_KEY] = "my type"; + originalNode[EXPECTED_FILES_KEY]; + + std::string uri1 = "/some/uri.txt"; + std::string uri2 = "www.anotheruri.com"; + std::string uri3 = "yet another uri"; + originalNode[EXPECTED_FILES_KEY].add_child(uri1); + originalNode[EXPECTED_FILES_KEY].add_child(uri2); + originalNode[EXPECTED_FILES_KEY].add_child(uri3); + Record record {originalNode}; + auto &files = record.getFiles(); + ASSERT_EQ(3u, files.size()); + EXPECT_EQ(1, files.count(File {uri1})); + EXPECT_EQ(1, files.count(File {uri2})); + EXPECT_EQ(1, files.count(File {uri3})); +} + +TEST(Record, create_fromNode_curveSets) +{ + conduit::Node recordAsNode = parseJsonValue(R"({ + "id": "myId", + "type": "myType", + "curve_sets": { + "cs1": { + "independent": { + "i1": { "value": [1, 2, 3]} + }, + "dependent": { + "d1": { "value": [4, 5, 6]} + } + } + } + })"); + Record record {recordAsNode}; + auto &curveSets = record.getCurveSets(); + ASSERT_THAT(curveSets, Contains(Key("cs1"))); +} + +TEST(Record, create_fromNode_userDefined) +{ + conduit::Node originalNode; + originalNode[EXPECTED_GLOBAL_ID_KEY] = "the ID"; + originalNode[EXPECTED_TYPE_KEY] = "my type"; + originalNode[EXPECTED_USER_DEFINED_KEY]["k1"] = "v1"; + originalNode[EXPECTED_USER_DEFINED_KEY]["k2"] = 123; + std::vector k3_vals {1, 2, 3}; + originalNode[EXPECTED_USER_DEFINED_KEY]["k3"] = k3_vals; + + Record record {originalNode}; + auto const &userDefined = record.getUserDefinedContent(); + EXPECT_EQ("v1", userDefined["k1"].as_string()); + EXPECT_EQ(123, userDefined["k2"].as_int()); + auto int_array = userDefined["k3"].as_int_ptr(); + std::vector udef_ints( + int_array, + int_array + userDefined["k3"].dtype().number_of_elements()); + EXPECT_THAT(udef_ints, ElementsAre(1, 2, 3)); +} + +TEST(Record, getUserDefined_initialConst) +{ + ID id {"the id", IDType::Local}; + Record const record {id, "my type"}; + conduit::Node const &userDefined = record.getUserDefinedContent(); + EXPECT_TRUE(userDefined.dtype().is_empty()); +} + +TEST(Record, getUserDefined_initialNonConst) +{ + ID id {"the id", IDType::Local}; + Record record {id, "my type"}; + conduit::Node &initialUserDefined = record.getUserDefinedContent(); + EXPECT_TRUE(initialUserDefined.dtype().is_empty()); + initialUserDefined["foo"] = 123; + EXPECT_EQ(123, record.getUserDefinedContent()["foo"].as_int()); +} + +TEST(Record, toNode_localId) +{ + ID id {"the id", IDType::Global}; + Record record {id, "my type"}; + auto asNode = record.toNode(); + EXPECT_TRUE(asNode.dtype().is_object()); + EXPECT_EQ("my type", asNode[EXPECTED_TYPE_KEY].as_string()); + EXPECT_EQ("the id", asNode[EXPECTED_GLOBAL_ID_KEY].as_string()); + EXPECT_TRUE(asNode[EXPECTED_LOCAL_ID_KEY].dtype().is_empty()); +} + +TEST(Record, toNode_globalId) +{ + ID id {"the id", IDType::Local}; + Record record {id, "my type"}; + auto asNode = record.toNode(); + EXPECT_TRUE(asNode.dtype().is_object()); + EXPECT_EQ("my type", asNode[EXPECTED_TYPE_KEY].as_string()); + EXPECT_EQ("the id", asNode[EXPECTED_LOCAL_ID_KEY].as_string()); + EXPECT_TRUE(asNode[EXPECTED_GLOBAL_ID_KEY].dtype().is_empty()); +} + +TEST(Record, toNode_default_values) +{ + ID id {"the id", IDType::Global}; + Record record {id, "my type"}; + auto asNode = record.toNode(); + EXPECT_TRUE(asNode.dtype().is_object()); + // We want to be sure that unset optional fields aren't present + EXPECT_FALSE(asNode.has_child(EXPECTED_DATA_KEY)); + EXPECT_FALSE(asNode.has_child(EXPECTED_FILES_KEY)); + EXPECT_FALSE(asNode.has_child(EXPECTED_USER_DEFINED_KEY)); +} + +TEST(Record, toNode_userDefined) +{ + ID id {"the id", IDType::Local}; + Record record {id, "my type"}; + conduit::Node userDef; + userDef["k1"] = "v1"; + userDef["k2"] = 123; + std::vector int_vals {1, 2, 3}; + userDef["k3"] = int_vals; + record.setUserDefinedContent(userDef); + + auto asNode = record.toNode(); + + auto userDefined = asNode[EXPECTED_USER_DEFINED_KEY]; + EXPECT_EQ("v1", userDefined["k1"].as_string()); + EXPECT_EQ(123, userDefined["k2"].as_int()); + auto int_array = userDefined["k3"].as_int_ptr(); + std::vector udef_ints( + int_array, + int_array + userDefined["k3"].dtype().number_of_elements()); + EXPECT_THAT(udef_ints, ElementsAre(1, 2, 3)); +} + +TEST(Record, toNode_data) +{ + ID id {"the id", IDType::Local}; + Record record {id, "my type"}; + std::string name1 = "name1"; + std::string value1 = "value1"; + Datum datum1 = Datum {value1}; + datum1.setUnits("some units"); + datum1.setTags({"tag1"}); + record.add(name1, datum1); + std::string name2 = "name2"; + record.add(name2, Datum {2.}); + auto asNode = record.toNode(); + ASSERT_EQ(2u, asNode[EXPECTED_DATA_KEY].number_of_children()); + EXPECT_EQ("value1", asNode[EXPECTED_DATA_KEY][name1]["value"].as_string()); + EXPECT_EQ("some units", asNode[EXPECTED_DATA_KEY][name1]["units"].as_string()); + EXPECT_EQ("tag1", asNode[EXPECTED_DATA_KEY][name1]["tags"][0].as_string()); + + EXPECT_THAT(asNode[EXPECTED_DATA_KEY][name2]["value"].as_double(), + DoubleEq(2.)); + EXPECT_TRUE(asNode[EXPECTED_DATA_KEY][name2]["units"].dtype().is_empty()); + EXPECT_TRUE(asNode[EXPECTED_DATA_KEY][name2]["tags"].dtype().is_empty()); +} + +TEST(Record, toNode_dataWithSlashes) +{ + ID id {"the id", IDType::Local}; + Record record {id, "my type"}; + std::string name = "name/with/slashes"; + std::string value = "the value"; + Datum datum = Datum {value}; + record.add(name, datum); + auto asNode = record.toNode(); + ASSERT_EQ(1u, asNode[EXPECTED_DATA_KEY].number_of_children()); + EXPECT_EQ("the value", + asNode[EXPECTED_DATA_KEY].child(name)["value"].as_string()); +} + +TEST(Record, toNode_files) +{ + ID id {"the id", IDType::Local}; + Record record {id, "my type"}; + std::string uri1 = "a/file/path/foo.png"; + std::string uri2 = "uri2"; + File file {uri1}; + file.setMimeType("mt1"); + record.add(file); + record.add(File {uri2}); + // Identical uris should overwrite + record.add(File {uri2}); + auto asNode = record.toNode(); + ASSERT_EQ(2u, asNode[EXPECTED_FILES_KEY].number_of_children()); + auto &child_with_slashes = asNode[EXPECTED_FILES_KEY].child(uri1); + EXPECT_EQ("mt1", child_with_slashes["mimetype"].as_string()); + EXPECT_TRUE(asNode[EXPECTED_FILES_KEY][uri2]["mimetype"].dtype().is_empty()); +} + +TEST(Record, toNode_curveSets) +{ + ID id {"the id", IDType::Local}; + Record record {id, "my type"}; + CurveSet cs {"myCurveSet/with/slash"}; + cs.addIndependentCurve(Curve {"myCurve", {1, 2, 3}}); + record.add(cs); + std::string expected = R"({ + "local_id": "the id", + "type": "my type", + "curve_sets": { + "myCurveSet/with/slash": { + "independent": { + "myCurve": { + "value": [1.0, 2.0, 3.0] + } + }, + "dependent": {} + } + } + })"; + EXPECT_THAT(record.toNode(), MatchesJsonMatcher(expected)); +} + +TEST(RecordLoader, load_missingLoader) +{ + RecordLoader loader; + conduit::Node asNode; + asNode[EXPECTED_GLOBAL_ID_KEY] = "the ID"; + asNode[EXPECTED_TYPE_KEY] = "unknownType"; + auto loaded = loader.load(asNode); + auto &actualType = typeid(*loaded); + EXPECT_EQ(typeid(Record), actualType) << "Type was " << actualType.name(); +} + +TEST(RecordLoader, load_loaderPresent) +{ + RecordLoader loader; + EXPECT_FALSE(loader.canLoad("TestInt")); + EXPECT_FALSE(loader.canLoad("TestString")); + + loader.addTypeLoader("TestInt", [](conduit::Node const &value) { + return std::make_unique>(value); + }); + EXPECT_TRUE(loader.canLoad("TestInt")); + + loader.addTypeLoader("TestString", [](conduit::Node const &value) { + return std::make_unique>(value); + }); + EXPECT_TRUE(loader.canLoad("TestString")); + + conduit::Node asNode; + asNode[EXPECTED_GLOBAL_ID_KEY] = "the ID"; + asNode[EXPECTED_TYPE_KEY] = "TestString"; + asNode[TEST_RECORD_VALUE_KEY] = "The value"; + auto loaded = loader.load(asNode); + auto testObjPointer = dynamic_cast *>(loaded.get()); + ASSERT_NE(nullptr, testObjPointer); + EXPECT_EQ("The value", testObjPointer->getValue()); + EXPECT_EQ("TestString", testObjPointer->getType()); +} + +TEST(RecordLoader, createRecordLoaderWithAllKnownTypes) +{ + RecordLoader loader = createRecordLoaderWithAllKnownTypes(); + EXPECT_TRUE(loader.canLoad("run")); +} + +} // namespace +} // namespace testing +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/tests/sina_Relationship.cpp b/src/axom/sina/tests/sina_Relationship.cpp new file mode 100644 index 0000000000..33109de8e2 --- /dev/null +++ b/src/axom/sina/tests/sina_Relationship.cpp @@ -0,0 +1,181 @@ +// 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) + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "axom/sina/core/Relationship.hpp" + +namespace axom +{ +namespace sina +{ +namespace testing +{ +namespace +{ + +char const EXPECTED_GLOBAL_OBJECT_ID_KEY[] = "object"; +char const EXPECTED_LOCAL_OBJECT_ID_KEY[] = "local_object"; +char const EXPECTED_GLOBAL_SUBJECT_ID_KEY[] = "subject"; +char const EXPECTED_LOCAL_SUBJECT_ID_KEY[] = "local_subject"; +char const EXPECTED_PREDICATE_KEY[] = "predicate"; + +using ::testing::HasSubstr; + +TEST(Relationship, create) +{ + std::string subjectID = "the subject"; + std::string objectID = "the object"; + std::string predicate = "is somehow related to"; + + Relationship relationship {ID {subjectID, IDType::Global}, + predicate, + ID {objectID, IDType::Local}}; + + EXPECT_EQ(subjectID, relationship.getSubject().getId()); + EXPECT_EQ(IDType::Global, relationship.getSubject().getType()); + EXPECT_EQ(objectID, relationship.getObject().getId()); + EXPECT_EQ(IDType::Local, relationship.getObject().getType()); + EXPECT_EQ(predicate, relationship.getPredicate()); +} + +TEST(Relationship, create_fromNode_validGlobalIDs) +{ + std::string subjectID = "the subject"; + std::string objectID = "the object"; + std::string predicate = "is somehow related to"; + + conduit::Node asNode; + asNode[EXPECTED_GLOBAL_SUBJECT_ID_KEY] = subjectID; + asNode[EXPECTED_GLOBAL_OBJECT_ID_KEY] = objectID; + asNode[EXPECTED_PREDICATE_KEY] = predicate; + + Relationship relationship {asNode}; + + EXPECT_EQ(subjectID, relationship.getSubject().getId()); + EXPECT_EQ(IDType::Global, relationship.getSubject().getType()); + EXPECT_EQ(objectID, relationship.getObject().getId()); + EXPECT_EQ(IDType::Global, relationship.getObject().getType()); + EXPECT_EQ(predicate, relationship.getPredicate()); +} + +TEST(Relationship, create_from_validLocalIDs) +{ + std::string subjectID = "the subject"; + std::string objectID = "the object"; + std::string predicate = "is somehow related to"; + + conduit::Node asNode; + asNode[EXPECTED_LOCAL_SUBJECT_ID_KEY] = subjectID; + asNode[EXPECTED_LOCAL_OBJECT_ID_KEY] = objectID; + asNode[EXPECTED_PREDICATE_KEY] = predicate; + + Relationship relationship {asNode}; + + EXPECT_EQ(subjectID, relationship.getSubject().getId()); + EXPECT_EQ(IDType::Local, relationship.getSubject().getType()); + EXPECT_EQ(objectID, relationship.getObject().getId()); + EXPECT_EQ(IDType::Local, relationship.getObject().getType()); + EXPECT_EQ(predicate, relationship.getPredicate()); +} + +TEST(Relationship, create_fromNode_missingSubect) +{ + conduit::Node asNode; + asNode[EXPECTED_LOCAL_OBJECT_ID_KEY] = "the object"; + asNode[EXPECTED_PREDICATE_KEY] = "some predicate"; + try + { + Relationship relationship {asNode}; + FAIL() << "Should have gotten an exception about a missing subject"; + } + catch(std::invalid_argument const &expected) + { + EXPECT_THAT(expected.what(), HasSubstr(EXPECTED_LOCAL_SUBJECT_ID_KEY)); + EXPECT_THAT(expected.what(), HasSubstr(EXPECTED_GLOBAL_SUBJECT_ID_KEY)); + } +} + +TEST(Relationship, create_fromNode_missingObject) +{ + conduit::Node asNode; + asNode[EXPECTED_LOCAL_SUBJECT_ID_KEY] = "the subject"; + asNode[EXPECTED_PREDICATE_KEY] = "some predicate"; + + try + { + Relationship relationship {asNode}; + FAIL() << "Should have gotten an exception about a missing object"; + } + catch(std::invalid_argument const &expected) + { + EXPECT_THAT(expected.what(), HasSubstr(EXPECTED_LOCAL_OBJECT_ID_KEY)); + EXPECT_THAT(expected.what(), HasSubstr(EXPECTED_GLOBAL_OBJECT_ID_KEY)); + } +} + +TEST(Relationship, create_fromNode_missingPredicate) +{ + conduit::Node asNode; + asNode[EXPECTED_LOCAL_SUBJECT_ID_KEY] = "the subject"; + asNode[EXPECTED_LOCAL_OBJECT_ID_KEY] = "the object"; + + try + { + Relationship relationship {asNode}; + FAIL() << "Should have gotten an exception about a missing predicate"; + } + catch(std::invalid_argument const &expected) + { + EXPECT_THAT(expected.what(), HasSubstr(EXPECTED_PREDICATE_KEY)); + EXPECT_THAT(expected.what(), HasSubstr("Relationship")); + } +} + +TEST(Relationship, toNode_localIds) +{ + std::string subjectID = "the subject"; + std::string objectID = "the object"; + std::string predicate = "is somehow related to"; + + Relationship relationship {ID {subjectID, IDType::Local}, + predicate, + ID {objectID, IDType::Local}}; + + conduit::Node asNode = relationship.toNode(); + + EXPECT_EQ(subjectID, asNode[EXPECTED_LOCAL_SUBJECT_ID_KEY].as_string()); + EXPECT_EQ(objectID, asNode[EXPECTED_LOCAL_OBJECT_ID_KEY].as_string()); + EXPECT_EQ(predicate, asNode[EXPECTED_PREDICATE_KEY].as_string()); + EXPECT_FALSE(asNode.has_child(EXPECTED_GLOBAL_SUBJECT_ID_KEY)); + EXPECT_FALSE(asNode.has_child(EXPECTED_GLOBAL_OBJECT_ID_KEY)); +} + +TEST(Relationship, toNode_globalIds) +{ + std::string subjectID = "the subject"; + std::string objectID = "the object"; + std::string predicate = "is somehow related to"; + + Relationship relationship {ID {subjectID, IDType::Global}, + predicate, + ID {objectID, IDType::Global}}; + + conduit::Node asNode = relationship.toNode(); + + EXPECT_EQ(subjectID, asNode[EXPECTED_GLOBAL_SUBJECT_ID_KEY].as_string()); + EXPECT_EQ(objectID, asNode[EXPECTED_GLOBAL_OBJECT_ID_KEY].as_string()); + EXPECT_EQ(predicate, asNode[EXPECTED_PREDICATE_KEY].as_string()); + EXPECT_FALSE(asNode.has_child(EXPECTED_LOCAL_SUBJECT_ID_KEY)); + EXPECT_FALSE(asNode.has_child(EXPECTED_LOCAL_OBJECT_ID_KEY)); +} + +} // namespace +} // namespace testing +} // namespace sina +} // namespace axom \ No newline at end of file diff --git a/src/axom/sina/tests/sina_Run.cpp b/src/axom/sina/tests/sina_Run.cpp new file mode 100644 index 0000000000..f843bc02e5 --- /dev/null +++ b/src/axom/sina/tests/sina_Run.cpp @@ -0,0 +1,109 @@ +// 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) + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "axom/sina/core/Run.hpp" + +namespace axom +{ +namespace sina +{ +namespace testing +{ +namespace +{ + +using ::testing::HasSubstr; + +char const EXPECTED_TYPE_KEY[] = "type"; +char const EXPECTED_LOCAL_ID_KEY[] = "local_id"; +char const EXPECTED_GLOBAL_ID_KEY[] = "id"; +char const EXPECTED_APPLICATION_KEY[] = "application"; +char const EXPECTED_VERSION_KEY[] = "version"; +char const EXPECTED_USER_KEY[] = "user"; + +// Throughout, we have to use "axom::sina::Run" instead of just "Run" due to +// a conflict with the Run() function in gtest + +TEST(Run, create_fromnode_valid) +{ + conduit::Node originNode; + originNode[EXPECTED_TYPE_KEY] = "run"; + originNode[EXPECTED_GLOBAL_ID_KEY] = "the id"; + originNode[EXPECTED_APPLICATION_KEY] = "the app"; + originNode[EXPECTED_VERSION_KEY] = "1.2.3"; + originNode[EXPECTED_USER_KEY] = "jdoe"; + axom::sina::Run run {originNode}; + EXPECT_EQ("run", run.getType()); + EXPECT_EQ("the id", run.getId().getId()); + EXPECT_EQ(IDType::Global, run.getId().getType()); + EXPECT_EQ("the app", run.getApplication()); + EXPECT_EQ("1.2.3", run.getVersion()); + EXPECT_EQ("jdoe", run.getUser()); +} + +TEST(Run, create_fromNode_missingApplication) +{ + conduit::Node originNode; + originNode[EXPECTED_TYPE_KEY] = "run"; + originNode[EXPECTED_GLOBAL_ID_KEY] = "the id"; + originNode[EXPECTED_VERSION_KEY] = "1.2.3"; + originNode[EXPECTED_USER_KEY] = "jdoe"; + try + { + axom::sina::Run run {originNode}; + FAIL() << "Application should be missing, but is " << run.getApplication(); + } + catch(std::invalid_argument const &expected) + { + EXPECT_THAT(expected.what(), HasSubstr(EXPECTED_APPLICATION_KEY)); + } +} + +TEST(Run, toNode) +{ + ID id {"the id", IDType::Global}; + axom::sina::Run run {id, "the app", "1.2.3", "jdoe"}; + auto asNode = run.toNode(); + EXPECT_TRUE(asNode.dtype().is_object()); + EXPECT_EQ("run", asNode[EXPECTED_TYPE_KEY].as_string()); + EXPECT_EQ("the id", asNode[EXPECTED_GLOBAL_ID_KEY].as_string()); + EXPECT_TRUE(asNode[EXPECTED_LOCAL_ID_KEY].dtype().is_empty()); + EXPECT_EQ("the app", asNode[EXPECTED_APPLICATION_KEY].as_string()); + EXPECT_EQ("1.2.3", asNode[EXPECTED_VERSION_KEY].as_string()); + EXPECT_EQ("jdoe", asNode[EXPECTED_USER_KEY].as_string()); +} + +TEST(Run, addRunLoader) +{ + conduit::Node originNode; + originNode[EXPECTED_TYPE_KEY] = "run"; + originNode[EXPECTED_GLOBAL_ID_KEY] = "the id"; + originNode[EXPECTED_APPLICATION_KEY] = "the app"; + originNode[EXPECTED_VERSION_KEY] = "1.2.3"; + originNode[EXPECTED_USER_KEY] = "jdoe"; + + RecordLoader loader; + addRunLoader(loader); + + auto record = loader.load(originNode); + auto run = dynamic_cast(record.get()); + ASSERT_NE(nullptr, run); + EXPECT_EQ("run", run->getType()); + EXPECT_EQ("the id", run->getId().getId()); + EXPECT_EQ(IDType::Global, run->getId().getType()); + EXPECT_EQ("the app", run->getApplication()); + EXPECT_EQ("1.2.3", run->getVersion()); + EXPECT_EQ("jdoe", run->getUser()); +} + +} // namespace +} // namespace testing +} // namespace sina +} // namespace axom diff --git a/src/axom/sina/tests/test_fortran_integration.py b/src/axom/sina/tests/test_fortran_integration.py new file mode 100644 index 0000000000..525f92ef62 --- /dev/null +++ b/src/axom/sina/tests/test_fortran_integration.py @@ -0,0 +1,125 @@ +import argparse +import io +import json +import os +import subprocess +import unittest + +def parse_args(): + """Helper function to obtain the binary directory path of Axom from CLI""" + parser = argparse.ArgumentParser(description="Unit test arguments") + parser.add_argument("-bd", "--binary-dir", type=str, + help="Path to the binary directory for Axom") + # Add other arguments as needed + return parser.parse_args() + + +class TestFortranExampleIntegration(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 + 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}/../../../" + + 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 """ + subprocess.run([f"{self.binary_dir}/examples/sina_fortran_ex"]) + self.dump_file = "sina_dump.json" + + def tearDown(self): + """ Clean up output directory after each test. """ + os.remove(self.dump_file) + + def test_file_validity(self): + """ Make sure the files we"re importing follow the Sina schema. """ + try: + import jsonschema + schema_file = os.path.join(f"{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: + file_json = json.load(loaded_test) + jsonschema.validate(file_json, schema) + 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 """ + with open(self.dump_file, "r", encoding="utf-8") as loaded_test: + rec = json.load(loaded_test) + + record = rec["records"][0] + + # Test the metadata in the record + self.assertEqual("my_rec_id", record["id"]) + self.assertEqual("my_type", record["type"]) + + # Test the files + 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"]) + self.assertEqual(10, record["data"]["int"]["value"]) + self.assertEqual(0, record["data"]["logical"]["value"]) + self.assertEqual(1000000000.0, record["data"]["long"]["value"]) + self.assertEqual(1.23456704616547, record["data"]["real"]["value"]) + self.assertEqual(0.810000002384186, record["data"]["double"]["value"]) + + # Test the unsigned variants + self.assertEqual("A", record["data"]["u_char"]["value"]) + self.assertEqual("kg", record["data"]["u_char"]["units"]) + self.assertEqual(10, record["data"]["u_int"]["value"]) + self.assertEqual("kg", record["data"]["u_int"]["units"]) + self.assertEqual(1.0, record["data"]["u_logical"]["value"]) + self.assertEqual("kg", record["data"]["u_logical"]["units"]) + self.assertEqual(1000000000.0, record["data"]["u_long"]["value"]) + self.assertEqual("kg", record["data"]["u_long"]["units"]) + self.assertEqual(1.23456704616547, record["data"]["u_real"]["value"]) + self.assertEqual("kg", record["data"]["u_real"]["units"]) + self.assertEqual(0.810000002384186, record["data"]["u_double"]["value"]) + self.assertEqual("kg", record["data"]["u_double"]["units"]) + self.assertEqual(0.810000002384186, record["data"]["u_double_w_tag"]["value"]) + self.assertEqual("kg", record["data"]["u_double_w_tag"]["units"]) + self.assertEqual(["new_fancy_tag"], record["data"]["u_double_w_tag"]["tags"]) + + # Test the curves + nums = 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] + curveset = "my_curveset" + for kind, loc in (("indep", "independent"), ("dep", "dependent")): + for val_type, target in (("real", real_arr), ("double", double_arr), ("int", int_arr), ("long", long_arr)): + name = "my_{}_curve_{}".format(kind, val_type) + self.assertEqual(target, record["curve_sets"][curveset][loc][name]["value"]) + double_2_name = "my_dep_curve_double_2" + self.assertEqual(double_arr, record["curve_sets"][curveset]["dependent"][double_2_name]["value"]) + + +if __name__ == "__main__": + # Doing the below instead of unittest.main() so that we can print to stdout + suite = unittest.TestLoader().loadTestsFromTestCase(TestFortranExampleIntegration) + runner = unittest.TextTestRunner(buffer=False) + runner.run(suite) \ No newline at end of file diff --git a/src/cmake/axom-config.cmake.in b/src/cmake/axom-config.cmake.in index c4bc87ba0b..2e79d5bafc 100644 --- a/src/cmake/axom-config.cmake.in +++ b/src/cmake/axom-config.cmake.in @@ -48,6 +48,7 @@ if(NOT AXOM_FOUND) set(AXOM_ENABLE_SLAM "@AXOM_ENABLE_SLAM@") set(AXOM_ENABLE_SLIC "@AXOM_ENABLE_SLIC@") set(AXOM_ENABLE_SPIN "@AXOM_ENABLE_SPIN@") + set(AXOM_ENABLE_SINA "@AXOM_ENABLE_SINA@") # Axom built-in TPLs set(AXOM_USE_CLI11 "@AXOM_USE_CLI11@") diff --git a/src/docs/dependencies.dot b/src/docs/dependencies.dot index 6ff79e1377..74eb633644 100644 --- a/src/docs/dependencies.dot +++ b/src/docs/dependencies.dot @@ -7,6 +7,7 @@ digraph dependencies { multimat -> {slic slam}; spin -> {slam primal}; sidre -> {slic core}; + sina -> {core}; slic -> core; slic -> lumberjack [style="dashed"]; lumberjack -> core; diff --git a/src/docs/doxygen/Doxyfile.in b/src/docs/doxygen/Doxyfile.in index 548d1bfad6..9d892011b6 100644 --- a/src/docs/doxygen/Doxyfile.in +++ b/src/docs/doxygen/Doxyfile.in @@ -797,6 +797,9 @@ INPUT = @PROJECT_SOURCE_DIR@/axom/doxygen_mainpage.md \ @PROJECT_SOURCE_DIR@/axom/sidre/doxygen_mainpage.md \ @PROJECT_SOURCE_DIR@/axom/sidre/core \ @PROJECT_SOURCE_DIR@/axom/sidre/spio \ + @PROJECT_SOURCE_DIR@/axom/sina \ + @PROJECT_SOURCE_DIR@/axom/sina/doxygen_mainpage.md \ + @PROJECT_SOURCE_DIR@/axom/sina/core \ @PROJECT_SOURCE_DIR@/axom/slam \ @PROJECT_SOURCE_DIR@/axom/slam/doxygen_mainpage.md \ @PROJECT_SOURCE_DIR@/axom/slam/policies \ diff --git a/src/index.rst b/src/index.rst index 091ee817bd..fdfffd8421 100644 --- a/src/index.rst +++ b/src/index.rst @@ -55,6 +55,7 @@ are identified. * Primal: Computational geometry primitives * Quest: Querying on surface tool * Sidre: Simulation data repository + * Sina: Write data in a common file format * Slam: Set-theoretic lightweight API for meshes * Slic: Simple Logging Interface Code * Spin: Spatial index structures for managing and accelerating spatial searches @@ -98,6 +99,9 @@ User guides and source code documentation are always linked on this site. * - Sidre - :doc:`User Guide ` - `Source documentation `__ + * - Sina + - :doc:`User Guide ` + - `Source documentation `__ * - Slam - :doc:`User Guide ` - `Source documentation `__ @@ -206,6 +210,7 @@ LLNL-CODE-741217 Primal (Computational geometry primitives) Quest (Querying on surface tool) Sidre (Simulation data repository) + Sina (Write data in a common format) Slam (Set-theoretic lightweight API for meshes) Slic (Simple Logging Interface Code) Spin (Spatial indexes)