diff --git a/demos/SpatialCoop2017/Makefile b/demos/SpatialCoop2017/Makefile index dced09e09b..4dcf2e10e5 100644 --- a/demos/SpatialCoop2017/Makefile +++ b/demos/SpatialCoop2017/Makefile @@ -12,9 +12,9 @@ CFLAGS_nat_debug := -g $(CFLAGS_all) # Emscripten compiler information CXX_web := emcc -OFLAGS_web_all := -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall', 'cwrap']" -s TOTAL_MEMORY=67108864 --js-library $(EMP_DIR)/emp/web/library_emp.js -s EXPORTED_FUNCTIONS="['_main', '_empCppCallback']" -s DISABLE_EXCEPTION_CATCHING=1 -s NO_EXIT_RUNTIME=1 #--embed-file configs +OFLAGS_web_all := -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall', 'cwrap']" -s TOTAL_MEMORY=67108864 --js-library $(EMP_DIR)/emp/web/library_emp.js -s EXPORTED_FUNCTIONS="['_main', '_empCppCallback', '_empDoCppCallback']" -s DISABLE_EXCEPTION_CATCHING=1 -s NO_EXIT_RUNTIME=1 -s WASM=0 #--embed-file configs OFLAGS_web := -Oz -DNDEBUG -OFLAGS_web_debug := -g4 -pedantic -Wno-dollar-in-identifier-extension +OFLAGS_web_debug := -pedantic -Wno-dollar-in-identifier-extension CFLAGS_web := $(CFLAGS_all) $(OFLAGS_web) $(OFLAGS_web_all) CFLAGS_web_debug := $(CFLAGS_all) $(OFLAGS_web_debug) $(OFLAGS_web_all) diff --git a/demos/SpatialCoop2017/source/SimplePDWorld.hpp b/demos/SpatialCoop2017/source/SimplePDWorld.hpp index 1d507500d1..bebaff0f0f 100644 --- a/demos/SpatialCoop2017/source/SimplePDWorld.hpp +++ b/demos/SpatialCoop2017/source/SimplePDWorld.hpp @@ -111,16 +111,17 @@ class SimplePDWorld { void Reset() { Setup(r,u,N,E); } - void Run(size_t steps=-1) { - if (steps > E) steps = E; - // Run the organisms! - size_t end_epoch = epoch + steps; - while (epoch < end_epoch) { + void Run(size_t steps=1) { + + for (size_t i = 0; i < steps; i++) { for (size_t o = 0; o < N; o++) Repro(); - epoch++; } } + void RunStep() { + for (size_t o = 0; o < N; o++) Repro(); + } + size_t CountCoop(); void PrintNeighborInfo(std::ostream & os); }; diff --git a/demos/SpatialCoop2017/source/web/SimplePDWorld-web.cpp b/demos/SpatialCoop2017/source/web/SimplePDWorld-web.cpp index 49bb79ce5b..755aa4b30c 100644 --- a/demos/SpatialCoop2017/source/web/SimplePDWorld-web.cpp +++ b/demos/SpatialCoop2017/source/web/SimplePDWorld-web.cpp @@ -2,7 +2,11 @@ // Copyright (C) Michigan State University, 2017. // Released under the MIT Software license; see doc/LICENSE +#include +#include + #include "emp/web/web.hpp" +#include "emp/prefab/QueueManager.hpp" #include "../SimplePDWorld.hpp" namespace UI = emp::web; @@ -15,6 +19,19 @@ SimplePDWorld world; int cur_x = -1; int cur_y = -1; +std::function SetupConfig = [](){ + emp::SettingConfig config; + config.AddSetting("r") = {world.GetR()}; + config.AddSetting("u") = {world.GetU()}; + config.AddSetting("N") = {world.GetN()}; + config.AddSetting("E") = {world.GetE()}; + + return config; +}; + +emp::SettingConfig config = SetupConfig(); +emp::QueueManager run_list(config); + void DrawCanvas() { UI::Canvas canvas = doc.Canvas("canvas"); canvas.Clear("black"); @@ -42,40 +59,7 @@ void CanvasClick(int x, int y) { DrawCanvas(); } -struct RunInfo { - size_t id; - - double r; - double u; - size_t N; - size_t E; - - size_t cur_epoch; - size_t num_coop; - size_t num_defect; - - RunInfo(size_t _id, double _r, double _u, size_t _N, size_t _E) - : id(_id), r(_r), u(_u), N(_N), E(_E) - , cur_epoch(0), num_coop(0), num_defect(0) - { ; } -}; - -struct RunList { - emp::vector runs; - size_t cur_run = 0; - - void AddRun(double r, double u, size_t N, size_t E) { - size_t id = runs.size(); - runs.emplace_back(id, r, u, N, E); - } - - bool Active() const { return cur_run < runs.size(); } -}; - -RunList run_list; -int anim_step = 1; - -void TogglePlay() +void TogglePlay() { auto & anim = doc.Animate("anim_world"); anim.ToggleActive(); @@ -88,48 +72,43 @@ void TogglePlay() else but.SetLabel("Fast Forward!"); } -int main() +int anim_step = 1; + +int main() { - doc << "

Spatial Prisoner's Dilema

"; + doc << "

Spatial Prisoner's Dillemma

"; + std::function coop_func = []() { return std::to_string(world.CountCoop()); }; + std::function defect_func = []() { return std::to_string(run_list.FrontRun().runinfo_config.GetValue("N") - world.CountCoop()); }; + + run_list.AddMetric(coop_func, "Num Coop"); + run_list.AddMetric(defect_func, "Num Defect"); + auto canvas = doc.AddCanvas(world_size, world_size, "canvas"); // canvas.On("click", CanvasClick); - auto & anim = doc.AddAnimation("anim_world", [](){ - if (run_list.Active()) { - size_t id = run_list.cur_run; - auto & run = run_list.runs[id]; - if (run.cur_epoch == 0) { // Are we starting a new run? - world.Setup(run.r, run.u, run.N, run.E); + doc.AddAnimation("anim_world", [](){ + // if queue has runs + if (!run_list.IsEmpty()) { + emp::QueueManager::RunInfo & run = run_list.FrontRun(); // Referencing current run + if (run.GetEpoch() == 0) { // Are we starting a new run? + world.Setup(run.runinfo_config.GetValue("r"), run.runinfo_config.GetValue("u"), run.runinfo_config.GetValue("N"), run.runinfo_config.GetValue("E")); DrawCanvas(); - } + } + run.IncEpoch(anim_step); } world.Run(anim_step); DrawCanvas(); - if (run_list.Active()) { - size_t id = run_list.cur_run; - size_t cur_epoch = world.GetEpoch(); - if (run_list.runs[id].E <= cur_epoch) { // Are we done with this run? - run_list.cur_run++; - } - run_list.runs[id].cur_epoch = cur_epoch; - run_list.runs[id].num_coop = world.CountCoop(); - run_list.runs[id].num_defect = run_list.runs[id].N - run_list.runs[id].num_coop; - - auto result_tab = doc.Table("result_tab"); - result_tab.Freeze(); - result_tab.GetCell(id+1,5).ClearChildren() << cur_epoch; - result_tab.GetCell(id+1,6).ClearChildren() << run_list.runs[id].num_coop; - result_tab.GetCell(id+1,7).ClearChildren() << run_list.runs[id].num_defect; - result_tab.Activate(); + if (!run_list.IsEmpty()) { + run_list.Update(); //calculations for table } - } ); + }); doc << "
"; - doc.AddButton([&anim](){ + doc.AddButton([](){ anim_step = 1; TogglePlay(); }, "Play", "start_but"); doc.AddButton([](){ world.Run(1); DrawCanvas(); }, "Step", "step_but"); - doc.AddButton([&anim](){ + doc.AddButton([](){ anim_step = 100; TogglePlay(); }, "Fast Forward!", "run_but"); @@ -173,52 +152,12 @@ int main() << "
" << "How many runs? "; - auto run_input = doc.AddTextArea([](const std::string & str){ - size_t num_runs = emp::from_string(str); - world.SetNumRuns(num_runs); - }, "run_count"); - run_input.SetText(emp::to_string(world.GetNumRuns())); - - doc.AddButton([run_input](){ - //size_t num_runs = emp::from_string(run_input.GetText()); - size_t num_runs = world.GetNumRuns(); - auto result_tab = doc.Table("result_tab"); - for (int run_id = 0; run_id < num_runs; run_id++) { - run_list.AddRun(world.GetR(), world.GetU(), world.GetN(), world.GetE()); - - // Update the table. - int line_id = result_tab.GetNumRows(); - result_tab.Rows(line_id+1); - result_tab.GetCell(line_id, 0) << run_id; - result_tab.GetCell(line_id, 1) << world.GetR(); - result_tab.GetCell(line_id, 2) << world.GetU(); - result_tab.GetCell(line_id, 3) << world.GetN(); - result_tab.GetCell(line_id, 4) << world.GetE(); - result_tab.GetCell(line_id, 5) << "Waiting..."; // world.GetE(); - result_tab.GetCell(line_id, 6) << "Waiting..."; // world.CountCoop(); - result_tab.GetCell(line_id, 7) << "Waiting..."; // (world.GetN() - world.CountCoop()); - - // Draw the new table. - result_tab.CellsCSS("border", "1px solid black"); - result_tab.Redraw(); - } - }, "Queue", "queue_but"); + run_list.AddQueueButton([](){return SetupConfig();}, [](){return world.GetE();}); doc << "
"; - auto result_tab = doc.AddTable(1,8, "result_tab"); - result_tab.SetCSS("border-collapse", "collapse"); - result_tab.SetCSS("border", "3px solid black"); - result_tab.CellsCSS("border", "1px solid black"); - - result_tab.GetCell(0,0).SetHeader() << "ID"; - result_tab.GetCell(0,1).SetHeader() << "r"; - result_tab.GetCell(0,2).SetHeader() << "u"; - result_tab.GetCell(0,3).SetHeader() << "N"; - result_tab.GetCell(0,4).SetHeader() << "E"; - result_tab.GetCell(0,5).SetHeader() << "Epoch"; - result_tab.GetCell(0,6).SetHeader() << "Num Coop"; - result_tab.GetCell(0,7).SetHeader() << "Num Defect"; + doc << run_list.GetDiv(); + run_list.BuildTable(); DrawCanvas(); -} +} \ No newline at end of file diff --git a/include/emp/config/SettingConfig.hpp b/include/emp/config/SettingConfig.hpp index 39f1ec651d..0e5fc664c6 100644 --- a/include/emp/config/SettingConfig.hpp +++ b/include/emp/config/SettingConfig.hpp @@ -52,6 +52,7 @@ namespace emp { virtual bool SetValueID(size_t) {return false; } ///< Setup cur value in linked variable virtual bool IsComboSetting() { return false; } ///< Do we have a combo setting? virtual size_t GetID() const { return (size_t) -1; } ///< Combination ID for this setting. + virtual emp::Ptr Clone() const = 0; bool IsOptionMatch(const std::string & test_option) const { return test_option == option; } bool IsFlagMatch(const char test_flag) const { return test_flag == flag; } @@ -70,6 +71,18 @@ namespace emp { emp::Ptr _var=nullptr) ///< Pointer to variable to set (optional) : SettingBase(_name, _desc, _flag, _arg), var_ptr(_var) { } + ~SettingInfo(){if(var_ptr){var_ptr.Delete();}} + + emp::Ptr Clone() const override { + emp::Ptr new_var_ptr = nullptr; + if (var_ptr) { + new_var_ptr = NewPtr(*var_ptr); + } + emp::Ptr> setting_info_ptr = emp::NewPtr>(name, desc, flag, args_label, new_var_ptr); + setting_info_ptr->value = this->value; + return setting_info_ptr; + } + size_t GetSize() const override { return 1; } std::string AsString() const override { return emp::to_string(value); } std::string AsString(size_t id) const override { @@ -99,6 +112,19 @@ namespace emp { emp::Ptr _var=nullptr) ///< Pointer to variable to set (optional) : SettingBase(_name, _desc, _flag, _args), var_ptr(_var) { } + ~ComboSettingInfo(){if(var_ptr){var_ptr.Delete();}} + + emp::Ptr Clone() const override { + emp::Ptr new_var_ptr = nullptr; + if (var_ptr) { + new_var_ptr = NewPtr(*var_ptr); + } + auto csi_ptr = emp::NewPtr>(name, desc, flag, args_label, new_var_ptr); + csi_ptr->values = this->values; + csi_ptr->id = this->id; + return csi_ptr; + } + size_t GetSize() const override { return values.size(); } std::string AsString() const override { std::stringstream ss; @@ -108,8 +134,11 @@ namespace emp { } return ss.str(); } + std::string AsString(size_t id) const override { - return emp::to_string(values[id]); + std::stringstream ss; + ss << values[id]; + return ss.str(); } bool FromString(const std::string_view & input) override { @@ -172,6 +201,23 @@ namespace emp { public: SettingConfig() = default; + SettingConfig(const SettingConfig &other) { + combo_settings.resize(other.combo_settings.size()); + for (const auto &entry : other.setting_map) { + setting_map[entry.first] = other.setting_map.at(entry.first)->Clone(); + } + for (size_t i = 0; i < combo_settings.size(); i++) { + combo_settings[i] = setting_map[other.combo_settings[i]->name]; + } + + action_map = other.action_map; + cur_combo = other.cur_combo; + combo_id = other.combo_id; + unused_args = other.unused_args; + errors = other.errors; + exe_name = other.exe_name; + } + ~SettingConfig() { for (auto [name,ptr] : setting_map) ptr.Delete(); } @@ -184,6 +230,24 @@ namespace emp { bool HasUnusedArgs() const { return unused_args.size(); } bool HasErrors() const { return errors.size(); } + /// Retrieves all of the setting names in config and places into std::vector + std::vector GetSettingMapNames() const { + std::vector result; + for (const auto &p : setting_map) { + result.push_back(p.first); + } + return result; + } + + /// Retrieves all of the setting names in config and places into std::vector + std::vector> GetSettingMapBase() const { + std::vector> result; + for (const auto &p : setting_map) { + result.push_back(p.second); + } + return result; + } + /// Get the current value of a specified setting. template const T & GetValue(const std::string & name) const { @@ -235,6 +299,18 @@ namespace emp { return new_ptr->value; } + /// Add a new setting not linked to a variable + + template + T &AddSetting(const std::string &name, + const std::string &desc = "", + const char option_flag = '\0') { + emp_assert(!emp::Has(setting_map, name)); + auto new_ptr = emp::NewPtr>(name, desc, option_flag, "Value"); + setting_map[name] = new_ptr; + return new_ptr->value; + } + /// Add a new setting of a specified type. Returns the (initially empty) vector of values /// to allow easy setting. /// Example: diff --git a/include/emp/datastructs/map_utils.hpp b/include/emp/datastructs/map_utils.hpp index 331ebe2104..067930bca3 100644 --- a/include/emp/datastructs/map_utils.hpp +++ b/include/emp/datastructs/map_utils.hpp @@ -16,12 +16,13 @@ #include "../base/map.hpp" #include "../base/vector.hpp" +#include "../meta/meta.hpp" namespace emp { /// Take any map type, and run find to determine if a key is present. template - inline bool Has( const MAP_T & in_map, const KEY_T & key ) { + inline auto Has( const MAP_T & in_map, const KEY_T & key ) -> emp::sfinae_decoy { return in_map.find(key) != in_map.end(); } diff --git a/include/emp/datastructs/vector_utils.hpp b/include/emp/datastructs/vector_utils.hpp index 3e7cc87c29..d94e40a3b8 100644 --- a/include/emp/datastructs/vector_utils.hpp +++ b/include/emp/datastructs/vector_utils.hpp @@ -80,6 +80,12 @@ namespace emp { return FindValue(v, val) >= 0; } + // String version to fix template type deduction error + template + bool Has(const emp::vector & v, const std::string & val) { + return FindValue(v, val) >= 0; + } + /// Return number of times a value occurs in a vector template int Count(const emp::vector & vec, const T & val) { diff --git a/include/emp/prefab/QueueManager.hpp b/include/emp/prefab/QueueManager.hpp new file mode 100644 index 0000000000..4950065160 --- /dev/null +++ b/include/emp/prefab/QueueManager.hpp @@ -0,0 +1,331 @@ +/** + * @note This file is part of Empirical, https://github.com/devosoft/Empirical + * @copyright Copyright (C) Michigan State University, MIT Software license; see doc/LICENSE.md + * @date 2021 + * + * @file queue-manager.h + * @brief A web widget for managing queued runs with different settings and displaying real-time statistics + * @note Status: + * @author Juan Chavez and Emily Dolson + * + * This tool provides a web interface that allows users of the web version of a program to queue up multiple + * runs of the program with different settings. It can produce a table that displays the progress of these + * runs and user-defined statistics about them. + * + * QueueManager uses SettingConfig objects to keep track of parameter values for each run. It requires + * a SettingConfig on construction in order to initialize the table header correctly. When runs are queued, + * they will each require their own SettingConfig option. All SettingConfigs used in the same QueueManager + * must have the same parameters, although those parameters can have different values. + * + * Example of constructing a QueueManager: + * + * emp::SettingConfig my_settings; + * my_settings.AddSetting("my_param") = {.5}; + * emp::QueueManager my_queue_manager = emp::QueueManager(my_settings); + * + * Once a QueueManager has been constructed, it can be told to keep track of additional metrics about the + * world via the AddMetric() method. This method takes a function to calculate the metric and a name for + * the column in the table containing the metric as input. + * + * Example of adding metrics: + * + * my_queue_manager.AddMetric([](){return emp::to_string(std::chrono::system_clock::now());}, "Wall time"); + * + * Once a QueueManager has all the metrics set up, a button and text input for queueing runs and a table for + * displaying progress can be added to a web page. To add the button/text input for queueing, the QueueManager + * needs to know two things: where to get the SettingConfig for the newly-queued runs, and how to figure out + * how many epochs (time steps) to run them for. Both can be specified with functions. + * + * Example: + * + * std::function get_setting_config = [](){ + * emp::SettingConfig my_settings; + * my_settings.AddSetting("my_param") = {.9}; + * return my_settings; + * }; + * std::function get_epochs = [](){return 50;}; // Always run for 50 epochs + * + * emp::web::Document doc("emp_base"); + * my_queue_manager.AddQueueButton(get_setting_config, get_epochs); // Add button and text input + * my_queue_manager.BuildTable(); // Add progress table + * doc << my_queue_manager.GetDiv(); // Get the div storing QueueManager stuff and put it on the document + * + * Once your QueueManager is set up, you'll need integrate it with the rest of your code so that it + * appropriately pulls new runs from the queue when appropriate, does set up for each run at the beginning, + * and updates the current epoch and table when appropriate. The way to do this will vary based on the rest + * of your code. Assuming each run of your code involves a loop that occurs for a certain number of time + * steps (which correspond to epochs in the QueueManger), here is a template for how you might set up + * the body of that loop: + * + * if (!run_list.IsEmpty()) { // If there isn't stuff in the queue, support running program normally + * emp::QueueManager::RunInfo & run = run_list.FrontRun(); + * if (run.GetEpoch() == 0) { // Are we starting a new run? + * // Add beginning of run set up here + * // e.g. load in parameter settings from run.GetConfig() + * // If you want to do an initial visualization of something, + * // that goes here too. + * // Make sure to fully clean up from any previous run + * } + * run.IncEpoch(); // Assumes the loop this is inside runs once per time step + * } + * + * // Add the rest of the work for an individual time step of your code here + * // e.g. update the world, draw stuff, etc. + * + * if (!run_list.IsEmpty()) { // If we're running something from the queue, update the table + * run_list.Update(); // and check whether we have finished the current job + * } + * + * For an example of this template in action, see the SpatialCoop2017 demo. + * + * More information is available in this blog post: https://mmore500.com/waves/blog/queuemanager.html (this + * tool was written by Juan Chavez as part of WAVES 2020) + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "emp/base/vector.hpp" +#include "emp/config/SettingConfig.hpp" +#include "emp/math/Random.hpp" +#include "emp/math/math.hpp" +#include "emp/web/Div.hpp" +#include "emp/web/web.hpp" + +namespace emp { + +/// Primary class that establishes queue for runs and processes them accordingly +class QueueManager { + public: + + /// Information of each element within queue. This info represents the information required for each run to be processed. + struct RunInfo { + SettingConfig runinfo_config; // Holds all program-specific settings + size_t id; // The id of this run in the queue + size_t cur_epoch; // The current epoch that this run is on (will either be 0 or `epochs` unless this run is in progress) + size_t epochs; // The number of epochs this run is supposed to run for + + RunInfo(SettingConfig _config, size_t _id) + : runinfo_config(_config), id(_id), cur_epoch(0) { ; } + + /// @returns current epoch + size_t GetEpoch() {return cur_epoch;} + /// @returns number of epochs to run for + size_t GetNEpochs() {return epochs;} + /// Increment current epoch by @param x + void IncEpoch(int x = 1) {cur_epoch += x;} + /// @returns configuration for this run + SettingConfig GetConfig() {return runinfo_config;} + }; + + + private: + SettingConfig queue_config; + std::queue runs; + emp::web::Div display_div; + emp::web::TextArea run_input; + emp::web::Button queue_button; + emp::web::Table display_table; + + emp::vector ordered_metric_names; + emp::vector> metric_funs; + + size_t num_runs = 10; + bool table_built = false; + + public: + + /// @param user_config An example configuration file for this program. Used to initialize table headers. + QueueManager(SettingConfig user_config) : queue_config(user_config) { ; } + + /// @returns True if queue is empty, false if it is not + bool IsEmpty() { + return runs.empty(); + } + + /// @returns Number of runs remaining in the queue + size_t RunsRemaining() { + return runs.size(); + } + + /// Adds new run to queue using settings specified in @param settings. + /// @param epochs indicates how many epochs this run should run for. + void AddRun(SettingConfig settings, size_t epochs) { + RunInfo new_run(settings, runs.size()); + new_run.epochs = epochs; + runs.push(new_run); + } + + /// Removes run from front of queue + void RemoveRun() { + emp_assert(!IsEmpty(), "Queue is empty! Cannot remove!"); + runs.pop(); + } + + /// @returns The a reference to the first run in the queue + /// (i.e. the one that is running currently or, if none are + /// in progress, the next run) + RunInfo& FrontRun() { + emp_assert(!IsEmpty(), "Queue is empty! Cannot access Front!"); + return runs.front(); + } + + /// @returns the Div associated with this QueueManager. + emp::web::Div GetDiv() { + return display_div; + } + + /// Clears the content of the div associated with this QueueManager + void ResetDiv() { + display_div.Clear(); + table_built = false; + } + + /// Adds table containing information for this QueueManager to the + /// Div associated with this QueueManager. + /// + /// @param id optionally allows you to choose the table's element id, + /// for ease of finding it from other parts of your code. + /// + /// Note that you still need to add this div to your document, + /// e.g. `my_doc << my_queue_manager.GetDiv()`; + void BuildTable(const std::string & id = "") { + emp_assert(!table_built && + "Trying to add QueueManager table but QueueManager table already built"); + + // Get parameter names + emp::vector setting_names = queue_config.GetSettingMapNames(); + + // Total number of columns is number of params + number of metrics + + // a column for the run id and a column for the current epoch. + size_t col = 2 + setting_names.size() + ordered_metric_names.size(); + + // Make and style table + display_table = emp::web::Table(1, col, id); + display_table.SetCSS("border-collapse", "collapse"); + display_table.SetCSS("border", "3px solid black"); + display_table.CellsCSS("border", "1px solid black"); + + // Fill out header + display_table.GetCell(0, 0).SetHeader() << "Run"; + int column_count = 1; + for (const auto& p : setting_names) { + display_table.GetCell(0, column_count).SetHeader() << "" << p << ""; + ++column_count; + } + + display_table.GetCell(0, column_count).SetHeader() << "Epoch"; + ++column_count; + + // if adding more features after this point, keep in mind of where + // the col count will be + for (size_t i = 0; i < ordered_metric_names.size(); i++) { + display_table.GetCell(0, column_count + i).SetHeader() << ordered_metric_names[i]; + } + + display_div << display_table; + table_built = true; + } + + /// Helper function to add the last run in the queue to the table + /// Called by queue button. + void AddNewQueuedRunToTable() { + emp_assert(table_built && + "Trying to add run to QueueManager table but table hasn't been initialized. Call BuildTable first."); + + // Update the table. + int line_id = display_table.GetNumRows(); + display_table.Rows(line_id + 1); + int col_count = 0; + display_table.GetCell(line_id, col_count) << runs.back().id; + + // Add correct parameter values + for (auto p : runs.back().runinfo_config.GetSettingMapBase()) { + display_table.GetCell(line_id, ++col_count) << (*p).AsString(); + } + + // Add placeholders for metrics and epoch column + for (int i = 0; i < ordered_metric_names.size() + 1; i++) { + display_table.GetCell(line_id, ++col_count) << "Waiting..."; + } + + // Draw the new table. + display_table.CellsCSS("border", "1px solid black"); + display_table.Redraw(); + } + + /// Update QueueManager to reflect current status of runs and metrics. + /// Handles updating table and updating queue (checking if current run is done). + void Update() { + emp_assert(table_built && + "Trying to update QueueManager table but table hasn't been initialized. Call BuildTable first."); + + size_t id = FrontRun().id; + RunInfo& current_run = FrontRun(); + + size_t n_settings = queue_config.GetSettingMapNames().size(); + + display_table.Freeze(); + display_table.GetCell(id + 1, n_settings+1).ClearChildren() << emp::to_string(current_run.cur_epoch); + + // user function configuration + for (int i = 0; i < metric_funs.size(); i++) { + display_table.GetCell(id + 1, n_settings + 2 + i).ClearChildren() << metric_funs[i](); + } + + + if (current_run.cur_epoch >= current_run.epochs) { // Are we done with this run? + RemoveRun(); // Updates to the next run + } + + display_table.Activate(); + } + + + /// Adds a button and text input to this QueueManager's div, allowing the user to queue runs. + /// The text input allows the user to enter a number specifying the number of runs to be queued. + /// Clicking the button queues the runs. + /// + /// @param get_conf is a function that creates and returns a SettingConfig object containing all of + /// the parameters that should be used for the run to be queued. + /// @param get_epochs is a function that will be used to determine how many epochs/time steps + /// the run is supposed to go for + void AddQueueButton(std::function get_conf, std::function get_epochs) { + run_input = emp::web::TextArea([this](const std::string & str){ + this->num_runs = emp::from_string(str); + }, "run_count"); + run_input.SetText(emp::to_string(num_runs)); + display_div << run_input; + + queue_button = emp::web::Button([this, get_conf, get_epochs]() { + for (int i = 0; i < this->num_runs; i++) { + AddRun(get_conf(), get_epochs()); + AddNewQueuedRunToTable(); + } + }, "Queue", "queue_but"); + display_div << queue_button; + } + + /// Adds new metric to table + /// @param func the function to be called to calculate this metric. Must take + /// no arguments and return a string. If you need arguments, we reccomend using + /// a lambda function and capturing the information you need instead. + /// @param header_name the name this column should have in the table + /// + void AddMetric(std::function func, std::string header_name) { + ordered_metric_names.push_back(header_name); + metric_funs.push_back(func); + + if (table_built) { + size_t col_id = display_table.GetNumCols(); + display_table.Cols(col_id + 1); + display_table.GetCell(0, col_id).SetHeader() << header_name; + } + } + }; +} // namespace emp diff --git a/tests/config/SettingConfig.cpp b/tests/config/SettingConfig.cpp index ae0675c2fe..676fdab819 100644 --- a/tests/config/SettingConfig.cpp +++ b/tests/config/SettingConfig.cpp @@ -2,8 +2,65 @@ #include "third-party/Catch/single_include/catch2/catch.hpp" +#include "emp/datastructs/vector_utils.hpp" #include "emp/config/SettingConfig.hpp" -TEST_CASE("Test SettingConfit", "[config]") +TEST_CASE("Test SettingConfig", "[config]") { +emp::SettingConfig config; + +config.AddSetting("num_runs") = 200; +config.AddComboSetting("pop_size") = { 100,200,400,800 }; + +// Test setup +emp::vector setting_names = config.GetSettingMapNames(); +CHECK(emp::Has(setting_names, "num_runs")); +CHECK(emp::Has(setting_names, "pop_size")); +auto setting_ptrs = config.GetSettingMapBase(); +CHECK(setting_names.size() == 2); + +// Test getting values +CHECK(config.GetValue("num_runs") == 200); +CHECK(config.GetValue("pop_size") == 100); +CHECK(config.MaxValue("pop_size") == 800); + +// Test parsing +emp::vector opts = {"example_prog", "example_unused", "--pop_size", "800,900,1000", "--num_runs", "500"}; +CHECK(config.ProcessOptions(opts)); +CHECK(config.HasUnusedArgs()); +CHECK(config.GetUnusedArgs()[0] == "example_unused"); +CHECK(config.GetExeName() == "example_prog"); +CHECK(!config.HasErrors()); +CHECK(config.CurSettings() == "500,800"); +CHECK(config.CountCombos() == 3); +CHECK(config.CurComboString() == "800"); +CHECK(config.GetComboHeaders() == "pop_size"); +CHECK(config.GetSettingHeaders() == "num_runs,pop_size"); +CHECK(config.GetValue("num_runs") == 500); +CHECK(config.GetValue("pop_size") == 800); + +// Test copy constructor +emp::SettingConfig other_config(config); +CHECK(other_config.HasUnusedArgs()); +CHECK(other_config.GetUnusedArgs()[0] == "example_unused"); +CHECK(other_config.GetExeName() == "example_prog"); +CHECK(!other_config.HasErrors()); +CHECK(other_config.CurSettings() == "500,800"); +CHECK(other_config.CountCombos() == 3); +CHECK(other_config.CurComboString() == "800"); +CHECK(other_config.GetComboHeaders() == "pop_size"); +CHECK(other_config.GetSettingHeaders() == "num_runs,pop_size"); +CHECK(other_config.GetValue("num_runs") == 500); +CHECK(other_config.GetValue("pop_size") == 800); + +// Test getting next combo +config.NextCombo(); +CHECK(config.CurSettings() == "500,900"); + +// Test actions +config.AddAction("test_action", "a test action", 'a', [](){std::cout << "An action happened!" << std::endl;}); + +// Test printing help +config.PrintHelp(); + } \ No newline at end of file diff --git a/tests/web/Makefile b/tests/web/Makefile index 43aa4cbbfd..a7647ddf50 100644 --- a/tests/web/Makefile +++ b/tests/web/Makefile @@ -1,10 +1,10 @@ SHELL := /bin/bash -TEST_NAMES = ConfigPanel Collapse LoadingModal Card CommentBox Modal ToggleSwitch CodeBlock LoadingIcon FontAwesomeIcon ClickCounterDemo ClickCollapseDemo Element TextFeed js_utils JSWrap Widget visualizations +TEST_NAMES = ConfigPanel Collapse LoadingModal Card CommentBox Modal ToggleSwitch CodeBlock LoadingIcon FontAwesomeIcon ClickCounterDemo ClickCollapseDemo Element TextFeed js_utils JSWrap Widget visualizations QueueManager # Currently a couple of the tests won't compile to native so this is a separate list for now. Eventually we should fix # that and just have one list -NATIVE_TEST_NAMES = ConfigPanel LoadingModal Card CommentBox Modal ToggleSwitch CodeBlock LoadingIcon FontAwesomeIcon ClickCounterDemo Element TextFeed js_utils JSWrap Widget +NATIVE_TEST_NAMES = ConfigPanel LoadingModal Card CommentBox Modal ToggleSwitch CodeBlock LoadingIcon FontAwesomeIcon ClickCounterDemo Element TextFeed js_utils JSWrap Widget QueueManager # Flags to use regardless of compiler CFLAGS_all := -Wall -Wno-unused-function -Wno-gnu-zero-variadic-macro-arguments -Wno-dollar-in-identifier-extension -std=c++17 -I../../include/ -I../../ diff --git a/tests/web/QueueManager.cpp b/tests/web/QueueManager.cpp new file mode 100644 index 0000000000..811b6f393f --- /dev/null +++ b/tests/web/QueueManager.cpp @@ -0,0 +1,169 @@ +// This file is part of Empirical, https://github.com/devosoft/Empirical +// Copyright (C) Michigan State University, 2020. +// Released under the MIT Software license; see doc/LICENSE + +#include +#include + +#include "emp/base/assert.hpp" +#include "emp/web/_MochaTestRunner.hpp" +#include "emp/web/Document.hpp" +#include "emp/prefab/QueueManager.hpp" +#include "emp/config/SettingConfig.hpp" +#include "emp/web/web.hpp" + + +struct Test_QueueManager : public emp::web::BaseTest { + + std::function get_setting_config = [](){ + emp::SettingConfig my_settings; + my_settings.AddSetting("my_param") = {9}; + return my_settings; + }; + + std::function get_epochs = [](){return 50;}; + std::function metric_fun = [](){return "Hello";}; + + emp::SettingConfig config; + emp::Ptr queue_manager; + + Test_QueueManager() + : BaseTest({"emp_test_container"}) // we can tell BaseTest that we want to create a set of emp::web::Document + // objects for each given html element ids. + { + + config.AddSetting("my_param") = {6}; + queue_manager.New(config); + + queue_manager->AddMetric(metric_fun, "Hello metric"); + + queue_manager->AddQueueButton(get_setting_config, get_epochs); // Add button and text input + queue_manager->BuildTable("my_table"); // Add progress table + + Doc("emp_test_container") + << queue_manager->GetDiv(); + + // Test adding metric after table built + queue_manager->AddMetric(metric_fun, "Late Hello"); + + queue_manager->AddRun(get_setting_config(), get_epochs()); + queue_manager->AddNewQueuedRunToTable(); + + emp_assert(!queue_manager->IsEmpty()); + emp_assert(queue_manager->RunsRemaining() == 1); + emp_assert(queue_manager->FrontRun().GetEpoch() == 0); + emp_assert(queue_manager->FrontRun().GetConfig().GetValue("my_param") == 9); + emp_assert(queue_manager->FrontRun().GetNEpochs() == 50); + + queue_manager->FrontRun().IncEpoch(); + queue_manager->Update(); + + queue_manager->AddRun(config, 20); + queue_manager->AddNewQueuedRunToTable(); + + emp_assert(queue_manager->RunsRemaining() == 2); + queue_manager->FrontRun().IncEpoch(49); + queue_manager->Update(); + + // Previous run should have finished + emp_assert(queue_manager->RunsRemaining() == 1); + } + + ~Test_QueueManager() { + queue_manager->ResetDiv(); + queue_manager.Delete(); + } + + void Describe() override { + + EM_ASM({ + describe("emp::QueueManager GUI", function() { + + describe("data table", function() { + + it('should exist and be a table', function() { + chai.assert.equal($( "table#my_table" ).length, 1); + }); + + it('should have grandparent #emp_test_container', function() { + const grand_parent_id = $("#my_table").parent().parent().attr("id"); + chai.assert.equal(grand_parent_id, "emp_test_container"); + }); + + it('should have 4 columns', function() { + columns = $("#my_table").find("th>span"); + chai.assert.equal(columns.length, 5); + chai.assert.equal(columns[0].firstChild.textContent, "Run"); + chai.assert.equal(columns[1].firstChild.textContent, "my_param"); + chai.assert.equal(columns[2].firstChild.textContent, "Epoch"); + chai.assert.equal(columns[3].firstChild.textContent, "Hello metric"); + chai.assert.equal(columns[4].firstChild.textContent, "Late Hello"); + }); + + it('should have 3 rows', function() { + rows = $("#my_table").find("tr"); + chai.assert.equal(rows.length, 3); + }); + + it('should have the correct data', function() { + rows = $("#my_table").find("td"); + chai.assert.equal(rows[0].children[0].firstChild.textContent, "0"); + chai.assert.equal(rows[1].children[0].firstChild.textContent, "9"); + chai.assert.equal(rows[2].children[0].firstChild.textContent, "50"); + chai.assert.equal(rows[3].children[0].firstChild.textContent, "Hello"); + chai.assert.equal(rows[4].children[0].firstChild.textContent, "Hello"); + chai.assert.equal(rows[5].children[0].firstChild.textContent, "1"); + chai.assert.equal(rows[6].children[0].firstChild.textContent, "6"); + chai.assert.equal(rows[7].children[0].firstChild.textContent, "Waiting..."); + chai.assert.equal(rows[8].children[0].firstChild.textContent, "Waiting..."); + chai.assert.equal(rows[9].children[0].firstChild.textContent, "Waiting..."); + }); + + + }); + + describe("Queue button", function() { + + it('button should exist', function() { + chai.assert.equal($( "button#queue_but" ).length, 1); + }); + + it('text area should exist', function() { + chai.assert.equal($( "textarea#run_count" ).length, 1); + }); + + it('should respond correctly to clicks', function() { + $( "button#queue_but" ).click(); + rows = $("#my_table").find("tr"); + chai.assert.equal(rows.length, 13); + }); + + }); + + }); + }); + } + +}; + +// Create a MochaTestRunner object in the global namespace so that it hangs around after main finishes. +emp::web::MochaTestRunner test_runner; + +int main() { + + // MochaTestRunner::Initialize will make sure empirical's web environment is initialized, and will + // append a set of div elements (with the given string ids) to the HTML document body. + // Between tests, the MochaTestRunner clears the contents of these div elements. + // Remember, karma is generating our HTML file, so this is useful for attaching any HTML divs that + // you want to interact with in your tests. + test_runner.Initialize({"emp_test_container"}); + + // We add tests to the test runner like this: + // where "Test Element" is the name of the test (and does not need to be unique) + test_runner.AddTest( + "Test QueueManager" + ); + + // Once we add all of the tests we want to run in this file, run them! + test_runner.Run(); +}