diff --git a/Code.v05-00/include/Core/Input_Mod.hpp b/Code.v05-00/include/Core/Input_Mod.hpp index 470ab310..2b08881d 100644 --- a/Code.v05-00/include/Core/Input_Mod.hpp +++ b/Code.v05-00/include/Core/Input_Mod.hpp @@ -43,7 +43,7 @@ struct OptInput bool SIMULATION_BOXMODEL; std::string SIMULATION_BOX_FILENAME; bool SIMULATION_FORCE_SEED; - int SIMULATION_SEED_VALUE; + unsigned int SIMULATION_SEED_VALUE; epm_type SIMULATION_EPM_TYPE; std::string SIMULATION_EXTERNAL_EPM_NETCDF_FILENAME; diff --git a/Code.v05-00/include/Util/MC_Rand.hpp b/Code.v05-00/include/Util/MC_Rand.hpp index c5e776a5..4099642b 100644 --- a/Code.v05-00/include/Util/MC_Rand.hpp +++ b/Code.v05-00/include/Util/MC_Rand.hpp @@ -17,8 +17,9 @@ #include #include "Core/Input_Mod.hpp" -/* Set seed for pseudo-random generator */ -void setSeed(const OptInput& input); +// Set the seed of the pseudo-random generator and return it. +// Also writes the resolved seed back into input.SIMULATION_SEED_VALUE. +unsigned int setSeed(OptInput& input); /* Generates a random number of type T between fMin and fMax */ template diff --git a/Code.v05-00/include/YamlInputReader/YamlInputReader.hpp b/Code.v05-00/include/YamlInputReader/YamlInputReader.hpp index 81ea22bf..65715b34 100644 --- a/Code.v05-00/include/YamlInputReader/YamlInputReader.hpp +++ b/Code.v05-00/include/YamlInputReader/YamlInputReader.hpp @@ -29,6 +29,12 @@ namespace YamlInputReader{ // Fill OptInput and Input from an already merged node. void populateInput(OptInput& input, Input& scenario, const YAML::Node& merged); + // Rewrite the seed of a merged node to the seed the run actually used, so that + // re-running APCEMM on the written node reproduces the run. Sets + // 'Force seed value (T/F)' to T and 'Seed value (positive int)' to seed. + // Throws if the node does not already carry that submenu and both keys. + void recordEffectiveSeed(YAML::Node& node, unsigned int seed); + // Write a node to outputDir/filename. The directory must already exist. void writeYaml(const YAML::Node& node, const std::filesystem::path& outputDir, const string& filename); diff --git a/Code.v05-00/src/Main.cpp b/Code.v05-00/src/Main.cpp index dab2e974..f969ef47 100644 --- a/Code.v05-00/src/Main.cpp +++ b/Code.v05-00/src/Main.cpp @@ -104,8 +104,10 @@ int main( int argc, char* argv[]) Diag::set_storePSD(true); } - // Set the seed once at the top-level - setSeed( Input_Opt ); + // Set the seed once at the top-level, update in place in + // OptInput and return it as well to ensure setSeed cannot be called + // after writeYaml + const unsigned int seed = setSeed( Input_Opt ); if ( folderIsOccupied( Input_Opt.SIMULATION_OUTPUT_FOLDER ) \ && !Input_Opt.SIMULATION_OVERWRITE ) { @@ -140,7 +142,8 @@ int main( int argc, char* argv[]) } - // Write merged YAML input files to output directory + // Write merged YAML input files to output directory, with the seed this run used + YamlInputReader::recordEffectiveSeed( mergedInput, seed ); YamlInputReader::writeYaml( mergedInput, Input_Opt.SIMULATION_OUTPUT_FOLDER, OutputFiles::MERGED_YAML ); /* The switch is a placeholder for future models. Only the plume model is diff --git a/Code.v05-00/src/Util/MC_Rand.cpp b/Code.v05-00/src/Util/MC_Rand.cpp index 9d877bd4..8d1fb123 100644 --- a/Code.v05-00/src/Util/MC_Rand.cpp +++ b/Code.v05-00/src/Util/MC_Rand.cpp @@ -10,30 +10,32 @@ /* File : MC_Rand.cpp */ /* */ /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ +#include #include #include "APCEMM.h" #include "Util/MC_Rand.hpp" #include "Core/Input_Mod.hpp" -void setSeed(const OptInput& input) { +unsigned int setSeed(OptInput& input) { - // Sets seed for pseudo-random generator. + // Sets the seed of the pseudo-random generator, stores it in OptInput and returns + // it, so the caller can record the seed the run actually used. #ifdef DEBUG // With DEBUG compile flag set a constant seed for reproducibility - std::cout << "Compiled in DEBUG mode: random seed is set to 0 for all simulations" << std::endl; - srand(0); + input.SIMULATION_SEED_VALUE = 0; + std::cout << "Compiled in DEBUG mode: random seed is set to " << input.SIMULATION_SEED_VALUE << " for all simulations" << std::endl; #else - if(input.SIMULATION_FORCE_SEED){ - srand(input.SIMULATION_SEED_VALUE); - std::cout << "Random seed is set to " << input.SIMULATION_SEED_VALUE << " for all simulations" << std::endl; + if(!input.SIMULATION_FORCE_SEED){ + // If the seed is not being forced to a value use the current unix timestamp + // instead. The conversion from time_t to unsigned int wraps (time_t is larger) + // but that's fine as any unsigned int is a usable seed. + input.SIMULATION_SEED_VALUE = static_cast(std::time(nullptr)); } - else{ - // If the seed is not being forced to a value use the current unix timestamp instead. - srand(time(NULL)); - } - + std::cout << "Random seed is set to " << input.SIMULATION_SEED_VALUE << " for all simulations" << std::endl; #endif + srand(input.SIMULATION_SEED_VALUE); + return input.SIMULATION_SEED_VALUE; } /* End of setSeed */ template diff --git a/Code.v05-00/src/YamlInputReader/YamlInputReader.cpp b/Code.v05-00/src/YamlInputReader/YamlInputReader.cpp index a7d5692e..12e5391e 100644 --- a/Code.v05-00/src/YamlInputReader/YamlInputReader.cpp +++ b/Code.v05-00/src/YamlInputReader/YamlInputReader.cpp @@ -1,4 +1,5 @@ #include "YamlInputReader/YamlInputReader.hpp" +#include "Core/Input_Mod.hpp" #include "YamlInputReader/YamlPathUtils.hpp" #include "APCEMM.h" #include "Util/ForwardDecl.hpp" @@ -253,6 +254,31 @@ namespace YamlInputReader{ } } + // Overwrite both seed fields with the actual seed used so the saved merged input file + // can exactly reproduce the run. + void recordEffectiveSeed(YAML::Node& node, unsigned int seed){ + const string submenuKey = "RANDOM NUMBER GENERATION SUBMENU"; + const string forceKey = "Force seed value (T/F)"; + const string seedKey = "Seed value (positive int)"; + + // Double check that these keys exist otherwise we'd be creating + // new nodes instead of updating in place. const[] look-up + // does not create a new node if it does not exist + // Only happens if the RNG submenu is changed and keys are not updated here + const YAML::Node& readOnly = node; + const YAML::Node existing = readOnly["SIMULATION MENU"][submenuKey]; + if (!existing.IsDefined() || !existing.IsMap() || !existing[forceKey].IsDefined() || !existing[seedKey].IsDefined()){ + throw std::runtime_error("Cannot record the effective seed: 'SIMULATION MENU -> " + + submenuKey + "' with keys '" + forceKey + "' and '" + + seedKey + "' is missing from the merged input."); + } + + // Update seed values here + YAML::Node seedSubmenu = node["SIMULATION MENU"][submenuKey]; + seedSubmenu[forceKey] = "T"; + seedSubmenu[seedKey] = seed; + } + // Output dir must exist before calling this void writeYaml(const YAML::Node& node, const std::filesystem::path& outputDir, const string& filename){ const std::filesystem::path fullPath = outputDir / filename; @@ -318,10 +344,7 @@ namespace YamlInputReader{ YAML::Node seedSubmenu = simNode["RANDOM NUMBER GENERATION SUBMENU"]; input.SIMULATION_FORCE_SEED = parseBoolString(seedSubmenu["Force seed value (T/F)"].as(), "Force seed value (T/F)"); - input.SIMULATION_SEED_VALUE = parseIntString(seedSubmenu["Seed value (positive int)"].as(), "Seed value (positive int)"); - if(input.SIMULATION_SEED_VALUE < 0){ - throw std::invalid_argument("Seed value (under SIMULATION MENU) cannot be less than 0!"); - } + input.SIMULATION_SEED_VALUE = parseScalarUIntParam(seedSubmenu["Seed value (positive int)"].as(), "Seed value (positive int)"); string epm = simNode["EPM type (original/external/new)"].as(); diff --git a/Code.v05-00/tests/test_yamlreader.cpp b/Code.v05-00/tests/test_yamlreader.cpp index 0926f0ea..7c4c65da 100644 --- a/Code.v05-00/tests/test_yamlreader.cpp +++ b/Code.v05-00/tests/test_yamlreader.cpp @@ -482,6 +482,30 @@ TEST_CASE("mergeYamlInputFiles resolves defaults and overrides into one node"){ REQUIRE(merged["SIMULATION MENU"]["RANDOM NUMBER GENERATION SUBMENU"]["Force seed value (T/F)"].as() == "F"); } +TEST_CASE("recordEffectiveSeed updates seed submenu"){ + string filename = string(APCEMM_TESTS_DIR) + YAML_DIR + "/test1.yaml"; + YAML::Node merged = YamlInputReader::mergeYamlInputFiles({filename}); + // Verify that the default is an unforced seed otherwise the rest of the test is useless + REQUIRE(merged["SIMULATION MENU"]["RANDOM NUMBER GENERATION SUBMENU"]["Force seed value (T/F)"].as() == "F"); + + SECTION("The seed submenu is rewritten in place"){ + YamlInputReader::recordEffectiveSeed(merged, 1755792000); + + // Verify that the node is correctly updated + YAML::Node seedSubmenu = merged["SIMULATION MENU"]["RANDOM NUMBER GENERATION SUBMENU"]; + REQUIRE(seedSubmenu["Force seed value (T/F)"].as() == "T"); + REQUIRE(seedSubmenu["Seed value (positive int)"].as() == "1755792000"); + + // Sanity check that if we recreate an input from the new node + // the seed menu is correct + OptInput input; + Input scenario; + YamlInputReader::populateInput(input, scenario, merged); + REQUIRE(input.SIMULATION_FORCE_SEED == true); + REQUIRE(input.SIMULATION_SEED_VALUE == 1755792000); + } +} + TEST_CASE("writeYaml round trip"){ string filename = string(APCEMM_TESTS_DIR) + YAML_DIR + "/test1.yaml"; YAML::Node merged = YamlInputReader::mergeYamlInputFiles({filename}); diff --git a/README.md b/README.md index 6a8eae71..80b48164 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Examples and their accompanying jupyter notebooks for postprocessing tutorials a The input file options are explained via comments in the file `Code.v05-00/defaults/input.yaml` -Each run writes `merged-input.yaml` into its output folder to record the result of the input merging step. It holds the defaults overwritten by every parameter in user input files passed to APCEMM, in the order they were passed. +Each run writes `merged-input.yaml` into its output folder which is an input file that can be used to reproduce exactly the run. It records the result of the input merging step and records the seed used for that run (even if it was not manually forced). It holds the defaults overwritten by every parameter in user input files passed to APCEMM, in the order they were passed. Advanced simulation parameters hidden in the input files (e.g. Aerosol bin size ratios, minimum/max bin aerosol sizes, etc) can be modified in `Code.v05-00/src/include/Parameters.hpp`.