diff --git a/include/flamegpu/detail/Any.h b/include/flamegpu/detail/Any.h index 00587de53..7064c7225 100644 --- a/include/flamegpu/detail/Any.h +++ b/include/flamegpu/detail/Any.h @@ -26,7 +26,7 @@ struct Any { memcpy(ptr, _ptr, length); } template - explicit Any(const T other) + explicit Any(const T &other) : ptr(malloc(sizeof(T))) , length(sizeof(T)) , type(typeid(T)) @@ -87,7 +87,6 @@ struct Any { */ const unsigned int elements; }; - } // namespace detail } // namespace flamegpu diff --git a/include/flamegpu/flamegpu.h b/include/flamegpu/flamegpu.h index 9602ba322..35cd7abe3 100644 --- a/include/flamegpu/flamegpu.h +++ b/include/flamegpu/flamegpu.h @@ -47,6 +47,8 @@ #include "flamegpu/simulation/LogFrame.h" #include "flamegpu/util/cleanup.h" #include "flamegpu/io/Telemetry.h" +#include "flamegpu/io/JSONRunPlanWriter.h" +#include "flamegpu/io/JSONRunPlanReader.h" // This include has no impact if FLAMEGPU_VISUALISATION is not defined #include "flamegpu/visualiser/visualiser_api.h" diff --git a/include/flamegpu/io/JSONRunPlanReader.h b/include/flamegpu/io/JSONRunPlanReader.h new file mode 100644 index 000000000..efd6aa45f --- /dev/null +++ b/include/flamegpu/io/JSONRunPlanReader.h @@ -0,0 +1,27 @@ +#ifndef INCLUDE_FLAMEGPU_IO_JSONRUNPLANREADER_H_ +#define INCLUDE_FLAMEGPU_IO_JSONRUNPLANREADER_H_ + +#include + +#include "flamegpu/simulation/RunPlanVector.h" + +namespace flamegpu { +class ModelDescription; +namespace io { + +/** + * JSON format reader of RunPlanVector + */ +class JSONRunPlanReader { + public: + /** + * Loads and returns the specified JSON file if contains a RunPlanVector + * @param input_filepath Path on disk to read the file from + * @param model The model used to initialise the RunPlanVector + */ + static RunPlanVector load(const std::string &input_filepath, const ModelDescription &model); +}; +} // namespace io +} // namespace flamegpu + +#endif // INCLUDE_FLAMEGPU_IO_JSONRUNPLANREADER_H_ diff --git a/include/flamegpu/io/JSONRunPlanWriter.h b/include/flamegpu/io/JSONRunPlanWriter.h new file mode 100644 index 000000000..ce6a488f0 --- /dev/null +++ b/include/flamegpu/io/JSONRunPlanWriter.h @@ -0,0 +1,44 @@ +#ifndef INCLUDE_FLAMEGPU_IO_JSONRUNPLANWRITER_H_ +#define INCLUDE_FLAMEGPU_IO_JSONRUNPLANWRITER_H_ + +#include +#include + +#include "flamegpu/simulation/RunPlanVector.h" + +namespace flamegpu { +namespace io { +/** + * JSON format writer of RunPlanVector + */ +class JSONRunPlanWriter { + /** + * Utility method for writing out the outer structure of a RunPlanVector + * @param writer An initialised RapidJSON writer. + * @param rpv RunPlanVector to be written + * @tparam T Should be rapidjson::Writer or rapidjson::PrettyWriter (one does not inherit the other) + */ + template + static void writeCommon(std::unique_ptr &writer, const RunPlanVector &rpv); + /** + * Utility method for writing out a single RunPlan + * @param writer An initialised RapidJSON writer. + * @param rp RunPlan to be writer + * @tparam T Should be rapidjson::Writer or rapidjson::PrettyWriter (one does not inherit the other) + */ + template + static void writeRunPlan(std::unique_ptr &writer, const RunPlan &rp); + + public: + /** + * Exports the provided RunPlanVector in JSON format to the specified output_filepath + * @param rpv The RunPlanVector to be exported + * @param output_filepath Location on disk to export the file + * @param pretty Whether the exported JSON is "prettified" (true) or "minified" (false), defaults true. + */ + static void save(const RunPlanVector &rpv, const std::string &output_filepath, bool pretty = true); +}; +} // namespace io +} // namespace flamegpu + +#endif // INCLUDE_FLAMEGPU_IO_JSONRUNPLANWRITER_H_ diff --git a/include/flamegpu/io/JSONStateReader.h b/include/flamegpu/io/JSONStateReader.h index 7056ee5f6..c3aec6704 100644 --- a/include/flamegpu/io/JSONStateReader.h +++ b/include/flamegpu/io/JSONStateReader.h @@ -15,7 +15,7 @@ namespace io { class JSONStateReader : public StateReader { public: /** - * Loads the specified XML file to an internal data-structure + * Loads the specified JSON file to an internal data-structure * @param input_file Path to file to be read * @param model Model description to ensure file loaded is suitable * @param verbosity Verbosity level to use during load diff --git a/include/flamegpu/model/ModelDescription.h b/include/flamegpu/model/ModelDescription.h index e07d4429e..583d75976 100644 --- a/include/flamegpu/model/ModelDescription.h +++ b/include/flamegpu/model/ModelDescription.h @@ -11,7 +11,9 @@ #include "flamegpu/runtime/messaging/MessageBruteForce/MessageBruteForceHost.h" namespace flamegpu { - +namespace io { +class JSONRunPlanReader; +} class AgentDescription; class CAgentDescription; class CLayerDescription; @@ -39,6 +41,7 @@ class ModelDescription { friend class LoggingConfig; friend class XMLStateReader; friend class JSONStateReader; + friend class io::JSONRunPlanReader; public: /** * Constructor diff --git a/include/flamegpu/simulation/RunPlan.h b/include/flamegpu/simulation/RunPlan.h index 24ccd3da4..c87d7b397 100644 --- a/include/flamegpu/simulation/RunPlan.h +++ b/include/flamegpu/simulation/RunPlan.h @@ -24,6 +24,8 @@ class CUDASimulation; namespace io { class JSONLogger; class XMLLogger; +class JSONRunPlanWriter; +class JSONRunPlanReader_impl; } // namespace io /** @@ -35,6 +37,12 @@ class RunPlan { friend class CUDASimulation; friend class io::JSONLogger; friend class io::XMLLogger; + friend class io::JSONRunPlanWriter; + friend class io::JSONRunPlanReader_impl; + /** + * Internal constructor used during file-io + */ + explicit RunPlan(const std::shared_ptr &model); public: /** diff --git a/include/flamegpu/simulation/RunPlanVector.h b/include/flamegpu/simulation/RunPlanVector.h index 166e252c0..eaf25ac83 100644 --- a/include/flamegpu/simulation/RunPlanVector.h +++ b/include/flamegpu/simulation/RunPlanVector.h @@ -15,7 +15,10 @@ namespace flamegpu { - +namespace io { +class JSONRunPlanReader; +class JSONRunPlanReader_impl; +} class ModelDescription; class EnvironmentDescription; @@ -27,6 +30,12 @@ class RunPlanVector : private std::vector { friend class RunPlan; friend class detail::AbstractSimRunner; friend unsigned int CUDAEnsemble::simulate(const RunPlanVector& plans); + friend class io::JSONRunPlanReader; + friend class io::JSONRunPlanReader_impl; + /** + * Internal constructor used during file-io + */ + explicit RunPlanVector(const std::shared_ptr &model, unsigned int initial_length); public: /** diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ae23febef..de9b4e47d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -151,6 +151,8 @@ SET(SRC_INCLUDE ${FLAMEGPU_ROOT}/include/flamegpu/flamegpu.h ${FLAMEGPU_ROOT}/include/flamegpu/io/StateReader.h ${FLAMEGPU_ROOT}/include/flamegpu/io/StateWriter.h + ${FLAMEGPU_ROOT}/include/flamegpu/io/JSONRunPlanReader.h + ${FLAMEGPU_ROOT}/include/flamegpu/io/JSONRunPlanWriter.h ${FLAMEGPU_ROOT}/include/flamegpu/io/JSONStateReader.h ${FLAMEGPU_ROOT}/include/flamegpu/io/JSONStateWriter.h ${FLAMEGPU_ROOT}/include/flamegpu/io/XMLStateReader.h @@ -369,6 +371,8 @@ SET(SRC_FLAMEGPU ${FLAMEGPU_ROOT}/src/flamegpu/runtime/environment/HostEnvironment.cu ${FLAMEGPU_ROOT}/src/flamegpu/runtime/environment/HostEnvironmentDirectedGraph.cu ${FLAMEGPU_ROOT}/src/flamegpu/runtime/random/HostRandom.cu + ${FLAMEGPU_ROOT}/src/flamegpu/io/JSONRunPlanReader.cpp + ${FLAMEGPU_ROOT}/src/flamegpu/io/JSONRunPlanWriter.cpp ${FLAMEGPU_ROOT}/src/flamegpu/io/JSONStateReader.cu ${FLAMEGPU_ROOT}/src/flamegpu/io/JSONStateWriter.cu ${FLAMEGPU_ROOT}/src/flamegpu/io/StateReader.cu diff --git a/src/flamegpu/io/JSONRunPlanReader.cpp b/src/flamegpu/io/JSONRunPlanReader.cpp new file mode 100644 index 000000000..a93917b01 --- /dev/null +++ b/src/flamegpu/io/JSONRunPlanReader.cpp @@ -0,0 +1,221 @@ +#include "flamegpu/io/JSONRunPlanReader.h" + +#include +#include +#include + +#include +#include + +#include "flamegpu/model/ModelDescription.h" + +namespace flamegpu { +namespace io { +class JSONRunPlanReader_impl : public rapidjson::BaseReaderHandler, JSONRunPlanReader_impl> { + enum Mode { Root, PlanVector, Plan, Properties, PropertyArray, Nop }; + std::stack mode; + std::string lastKey; + /** + * Tracks current position reading environment property arrays + */ + unsigned int current_array_index; + std::string filename; + RunPlanVector &rpv; + + public: + JSONRunPlanReader_impl(const std::string& _filename, RunPlanVector& _rpv) + : current_array_index(0) + , filename(_filename) + , rpv(_rpv) { } + template + bool processValue(const T val) { + Mode isArray = Nop; + if (mode.top() == PropertyArray) { + isArray = mode.top(); + mode.pop(); + } + if (mode.top() == Properties) { + const auto it = rpv.environment->find(lastKey); + if (it == rpv.environment->end()) { + THROW exception::RapidJSONError("Input file contains unrecognised environment property '%s'," + "in JSONRunPlanReader::load()\n", lastKey.c_str()); + } + if (current_array_index >= it->second.data.elements) { + THROW exception::RapidJSONError("Input file contains environment property '%s' with %u elements expected %u," + "in JSONRunPlanReader::load()\n", lastKey.c_str(), current_array_index, it->second.data.elements); + } + // Retrieve the linked any and replace the value + auto &rp = rpv.back(); + const std::type_index val_type = it->second.data.type; + if (it->second.data.elements == 1) { + // Properties don't exist by default, so must be created + if (val_type == std::type_index(typeid(float))) { + rp.setProperty(lastKey, static_cast(val)); + } else if (val_type == std::type_index(typeid(double))) { + rp.setProperty(lastKey, static_cast(val)); + } else if (val_type == std::type_index(typeid(int64_t))) { + rp.setProperty(lastKey, static_cast(val)); + } else if (val_type == std::type_index(typeid(uint64_t))) { + rp.setProperty(lastKey, static_cast(val)); + } else if (val_type == std::type_index(typeid(int32_t))) { + rp.setProperty(lastKey, static_cast(val)); + } else if (val_type == std::type_index(typeid(uint32_t))) { + rp.setProperty(lastKey, static_cast(val)); + } else if (val_type == std::type_index(typeid(int16_t))) { + rp.setProperty(lastKey, static_cast(val)); + } else if (val_type == std::type_index(typeid(uint16_t))) { + rp.setProperty(lastKey, static_cast(val)); + } else if (val_type == std::type_index(typeid(int8_t))) { + rp.setProperty(lastKey, static_cast(val)); + } else if (val_type == std::type_index(typeid(uint8_t))) { + rp.setProperty(lastKey, static_cast(val)); + } else { + THROW exception::RapidJSONError("RunPlan contains property '%s' of unsupported type '%s', " + "in JSONRunPlanReader::load()\n", lastKey.c_str(), val_type.name()); + } + } else { + // Arrays require more fiddly handling + // Create the array if this is the first item + if (current_array_index == 0) { + rp.property_overrides.emplace(lastKey, detail::Any(it->second.data)); + } + // Copy in the specific value + const auto &prop_it = rp.property_overrides.at(lastKey); + if (val_type == std::type_index(typeid(float))) { + static_cast(const_cast(prop_it.ptr))[current_array_index++] = static_cast(val); + } else if (val_type == std::type_index(typeid(double))) { + static_cast(const_cast(prop_it.ptr))[current_array_index++] = static_cast(val); + } else if (val_type == std::type_index(typeid(int64_t))) { + static_cast(const_cast(prop_it.ptr))[current_array_index++] = static_cast(val); + } else if (val_type == std::type_index(typeid(uint64_t))) { + static_cast(const_cast(prop_it.ptr))[current_array_index++] = static_cast(val); + } else if (val_type == std::type_index(typeid(int32_t))) { + static_cast(const_cast(prop_it.ptr))[current_array_index++] = static_cast(val); + } else if (val_type == std::type_index(typeid(uint32_t))) { + static_cast(const_cast(prop_it.ptr))[current_array_index++] = static_cast(val); + } else if (val_type == std::type_index(typeid(int16_t))) { + static_cast(const_cast(prop_it.ptr))[current_array_index++] = static_cast(val); + } else if (val_type == std::type_index(typeid(uint16_t))) { + static_cast(const_cast(prop_it.ptr))[current_array_index++] = static_cast(val); + } else if (val_type == std::type_index(typeid(int8_t))) { + static_cast(const_cast(prop_it.ptr))[current_array_index++] = static_cast(val); + } else if (val_type == std::type_index(typeid(uint8_t))) { + static_cast(const_cast(prop_it.ptr))[current_array_index++] = static_cast(val); + } else { + THROW exception::RapidJSONError("RunPlan contains property '%s' of unsupported type '%s', " + "in JSONRunPlanReader::load()\n", lastKey.c_str(), val_type.name()); + } + } + } else if (mode.top() == Plan) { + if (lastKey == "steps") { + rpv.back().setSteps(static_cast(val)); + } else if (lastKey == "random_seed") { + rpv.back().setRandomSimulationSeed(static_cast(val)); + } else { + THROW exception::RapidJSONError("Unexpected value whilst parsing input file '%s'.\n", filename.c_str()); + } + } else { + THROW exception::RapidJSONError("Unexpected value whilst parsing input file '%s'.\n", filename.c_str()); + } + if (isArray == PropertyArray) { + mode.push(isArray); + } + return true; + } + bool Null() { return true; } + bool Bool(bool b) { return processValue(b); } + bool Int(int i) { return processValue(i); } + bool Uint(unsigned u) { + return processValue(u); + } + bool Int64(int64_t i) { return processValue(i); } + bool Uint64(uint64_t u) { + return processValue(u); + } + bool Double(double d) { return processValue(d); } + bool String(const char*s, rapidjson::SizeType, bool) { + if (mode.top() == Plan) { + if (lastKey == "output_subdirectory") { + rpv.back().setOutputSubdirectory(s); + return true; + } + } + // Properties never contain strings + THROW exception::RapidJSONError("Unexpected string whilst parsing input file '%s'.\n", filename.c_str()); + } + bool StartObject() { + if (mode.empty()) { + mode.push(Root); + } else if (mode.top() == PlanVector) { + // Starting a new element of the RunPlanVector + rpv.push_back(RunPlan(rpv.environment, rpv.allow_0_steps)); + mode.push(Plan); + } else if (mode.top() == Plan && lastKey == "properties") { + // Starting a new element of the RunPlanVector + mode.push(Properties); + } else { + THROW exception::RapidJSONError("Unexpected object start whilst parsing input file '%s'.\n", filename.c_str()); + } + return true; + } + bool Key(const char* str, rapidjson::SizeType, bool) { + lastKey = str; + return true; + } + bool EndObject(rapidjson::SizeType) { + mode.pop(); + return true; + } + bool StartArray() { + if (mode.top() == Properties) { + if (current_array_index != 0) { + THROW exception::RapidJSONError("Array start when current_array_index !=0, in file '%s'. This should never happen.\n", filename.c_str()); + } + mode.push(PropertyArray); + } else if (mode.top() == Root) { + mode.push(PlanVector); + } else { + THROW exception::RapidJSONError("Unexpected array start whilst parsing input file '%s'.\n", filename.c_str()); + } + return true; + } + bool EndArray(rapidjson::SizeType) { + if (mode.top() == PropertyArray) { + // Confirm env array had correct number of elements + const auto& prop = rpv.environment->at(lastKey); + if (current_array_index != prop.data.elements) { + THROW exception::RapidJSONError("Input file contains property '%s' with %u elements expected %u," + "in JSONRunPlanReader::load()\n", lastKey.c_str(), current_array_index, prop.data.elements); + } + current_array_index = 0; + mode.pop(); + } else if (mode.top() == PlanVector) { + mode.pop(); + } else { + THROW exception::RapidJSONError("Unexpected array end whilst parsing input file '%s'.\n", filename.c_str()); + } + return true; + } +}; +RunPlanVector JSONRunPlanReader::load(const std::string &input_filepath, const ModelDescription& model) { + // Read the input file into a stringstream + std::ifstream in(input_filepath, std::ios::in | std::ios::binary); + if (!in.is_open()) { + THROW exception::InvalidFilePath("Unable to open file '%s' for reading, in JSONRunPlanReader::load().", input_filepath.c_str()); + } + const std::string filestring = std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + rapidjson::StringStream filess = rapidjson::StringStream(filestring.c_str()); + in.close(); + // Attempt to parse the JSON into a RunPlanVector + RunPlanVector result(model.model, 0); + rapidjson::Reader reader; + JSONRunPlanReader_impl handler(input_filepath, result); + rapidjson::ParseResult pr = reader.Parse(filess, handler); + if (pr.Code() != rapidjson::ParseErrorCode::kParseErrorNone) { + THROW exception::RapidJSONError("Whilst parsing input file '%s', RapidJSON returned error: %s\n", input_filepath.c_str(), rapidjson::GetParseError_En(pr.Code())); + } + // Return the result + return result; +} +} // namespace io +} // namespace flamegpu diff --git a/src/flamegpu/io/JSONRunPlanWriter.cpp b/src/flamegpu/io/JSONRunPlanWriter.cpp new file mode 100644 index 000000000..645e3a39c --- /dev/null +++ b/src/flamegpu/io/JSONRunPlanWriter.cpp @@ -0,0 +1,110 @@ +#include "flamegpu/io/JSONRunPlanWriter.h" + +#include +#include +#include + +#include + +namespace flamegpu { +namespace io { +// Typedef for the writer used, as the full template specification is way too long +typedef rapidjson::Writer, rapidjson::UTF8<>, rapidjson::CrtAllocator, rapidjson::kWriteNanAndInfFlag> GenericJSONWriter; +typedef rapidjson::PrettyWriter, rapidjson::UTF8<>, rapidjson::CrtAllocator, rapidjson::kWriteNanAndInfFlag> PrettyJSONWriter; + +template +void JSONRunPlanWriter::writeCommon(std::unique_ptr &writer, const RunPlanVector &rpv) { + writer->StartObject(); + writer->Key("RunPlanVector"); + writer->StartArray(); + // Write out RunPlan records + for (const auto& rp : rpv) { + writeRunPlan(writer, rp); + } + // Finalise and dump to file + writer->EndArray(); + writer->EndObject(); +} + +template +void JSONRunPlanWriter::writeRunPlan(std::unique_ptr &writer, const RunPlan &rp) { + writer->StartObject(); + // Core + writer->Key("random_seed"); + writer->Uint64(rp.random_seed); + writer->Key("steps"); + writer->Uint(rp.steps); + writer->Key("output_subdirectory"); + writer->String(rp.output_subdirectory.c_str()); + // This value is internal and is based on whether the corresponding ModelDescription has an exit condition + // writer->Key("allow_0_steps"); + // writer->Bool(rp.allow_0_steps); + // Properties + writer->Key("properties"); + writer->StartObject(); + for (const auto &[name, p] : rp.property_overrides) { + writer->Key(name.c_str()); + // Locate the environment property's metadata + const auto p_meta = rp.environment->at(name); + // Output value + if (p_meta.data.elements > 1) { + // Value is an array + writer->StartArray(); + } + // Loop through elements, to construct array + for (unsigned int el = 0; el < p_meta.data.elements; ++el) { + if (p_meta.data.type == std::type_index(typeid(float))) { + writer->Double(*(reinterpret_cast(p.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(double))) { + writer->Double(*(reinterpret_cast(p.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(int64_t))) { + writer->Int64(*(reinterpret_cast(p.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(uint64_t))) { + writer->Uint64(*(reinterpret_cast(p.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(int32_t))) { + writer->Int(*(reinterpret_cast(p.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(uint32_t))) { + writer->Uint(*(reinterpret_cast(p.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(int16_t))) { + writer->Int(*(reinterpret_cast(p.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(uint16_t))) { + writer->Uint(*(reinterpret_cast(p.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(int8_t))) { + writer->Int(static_cast(*(reinterpret_cast(p.ptr) + el))); // Char outputs weird if being used as an integer + } else if (p_meta.data.type == std::type_index(typeid(uint8_t))) { + writer->Uint(static_cast(*(reinterpret_cast(p.ptr) + el))); // Char outputs weird if being used as an integer + } else { + THROW exception::RapidJSONError("RunPlan contains environment property '%s' of unsupported type '%s', " + "in JSONRunPlanWriter::writeRunPlan()\n", name.c_str(), p_meta.data.type.name()); + } + } + if (p_meta.data.elements > 1) { + // Value is an array + writer->EndArray(); + } + } + writer->EndObject(); + writer->EndObject(); +} +void JSONRunPlanWriter::save(const RunPlanVector &rpv, const std::string &output_filepath, const bool pretty_print) { + // Init writer + auto buffer = rapidjson::StringBuffer(); + if (pretty_print) { + std::unique_ptr writer = std::make_unique(buffer); + writer->SetIndent('\t', 1); + writeCommon(writer, rpv); + } else { + std::unique_ptr writer = std::make_unique(buffer); + writeCommon(writer, rpv); + } + std::ofstream out(output_filepath, std::ofstream::trunc); + if (!out.is_open()) { + THROW exception::InvalidFilePath("Unable to open '%s' for writing, in JSONRunPlanWriter::save().", output_filepath.c_str()); + } + out << buffer.GetString(); + // Redundant cleanup + out.close(); + buffer.Clear(); +} +} // namespace io +} // namespace flamegpu diff --git a/src/flamegpu/simulation/RunPlan.cpp b/src/flamegpu/simulation/RunPlan.cpp index 38c36354d..ebb05c889 100644 --- a/src/flamegpu/simulation/RunPlan.cpp +++ b/src/flamegpu/simulation/RunPlan.cpp @@ -8,6 +8,9 @@ namespace flamegpu { RunPlan::RunPlan(const ModelDescription &model) : RunPlan(std::make_shared const>(model.model->environment->properties), model.model->exitConditions.size() + model.model->exitConditionCallbacks.size() > 0) { } +RunPlan::RunPlan(const std::shared_ptr &model) + : RunPlan(std::make_shared const>(model->environment->properties), + model->exitConditions.size() + model->exitConditionCallbacks.size() > 0) { } RunPlan::RunPlan(const std::shared_ptr> &environment, const bool allow_0) : random_seed(0) , steps(1) diff --git a/src/flamegpu/simulation/RunPlanVector.cpp b/src/flamegpu/simulation/RunPlanVector.cpp index 20e2b5d5f..712fa4297 100644 --- a/src/flamegpu/simulation/RunPlanVector.cpp +++ b/src/flamegpu/simulation/RunPlanVector.cpp @@ -4,11 +4,13 @@ namespace flamegpu { RunPlanVector::RunPlanVector(const ModelDescription &model, unsigned int initial_length) + : RunPlanVector(model.model, initial_length) { } +RunPlanVector::RunPlanVector(const std::shared_ptr &model, unsigned int initial_length) : std::vector(initial_length, RunPlan(model)) , randomPropertySeed(std::random_device()()) , rand(randomPropertySeed) - , environment(std::make_shared const>(model.model->environment->properties)) - , allow_0_steps(model.model->exitConditions.size() + model.model->exitConditionCallbacks.size() > 0) { + , environment(std::make_shared const>(model->environment->properties)) + , allow_0_steps(model->exitConditions.size() + model->exitConditionCallbacks.size() > 0) { this->resize(initial_length, RunPlan(environment, allow_0_steps)); } diff --git a/swig/python/flamegpu.i b/swig/python/flamegpu.i index 891fdf9ec..e4ee4c687 100644 --- a/swig/python/flamegpu.i +++ b/swig/python/flamegpu.i @@ -715,9 +715,14 @@ class ModelVis; %include "flamegpu/simulation/LogFrame.h" // Includes RunLog. // Include ensemble implementations +// Must wrap these prior to JSONRunPlanReader etc where they are used to avoid issues with no default constructors. %include "flamegpu/simulation/RunPlan.h" %include "flamegpu/simulation/RunPlanVector.h" +// Standalone Access to RunPlanVector IO +%include "flamegpu/io/JSONRunPlanReader.h" +%include "flamegpu/io/JSONRunPlanWriter.h" + // Include public utility headers %include "flamegpu/util/cleanup.h" // Don't flatnest this, range is explicitly not included incase of GC related issues. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 269db9ffa..7c83391b7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -27,6 +27,7 @@ SET(TESTS_SRC ${CMAKE_CURRENT_SOURCE_DIR}/test_cases/io/test_io.cu ${CMAKE_CURRENT_SOURCE_DIR}/test_cases/io/test_logging.cu ${CMAKE_CURRENT_SOURCE_DIR}/test_cases/io/test_logging_exceptions.cu + ${CMAKE_CURRENT_SOURCE_DIR}/test_cases/io/test_runplan_io.cu ${CMAKE_CURRENT_SOURCE_DIR}/test_cases/io/test_telemetry.cu ${CMAKE_CURRENT_SOURCE_DIR}/test_cases/model/test_environment_description.cu ${CMAKE_CURRENT_SOURCE_DIR}/test_cases/model/test_model.cu diff --git a/tests/test_cases/io/test_runplan_io.cu b/tests/test_cases/io/test_runplan_io.cu new file mode 100644 index 000000000..c55ed33f8 --- /dev/null +++ b/tests/test_cases/io/test_runplan_io.cu @@ -0,0 +1,227 @@ +#include +#include + +#include "gtest/gtest.h" + +#include "flamegpu/flamegpu.h" + +namespace flamegpu { + +namespace test_runplan_io { +const char *XML_FILE_NAME = "test.xml"; +const char *JSON_FILE_NAME = "test.json"; +const char *BIN_FILE_NAME = "test.bin"; +FLAMEGPU_EXIT_CONDITION(allow_0_steps) { + return flamegpu::EXIT; +} +class MiniSim { + std::string test_file; + + public: + ~MiniSim() { + // Cleanup + if (!test_file.empty()) + ::remove(test_file.c_str()); + } + void run(const std::string &test_file_name) { + this->test_file = test_file_name; + // Assertions for limits + ASSERT_TRUE(std::numeric_limits::has_quiet_NaN); + ASSERT_TRUE(std::numeric_limits::has_signaling_NaN); + ASSERT_TRUE(std::numeric_limits::has_quiet_NaN); + ASSERT_TRUE(std::numeric_limits::has_signaling_NaN); + // Model description + ModelDescription model("test_model"); + AgentDescription a = model.newAgent("a"); + model.addExitCondition(allow_0_steps); + { + EnvironmentDescription e = model.Environment(); + // Ensure default values differ!!! + e.newProperty("float", 112.0f); + e.newProperty("double", 113.0); + e.newProperty("int64_t", 114); + e.newProperty("uint64_t", 115u); + e.newProperty("int32_t", 116); + e.newProperty("uint32_t", 117u); + e.newProperty("int16_t", 118); + e.newProperty("uint16_t", 119u); + e.newProperty("int8_t", 120); + e.newProperty("uint8_t", 121u); + e.newProperty("float_a", { 112.0f, 10.0f, 11.0f }); + e.newProperty("double_a", { 113.0, 10.0, 11.0 }); + e.newProperty("int64_t_a", { 114, 10, 11 }); + e.newProperty("uint64_t_a", { 115u, 10u, 11u }); + e.newProperty("int32_t_a", { 116, 10, 11 }); + e.newProperty("uint32_t_a", { 117u, 10u, 11u }); + e.newProperty("int16_t_a", { 118, 10, 11}); + e.newProperty("uint16_t_a", { 119u, 10u, 11u }); + e.newProperty("int8_t_a", { 120, 10, 11 }); + e.newProperty("uint8_t_a", {121u, 10u, 11u}); + // Limit values + e.newProperty("float_qnan", 0); + e.newProperty("float_snan", 0); + e.newProperty("float_inf", 0); + e.newProperty("float_inf_neg", 0); + e.newProperty("double_qnan", 0); + e.newProperty("double_snan", 0); + e.newProperty("double_inf", 0); + e.newProperty("double_inf_neg", 0); + } + { // Run export + RunPlanVector rpv(model, 3); + { + auto& rp = rpv[0]; + rp.setOutputSubdirectory("foo"); + rp.setRandomSimulationSeed(23); + rp.setSteps(22); + // Scalars + rp.setProperty("float", 12.0f); + rp.setProperty("double", 13.0); + rp.setProperty("int64_t", 14); + rp.setProperty("uint64_t", 15u); + rp.setProperty("int32_t", 16); + rp.setProperty("uint32_t", 17u); + rp.setProperty("int16_t", 18); + rp.setProperty("float_a", { 12.0f, 0.0f, 1.0f }); // Edge case + rp.setProperty("uint16_t", 19u); + rp.setProperty("int8_t", 20); + rp.setProperty("uint8_t", 21u); + } + { + auto& rp = rpv[1]; + rp.setOutputSubdirectory("BAR"); + rp.setRandomSimulationSeed(std::numeric_limits::max()); + rp.setSteps(std::numeric_limits::max()); + // Limit values + rp.setProperty("float_qnan", std::numeric_limits::quiet_NaN()); + rp.setProperty("float_snan", std::numeric_limits::signaling_NaN()); + rp.setProperty("float_inf", std::numeric_limits::infinity()); + rp.setProperty("float_inf_neg", -std::numeric_limits::infinity()); + rp.setProperty("double_qnan", std::numeric_limits::quiet_NaN()); + rp.setProperty("double_snan", std::numeric_limits::quiet_NaN()); + rp.setProperty("double_inf", std::numeric_limits::infinity()); + rp.setProperty("double_inf_neg", -std::numeric_limits::infinity()); + } + { + auto& rp = rpv[2]; + rp.setOutputSubdirectory("FOObar"); + rp.setRandomSimulationSeed(0); + rp.setSteps(0); + // Arrays + rp.setProperty("float_a", { 12.0f, 0.0f, 1.0f }); + rp.setProperty("double_a", { 13.0, 0.0, 1.0 }); + rp.setProperty("int64_t_a", { 14, 0, 1 }); + rp.setProperty("uint64_t_a", { 15u, 0u, 1u }); + rp.setProperty("int32_t_a", { 16, 0, 1 }); + rp.setProperty("float", 12.0f); // Edge case + rp.setProperty("uint32_t_a", { 17u, 0u, 1u }); + rp.setProperty("int16_t_a", { 18, 0, 1 }); + rp.setProperty("uint16_t_a", { 19u, 0u, 1u }); + rp.setProperty("int8_t_a", { 20, 0, 1 }); + rp.setProperty("uint8_t_a", { 21u, 0u, 1u }); + } + io::JSONRunPlanWriter::save(rpv, test_file_name); + } + { // Run Import + RunPlanVector rpv = io::JSONRunPlanReader::load(test_file_name, model); + // Validate config matches + { + auto& rp = rpv[0]; + EXPECT_EQ(rp.getOutputSubdirectory(), "foo"); + EXPECT_EQ(rp.getRandomSimulationSeed(), 23u); + EXPECT_EQ(rp.getSteps(), 22u); + // Scalars + EXPECT_EQ(rp.getProperty("float"), 12.0f); + EXPECT_EQ(rp.getProperty("double"), 13.0); + EXPECT_EQ(rp.getProperty("int64_t"), 14); + EXPECT_EQ(rp.getProperty("uint64_t"), 15u); + EXPECT_EQ(rp.getProperty("int32_t"), 16); + EXPECT_EQ(rp.getProperty("uint32_t"), 17u); + EXPECT_EQ(rp.getProperty("int16_t"), 18); + EXPECT_EQ(rp.getProperty("uint16_t"), 19u); + EXPECT_EQ(rp.getProperty("int8_t"), 20); + EXPECT_EQ(rp.getProperty("uint8_t"), 21u); + // Edge case + const bool float_array_eq = rp.getProperty("float_a") == std::array{ 12.0f, 0.0f, 1.0f }; + EXPECT_TRUE(float_array_eq); + } + { + auto& rp = rpv[1]; + EXPECT_EQ(rp.getOutputSubdirectory(), "BAR"); + EXPECT_EQ(rp.getRandomSimulationSeed(), std::numeric_limits::max()); + EXPECT_EQ(rp.getSteps(), std::numeric_limits::max()); + // Limits + EXPECT_TRUE(std::isnan(rp.getProperty("float_qnan"))); + EXPECT_TRUE(std::isnan(rp.getProperty("float_snan"))); + EXPECT_EQ(rp.getProperty("float_inf"), std::numeric_limits::infinity()); + EXPECT_EQ(rp.getProperty("float_inf_neg"), -std::numeric_limits::infinity()); + EXPECT_TRUE(std::isnan(rp.getProperty("double_qnan"))); + EXPECT_TRUE(std::isnan(rp.getProperty("double_snan"))); + EXPECT_EQ(rp.getProperty("double_inf"), std::numeric_limits::infinity()); + EXPECT_EQ(rp.getProperty("double_inf_neg"), -std::numeric_limits::infinity()); + } + { + auto& rp = rpv[2]; + EXPECT_EQ(rp.getOutputSubdirectory(), "FOObar"); + EXPECT_EQ(rp.getRandomSimulationSeed(), 0u); + EXPECT_EQ(rp.getSteps(), 0u); + // Arrays + const bool float_array_eq = rp.getProperty("float_a") == std::array{ 12.0f, 0.0f, 1.0f }; + EXPECT_TRUE(float_array_eq); + const bool double_array_eq = rp.getProperty("double_a") == std::array{ 13.0, 0.0, 1.0 }; + EXPECT_TRUE(double_array_eq); + const bool int64_t_array_eq = rp.getProperty("int64_t_a") == std::array{ 14, 0, 1 }; + EXPECT_TRUE(int64_t_array_eq); + const bool uint64_t_array_eq = rp.getProperty("uint64_t_a") == std::array{ 15u, 0u, 1u }; + EXPECT_TRUE(uint64_t_array_eq); + const bool int32_t_array_eq = rp.getProperty("int32_t_a") == std::array{ 16, 0, 1 }; + EXPECT_TRUE(int32_t_array_eq); + const bool uint32_t_array_eq = rp.getProperty("uint32_t_a") == std::array { 17u, 0u, 1u }; + EXPECT_TRUE(uint32_t_array_eq); + const bool int16_t_array_eq = rp.getProperty("int16_t_a") == std::array{ 18, 0, 1 }; + EXPECT_TRUE(int16_t_array_eq); + const bool uint16_t_array_eq = rp.getProperty("uint16_t_a") == std::array{ 19u, 0u, 1u }; + EXPECT_TRUE(uint16_t_array_eq); + const bool int8_t_array_eq = rp.getProperty("int8_t_a") == std::array{ 20, 0, 1 }; + EXPECT_TRUE(int8_t_array_eq); + const bool uint8_t_array_eq = rp.getProperty("uint8_t_a") == std::array{ 21u, 0u, 1u }; + EXPECT_TRUE(uint8_t_array_eq); + // Edge case + EXPECT_EQ(rp.getProperty("float"), 12.0f); + } + EXPECT_EQ(rpv.size(), 3u); + } + // Cleanup + ASSERT_EQ(::remove(test_file_name.c_str()), 0); + } +}; + +/** +* This defines a common fixture used as a base for all test cases in the file +* @see https://github.com/google/googletest/blob/master/googletest/samples/sample5_unittest.cc +*/ +class TestRunPlanIO : public testing::Test { + protected: + void SetUp() override { + ms = new MiniSim(); + } + + void TearDown() override { + delete ms; + } + + MiniSim *ms = nullptr; +}; +/* TEST IS CURRENTLY HARDCODED TO USE JSON IO +TEST_F(TestRunPlanIO, XML_WriteRead) { + // Avoid fail if previous run didn't cleanup properly + ::remove(XML_FILE_NAME); + ms->run(XML_FILE_NAME); +}*/ +TEST_F(TestRunPlanIO, JSON_WriteRead) { + // Avoid fail if previous run didn't cleanup properly + ::remove(JSON_FILE_NAME); + ms->run(JSON_FILE_NAME); +} +} // namespace test_runplan_io +} // namespace flamegpu