diff --git a/include/flamegpu/io/JSONRunPlanReader.h b/include/flamegpu/io/JSONRunPlanReader.h new file mode 100644 index 000000000..d2ce4bf10 --- /dev/null +++ b/include/flamegpu/io/JSONRunPlanReader.h @@ -0,0 +1,25 @@ +#ifndef INCLUDE_FLAMEGPU_IO_JSONRUNPLANREADER_H_ +#define INCLUDE_FLAMEGPU_IO_JSONRUNPLANREADER_H_ + +#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..81a7ce4c7 --- /dev/null +++ b/include/flamegpu/io/JSONRunPlanWriter.h @@ -0,0 +1,35 @@ +#ifndef INCLUDE_FLAMEGPU_IO_JSONRUNPLANWRITER_H_ +#define INCLUDE_FLAMEGPU_IO_JSONRUNPLANWRITER_H_ + +#include + +#include "flamegpu/simulation/RunPlanVector.h" + +namespace flamegpu { +namespace io { +/** + * JSON format writer of RunPlanVector + */ +class JSONRunPlanWriter { + // Typedef for the writer used, as the full template specification is way too long + typedef rapidjson::Writer, rapidjson::UTF8<>, rapidjson::CrtAllocator, rapidjson::kWriteNanAndInfFlag> GenericJSONWriter; + /** + * Utility method for writing out a single RunPlan + * @param writer An initialised RapidJSON writer. + * @param rp RunPlan to be writer + */ + 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" or "minified" + */ + 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 d7a791166..0389c801e 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..5b001435f --- /dev/null +++ b/src/flamegpu/io/JSONRunPlanReader.cpp @@ -0,0 +1,235 @@ +#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, Plan, Core, 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) + : 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 + const auto rp = rpv.end(); + const std::type_index val_type = it->second.data.type; + if (it->second.data.elements ==0) { + // 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)); + } + // 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 { + 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) { + if (mode.top() == Plan) { + if (lastKey == "steps") { + rpv.end()->setSteps(u); + return true; + } + return false; + } + return processValue(u); + } + bool Int64(int64_t i) { return processValue(i); } + bool Uint64(uint64_t u) { + if (mode.top() == Plan) { + if (lastKey == "random_seed") { + rpv.end()->setRandomSimulationSeed(u); + return true; + } + return false; + } + 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.end()->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() == Plan) { + if (lastKey == "RunPlanVector") { + mode.push(Core); + } else { + THROW exception::RapidJSONError("Unexpected object start whilst parsing input file '%s'.\n", filename.c_str()); + } + } else if (mode.top() == Core) { + if (lastKey == "properties") { + mode.push(Properties); + } else { + THROW exception::RapidJSONError("Unexpected object start whilst parsing input file '%s'.\n", filename.c_str()); + } + } else if (mode.top() == PropertyArray) { + rpv.push_back(RunPlan(rpv.environment, rpv.allow_0_steps)); + } 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 (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()); + } + if (mode.top() == Plan && lastKey == "properties") { + mode.push(Properties); + } else if (mode.top() == Properties) { + mode.push(PropertyArray); + } 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) { + mode.pop(); + if (mode.top() == Properties) { + // 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; + } else if (mode.top() == Properties) { + 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; +} +} +} diff --git a/src/flamegpu/io/JSONRunPlanWriter.cpp b/src/flamegpu/io/JSONRunPlanWriter.cpp new file mode 100644 index 000000000..ffc935e85 --- /dev/null +++ b/src/flamegpu/io/JSONRunPlanWriter.cpp @@ -0,0 +1,104 @@ +#include "flamegpu/io/JSONRunPlanWriter.h" + +#include + +#include +#include +#include +namespace flamegpu { +namespace io { +void JSONRunPlanWriter::save(const RunPlanVector& rpv, const std::string& output_filepath, const bool pretty_print) { + // Init writer + auto buffer = rapidjson::StringBuffer(); + std::unique_ptr writer; + if (pretty_print) { + auto t_writer = std::make_unique, rapidjson::UTF8<>, rapidjson::CrtAllocator, rapidjson::kWriteNanAndInfFlag>>(buffer); + t_writer->SetIndent('\t', 1); + writer = std::move(t_writer); + } else { + writer = std::make_unique, rapidjson::UTF8<>, rapidjson::CrtAllocator, rapidjson::kWriteNanAndInfFlag>>(buffer); + } + 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(); + 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(); + out.close(); + // Cleanup (redundant in a static method) + writer.reset(); + buffer.Clear(); +} +/** + * Utility method for writing out a single RunPlan + * @param writer An initialised RapidJSON writer. + * @param rp RunPlan to be writer + */ +void JSONRunPlanWriter::writeRunPlan(std::unique_ptr &writer, const RunPlan &rp) { + // 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_meta.data.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(double))) { + writer->Double(*(reinterpret_cast(p_meta.data.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(int64_t))) { + writer->Int64(*(reinterpret_cast(p_meta.data.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(uint64_t))) { + writer->Uint64(*(reinterpret_cast(p_meta.data.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(int32_t))) { + writer->Int(*(reinterpret_cast(p_meta.data.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(uint32_t))) { + writer->Uint(*(reinterpret_cast(p_meta.data.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(int16_t))) { + writer->Int(*(reinterpret_cast(p_meta.data.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(uint16_t))) { + writer->Uint(*(reinterpret_cast(p_meta.data.ptr) + el)); + } else if (p_meta.data.type == std::type_index(typeid(int8_t))) { + writer->Int(static_cast(*(reinterpret_cast(p_meta.data.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_meta.data.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(); +} +} +} 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)); }