From 4a3d8ffd7777bbd430f080bc458c89828553791d Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Fri, 30 Jul 2021 20:57:53 -0400 Subject: [PATCH 01/19] Initial design for `Event` static methods - Added `Event` class - Added basic macros for event parameters and their associated static methods --- .../tau_hybrid_cpp_solver/hybrid_template.cpp | 63 +++++++++++++++++++ .../tau_hybrid_cpp_solver/hybrid_template.h | 27 ++++++++ 2 files changed, 90 insertions(+) diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp index ba42a0427..c8994eaaa 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp @@ -50,5 +50,68 @@ namespace Gillespy #undef RATE_RULE } + + bool Event::trigger(int event_id, double t, const double *S) + { + #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ + case event_id: return (bool) (trigger); + + switch (event_id) + { + GPY_HYBRID_EVENTS + + default: + return false; + } + + #undef EVENT + } + + double Event::delay(int event_id, double t, const double *S) + { + #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ + case event_id: return static_cast(delay); + + switch (event_id) + { + GPY_HYBRID_EVENTS + + default: + return false; + } + + #undef EVENT + } + + double Event::priority(int event_id, double t, const double *S) + { + #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ + case event_id: return static_cast(priority); + + switch (event_id) + { + GPY_HYBRID_EVENTS + + default: + return false; + } + + #undef EVENT + } + + void Event::use_events(std::vector &events) + { + events.clear(); + events.reserve(GPY_HYBRID_NUM_EVENTS); + + #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ + if ((event_id) < GPY_HYBRID_NUM_EVENTS) events[event_id] = Event(event_id, trigger); + GPY_HYBRID_EVENTS + #undef GPY_HYBRID_EVENTS + } + + Event::Event(int event_id, std::initializer_list assignment_ids) + : m_event_id(event_id) + {} } } diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.h index 92f891184..82737f751 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.h @@ -24,8 +24,35 @@ #include "HybridModel.h" #include "template_defaults.h" +#include +#include + +#ifndef GPY_HYBRID_EVENTS +#define GPY_HYBRID_EVENTS +#define GPY_HYBRID_NUM_EVENTS 0 +#endif + +#ifndef GPY_HYBRID_EVENT_ASSIGNMENTS +#define GPY_HYBRID_EVENT_ASSIGNMENTS +#endif + namespace Gillespy::TauHybrid { + using EventAssignment = std::function; + + class Event + { + public: + static void use_events(std::vector &events); + + private: + int m_event_id; + + Event(int event_id, std::initializer_list assignment_ids); + static bool trigger(int event_id, double t, const double *state); + static double delay(int event_id, double t, const double *state); + static double priority(int event_id, double t, const double *state); + }; void map_species_modes(std::vector &species); void map_rate_rules(std::vector &species); From 4878386fb01edbc6e759864bd11f8ea71a1b5c71 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Wed, 4 Aug 2021 18:24:53 -0400 Subject: [PATCH 02/19] Refactor C++ parameters to be vectorized In order to accommodate for SBML features, parameters must be vectorized instead of global variables. - Added `get_parameters` and `get_constants` template functions - Heap-allocates data for each type of variable - Replaced all references in C++ solver to parameters with vectorized equivalents - Replaced call to `sanitized_parameter_names` with manual sanitization in Python model - Instead of `P0, P1, etc` parameters are `P[0], P[1], etc` for variables and `C[0], C[1], etc` for constants - Modified rate rules to accept vectorized parameters - Removed `IPropensityFunction` and replaced with static `propensity` method - Static member of the `Reaction` class --- gillespy2/solvers/cpp/build/template_gen.py | 21 ++++-- gillespy2/solvers/cpp/c_base/model.cpp | 5 ++ gillespy2/solvers/cpp/c_base/model.h | 71 +++++++++++++++---- .../c_base/ode_cpp_solver/ODESimulation.cpp | 24 +------ .../cpp/c_base/ode_cpp_solver/ODESolver.cpp | 2 +- .../c_base/ssa_cpp_solver/SSASimulation.cpp | 29 ++------ .../cpp/c_base/ssa_cpp_solver/SSASolver.cpp | 4 +- .../tau_hybrid_cpp_solver/HybridModel.cpp | 6 +- .../tau_hybrid_cpp_solver/HybridModel.h | 5 +- .../TauHybridSimulation.cpp | 31 +------- .../tau_hybrid_cpp_solver/TauHybridSolver.cpp | 6 +- .../tau_hybrid_cpp_solver/hybrid_template.cpp | 3 +- .../tau_hybrid_cpp_solver/integrator.cpp | 6 +- .../TauLeapingSimulation.cpp | 24 +------ .../TauLeapingSolver.cpp | 2 +- .../solvers/cpp/c_base/template/template.cpp | 56 +++++++-------- .../solvers/cpp/c_base/template/template.h | 17 +++-- .../cpp/c_base/template/template_defaults.h | 2 + .../cpp/c_base/template/template_params.h | 9 --- 19 files changed, 139 insertions(+), 184 deletions(-) diff --git a/gillespy2/solvers/cpp/build/template_gen.py b/gillespy2/solvers/cpp/build/template_gen.py index 31f0f73ad..7ab8ef063 100644 --- a/gillespy2/solvers/cpp/build/template_gen.py +++ b/gillespy2/solvers/cpp/build/template_gen.py @@ -34,11 +34,10 @@ class SanitizedModel: :type model: gillespy2.Model """ reserved_names = { - "vol": "V", "t": "t", } - def __init__(self, model: Model): + def __init__(self, model: Model, variable=False): self.model = model self.species: "OrderedDict[str, Species]" = OrderedDict() @@ -47,10 +46,14 @@ def __init__(self, model: Model): self.species[sanitized_name] = model.get_species(species_name) self.parameters: "OrderedDict[str, Parameter]" = OrderedDict() - self.parameter_names = model.sanitized_parameter_names() + self.parameter_names: "OrderedDict[str, str]" = OrderedDict() + self.parameter_names["vol"] = "P[0]" if variable else "C[0]" + for param_id, param_name in enumerate(model.listOfParameters.keys(), start=1): + if param_name not in self.parameter_names: + self.parameter_names[param_name] = f"P[{param_id}]" if variable else f"C[{param_id}]" for parameter_name, sanitized_name in self.parameter_names.items(): self.parameters[sanitized_name] = model.get_parameter(parameter_name) \ - if parameter_name != "vol" else Parameter(name="V", expression=model.volume) + if parameter_name != "vol" else Parameter(name=sanitized_name, expression=str(model.volume)) base_namespace = { # ORDER IS IMPORTANT HERE! @@ -322,11 +325,15 @@ def template_def_variables(model: SanitizedModel, variable=False) -> "dict[str, parameter_type = "VARIABLE" if variable else "CONSTANT" # Parameter entries, parsed and formatted parameter_set = [] - for param_name, parameter in model.parameters.items(): - parameter_set.append(f"{parameter_type}({param_name},{parameter.expression})") + for param_id, parameter in enumerate(model.parameters.values()): + parameter_set.append(f"{parameter_type}({param_id},{parameter.expression})") return { - "GPY_PARAMETER_VALUES": " ".join(parameter_set) + "GPY_PARAMETER_VALUES": " ".join(parameter_set), + # Currently assumes all variable or all constant. + # For partially variable models, modify to compute these two separately. + "GPY_PARAMETER_NUM_VARIABLES": str(len(parameter_set)) if variable else "0", + "GPY_PARAMETER_NUM_CONSTANTS": str(len(parameter_set)) if not variable else "0", } diff --git a/gillespy2/solvers/cpp/c_base/model.cpp b/gillespy2/solvers/cpp/c_base/model.cpp index 6d142dbc8..f6b6e8e36 100644 --- a/gillespy2/solvers/cpp/c_base/model.cpp +++ b/gillespy2/solvers/cpp/c_base/model.cpp @@ -20,6 +20,11 @@ namespace Gillespy { + int Reaction::s_num_constants; + int Reaction::s_num_variables; + std::shared_ptr Reaction::s_variables; + std::shared_ptr Reaction::s_constants; + template Model::Model( std::vector species_names, diff --git a/gillespy2/solvers/cpp/c_base/model.h b/gillespy2/solvers/cpp/c_base/model.h index 2b2f40577..7632874f6 100644 --- a/gillespy2/solvers/cpp/c_base/model.h +++ b/gillespy2/solvers/cpp/c_base/model.h @@ -18,13 +18,17 @@ #pragma once +#include "template.h" + #include #include #include #include #include -namespace Gillespy { +namespace Gillespy +{ + typedef unsigned int ReactionId; template struct Species { @@ -48,6 +52,59 @@ namespace Gillespy { // List of reactions who's propensities will change when this reaction fires. std::unique_ptr species_change; + + inline static double propensity( + ReactionId reaction_id, + double *state, + double *parameters, + const double *constants) + { + return map_propensity(reaction_id, state, parameters, constants); + } + + inline static double propensity( + ReactionId reaction_id, + unsigned int *state, + double *parameters, + const double *constants) + { + return map_propensity(reaction_id, state, parameters, constants); + } + + inline static double propensity( + ReactionId reaction_id, + int *state, + double *parameters, + const double *constants) + { + return map_propensity(reaction_id, state, parameters, constants); + } + + inline static double propensity(ReactionId reaction_id, double *state) + { + return map_propensity(reaction_id, state, s_variables.get(), s_constants.get()); + } + + inline static double propensity(ReactionId reaction_id, int *state) + { + return map_propensity(reaction_id, state, s_variables.get(), s_constants.get()); + } + + inline static double propensity(ReactionId reaction_id, unsigned int *state) + { + return map_propensity(reaction_id, state, s_variables.get(), s_constants.get()); + } + + inline static void load_parameters() + { + s_variables = std::shared_ptr(get_variables(&s_num_variables)); + s_constants = std::shared_ptr(get_constants(&s_num_constants)); + } + + static int s_num_variables; + static int s_num_constants; + static std::shared_ptr s_variables; + static std::shared_ptr s_constants; }; template @@ -67,16 +124,6 @@ namespace Gillespy { ); }; - class IPropensityFunction { - public: - virtual double evaluate(unsigned int reaction_number, unsigned int *state) = 0; - - virtual double TauEvaluate(unsigned int reaction_number, const std::vector &S) = 0; - virtual double ODEEvaluate(int reaction_number, const std::vector &S) = 0; - - virtual ~IPropensityFunction() {}; - }; - template struct Simulation { int random_seed; @@ -93,8 +140,6 @@ namespace Gillespy { Model *model; - IPropensityFunction *propensity_function; - template friend std::ostream &operator << (std::ostream &os, const Simulation &simulation); void output_results_buffer(std::ostream &os); diff --git a/gillespy2/solvers/cpp/c_base/ode_cpp_solver/ODESimulation.cpp b/gillespy2/solvers/cpp/c_base/ode_cpp_solver/ODESimulation.cpp index ad93d4693..471e00ae0 100644 --- a/gillespy2/solvers/cpp/c_base/ode_cpp_solver/ODESimulation.cpp +++ b/gillespy2/solvers/cpp/c_base/ode_cpp_solver/ODESimulation.cpp @@ -39,25 +39,6 @@ unsigned int number_timesteps = 0; double end_time = 100.0; double increment = 0; -class PropensityFunction : public IPropensityFunction -{ -public: - double evaluate(unsigned int reaction_number, unsigned int *S) - { - return 1.0; - } - - double TauEvaluate(unsigned int reaction_number, const std::vector &S) - { - return 1.0; - } - - double ODEEvaluate(int reaction_number, const std::vector &S) - { - return map_ode_propensity(reaction_number, S); - } -}; - int main(int argc, char *argv[]) { ArgParser parser = ArgParser(argc, argv); @@ -72,6 +53,7 @@ int main(int argc, char *argv[]) { number_timesteps = parser.timesteps; increment = parser.increment; + Reaction::load_parameters(); Model model(species_names, species_populations, reaction_names); add_reactions(model); @@ -80,7 +62,6 @@ int main(int argc, char *argv[]) { random_seed = time(NULL); } - IPropensityFunction *propensity_function = new PropensityFunction(); Simulation simulation; simulation.model = &model; @@ -88,14 +69,11 @@ int main(int argc, char *argv[]) { simulation.random_seed = random_seed; simulation.number_timesteps = number_timesteps; simulation.number_trajectories = number_trajectories; - simulation.propensity_function = propensity_function; init_simulation(&model, simulation); ODESolver(&simulation, increment); simulation.output_results_buffer(std::cout); - delete propensity_function; - return 0; } diff --git a/gillespy2/solvers/cpp/c_base/ode_cpp_solver/ODESolver.cpp b/gillespy2/solvers/cpp/c_base/ode_cpp_solver/ODESolver.cpp index 51758e76a..dba475add 100644 --- a/gillespy2/solvers/cpp/c_base/ode_cpp_solver/ODESolver.cpp +++ b/gillespy2/solvers/cpp/c_base/ode_cpp_solver/ODESolver.cpp @@ -159,7 +159,7 @@ namespace Gillespy for (sunindextype reaction_index = 0; reaction_index < number_reactions; reaction_index++) { // Calculate propensity for each reaction at the current state. - propensity.push_back(simulation->propensity_function->ODEEvaluate((int)reaction_index, current_state)); + propensity.push_back(Reaction::propensity(reaction_index, ydata)); for (sunindextype species_index = 0; species_index < number_species; species_index++) { diff --git a/gillespy2/solvers/cpp/c_base/ssa_cpp_solver/SSASimulation.cpp b/gillespy2/solvers/cpp/c_base/ssa_cpp_solver/SSASimulation.cpp index a3a1c4d76..1682430e7 100644 --- a/gillespy2/solvers/cpp/c_base/ssa_cpp_solver/SSASimulation.cpp +++ b/gillespy2/solvers/cpp/c_base/ssa_cpp_solver/SSASimulation.cpp @@ -38,25 +38,6 @@ unsigned int number_timesteps = 0; double end_time = 0; -class PropensityFunction : public IPropensityFunction -{ -public: - double evaluate(unsigned int reaction_number, unsigned int *S) - { - return map_propensity(reaction_number, S); - } - - double TauEvaluate(unsigned int reaction_number, const std::vector &S) - { - return 1.0; - } - - double ODEEvaluate(int reaction_number, const std::vector &S) - { - return 1.0; - } -}; - int main(int argc, char *argv[]) { //Parse command line arguments @@ -71,7 +52,8 @@ int main(int argc, char *argv[]) end_time = parser.end; number_trajectories = parser.trajectories; number_timesteps = parser.timesteps; - + + Reaction::load_parameters(); Model model(species_names, species_populations, reaction_names); add_reactions(model); @@ -79,8 +61,7 @@ int main(int argc, char *argv[]) { random_seed = time(NULL); } - - IPropensityFunction *propensity_function = new PropensityFunction(); + Simulation simulation; simulation.model = &model; @@ -88,13 +69,11 @@ int main(int argc, char *argv[]) simulation.random_seed = random_seed; simulation.number_timesteps = number_timesteps; simulation.number_trajectories = number_trajectories; - simulation.propensity_function = propensity_function; init_simulation(&model, simulation); ssa_direct(&simulation); simulation.output_results_buffer(std::cout); - - delete propensity_function; + return 0; } diff --git a/gillespy2/solvers/cpp/c_base/ssa_cpp_solver/SSASolver.cpp b/gillespy2/solvers/cpp/c_base/ssa_cpp_solver/SSASolver.cpp index 7ee4f788b..892f5576f 100644 --- a/gillespy2/solvers/cpp/c_base/ssa_cpp_solver/SSASolver.cpp +++ b/gillespy2/solvers/cpp/c_base/ssa_cpp_solver/SSASolver.cpp @@ -96,7 +96,7 @@ namespace Gillespy //calculate initial propensities for (unsigned int reaction_number = 0; reaction_number < ((simulation->model)->number_reactions); reaction_number++) { - propensity_values[reaction_number] = (simulation->propensity_function)->evaluate(reaction_number, current_state); + propensity_values[reaction_number] = Reaction::propensity(reaction_number, current_state); } double propensity_sum; @@ -160,7 +160,7 @@ namespace Gillespy //Recalculate needed propensities for (unsigned int &affected_reaction : reaction.affected_reactions) { - propensity_values[affected_reaction] = (simulation->propensity_function)->evaluate(affected_reaction, current_state); + propensity_values[affected_reaction] = Reaction::propensity(affected_reaction, current_state); } break; diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp index ef488a1e4..68ee4db4b 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp @@ -69,7 +69,7 @@ namespace Gillespy::TauHybrid for (auto &rate_rule : rate_rules) { - sum += rate_rule(t, ode_state); + sum += rate_rule(t, ode_state, Reaction::s_variables.get(), Reaction::s_constants.get()); } for (auto &formula : formulas) @@ -114,7 +114,7 @@ namespace Gillespy::TauHybrid double *ode_state, int *ssa_state) { - return spec_diff * HybridReaction::ode_propensity(rxn_i, ode_state); + return spec_diff * Reaction::propensity(rxn_i, ode_state); }); break; @@ -123,7 +123,7 @@ namespace Gillespy::TauHybrid double *ode_state, int *ssa_state) { - return spec_diff * HybridReaction::ssa_propensity(rxn_i, ssa_state); + return spec_diff * Reaction::propensity(rxn_i, ssa_state); }); break; diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h index 180ba99d1..08709f4c2 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h @@ -41,7 +41,7 @@ namespace Gillespy::TauHybrid { public: std::vector> formulas; - std::vector> rate_rules; + std::vector> rate_rules; double evaluate(double t, double *ode_state, int *ssa_state); }; @@ -93,9 +93,6 @@ namespace Gillespy::TauHybrid SimulationState mode; HybridReaction(); - - static double ode_propensity(ReactionId reaction_number, double *state); - static double ssa_propensity(ReactionId reaction_number, int *state); }; struct HybridSimulation : Simulation diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSimulation.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSimulation.cpp index f693b8424..7d9e6bc53 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSimulation.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSimulation.cpp @@ -37,33 +37,6 @@ bool seed_time = true; double increment = 0; double tau_tol = 0.03; -class PropensityFunction : public IPropensityFunction -{ -public: - - double ODEEvaluate(int reaction_number, const std::vector &S){ - return map_ode_propensity(reaction_number, S); - } - double TauEvaluate(unsigned int reaction_number, const std::vector &S) { - return map_propensity(reaction_number, S); - } - double evaluate(unsigned int reaction_number, unsigned int* S){return 1.0;} -}; - -double Gillespy::TauHybrid::HybridReaction::ode_propensity( - ReactionId reaction_number, - double *state) -{ - return map_ode_propensity(reaction_number, state); -} - -double Gillespy::TauHybrid::HybridReaction::ssa_propensity( - ReactionId reaction_number, - int *state) -{ - return map_propensity(reaction_number, state); -} - int main(int argc, char* argv[]) { ArgParser parser(argc, argv); @@ -75,13 +48,13 @@ int main(int argc, char* argv[]) number_timesteps = parser.timesteps; tau_tol = parser.tau_tol; + Reaction::load_parameters(); Model model(species_names, species_populations, reaction_names); add_reactions(model); if(seed_time){ random_seed = time(NULL); } - IPropensityFunction *propFun = new PropensityFunction(); //Simulation INIT TauHybrid::HybridSimulation simulation(model); simulation.model = &model; @@ -89,13 +62,11 @@ int main(int argc, char* argv[]) simulation.random_seed = random_seed; simulation.number_timesteps = number_timesteps; simulation.number_trajectories = number_trajectories; - simulation.propensity_function = propFun; init_simulation(&model, simulation); Gillespy::TauHybrid::map_species_modes(simulation.species_state); Gillespy::TauHybrid::map_rate_rules(simulation.species_state); // Perform ODE // TauHybrid::TauHybridCSolver(&simulation, tau_tol); simulation.output_results_buffer(std::cout); - delete propFun; return 0; } diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp index bf07ebfc8..772543932 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp @@ -122,17 +122,17 @@ namespace Gillespy::TauHybrid while (integration_guard > 0 && simulation->current_time < simulation->end_time) { // Compute current propensity values based on existing state. - for (int rxn_i = 0; rxn_i < num_reactions; ++rxn_i) + for (unsigned int rxn_i = 0; rxn_i < num_reactions; ++rxn_i) { HybridReaction &rxn = simulation->reaction_state[rxn_i]; double propensity = 0.0; switch (rxn.mode) { case SimulationState::CONTINUOUS: - propensity = HybridReaction::ode_propensity(rxn_i, ¤t_state[0]); + propensity = Reaction::propensity(rxn_i, current_state.data()); break; case SimulationState::DISCRETE: - propensity = HybridReaction::ssa_propensity(rxn_i, ¤t_populations[0]); + propensity = Reaction::propensity(rxn_i, current_populations.data()); break; default: break; diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp index c8994eaaa..ebe7b3f84 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp @@ -45,7 +45,8 @@ namespace Gillespy void map_rate_rules(std::vector &species) { - #define RATE_RULE(spec_id, rate_rule) species[spec_id].diff_equation.rate_rules.push_back([](double t, double *S) { return (rate_rule); }); + #define RATE_RULE(spec_id, rate_rule) species[spec_id].diff_equation.rate_rules.push_back(\ + [](double t, double *S, double *P, const double *C){ return (rate_rule); }); GPY_RATE_RULES #undef RATE_RULE } diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp index 19bff198d..6f5639606 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp @@ -243,18 +243,18 @@ int Gillespy::TauHybrid::rhs(realtype t, N_Vector y, N_Vector ydot, void *user_d } else { - dydt[spec_i] = (*species)[spec_i].diff_equation.evaluate(t, Y, &populations[0]); + dydt[spec_i] = (*species)[spec_i].diff_equation.evaluate(t, Y, populations.data()); } } // Process deterministic propensity state // These updates get written directly to the integrator's concentration state - for (int rxn_i = 0; rxn_i < num_reactions; ++rxn_i) + for (unsigned int rxn_i = 0; rxn_i < num_reactions; ++rxn_i) { switch ((*reactions)[rxn_i].mode) { case SimulationState::DISCRETE: // Process stochastic reaction state by updating the root offset for each reaction. - propensity = HybridReaction::ssa_propensity(rxn_i, &populations[0]); + propensity = Reaction::propensity(rxn_i, populations.data()); dydt_offsets[rxn_i] = propensity; propensities[rxn_i] = propensity; break; diff --git a/gillespy2/solvers/cpp/c_base/tau_leaping_cpp_solver/TauLeapingSimulation.cpp b/gillespy2/solvers/cpp/c_base/tau_leaping_cpp_solver/TauLeapingSimulation.cpp index f544dca2a..955df1f88 100644 --- a/gillespy2/solvers/cpp/c_base/tau_leaping_cpp_solver/TauLeapingSimulation.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_leaping_cpp_solver/TauLeapingSimulation.cpp @@ -39,25 +39,6 @@ unsigned int number_timesteps = 0; double end_time = 0; double tau_tol = 0.03; -class PropensityFunction : public IPropensityFunction -{ -public: - double TauEvaluate(unsigned int reaction_number, const std::vector &S) - { - return map_propensity(reaction_number, S); - } - - double evaluate(unsigned int reaction_number, unsigned int *state) - { - return 1.0; - } - - double ODEEvaluate(int reaction_number, const std::vector &S) - { - return 1.0; - } -}; - int main(int argc, char* argv[]){ ArgParser parser = ArgParser(argc, argv); @@ -73,14 +54,13 @@ int main(int argc, char* argv[]){ number_timesteps = parser.timesteps; tau_tol = parser.tau_tol; + Reaction::load_parameters(); Model model(species_names, species_populations, reaction_names); add_reactions(model); if(seed_time) { random_seed = time(NULL); } - - IPropensityFunction *propFun = new PropensityFunction(); Simulation simulation; simulation.model = &model; @@ -88,13 +68,11 @@ int main(int argc, char* argv[]){ simulation.random_seed = random_seed; simulation.number_timesteps = number_timesteps; simulation.number_trajectories = number_trajectories; - simulation.propensity_function = propFun; init_simulation(&model, simulation); tau_leaper(&simulation, tau_tol); simulation.output_results_buffer(std :: cout); - delete propFun; return 0; } diff --git a/gillespy2/solvers/cpp/c_base/tau_leaping_cpp_solver/TauLeapingSolver.cpp b/gillespy2/solvers/cpp/c_base/tau_leaping_cpp_solver/TauLeapingSolver.cpp index 8374a5661..9a7bf6a4c 100644 --- a/gillespy2/solvers/cpp/c_base/tau_leaping_cpp_solver/TauLeapingSolver.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_leaping_cpp_solver/TauLeapingSolver.cpp @@ -151,7 +151,7 @@ namespace Gillespy //calculate propensities for each step for (unsigned int reaction_number = 0; reaction_number < simulation->model->number_reactions; reaction_number++) { - propensity_values[reaction_number] = simulation->propensity_function->TauEvaluate(reaction_number, current_state); + propensity_values[reaction_number] = Reaction::propensity(reaction_number, current_state.data()); } tau_step = select(*(simulation->model), tau_args, tau_tol, simulation->current_time, save_time, propensity_values, current_state); diff --git a/gillespy2/solvers/cpp/c_base/template/template.cpp b/gillespy2/solvers/cpp/c_base/template/template.cpp index e695937fd..d5353cf84 100644 --- a/gillespy2/solvers/cpp/c_base/template/template.cpp +++ b/gillespy2/solvers/cpp/c_base/template/template.cpp @@ -29,6 +29,8 @@ namespace Gillespy { + static double param_overrides[GPY_PARAMETER_NUM_VARIABLES]; + double populations[GPY_NUM_SPECIES] = GPY_INIT_POPULATIONS; std::vector species_populations( populations, @@ -57,39 +59,33 @@ namespace Gillespy r_names, r_names + sizeof(r_names) / sizeof(std::string)); - #define VARIABLE(name, value) double name = value; - #define CONSTANT(name, value) const double name = value; - GPY_PARAMETER_VALUES - #undef CONSTANT - #undef VARIABLE - - double map_propensity(int reaction_id, const std::vector &S) + double *get_variables(int *num_variables) { - switch (reaction_id) - { - #define PROPENSITY(id, func) case(id): return(func); - GPY_PROPENSITIES - #undef PROPENSITY + double *variables = new double[GPY_PARAMETER_NUM_VARIABLES]; - default: - return -1.0; - } + #define CONSTANT(id, value) + #define VARIABLE(id, value) variables[id] = value; (*num_variables)++; + GPY_PARAMETER_VALUES + #undef VARIABLE + #undef CONSTANT + + return variables; } - double map_propensity(int reaction_id, unsigned int *S) + double *get_constants(int *num_constants) { - switch (reaction_id) - { - #define PROPENSITY(id, func) case(id): return(func); - GPY_PROPENSITIES - #undef PROPENSITY + double *constants = new double[GPY_PARAMETER_NUM_CONSTANTS]; - default: - return -1.0; - } + #define VARIABLE(id, value) + #define CONSTANT(id, value) constants[id] = value; (*num_constants)++; + GPY_PARAMETER_VALUES + #undef CONSTANT + #undef VARIABLE + + return constants; } - double map_propensity(int reaction_id, int *S) + double map_propensity(unsigned int reaction_id, unsigned int *S, double *P, const double *C) { switch (reaction_id) { @@ -102,12 +98,12 @@ namespace Gillespy } } - double map_ode_propensity(int reaction_id, const std::vector &S) + double map_propensity(unsigned int reaction_id, int *S, double *P, const double *C) { switch (reaction_id) { #define PROPENSITY(id, func) case(id): return(func); - GPY_ODE_PROPENSITIES + GPY_PROPENSITIES #undef PROPENSITY default: @@ -115,7 +111,7 @@ namespace Gillespy } } - double map_ode_propensity(int reaction_id, double *S) + double map_propensity(unsigned int reaction_id, double *S, double *P, const double *C) { switch (reaction_id) { @@ -130,8 +126,8 @@ namespace Gillespy void map_variable_parameters(std::stringstream &stream) { - #define VARIABLE(name, value) stream >> (name); - #define CONSTANT(name, value) + #define VARIABLE(id, value) stream >> param_overrides[id]; + #define CONSTANT(id, value) GPY_PARAMETER_VALUES #undef CONSTANT #undef VARIABLE diff --git a/gillespy2/solvers/cpp/c_base/template/template.h b/gillespy2/solvers/cpp/c_base/template/template.h index 54d8d8faa..e0a196664 100644 --- a/gillespy2/solvers/cpp/c_base/template/template.h +++ b/gillespy2/solvers/cpp/c_base/template/template.h @@ -22,23 +22,28 @@ * Includes functions for loading and defining simulation parameters. */ -#include "model.h" +#include +#include namespace Gillespy { + template + struct Model; + extern std::vector species_populations; extern std::vector species_names; extern std::vector reaction_names; - double map_propensity(int reaction_id, const std::vector &state); - double map_propensity(int reaction_id, unsigned int *S); - double map_propensity(int reaction_id, int *S); - double map_ode_propensity(int reaction_id, const std::vector &state); - double map_ode_propensity(int reaction_id, double *S); + double map_propensity(unsigned int reaction_id, int *state, double *parameters, const double *constants); + double map_propensity(unsigned int reaction_id, unsigned int *S, double *parameters, const double *constants); + double map_propensity(unsigned int reaction_id, double *S, double *parameters, const double *constants); template void add_reactions(Model &model); + double *get_variables(int *num_variables); + double *get_constants(int *num_constants); + void map_variable_parameters(std::stringstream &stream); void map_variable_populations(std::stringstream &stream); diff --git a/gillespy2/solvers/cpp/c_base/template/template_defaults.h b/gillespy2/solvers/cpp/c_base/template/template_defaults.h index f8278d95c..1e43bd1b7 100644 --- a/gillespy2/solvers/cpp/c_base/template/template_defaults.h +++ b/gillespy2/solvers/cpp/c_base/template/template_defaults.h @@ -34,6 +34,8 @@ #ifndef GPY_PARAMETER_VALUES #define GPY_PARAMETER_VALUES +#define GPY_PARAMETER_NUM_VARIABLES 0 +#define GPY_PARAMETER_NUM_CONSTANTS 0 #endif #ifndef GPY_INIT_POPULATIONS diff --git a/gillespy2/solvers/cpp/c_base/template/template_params.h b/gillespy2/solvers/cpp/c_base/template/template_params.h index aa9b699ec..f6152278c 100644 --- a/gillespy2/solvers/cpp/c_base/template/template_params.h +++ b/gillespy2/solvers/cpp/c_base/template/template_params.h @@ -41,12 +41,3 @@ #endif using namespace std; - -namespace Gillespy -{ - #define VARIABLE(name, value) extern double name; - #define CONSTANT(name, value) extern const double name; - GPY_PARAMETER_VALUES - #undef CONSTANT - #undef VARIABLE -} From afd9d977dc9e2ead7ffc25bf5138d2ed487c8727 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Thu, 5 Aug 2021 16:47:19 -0400 Subject: [PATCH 03/19] Implement basic `Event` class in C++ - Added `execute` member methods to assign variables to state vecs - Added `use_state` member method to save the current state for later execution - Implemented rule of 5 for `Event` class - Implemented template injection functions - Added new implementation of `use_events`, no longer uses lambdas - Added new `assign` static method to dispatch event assignments --- .../tau_hybrid_cpp_solver/HybridModel.cpp | 559 +++++++++++------- .../tau_hybrid_cpp_solver/HybridModel.h | 245 +++++--- .../tau_hybrid_cpp_solver/hybrid_template.cpp | 29 +- .../tau_hybrid_cpp_solver/hybrid_template.h | 23 +- 4 files changed, 538 insertions(+), 318 deletions(-) diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp index 68ee4db4b..6360e9b96 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp @@ -18,260 +18,423 @@ #include "HybridModel.h" -namespace Gillespy::TauHybrid -{ +#include - HybridReaction::HybridReaction() - : mode(SimulationState::DISCRETE), - base_reaction(nullptr) +namespace Gillespy +{ + namespace TauHybrid { - // Empty constructor body - } + Event::Event(int event_id, std::initializer_list assignment_ids) + : m_event_id(event_id), + m_assignments(assignment_ids) + {} + + Event::Event(const Event &old_event) + : m_num_state(old_event.m_num_state), + m_num_variables(old_event.m_num_variables), + m_event_id(old_event.m_event_id), + // Yes, this invokes the copy constructor. Yes, this is intentional. + m_assignments(old_event.m_assignments) + { + if (old_event.m_state != nullptr && m_num_state > 0) + { + m_state = new double[m_num_state]; + std::memcpy(m_state, old_event.m_state, m_num_state); + } - HybridSpecies::HybridSpecies() - : user_mode(SimulationState::DYNAMIC), - partition_mode(SimulationState::DISCRETE), - switch_tol(0.03), - switch_min(0) - { - // Empty constructor body - } + if (old_event.m_variables != nullptr && m_num_variables > 0) + { + m_variables = new double[m_num_variables]; + std::memcpy(m_variables, old_event.m_variables, m_num_variables); + } + } - HybridSimulation::HybridSimulation() - : Simulation() - { - // Empty constructor body - } + Event::Event(Event &&old_event) noexcept + : m_num_state(old_event.m_num_state), + m_state(old_event.m_state), + m_num_variables(old_event.m_num_variables), + m_variables(old_event.m_variables), + m_event_id(old_event.m_event_id), + m_assignments(std::move(old_event.m_assignments)) + { + old_event.m_num_state = 0; + old_event.m_state = nullptr; + old_event.m_num_variables = 0; + old_event.m_variables = nullptr; + } - HybridSimulation::HybridSimulation(const Model &model) - : Simulation(), - species_state(model.number_species), - reaction_state(model.number_reactions) - { - for (int spec_i = 0; spec_i < model.number_species; ++spec_i) + Event &Event::operator=(const Event &old_event) { - species_state[spec_i].base_species = &model.species[spec_i]; + m_assignments = old_event.m_assignments; + + if (this != &old_event) + { + if (old_event.m_state != nullptr) + { + // If the containers are not of equal size, then we cannot reuse heap data. + if (m_num_state != old_event.m_num_state) + { + delete[] m_state; + m_num_state = old_event.m_num_state; + m_state = new double[m_num_state]; + } + std::memcpy(m_state, old_event.m_state, m_num_state); + } + + if (old_event.m_variables != nullptr) + { + if (m_num_variables != old_event.m_num_variables) + { + delete[] m_variables; + m_num_variables = old_event.m_num_variables; + m_variables = new double[m_num_variables]; + } + std::memcpy(m_variables, old_event.m_variables, m_num_variables); + } + } + + return *this; } - for (int rxn_i = 0; rxn_i < model.number_reactions; ++rxn_i) + Event &Event::operator=(Event &&old_event) noexcept { - reaction_state[rxn_i].base_reaction = &model.reactions[rxn_i]; + m_assignments = std::move(old_event.m_assignments); + + if (this != &old_event) + { + m_num_state = old_event.m_num_state; + m_state = old_event.m_state; + old_event.m_num_state = 0; + old_event.m_state = nullptr; + + m_num_variables = old_event.m_num_variables; + m_variables = old_event.m_variables; + old_event.m_num_variables = 0; + old_event.m_variables = nullptr; + } + + return *this; } - } + Event::~Event() + { + delete[] m_state; + delete[] m_variables; + } - double DifferentialEquation::evaluate( - const double t, - double *ode_state, - int *ssa_state) - { - double sum = 0.0; + void Event::use_state( + const double *state, int num_state, + const double *variables, int num_variables) + { + if (m_state == nullptr || m_num_state != num_state) + { + delete[] m_state; + m_num_state = num_state; + m_state = new double[num_state]; + } + + if (m_variables == nullptr || m_num_variables != num_variables) + { + delete[] m_variables; + m_num_variables = num_variables; + m_variables = new double[num_variables]; + } + + std::memcpy(m_state, state, num_state); + std::memcpy(m_variables, variables, num_variables); + } - for (auto &rate_rule : rate_rules) + void Event::execute(double t, EventOutput output) const { - sum += rate_rule(t, ode_state, Reaction::s_variables.get(), Reaction::s_constants.get()); + Event::assign(m_event_id, t, output); } - for (auto &formula : formulas) + void Event::execute(double t, double *state, double *variables, const double *constants) { - sum += formula(ode_state, ssa_state); + execute(t, EventOutput + { + state, + variables, + m_state, + m_variables, + constants + }); } - return sum; - } + HybridReaction::HybridReaction() + : mode(SimulationState::DISCRETE), + base_reaction(nullptr) + { + // Empty constructor body + } - void create_differential_equations( - std::vector &species, - std::vector &reactions) - { - // For now, differential equations are generated from scratch. - // It may be more efficient to determine which formulas need to change. - // Until then, the compound formulas in every species are cleared. - for (HybridSpecies &spec : species) { - spec.diff_equation.formulas.clear(); + HybridSpecies::HybridSpecies() + : user_mode(SimulationState::DYNAMIC), + partition_mode(SimulationState::DISCRETE), + switch_tol(0.03), + switch_min(0) + { + // Empty constructor body + } + + HybridSimulation::HybridSimulation() + : Simulation() + { + // Empty constructor body } - for (int rxn_i = 0; rxn_i < reactions.size(); ++rxn_i) { - HybridReaction rxn = reactions[rxn_i]; - if (rxn.mode == SimulationState::DISCRETE) { - continue; + HybridSimulation::HybridSimulation(const Model &model) + : Simulation(), + species_state(model.number_species), + reaction_state(model.number_reactions) + { + for (int spec_i = 0; spec_i < model.number_species; ++spec_i) + { + species_state[spec_i].base_species = &model.species[spec_i]; + } + + for (int rxn_i = 0; rxn_i < model.number_reactions; ++rxn_i) + { + reaction_state[rxn_i].base_reaction = &model.reactions[rxn_i]; + } + } + + + double DifferentialEquation::evaluate( + const double t, + double *ode_state, + int *ssa_state) + { + double sum = 0.0; + + for (auto &rate_rule : rate_rules) + { + sum += rate_rule(t, ode_state, Reaction::s_variables.get(), Reaction::s_constants.get()); } - for (int spec_i = 0; spec_i < species.size(); ++spec_i) { - // A species change of 0 indicates that this species is not a dependency for this reaction. - if (rxn.base_reaction->species_change[spec_i] == 0) { + for (auto &formula : formulas) + { + sum += formula(ode_state, ssa_state); + } + + return sum; + } + + + void create_differential_equations( + std::vector &species, + std::vector &reactions) + { + // For now, differential equations are generated from scratch. + // It may be more efficient to determine which formulas need to change. + // Until then, the compound formulas in every species are cleared. + for (HybridSpecies &spec : species) + { + spec.diff_equation.formulas.clear(); + } + + for (int rxn_i = 0; rxn_i < reactions.size(); ++rxn_i) + { + HybridReaction rxn = reactions[rxn_i]; + if (rxn.mode == SimulationState::DISCRETE) + { continue; } - HybridSpecies &spec = species[spec_i]; - auto &formula_set = spec.diff_equation.formulas; - int spec_diff = rxn.base_reaction->species_change[spec_i]; - - switch (spec.partition_mode) { - case SimulationState::CONTINUOUS: - formula_set.push_back([rxn_i, spec_diff]( - double *ode_state, - int *ssa_state) + for (int spec_i = 0; spec_i < species.size(); ++spec_i) + { + // A species change of 0 indicates that this species is not a dependency for this reaction. + if (rxn.base_reaction->species_change[spec_i] == 0) { - return spec_diff * Reaction::propensity(rxn_i, ode_state); - }); - break; + continue; + } - case SimulationState::DISCRETE: - formula_set.push_back([rxn_i, spec_diff]( - double *ode_state, - int *ssa_state) - { - return spec_diff * Reaction::propensity(rxn_i, ssa_state); - }); - break; + HybridSpecies &spec = species[spec_i]; + auto &formula_set = spec.diff_equation.formulas; + int spec_diff = rxn.base_reaction->species_change[spec_i]; - default: - break; + switch (spec.partition_mode) + { + case SimulationState::CONTINUOUS: + formula_set.push_back([rxn_i, spec_diff]( + double *ode_state, + int *ssa_state) + { + return spec_diff * Reaction::propensity(rxn_i, ode_state); + }); + break; + + case SimulationState::DISCRETE: + formula_set.push_back([rxn_i, spec_diff]( + double *ode_state, + int *ssa_state) + { + return spec_diff * Reaction::propensity(rxn_i, ssa_state); + }); + break; + + default: + break; + } } } } - } - // Helper method to flag reactions that can be processed deterministically (continuous change) - // without exceeding the user-supplied tolerance - std::set flag_det_rxns( - std::vector &reactions, - std::vector &species) - { - int num_reactions = reactions.size(); - int num_species = species.size(); - std::set det_rxns; - - for (int rxn_i = 0; rxn_i < reactions.size(); ++rxn_i) { - // start with the assumption that reaction is determinstic - HybridReaction &rxn = reactions[rxn_i]; - rxn.mode = SimulationState::CONTINUOUS; - - // iterate through the dependent species of this reaction - // Loop breaks if we've already determined that it is to be marked as discrete. - for (int spec_i = 0; spec_i < num_species && rxn.mode == SimulationState::CONTINUOUS; ++spec_i) { - // Reaction has a dependency on a species if its dx is positive or negative. - // Any species with "dependency" change of 0 is by definition not a dependency. - if (rxn.base_reaction->species_change[spec_i] == 0) { - continue; - } + // Helper method to flag reactions that can be processed deterministically (continuous change) + // without exceeding the user-supplied tolerance + std::set flag_det_rxns( + std::vector &reactions, + std::vector &species) + { + int num_reactions = reactions.size(); + int num_species = species.size(); + std::set det_rxns; - // if any of the dependencies are set by the user as discrete OR - // have been set as dynamic and has not been flagged as deterministic, - // allow it to be modelled discretely - if (species[spec_i].user_mode == SimulationState::DYNAMIC) { - rxn.mode = species[spec_i].partition_mode; + for (int rxn_i = 0; rxn_i < reactions.size(); ++rxn_i) + { + // start with the assumption that reaction is determinstic + HybridReaction &rxn = reactions[rxn_i]; + rxn.mode = SimulationState::CONTINUOUS; + + // iterate through the dependent species of this reaction + // Loop breaks if we've already determined that it is to be marked as discrete. + for (int spec_i = 0; spec_i < num_species && rxn.mode == SimulationState::CONTINUOUS; ++spec_i) + { + // Reaction has a dependency on a species if its dx is positive or negative. + // Any species with "dependency" change of 0 is by definition not a dependency. + if (rxn.base_reaction->species_change[spec_i] == 0) + { + continue; + } + + // if any of the dependencies are set by the user as discrete OR + // have been set as dynamic and has not been flagged as deterministic, + // allow it to be modelled discretely + if (species[spec_i].user_mode == SimulationState::DYNAMIC) + { + rxn.mode = species[spec_i].partition_mode; + } else + { + rxn.mode = species[spec_i].user_mode; + } } - else { - rxn.mode = species[spec_i].user_mode; + + if (rxn.mode == SimulationState::CONTINUOUS) + { + det_rxns.insert(rxn_i); } } - if (rxn.mode == SimulationState::CONTINUOUS) { - det_rxns.insert(rxn_i); - } + return det_rxns; } - return det_rxns; - } + void partition_species( + std::vector &reactions, + std::vector &species, + const std::vector &propensity_values, + std::vector &curr_state, + double tau_step, + const TauArgs &tauArgs) + { + // coefficient of variance- key:species id, value: cv + std::map cv; + // means + std::map means; + // standard deviation + std::map sd; + + // Initialize means and sd's + for (int spec_i = 0; spec_i < species.size(); ++spec_i) + { + HybridSpecies &spec = species[spec_i]; - void partition_species( - std::vector &reactions, - std::vector &species, - const std::vector &propensity_values, - std::vector &curr_state, - double tau_step, - const TauArgs &tauArgs) - { - // coefficient of variance- key:species id, value: cv - std::map cv; - // means - std::map means; - // standard deviation - std::map sd; - - // Initialize means and sd's - for (int spec_i = 0; spec_i < species.size(); ++spec_i) { - HybridSpecies &spec = species[spec_i]; - - if (spec.user_mode == SimulationState::DYNAMIC) { - means.insert({ spec_i, curr_state[spec_i] }); - sd.insert({ spec_i, 0 }); + if (spec.user_mode == SimulationState::DYNAMIC) + { + means.insert({spec_i, curr_state[spec_i]}); + sd.insert({spec_i, 0}); + } } - } - // calculate means and standard deviations for dynamic-mode species involved in reactions - for (int rxn_i = 0; rxn_i < reactions.size(); ++rxn_i) { - HybridReaction &rxn = reactions[rxn_i]; + // calculate means and standard deviations for dynamic-mode species involved in reactions + for (int rxn_i = 0; rxn_i < reactions.size(); ++rxn_i) + { + HybridReaction &rxn = reactions[rxn_i]; - for (int spec_i = 0; spec_i < species.size(); ++spec_i) { - // Only dynamic species whose mean/SD is requested are to be considered. - if (means.count(spec_i) <= 0) { - continue; - } - // Selected species is either a reactant or a product, depending on whether - // dx is positive or negative. - // 0-dx species are not dependencies of this reaction, so dx == 0 is ignored. - int spec_dx = rxn.base_reaction->species_change[spec_i]; - if (spec_dx < 0) { - // Selected species is a reactant. - means[spec_i] -= (tau_step * propensity_values[rxn_i] * spec_dx); - sd[spec_i] += (tau_step * propensity_values[rxn_i] * std::pow(spec_dx, 2)); - } - else if (spec_dx > 0) { - // Selected species is a product. - HybridSpecies &product = species[spec_i]; - means[spec_i] += (tau_step * propensity_values[rxn_i] * spec_dx); - sd[spec_i] += (tau_step * propensity_values[rxn_i] * std::pow(spec_dx, 2)); + for (int spec_i = 0; spec_i < species.size(); ++spec_i) + { + // Only dynamic species whose mean/SD is requested are to be considered. + if (means.count(spec_i) <= 0) + { + continue; + } + // Selected species is either a reactant or a product, depending on whether + // dx is positive or negative. + // 0-dx species are not dependencies of this reaction, so dx == 0 is ignored. + int spec_dx = rxn.base_reaction->species_change[spec_i]; + if (spec_dx < 0) + { + // Selected species is a reactant. + means[spec_i] -= (tau_step * propensity_values[rxn_i] * spec_dx); + sd[spec_i] += (tau_step * propensity_values[rxn_i] * std::pow(spec_dx, 2)); + } else if (spec_dx > 0) + { + // Selected species is a product. + HybridSpecies &product = species[spec_i]; + means[spec_i] += (tau_step * propensity_values[rxn_i] * spec_dx); + sd[spec_i] += (tau_step * propensity_values[rxn_i] * std::pow(spec_dx, 2)); + } } } - } - - // calculate coefficient of variation using means and sd - for (int spec_i = 0; spec_i < species.size(); ++spec_i) { - if (means.count(spec_i) <= 0) { - continue; - } - HybridSpecies &spec = species[spec_i]; - if (spec.switch_min == 0) + // calculate coefficient of variation using means and sd + for (int spec_i = 0; spec_i < species.size(); ++spec_i) { - // (default value means switch min not set, use switch tol) - if (means[spec_i] > 0) { - cv[spec_i] = (sd[spec_i] /means[spec_i]); - } - else { - cv[spec_i] = 1; + if (means.count(spec_i) <= 0) + { + continue; } - spec.partition_mode = cv[spec_i] < spec.switch_tol - ? SimulationState::CONTINUOUS - : SimulationState::DISCRETE; - } - else - { - spec.partition_mode = means[spec_i] > spec.switch_min - ? SimulationState::CONTINUOUS - : SimulationState::DISCRETE; + HybridSpecies &spec = species[spec_i]; + if (spec.switch_min == 0) + { + // (default value means switch min not set, use switch tol) + if (means[spec_i] > 0) + { + cv[spec_i] = (sd[spec_i] / means[spec_i]); + } else + { + cv[spec_i] = 1; + } + + spec.partition_mode = cv[spec_i] < spec.switch_tol + ? SimulationState::CONTINUOUS + : SimulationState::DISCRETE; + } else + { + spec.partition_mode = means[spec_i] > spec.switch_min + ? SimulationState::CONTINUOUS + : SimulationState::DISCRETE; + } } } - } - void update_species_state( - std::vector &species, - std::vector ¤t_state) - { - for (int spec_i = 0; spec_i < species.size(); ++spec_i) { - switch (species[spec_i].partition_mode) { - case SimulationState::DISCRETE: - current_state[spec_i] = std::floor(current_state[spec_i]); - break; - default: - break; + void update_species_state( + std::vector &species, + std::vector ¤t_state) + { + for (int spec_i = 0; spec_i < species.size(); ++spec_i) + { + switch (species[spec_i].partition_mode) + { + case SimulationState::DISCRETE: + current_state[spec_i] = std::floor(current_state[spec_i]); + break; + default: + break; + } } } - } + } } diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h index 08709f4c2..f45aa5c99 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h @@ -18,110 +18,159 @@ #pragma once -#include #include "model.h" #include "tau.h" +#include + #define GPY_HYBRID_ABSTOL 1e-8 #define GPY_HYBRID_RELTOL 1e-8 -namespace Gillespy::TauHybrid +namespace Gillespy { - - typedef int ReactionId; - - /* Gillespy::TauHybrid::DiffEquation - * A vector containing evaluable functions, which accept integrator state and return propensities. - * - * The vector is understood to be an arbitrarily sized collection of propensity evaluations, - * each weighted by some individual, constant factor. - * The sum of evaulations of all collected functions is interpreted to be the dydt of that state. - */ - struct DifferentialEquation - { - public: - std::vector> formulas; - std::vector> rate_rules; - double evaluate(double t, double *ode_state, int *ssa_state); - }; - - enum SimulationState : unsigned int - { - CONTINUOUS = 0, - DISCRETE = 1, - DYNAMIC = 2 - }; - - struct HybridSpecies + namespace TauHybrid { - Species *base_species; - - // allows the user to specify if a species' population should definitely be modeled continuously or - // discretely - // CONTINUOUS or DISCRETE - // otherwise, mode will be determined by the program (DYNAMIC) - // if no choice is made, DYNAMIC will be assumed - SimulationState user_mode; - - // during simulation execution, a species will fall into either of the two categories, CONTINUOUS or DISCRETE - // this is pre-determined only if the user_mode specifies CONTINUOUS or DISCRETE. - // otherwise, if DYNAMIC is specified, partition_mode will be continually calculated throughout the simulation - // according to standard deviation and coefficient of variance. - SimulationState partition_mode; - - // Tolerance level for considering a dynamic species deterministically, value is compared - // to an estimated sd/mean population of a species after a given time step. - // This value will be used if a switch_min is not provided. The default value is 0.03 - double switch_tol = 0.03; - - //Minimum population value at which species will be represented as continuous. - // If a value is given, switch_min will be used instead of switch_tol. - unsigned int switch_min = 0; - - DifferentialEquation diff_equation; - - // Boundary condition species are not directly updated by reactions, while standard ones are. - // If `boundary_condition` is true, then reactants are not consumed, and products are not produced. - bool boundary_condition = false; - - HybridSpecies(); - }; - - struct HybridReaction - { - Reaction *base_reaction; - SimulationState mode; - - HybridReaction(); - }; - - struct HybridSimulation : Simulation - { - std::vector species_state; - std::vector reaction_state; - - HybridSimulation(); - HybridSimulation(const Model &model); - }; - - std::set flag_det_rxns( - std::vector &reactions, - std::vector &species); - - void partition_species( - std::vector &reactions, - std::vector &species, - const std::vector &propensity_values, - std::vector &curr_state, - double tau_step, - const TauArgs &TauArgs); - - void update_species_state( - std::vector &species, - std::vector ¤t_state); - - void create_differential_equations( - std::vector &species, - std::vector &reactions); - + typedef int ReactionId; + + struct EventOutput + { + double *species_out; + double *variable_out; + const double *species; + const double *variables; + const double *constants; + }; + + class Event + { + public: + static void use_events(std::vector &events); + + void execute(double t, double *state, double *variables, const double *constants); + void execute(double t, EventOutput output) const; + void use_state(const double *state, int num_state, const double *variables, int num_variables); + + ~Event(); + Event(const Event &); + Event &operator=(const Event &); + Event(Event &&) noexcept; + Event &operator=(Event &&) noexcept; + + private: + int m_num_state = 0; + double *m_state = nullptr; + + int m_num_variables = 0; + double *m_variables = nullptr; + + int m_event_id; + std::vector m_assignments; + + Event(int event_id, std::initializer_list assignment_ids); + + static bool trigger(int event_id, double t, const double *state); + + static double delay(int event_id, double t, const double *state); + + static double priority(int event_id, double t, const double *state); + + static void assign(int event_id, double t, EventOutput output); + }; + + /* Gillespy::TauHybrid::DiffEquation + * A vector containing evaluable functions, which accept integrator state and return propensities. + * + * The vector is understood to be an arbitrarily sized collection of propensity evaluations, + * each weighted by some individual, constant factor. + * The sum of evaulations of all collected functions is interpreted to be the dydt of that state. + */ + struct DifferentialEquation + { + public: + std::vector> formulas; + std::vector> rate_rules; + + double evaluate(double t, double *ode_state, int *ssa_state); + }; + + enum SimulationState : unsigned int + { + CONTINUOUS = 0, + DISCRETE = 1, + DYNAMIC = 2 + }; + + struct HybridSpecies + { + Species *base_species; + + // allows the user to specify if a species' population should definitely be modeled continuously or + // discretely + // CONTINUOUS or DISCRETE + // otherwise, mode will be determined by the program (DYNAMIC) + // if no choice is made, DYNAMIC will be assumed + SimulationState user_mode; + + // during simulation execution, a species will fall into either of the two categories, CONTINUOUS or DISCRETE + // this is pre-determined only if the user_mode specifies CONTINUOUS or DISCRETE. + // otherwise, if DYNAMIC is specified, partition_mode will be continually calculated throughout the simulation + // according to standard deviation and coefficient of variance. + SimulationState partition_mode; + + // Tolerance level for considering a dynamic species deterministically, value is compared + // to an estimated sd/mean population of a species after a given time step. + // This value will be used if a switch_min is not provided. The default value is 0.03 + double switch_tol = 0.03; + + //Minimum population value at which species will be represented as continuous. + // If a value is given, switch_min will be used instead of switch_tol. + unsigned int switch_min = 0; + + DifferentialEquation diff_equation; + + // Boundary condition species are not directly updated by reactions, while standard ones are. + // If `boundary_condition` is true, then reactants are not consumed, and products are not produced. + bool boundary_condition = false; + + HybridSpecies(); + }; + + struct HybridReaction + { + Reaction *base_reaction; + SimulationState mode; + + HybridReaction(); + }; + + struct HybridSimulation : Simulation + { + std::vector species_state; + std::vector reaction_state; + + HybridSimulation(); + + HybridSimulation(const Model &model); + }; + + std::set flag_det_rxns( + std::vector &reactions, + std::vector &species); + + void partition_species( + std::vector &reactions, + std::vector &species, + const std::vector &propensity_values, + std::vector &curr_state, + double tau_step, + const TauArgs &TauArgs); + + void update_species_state( + std::vector &species, + std::vector ¤t_state); + + void create_differential_equations( + std::vector &species, + std::vector &reactions); + } } diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp index ebe7b3f84..37d8760ac 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp @@ -106,13 +106,32 @@ namespace Gillespy events.reserve(GPY_HYBRID_NUM_EVENTS); #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ - if ((event_id) < GPY_HYBRID_NUM_EVENTS) events[event_id] = Event(event_id, trigger); + events[event_id] = Event(event_id, targets); GPY_HYBRID_EVENTS - #undef GPY_HYBRID_EVENTS + #undef EVENT } - Event::Event(int event_id, std::initializer_list assignment_ids) - : m_event_id(event_id) - {} + void Event::assign(int assign_id, double t, EventOutput output) + { + const double *S = output.species; + const double *P = output.variables; + const double *C = output.constants; + + #define SPECIES_ASSIGNMENT(id, spec_id, expr) \ + case id: output.species_out[spec_id] = expr; + #define VARIABLE_ASSIGNMENT(id, var_id, expr) \ + case id: output.variable_out[var_id] = expr; + + switch (assign_id) + { + GPY_HYBRID_EVENT_ASSIGNMENTS + + default: + return; + } + + #undef VARIABLE_ASSIGNMENT + #undef SPECIES_ASSIGNMENT + } } } diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.h index 82737f751..a853eb305 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.h @@ -34,27 +34,16 @@ #ifndef GPY_HYBRID_EVENT_ASSIGNMENTS #define GPY_HYBRID_EVENT_ASSIGNMENTS +#define GPY_HYBRID_NUM_EVENT_ASSIGNMENTS 0 #endif -namespace Gillespy::TauHybrid +namespace Gillespy { - using EventAssignment = std::function; - - class Event + namespace TauHybrid { - public: - static void use_events(std::vector &events); - - private: - int m_event_id; - - Event(int event_id, std::initializer_list assignment_ids); - static bool trigger(int event_id, double t, const double *state); - static double delay(int event_id, double t, const double *state); - static double priority(int event_id, double t, const double *state); - }; - void map_species_modes(std::vector &species); - void map_rate_rules(std::vector &species); + void map_species_modes(std::vector &species); + void map_rate_rules(std::vector &species); + } } From 743a59c40fbbd91e85112bf14c4e1fe7ecc077f4 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Thu, 5 Aug 2021 16:53:56 -0400 Subject: [PATCH 04/19] Python-side template macros for events - Added `species_id` and `param_id` dicts to `SanitizedModel` class - Maps un-sanitized species/parameter names to their IDs - Modified `TauHybridCSolver` options to provide definitions - Defs for: events, # of events, assignments, # of event assignments --- gillespy2/solvers/cpp/build/template_gen.py | 7 +- gillespy2/solvers/cpp/tau_hybrid_c_solver.py | 72 ++++++++++++++++++-- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/gillespy2/solvers/cpp/build/template_gen.py b/gillespy2/solvers/cpp/build/template_gen.py index 7ab8ef063..24d38e059 100644 --- a/gillespy2/solvers/cpp/build/template_gen.py +++ b/gillespy2/solvers/cpp/build/template_gen.py @@ -42,15 +42,20 @@ def __init__(self, model: Model, variable=False): self.species: "OrderedDict[str, Species]" = OrderedDict() self.species_names = model.sanitized_species_names() - for species_name, sanitized_name in self.species_names.items(): + self.species_id: "OrderedDict[str, int]" = OrderedDict() + for spec_id, spec_entry in enumerate(self.species_names.items()): + species_name, sanitized_name = spec_entry self.species[sanitized_name] = model.get_species(species_name) + self.species_id[species_name] = spec_id self.parameters: "OrderedDict[str, Parameter]" = OrderedDict() self.parameter_names: "OrderedDict[str, str]" = OrderedDict() self.parameter_names["vol"] = "P[0]" if variable else "C[0]" + self.parameter_id: "OrderedDict[str, int]" = OrderedDict() for param_id, param_name in enumerate(model.listOfParameters.keys(), start=1): if param_name not in self.parameter_names: self.parameter_names[param_name] = f"P[{param_id}]" if variable else f"C[{param_id}]" + self.parameter_id[param_name] = param_id for parameter_name, sanitized_name in self.parameter_names.items(): self.parameters[sanitized_name] = model.get_parameter(parameter_name) \ if parameter_name != "vol" else Parameter(name=sanitized_name, expression=str(model.volume)) diff --git a/gillespy2/solvers/cpp/tau_hybrid_c_solver.py b/gillespy2/solvers/cpp/tau_hybrid_c_solver.py index 747511f47..fa861c116 100644 --- a/gillespy2/solvers/cpp/tau_hybrid_c_solver.py +++ b/gillespy2/solvers/cpp/tau_hybrid_c_solver.py @@ -12,15 +12,15 @@ class TauHybridCSolver(GillesPySolver, CSolver): target = "hybrid" @classmethod - def __create_options(cls, model: "SanitizedModel") -> "SanitizedModel": + def __create_options(cls, sanitized_model: "SanitizedModel") -> "SanitizedModel": """ Populate the given list of species modes into a set of template macro definitions. Generated options are specific to the Tau Hybrid solver, and get passed as custom definitions to the build engine. - :param model: Sanitized model containing sanitized species definitions. + :param sanitized_model: Sanitized model containing sanitized species definitions. The GPY_HYBRID_SPECIES_MODES option will be set as an option for the model. - :type model: SanitizedModel + :type sanitized_model: SanitizedModel :returns: Pass-through of sanitized model object. :rtype: SanitizedModel @@ -36,9 +36,21 @@ def __create_options(cls, model: "SanitizedModel") -> "SanitizedModel": # When species.boundary_condition == True "BOUNDARY", ] + trigger_mode_types = [ + # When event.use_values_from_trigger_time == False + "USE_EVAL", + # When event.use_values_from_trigger_time == True + "USE_TRIGGER", + ] + persist_types = [ + # When event.trigger.persistent == False + "IRREGULAR", + # When event.trigger.persistent == True + "PERSISTENT", + ] species_mode_list = [] - for spec_id, species in enumerate(model.species.values()): + for spec_id, species in enumerate(sanitized_model.species.values()): mode_keyword = species_mode_map.get(species.mode, species_mode_map["dynamic"]) # Casting a bool to an int evaluates: False -> 0, and True -> 1 # Explicit cast to bool for safety, in case boundary_condition is given weird values @@ -47,8 +59,56 @@ def __create_options(cls, model: "SanitizedModel") -> "SanitizedModel": entry = f"SPECIES_MODE({spec_id},{species.switch_min},{mode_keyword},{boundary_keyword})" species_mode_list.append(entry) - model.options["GPY_HYBRID_SPECIES_MODES"] = " ".join(species_mode_list) - return model + # EVENT(event_id, {targets}, trigger, delay, priority, use_trigger, use_persist) + event_list = [] + # [SPECIES/VARIABLE]_ASSIGNMENT(assign_id, target_id, expr) + event_assignment_list = [] + for event_id, event in enumerate(sanitized_model.model.listOfEvents): + trigger = sanitized_model.expr.getexpr_cpp(event.trigger.expression) + delay = sanitized_model.expr.getexpr_cpp(event.delay) \ + if event.delay is not None else "0" + priority = sanitized_model.expr.getexpr_cpp(event.priority) \ + if event.priority is not None else "0" + use_trigger = trigger_mode_types[int(bool(event.use_values_from_trigger_time))] + use_persist = persist_types[int(bool(event.trigger.persistent))] + + assignments: "list[str]" = [] + for assign_id, assign in enumerate(event.assignments): + variable = assign.variable + expression = sanitized_model.expr.getexpr_cpp(assign.expression) + + if isinstance(variable, str): + if variable in sanitized_model.model.listOfSpecies: + variable = sanitized_model.model.listOfSpecies.get(variable) + elif variable in sanitized_model.model.listOfParameters: + variable = sanitized_model.model.listOfParameters.get(variable) + else: + raise ValueError(f"Invalid event assignment {assign}: received name {variable} " + f"Must match the name of a valid Species or Parameter.") + + if isinstance(variable, gillespy2.Species): + assign_str = f"SPECIES_ASSIGNMENT(" \ + f"{assign_id},{sanitized_model.species_id.get(variable.name)},{expression})" + elif isinstance(variable, gillespy2.Parameter): + assign_str = f"VARIABLE_ASSIGNMENT(" \ + f"{assign_id},{sanitized_model.parameter_id.get(variable.name)},{expression})" + else: + raise ValueError(f"Invalid event assignment {assign}: received variable of type {type(variable)} " + f"Must be of type str, Species, or Parameter") + + assignments.append(str(assign_id)) + event_assignment_list.append(assign_str) + assignments: "str" = " AND ".join(assignments) + event_list.append( + f"EVENT({event_id},{{{assignments}}},{trigger},{delay},{priority},{use_trigger},{use_persist}" + ) + + sanitized_model.options["GPY_HYBRID_SPECIES_MODES"] = " ".join(species_mode_list) + sanitized_model.options["GPY_HYBRID_EVENTS"] = " ".join(event_list) + sanitized_model.options["GPY_HYBRID_NUM_EVENTS"] = str(len(event_list)) + sanitized_model.options["GPY_HYBRID_EVENT_ASSIGNMENTS"] = " ".join(event_assignment_list) + sanitized_model.options["GPY_HYBRID_NUM_EVENT_ASSIGNMENTS"] = str(len(event_assignment_list)) + return sanitized_model def _build(self, model: "Union[Model, SanitizedModel]", simulation_name: str, variable: bool, debug: bool = False, custom_definitions=None) -> str: From ed832916509ec2c33beece2ef9d1faac818c39d0 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Thu, 5 Aug 2021 17:16:26 -0400 Subject: [PATCH 05/19] Fix assignment ID templating - Modified `TauHybridCSolver` template option generation to use a globally unique assignment ID - Added a (currently useless) call to `use_events` just to test --- .../c_base/tau_hybrid_cpp_solver/TauHybridSimulation.cpp | 5 ++++- .../cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp | 4 ++++ gillespy2/solvers/cpp/tau_hybrid_c_solver.py | 8 +++++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSimulation.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSimulation.cpp index 7d9e6bc53..f7a1487cf 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSimulation.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSimulation.cpp @@ -65,7 +65,10 @@ int main(int argc, char* argv[]) init_simulation(&model, simulation); Gillespy::TauHybrid::map_species_modes(simulation.species_state); Gillespy::TauHybrid::map_rate_rules(simulation.species_state); - // Perform ODE // + + std::vector events; + Gillespy::TauHybrid::Event::use_events(events); + TauHybrid::TauHybridCSolver(&simulation, tau_tol); simulation.output_results_buffer(std::cout); return 0; diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp index 37d8760ac..a918e6aff 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp @@ -19,6 +19,10 @@ #include "hybrid_template.h" #include "template_params.h" +// , cannot be overridden, so we can't use it as a delimiter +// Use a separate macro to represent a delimiter +#define AND , + namespace Gillespy { namespace TauHybrid diff --git a/gillespy2/solvers/cpp/tau_hybrid_c_solver.py b/gillespy2/solvers/cpp/tau_hybrid_c_solver.py index fa861c116..3f2407f87 100644 --- a/gillespy2/solvers/cpp/tau_hybrid_c_solver.py +++ b/gillespy2/solvers/cpp/tau_hybrid_c_solver.py @@ -63,7 +63,8 @@ def __create_options(cls, sanitized_model: "SanitizedModel") -> "SanitizedModel" event_list = [] # [SPECIES/VARIABLE]_ASSIGNMENT(assign_id, target_id, expr) event_assignment_list = [] - for event_id, event in enumerate(sanitized_model.model.listOfEvents): + assign_id = 0 + for event_id, event in enumerate(sanitized_model.model.listOfEvents.values()): trigger = sanitized_model.expr.getexpr_cpp(event.trigger.expression) delay = sanitized_model.expr.getexpr_cpp(event.delay) \ if event.delay is not None else "0" @@ -73,7 +74,7 @@ def __create_options(cls, sanitized_model: "SanitizedModel") -> "SanitizedModel" use_persist = persist_types[int(bool(event.trigger.persistent))] assignments: "list[str]" = [] - for assign_id, assign in enumerate(event.assignments): + for assign in event.assignments: variable = assign.variable expression = sanitized_model.expr.getexpr_cpp(assign.expression) @@ -95,12 +96,13 @@ def __create_options(cls, sanitized_model: "SanitizedModel") -> "SanitizedModel" else: raise ValueError(f"Invalid event assignment {assign}: received variable of type {type(variable)} " f"Must be of type str, Species, or Parameter") + assign_id += 1 assignments.append(str(assign_id)) event_assignment_list.append(assign_str) assignments: "str" = " AND ".join(assignments) event_list.append( - f"EVENT({event_id},{{{assignments}}},{trigger},{delay},{priority},{use_trigger},{use_persist}" + f"EVENT({event_id},{{{assignments}}},{trigger},{delay},{priority},{use_trigger},{use_persist})" ) sanitized_model.options["GPY_HYBRID_SPECIES_MODES"] = " ".join(species_mode_list) From 7b9e7d2736ba6b42def74dc9eca665b7f30a5700 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Thu, 5 Aug 2021 19:49:28 -0400 Subject: [PATCH 06/19] Fix assignment ID templating - Updated template to determine `use_trigger_state` and `persistent` flags - Updated event eval functions (`trigger`, `delay`, `priority`) to support expressions - Added `EventExecution` object to represent execution state of event - Call `Even#get_execution` to use, saves state automatically - Added `execution_time` property to execution state - Moved persistent state from `Event` to `EventExecution` - Removed `Event` as "unsupported SBML feature" in Python solver --- .../tau_hybrid_cpp_solver/HybridModel.cpp | 115 ++++++++++-------- .../tau_hybrid_cpp_solver/HybridModel.h | 86 ++++++++++--- .../tau_hybrid_cpp_solver/hybrid_template.cpp | 47 ++++++- gillespy2/solvers/cpp/tau_hybrid_c_solver.py | 1 - 4 files changed, 178 insertions(+), 71 deletions(-) diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp index 6360e9b96..2bdba4573 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp @@ -24,17 +24,45 @@ namespace Gillespy { namespace TauHybrid { - Event::Event(int event_id, std::initializer_list assignment_ids) + Event::Event(int event_id, bool use_trigger_state, bool use_persist) : m_event_id(event_id), - m_assignments(assignment_ids) + m_use_trigger_state(use_trigger_state), + m_use_persist(use_persist) {} - Event::Event(const Event &old_event) - : m_num_state(old_event.m_num_state), - m_num_variables(old_event.m_num_variables), - m_event_id(old_event.m_event_id), - // Yes, this invokes the copy constructor. Yes, this is intentional. - m_assignments(old_event.m_assignments) + EventExecution Event::get_execution(double t, + const double *state, int num_state) const + { + return m_use_trigger_state + ? EventExecution(m_event_id, t, state, num_state, + Reaction::s_variables.get(), Reaction::s_num_variables) + : EventExecution(m_event_id, t); + } + + + EventExecution::EventExecution(int event_id, double t) + : m_execution_time(t), + m_event_id(event_id) + { + use_assignments(); + } + + EventExecution::EventExecution(int event_id, double t, + const double *state, int num_state, + const double *variables, int num_variables) + : m_execution_time(t), + m_event_id(event_id), + m_num_state(num_state), + m_state(new double[num_state]), + m_num_variables(num_variables), + m_variables(new double[num_variables]) + { + use_assignments(); + } + + EventExecution::EventExecution(const EventExecution &old_event) + : m_execution_time(old_event.m_execution_time), + m_event_id(old_event.m_event_id) { if (old_event.m_state != nullptr && m_num_state > 0) { @@ -49,13 +77,9 @@ namespace Gillespy } } - Event::Event(Event &&old_event) noexcept - : m_num_state(old_event.m_num_state), - m_state(old_event.m_state), - m_num_variables(old_event.m_num_variables), - m_variables(old_event.m_variables), - m_event_id(old_event.m_event_id), - m_assignments(std::move(old_event.m_assignments)) + EventExecution::EventExecution(EventExecution &&old_event) noexcept + : m_execution_time(old_event.m_execution_time), + m_event_id(old_event.m_event_id) { old_event.m_num_state = 0; old_event.m_state = nullptr; @@ -63,8 +87,9 @@ namespace Gillespy old_event.m_variables = nullptr; } - Event &Event::operator=(const Event &old_event) + EventExecution &EventExecution::operator=(const EventExecution &old_event) { + m_execution_time = old_event.m_execution_time; m_assignments = old_event.m_assignments; if (this != &old_event) @@ -96,8 +121,9 @@ namespace Gillespy return *this; } - Event &Event::operator=(Event &&old_event) noexcept + EventExecution &EventExecution::operator=(EventExecution &&old_event) noexcept { + m_execution_time = old_event.m_execution_time; m_assignments = std::move(old_event.m_assignments); if (this != &old_event) @@ -116,49 +142,42 @@ namespace Gillespy return *this; } - Event::~Event() + EventExecution::~EventExecution() { delete[] m_state; delete[] m_variables; } - void Event::use_state( - const double *state, int num_state, - const double *variables, int num_variables) + void EventExecution::execute(double t, EventOutput output) const { - if (m_state == nullptr || m_num_state != num_state) + for (int assign_id : m_assignments) { - delete[] m_state; - m_num_state = num_state; - m_state = new double[num_state]; + Event::assign(assign_id, t, output); } - - if (m_variables == nullptr || m_num_variables != num_variables) - { - delete[] m_variables; - m_num_variables = num_variables; - m_variables = new double[num_variables]; - } - - std::memcpy(m_state, state, num_state); - std::memcpy(m_variables, variables, num_variables); - } - - void Event::execute(double t, EventOutput output) const - { - Event::assign(m_event_id, t, output); } - void Event::execute(double t, double *state, double *variables, const double *constants) + void EventExecution::execute(double t, double *state) { - execute(t, EventOutput + if (m_state == nullptr || m_variables == nullptr) { - state, - variables, - m_state, - m_variables, - constants - }); + execute(t, EventOutput { + state, + Reaction::s_variables.get(), + state, + Reaction::s_variables.get(), + Reaction::s_constants.get() + }); + } + else + { + execute(t, EventOutput { + state, + Reaction::s_variables.get(), + m_state, + m_variables, + Reaction::s_constants.get() + }); + } } diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h index f45aa5c99..e80e783e5 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h @@ -41,40 +41,90 @@ namespace Gillespy const double *constants; }; + class EventExecution; + class Event { public: + friend class EventExecution; + + inline bool trigger(double t, const double *state) const + { + return Event::trigger(m_event_id, t, state, Reaction::s_variables.get(), Reaction::s_constants.get()); + } + + inline double delay(double t, const double *state) const + { + return Event::delay(m_event_id, t, state, Reaction::s_variables.get(), Reaction::s_constants.get()); + } + + inline double priority(double t, const double *state) const + { + return Event::priority(m_event_id, t, state, Reaction::s_variables.get(), Reaction::s_constants.get()); + } + + EventExecution get_execution(double t, + const double *state, int num_state) const; static void use_events(std::vector &events); - void execute(double t, double *state, double *variables, const double *constants); + private: + int m_event_id; + bool m_use_trigger_state; + bool m_use_persist; + + explicit Event(int event_id, bool use_trigger_state, bool use_persist); + + static bool trigger( + int event_id, double t, + const double *state, + const double *variables, + const double *constants); + static double delay( + int event_id, double t, + const double *state, + const double *variables, + const double *constants); + static double priority( + int event_id, double t, + const double *state, + const double *variables, + const double *constants); + static void assign(int event_id, double t, EventOutput output); + }; + + class EventExecution + { + public: + + friend class Event; + ~EventExecution(); + EventExecution(const EventExecution&); + EventExecution(EventExecution&&) noexcept; + EventExecution &operator=(const EventExecution&); + EventExecution &operator=(EventExecution&&) noexcept; + void execute(double t, EventOutput output) const; - void use_state(const double *state, int num_state, const double *variables, int num_variables); + void execute(double t, double *state); - ~Event(); - Event(const Event &); - Event &operator=(const Event &); - Event(Event &&) noexcept; - Event &operator=(Event &&) noexcept; + inline double get_execution_time() const { return m_execution_time; } private: + double m_execution_time; + int m_event_id; + int m_num_state = 0; double *m_state = nullptr; int m_num_variables = 0; double *m_variables = nullptr; - int m_event_id; - std::vector m_assignments; + std::vector m_assignments; + void use_assignments(); - Event(int event_id, std::initializer_list assignment_ids); - - static bool trigger(int event_id, double t, const double *state); - - static double delay(int event_id, double t, const double *state); - - static double priority(int event_id, double t, const double *state); - - static void assign(int event_id, double t, EventOutput output); + EventExecution(int event_id, double t); + EventExecution(int event_id, double t, + const double *state, int num_state, + const double *variables, int num_variables); }; /* Gillespy::TauHybrid::DiffEquation diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp index a918e6aff..f53592145 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp @@ -56,7 +56,11 @@ namespace Gillespy } - bool Event::trigger(int event_id, double t, const double *S) + bool Event::trigger( + int event_id, double t, + const double *S, + const double *P, + const double *C) { #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ case event_id: return (bool) (trigger); @@ -72,7 +76,11 @@ namespace Gillespy #undef EVENT } - double Event::delay(int event_id, double t, const double *S) + double Event::delay( + int event_id, double t, + const double *S, + const double *P, + const double *C) { #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ case event_id: return static_cast(delay); @@ -88,7 +96,11 @@ namespace Gillespy #undef EVENT } - double Event::priority(int event_id, double t, const double *S) + double Event::priority( + int event_id, double t, + const double *S, + const double *P, + const double *C) { #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ case event_id: return static_cast(priority); @@ -109,10 +121,37 @@ namespace Gillespy events.clear(); events.reserve(GPY_HYBRID_NUM_EVENTS); + #define USE_TRIGGER true + #define USE_EVAL false + #define PERSISTENT true + #define IRREGULAR false #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ - events[event_id] = Event(event_id, targets); + events[event_id] = Event(event_id, use_trigger, use_persist); GPY_HYBRID_EVENTS #undef EVENT + #undef IRREGULAR + #undef PERSISTENT + #undef USE_EVAL + #undef USE_TRIGGER + } + + void EventExecution::use_assignments() + { + m_assignments.clear(); + m_assignments.reserve(GPY_HYBRID_NUM_EVENT_ASSIGNMENTS); + + #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ + case event_id: m_assignments.assign(targets); + + switch (m_event_id) + { + GPY_HYBRID_EVENTS + + default: + return; + } + + #undef EVENT } void Event::assign(int assign_id, double t, EventOutput output) diff --git a/gillespy2/solvers/cpp/tau_hybrid_c_solver.py b/gillespy2/solvers/cpp/tau_hybrid_c_solver.py index 3f2407f87..68169fa31 100644 --- a/gillespy2/solvers/cpp/tau_hybrid_c_solver.py +++ b/gillespy2/solvers/cpp/tau_hybrid_c_solver.py @@ -139,7 +139,6 @@ def run(self=None, model: Model = None, t: int = 20, number_of_trajectories: int self._validate_kwargs(**kwargs) self._validate_sbml_features({ "Assignment Rules": len(model.listOfAssignmentRules), - "Events": len(model.listOfEvents), "Function Definitions": len(model.listOfFunctionDefinitions) }) From 3d678e659955bfb9800902210eb55caeab644270 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Thu, 5 Aug 2021 23:10:00 -0400 Subject: [PATCH 07/19] Unfinished version of events for simulation loop Still missing trigger state memory (currently executes constantly) - Added events vector to solver function signature - Added priority queues to Hybrid solver iteration - Added comparison operators to `EventExecution` object - Moved `priority` method from `Event` to `EventExecution` - Added copy of assignments list on copy/move constructors - Fixed sizing issue for event template - Fixed off-by-1 error when generating assignment IDs in Python --- .../tau_hybrid_cpp_solver/HybridModel.cpp | 16 +- .../tau_hybrid_cpp_solver/HybridModel.h | 12 +- .../TauHybridSimulation.cpp | 2 +- .../tau_hybrid_cpp_solver/TauHybridSolver.cpp | 480 ++++++++++-------- .../tau_hybrid_cpp_solver/TauHybridSolver.h | 8 +- .../tau_hybrid_cpp_solver/hybrid_template.cpp | 10 +- gillespy2/solvers/cpp/tau_hybrid_c_solver.py | 3 +- 7 files changed, 303 insertions(+), 228 deletions(-) diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp index 2bdba4573..fe8be186b 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp @@ -62,7 +62,8 @@ namespace Gillespy EventExecution::EventExecution(const EventExecution &old_event) : m_execution_time(old_event.m_execution_time), - m_event_id(old_event.m_event_id) + m_event_id(old_event.m_event_id), + m_assignments(old_event.m_assignments) { if (old_event.m_state != nullptr && m_num_state > 0) { @@ -79,7 +80,8 @@ namespace Gillespy EventExecution::EventExecution(EventExecution &&old_event) noexcept : m_execution_time(old_event.m_execution_time), - m_event_id(old_event.m_event_id) + m_event_id(old_event.m_event_id), + m_assignments(std::move(old_event.m_assignments)) { old_event.m_num_state = 0; old_event.m_state = nullptr; @@ -180,6 +182,16 @@ namespace Gillespy } } + bool EventExecution::operator<(const EventExecution &rhs) const + { + return m_execution_time < rhs.m_execution_time; + } + + bool EventExecution::operator>(const EventExecution &rhs) const + { + return m_execution_time > rhs.m_execution_time; + } + HybridReaction::HybridReaction() : mode(SimulationState::DISCRETE), diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h index e80e783e5..a00fcc8bd 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h @@ -58,11 +58,6 @@ namespace Gillespy return Event::delay(m_event_id, t, state, Reaction::s_variables.get(), Reaction::s_constants.get()); } - inline double priority(double t, const double *state) const - { - return Event::priority(m_event_id, t, state, Reaction::s_variables.get(), Reaction::s_constants.get()); - } - EventExecution get_execution(double t, const double *state, int num_state) const; static void use_events(std::vector &events); @@ -105,9 +100,16 @@ namespace Gillespy void execute(double t, EventOutput output) const; void execute(double t, double *state); + inline double priority(double t, const double *state) const + { + return Event::priority(m_event_id, t, state, Reaction::s_variables.get(), Reaction::s_constants.get()); + } inline double get_execution_time() const { return m_execution_time; } + bool operator<(const EventExecution &rhs) const; + bool operator>(const EventExecution &rhs) const; + private: double m_execution_time; int m_event_id; diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSimulation.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSimulation.cpp index f7a1487cf..a0846bd15 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSimulation.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSimulation.cpp @@ -69,7 +69,7 @@ int main(int argc, char* argv[]) std::vector events; Gillespy::TauHybrid::Event::use_events(events); - TauHybrid::TauHybridCSolver(&simulation, tau_tol); + TauHybrid::TauHybridCSolver(&simulation, events, tau_tol); simulation.output_results_buffer(std::cout); return 0; } diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp index 772543932..1b09cca4e 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp @@ -19,6 +19,7 @@ #include #include //Included for timeout signal handling #include +#include #include "cvode.h" // prototypes for CVODE fcts., consts. #include "nvector_serial.h" // access to serial N_Vector #include "sunlinsol_spgmr.h" //access to SPGMR SUNLinearSolver @@ -30,104 +31,109 @@ #include "integrator.h" #include "tau.h" -namespace Gillespy::TauHybrid +namespace Gillespy { - bool interrupted = false; - - void signalHandler(int signum) + namespace TauHybrid { - interrupted = true; - } + bool interrupted = false; - void TauHybridCSolver(HybridSimulation *simulation, const double tau_tol) - { - if (simulation == NULL) + void signalHandler(int signum) { - return; + interrupted = true; } - Model &model = *(simulation->model); - int num_species = model.number_species; - int num_reactions = model.number_reactions; - int num_trajectories = simulation->number_trajectories; - std::unique_ptr[]> &species = model.species; - double increment = simulation->timeline[1] - simulation->timeline[0]; - - URNGenerator urn(simulation->random_seed); - // The contents of y0 are "stolen" by the integrator. - // Do not attempt to directly use y0 after being passed to sol! - N_Vector y0 = init_model_vector(model, urn); - N_Vector y; - if (num_trajectories > 0) + void TauHybridCSolver(HybridSimulation *simulation, std::vector &events, const double tau_tol) { - y = init_model_vector(model, urn); - } - else - { - y = y0; - } - Integrator sol(simulation, y, GPY_HYBRID_RELTOL, GPY_HYBRID_ABSTOL); - - // Tau selector initialization. Used to select a valid tau step. - TauArgs tau_args = initialize(model, tau_tol); - - // Simulate for each trajectory - for (int traj = 0; traj < num_trajectories; traj++) - { - if (traj > 0) + if (simulation == NULL) { - sol.reinitialize(y0); + return; } - // Initialize each species with their respective user modes. - for (int spec_i = 0; spec_i < num_species; ++spec_i) + Model &model = *(simulation->model); + int num_species = model.number_species; + int num_reactions = model.number_reactions; + int num_trajectories = simulation->number_trajectories; + std::unique_ptr[]> &species = model.species; + double increment = simulation->timeline[1] - simulation->timeline[0]; + + URNGenerator urn(simulation->random_seed); + // The contents of y0 are "stolen" by the integrator. + // Do not attempt to directly use y0 after being passed to sol! + N_Vector y0 = init_model_vector(model, urn); + N_Vector y; + if (num_trajectories > 0) + { + y = init_model_vector(model, urn); + } else { - HybridSpecies *spec = &simulation->species_state[spec_i]; - spec->partition_mode = spec->user_mode == SimulationState::DYNAMIC - ? SimulationState::DISCRETE - : spec->user_mode; - simulation->trajectories[traj][0][spec_i] = spec->base_species->initial_population; + y = y0; } + Integrator sol(simulation, y, GPY_HYBRID_RELTOL, GPY_HYBRID_ABSTOL); - // Population/concentration state values for each species. - // TODO: change back double -> hybrid_state, once we figure out how that works - std::vector current_state(num_species); - std::vector current_populations(num_species); + // Tau selector initialization. Used to select a valid tau step. + TauArgs tau_args = initialize(model, tau_tol); - // Initialize the species population for the trajectory. - for (int spec_i = 0; spec_i < num_species; ++spec_i) + // Simulate for each trajectory + for (int traj = 0; traj < num_trajectories; traj++) { - current_state[spec_i] = species[spec_i].initial_population; - current_populations[spec_i] = species[spec_i].initial_population; - } + if (traj > 0) + { + sol.reinitialize(y0); + } - // SIMULATION STEP LOOP - int save_idx = 1; - double next_time; - double tau_step = 0.0; - double save_time = simulation->timeline[save_idx]; - - // Temporary array to store changes to dependent species. - // Should be 0-initialized each time it's used. - int *population_changes = new int[num_species]; - simulation->current_time = 0; - - // An invalid simulation state indicates that an unrecoverable error has occurred, - // and the trajectory should terminate early. - bool invalid_state = false; - // This is a temporary fix. Ideally, invalid state should allow for integrator options change. - // For now, a "guard" is put in place to prevent potentially infinite loops from occurring. - unsigned int integration_guard = 1000; - - while (integration_guard > 0 && simulation->current_time < simulation->end_time) - { - // Compute current propensity values based on existing state. - for (unsigned int rxn_i = 0; rxn_i < num_reactions; ++rxn_i) + // Initialize each species with their respective user modes. + for (int spec_i = 0; spec_i < num_species; ++spec_i) + { + HybridSpecies *spec = &simulation->species_state[spec_i]; + spec->partition_mode = spec->user_mode == SimulationState::DYNAMIC + ? SimulationState::DISCRETE + : spec->user_mode; + simulation->trajectories[traj][0][spec_i] = spec->base_species->initial_population; + } + + // Population/concentration state values for each species. + // TODO: change back double -> hybrid_state, once we figure out how that works + std::vector current_state(num_species); + std::vector current_populations(num_species); + + // Initialize the species population for the trajectory. + for (int spec_i = 0; spec_i < num_species; ++spec_i) + { + current_state[spec_i] = species[spec_i].initial_population; + current_populations[spec_i] = species[spec_i].initial_population; + } + + // SIMULATION STEP LOOP + int save_idx = 1; + double next_time; + double tau_step = 0.0; + double save_time = simulation->timeline[save_idx]; + + // Temporary array to store changes to dependent species. + // Should be 0-initialized each time it's used. + int *population_changes = new int[num_species]; + simulation->current_time = 0; + + // An invalid simulation state indicates that an unrecoverable error has occurred, + // and the trajectory should terminate early. + bool invalid_state = false; + // This is a temporary fix. Ideally, invalid state should allow for integrator options change. + // For now, a "guard" is put in place to prevent potentially infinite loops from occurring. + unsigned int integration_guard = 1000; + + std::priority_queue< + EventExecution, + std::vector, + std::greater> delay_queue; + while (integration_guard > 0 && simulation->current_time < simulation->end_time) { - HybridReaction &rxn = simulation->reaction_state[rxn_i]; - double propensity = 0.0; - switch (rxn.mode) + // Compute current propensity values based on existing state. + for (unsigned int rxn_i = 0; rxn_i < num_reactions; ++rxn_i) { + HybridReaction &rxn = simulation->reaction_state[rxn_i]; + double propensity = 0.0; + switch (rxn.mode) + { case SimulationState::CONTINUOUS: propensity = Reaction::propensity(rxn_i, current_state.data()); break; @@ -136,165 +142,219 @@ namespace Gillespy::TauHybrid break; default: break; + } + sol.data.propensities[rxn_i] = propensity; } - sol.data.propensities[rxn_i] = propensity; - } - // Expected tau step is determined. - tau_step = select( - model, - tau_args, - tau_tol, - simulation->current_time, - save_time, - sol.data.propensities, - current_populations - ); - partition_species( - simulation->reaction_state, - simulation->species_state, - sol.data.propensities, - current_state, - tau_step, - tau_args - ); - flag_det_rxns( - simulation->reaction_state, - simulation->species_state - ); - update_species_state(simulation->species_state, current_state); - create_differential_equations(simulation->species_state, simulation->reaction_state); - - // Determine what the next time point is. - // This will become current_time on the next iteration. - // If a retry with a smaller tau_step is deemed necessary, this will change. - next_time = simulation->current_time + tau_step; - - // The integration loop continues until a valid solution is found. - // Any invalid Tau steps (which cause negative populations) are discarded. - sol.save_state(); - do { - // Integration Step - // For deterministic reactions, the concentrations are updated directly. - // For stochastic reactions, integration updates the rxn_offsets vector. - IntegrationResults result = sol.integrate(&next_time); - if (sol.status == IntegrationStatus::BAD_STEP_SIZE) - { - invalid_state = true; - // Breaking early causes `invalid_state` to remain set, - // resulting in an early termination of the trajectory. - break; - } + // Expected tau step is determined. + tau_step = select( + model, + tau_args, + tau_tol, + simulation->current_time, + save_time, + sol.data.propensities, + current_populations + ); + partition_species( + simulation->reaction_state, + simulation->species_state, + sol.data.propensities, + current_state, + tau_step, + tau_args + ); + flag_det_rxns( + simulation->reaction_state, + simulation->species_state + ); + update_species_state(simulation->species_state, current_state); + create_differential_equations(simulation->species_state, simulation->reaction_state); - // The integrator has, at this point, been validated. - // Any errors beyond this point is assumed to be a stochastic state failure. - invalid_state = false; + // Determine what the next time point is. + // This will become current_time on the next iteration. + // If a retry with a smaller tau_step is deemed necessary, this will change. + next_time = events.empty() + ? simulation->current_time + tau_step + : simulation->current_time + std::min(tau_step, increment); - // 0-initialize our population_changes array. - for (int p_i = 0; p_i < num_species; ++p_i) + // The integration loop continues until a valid solution is found. + // Any invalid Tau steps (which cause negative populations) are discarded. + sol.save_state(); + do { - population_changes[p_i] = 0; - } + // Integration Step + // For deterministic reactions, the concentrations are updated directly. + // For stochastic reactions, integration updates the rxn_offsets vector. + IntegrationResults result = sol.integrate(&next_time); + if (sol.status == IntegrationStatus::BAD_STEP_SIZE) + { + invalid_state = true; + // Breaking early causes `invalid_state` to remain set, + // resulting in an early termination of the trajectory. + break; + } - // Start with the species concentration as a baseline value. - // Stochastic reactions will update populations relative to their concentrations. - for (int spec_i = 0; spec_i < num_species; ++spec_i) - { - current_state[spec_i] = result.concentrations[spec_i]; - } + // The integrator has, at this point, been validated. + // Any errors beyond this point is assumed to be a stochastic state failure. + invalid_state = false; - // The newly-updated reaction_states vector may need to be reconciled now. - // A positive reaction_state means reactions have potentially fired. - // NOTE: it is possible for a population to swing negative, where a smaller Tau is needed. - for (int rxn_i = 0; rxn_i < num_reactions; ++rxn_i) - { - // Temporary variable for the reaction's state. - // Does not get updated unless the changes are deemed valid. - double rxn_state = result.reactions[rxn_i]; + // 0-initialize our population_changes array. + for (int p_i = 0; p_i < num_species; ++p_i) + { + population_changes[p_i] = 0; + } - switch (simulation->reaction_state[rxn_i].mode) + // Start with the species concentration as a baseline value. + // Stochastic reactions will update populations relative to their concentrations. + for (int spec_i = 0; spec_i < num_species; ++spec_i) { - case SimulationState::DISCRETE: - while (rxn_state >= 0) { - // "Fire" a reaction by recording changes in dependent species. - // If a negative value is detected, break without saving changes. - for (int spec_i = 0; spec_i < num_species; ++spec_i) { - population_changes[spec_i] += - model.reactions[rxn_i].species_change[spec_i]; - if (current_state[spec_i] + population_changes[spec_i] < 0) { - invalid_state = true; + current_state[spec_i] = result.concentrations[spec_i]; + } + + // The newly-updated reaction_states vector may need to be reconciled now. + // A positive reaction_state means reactions have potentially fired. + // NOTE: it is possible for a population to swing negative, where a smaller Tau is needed. + for (int rxn_i = 0; rxn_i < num_reactions; ++rxn_i) + { + // Temporary variable for the reaction's state. + // Does not get updated unless the changes are deemed valid. + double rxn_state = result.reactions[rxn_i]; + + switch (simulation->reaction_state[rxn_i].mode) + { + case SimulationState::DISCRETE: + while (rxn_state >= 0) + { + // "Fire" a reaction by recording changes in dependent species. + // If a negative value is detected, break without saving changes. + for (int spec_i = 0; spec_i < num_species; ++spec_i) + { + population_changes[spec_i] += + model.reactions[rxn_i].species_change[spec_i]; + if (current_state[spec_i] + population_changes[spec_i] < 0) + { + invalid_state = true; + } } + + rxn_state += log(urn.next()); } + result.reactions[rxn_i] = rxn_state; + break; - rxn_state += log(urn.next()); + case SimulationState::CONTINUOUS: + default: + break; } - result.reactions[rxn_i] = rxn_state; - break; + } - case SimulationState::CONTINUOUS: - default: - break; + // Positive reaction state means a negative population was detected. + // Only update state with the given population changes if valid. + if (invalid_state) + { + sol.restore_state(); + tau_step /= 2; + next_time = simulation->current_time + tau_step; + } else + { + // "Permanently" update the rxn_state and populations. + for (int p_i = 0; p_i < num_species; ++p_i) + { + if (!simulation->species_state[p_i].boundary_condition) + { + // Boundary conditions are not modified directly by reactions. + // As such, population dx in stochastic regime is not considered. + // For deterministic species, their effective dy/dt should always be 0. + current_state[p_i] += population_changes[p_i]; + result.concentrations[p_i] = current_state[p_i]; + } + current_populations[p_i] = (int) current_state[p_i]; + } } - } + } while (invalid_state); - // Positive reaction state means a negative population was detected. - // Only update state with the given population changes if valid. - if (invalid_state) - { - sol.restore_state(); - tau_step /= 2; - next_time = simulation->current_time + tau_step; - } - else + // Invalid state after the do-while loop implies that an unrecoverable error has occurred. + // While prior results are considered usable, the current integration results are not. + // Calling `continue` with an invalid state will discard the results and terminate the trajectory. + integration_guard = invalid_state + ? integration_guard - 1 + : 1000; + + if (!events.empty()) { - // "Permanently" update the rxn_state and populations. - for (int p_i = 0; p_i < num_species; ++p_i) + double t = simulation->current_time; + double *event_state = N_VGetArrayPointer(sol.y); + auto compare = [t, event_state](EventExecution &lhs, EventExecution &rhs) -> bool + { + return lhs.priority(t, event_state) < rhs.priority(t, event_state); + }; + std::priority_queue, decltype(compare)> + trigger_queue(compare); + + for (auto &event : events) { - if (!simulation->species_state[p_i].boundary_condition) + if (event.trigger(t, event_state)) { - // Boundary conditions are not modified directly by reactions. - // As such, population dx in stochastic regime is not considered. - // For deterministic species, their effective dy/dt should always be 0. - current_state[p_i] += population_changes[p_i]; - result.concentrations[p_i] = current_state[p_i]; + double delay = event.delay(t, event_state); + if (delay > 0) + { + // Put EventExecution on "delayed" pile + delay_queue.push(event.get_execution(delay, event_state, num_species)); + } + else + { + // Put EventExecution on "triggered" pile + trigger_queue.push(event.get_execution(t, event_state, num_species)); + } } - current_populations[p_i] = (int) current_state[p_i]; } - } - } while (invalid_state); - // Invalid state after the do-while loop implies that an unrecoverable error has occurred. - // While prior results are considered usable, the current integration results are not. - // Calling `continue` with an invalid state will discard the results and terminate the trajectory. - integration_guard = invalid_state - ? integration_guard - 1 - : 1000; + while (!delay_queue.empty()) + { + auto &event = delay_queue.top(); + if (event.get_execution_time() < simulation->current_time) + { + break; + } + trigger_queue.push(event); + delay_queue.pop(); + } - // Output the results for this time step. - sol.refresh_state(); - simulation->current_time = next_time; + while (!trigger_queue.empty()) + { + auto event = trigger_queue.top(); + event.execute(t, event_state); + trigger_queue.pop(); + } + } - // Seek forward, writing out any values on the timeline which are on current timestep range. - while (save_idx < simulation->number_timesteps && save_time <= next_time) - { - for (int spec_i = 0; spec_i < num_species; ++spec_i) + // Output the results for this time step. + sol.refresh_state(); + simulation->current_time = next_time; + + // Seek forward, writing out any values on the timeline which are on current timestep range. + while (save_idx < simulation->number_timesteps && save_time <= next_time) { - simulation->trajectories[traj][save_idx][spec_i] = current_state[spec_i]; + for (int spec_i = 0; spec_i < num_species; ++spec_i) + { + simulation->trajectories[traj][save_idx][spec_i] = current_state[spec_i]; + } + save_time = simulation->timeline[++save_idx]; } - save_time = simulation->timeline[++save_idx]; } - } - if (integration_guard == 0) - { - std::cerr - << "[Trajectory #" << traj << "] " - << "Integration guard triggered; problem space too stiff at t=" - << simulation->current_time << std::endl; - } + if (integration_guard == 0) + { + std::cerr + << "[Trajectory #" << traj << "] " + << "Integration guard triggered; problem space too stiff at t=" + << simulation->current_time << std::endl; + } - // End of trajectory - delete[] population_changes; + // End of trajectory + delete[] population_changes; + } } } } diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.h index d45f3d7d7..319ceb59a 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.h @@ -19,8 +19,12 @@ #pragma once #include "HybridModel.h" +#include -namespace Gillespy::TauHybrid +namespace Gillespy { - void TauHybridCSolver(HybridSimulation* simulation, const double tau_tol); + namespace TauHybrid + { + void TauHybridCSolver(HybridSimulation* simulation, std::vector &events, double tau_tol); + } } diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp index f53592145..bc026451c 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp @@ -126,7 +126,7 @@ namespace Gillespy #define PERSISTENT true #define IRREGULAR false #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ - events[event_id] = Event(event_id, use_trigger, use_persist); + events.emplace_back(Event(event_id, use_trigger, use_persist)); GPY_HYBRID_EVENTS #undef EVENT #undef IRREGULAR @@ -138,10 +138,8 @@ namespace Gillespy void EventExecution::use_assignments() { m_assignments.clear(); - m_assignments.reserve(GPY_HYBRID_NUM_EVENT_ASSIGNMENTS); - #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ - case event_id: m_assignments.assign(targets); + case event_id: m_assignments = std::vector(targets); break; switch (m_event_id) { @@ -161,9 +159,9 @@ namespace Gillespy const double *C = output.constants; #define SPECIES_ASSIGNMENT(id, spec_id, expr) \ - case id: output.species_out[spec_id] = expr; + case id: output.species_out[spec_id] = expr; break; #define VARIABLE_ASSIGNMENT(id, var_id, expr) \ - case id: output.variable_out[var_id] = expr; + case id: output.variable_out[var_id] = expr; break; switch (assign_id) { diff --git a/gillespy2/solvers/cpp/tau_hybrid_c_solver.py b/gillespy2/solvers/cpp/tau_hybrid_c_solver.py index 68169fa31..d8a8b336f 100644 --- a/gillespy2/solvers/cpp/tau_hybrid_c_solver.py +++ b/gillespy2/solvers/cpp/tau_hybrid_c_solver.py @@ -96,10 +96,9 @@ def __create_options(cls, sanitized_model: "SanitizedModel") -> "SanitizedModel" else: raise ValueError(f"Invalid event assignment {assign}: received variable of type {type(variable)} " f"Must be of type str, Species, or Parameter") - assign_id += 1 - assignments.append(str(assign_id)) event_assignment_list.append(assign_str) + assign_id += 1 assignments: "str" = " AND ".join(assignments) event_list.append( f"EVENT({event_id},{{{assignments}}},{trigger},{delay},{priority},{use_trigger},{use_persist})" From a026adfb7a238344319721d8c698167ea71ccea9 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Sat, 14 Aug 2021 14:58:31 -0400 Subject: [PATCH 08/19] Queue for non-persistent events - Added `is_persistent()` method to `Event` class - Added `volatile_queue` list for events that need to be re-checked continuously --- .../tau_hybrid_cpp_solver/HybridModel.h | 10 +++- .../tau_hybrid_cpp_solver/TauHybridSolver.cpp | 49 +++++++++++++++---- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h index a00fcc8bd..b1a9c3328 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h @@ -58,6 +58,8 @@ namespace Gillespy return Event::delay(m_event_id, t, state, Reaction::s_variables.get(), Reaction::s_constants.get()); } + inline bool is_persistent() const { return m_use_persist; } + EventExecution get_execution(double t, const double *state, int num_state) const; static void use_events(std::vector &events); @@ -104,6 +106,10 @@ namespace Gillespy { return Event::priority(m_event_id, t, state, Reaction::s_variables.get(), Reaction::s_constants.get()); } + inline bool trigger(double t, const double *state) const + { + return Event::trigger(m_event_id, t, state, Reaction::s_variables.get(), Reaction::s_constants.get()); + } inline double get_execution_time() const { return m_execution_time; } @@ -116,7 +122,7 @@ namespace Gillespy int m_num_state = 0; double *m_state = nullptr; - + int m_num_variables = 0; double *m_variables = nullptr; @@ -134,7 +140,7 @@ namespace Gillespy * * The vector is understood to be an arbitrarily sized collection of propensity evaluations, * each weighted by some individual, constant factor. - * The sum of evaulations of all collected functions is interpreted to be the dydt of that state. + * The sum of evaluations of all collected functions is interpreted to be the dy/dt of that state. */ struct DifferentialEquation { diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp index 1b09cca4e..8fb606b4e 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp @@ -20,6 +20,7 @@ #include //Included for timeout signal handling #include #include +#include #include "cvode.h" // prototypes for CVODE fcts., consts. #include "nvector_serial.h" // access to serial N_Vector #include "sunlinsol_spgmr.h" //access to SPGMR SUNLinearSolver @@ -35,6 +36,9 @@ namespace Gillespy { namespace TauHybrid { + template + using DelayedExecutionQueue = std::priority_queue, T>; + bool interrupted = false; void signalHandler(int signum) @@ -121,10 +125,17 @@ namespace Gillespy // For now, a "guard" is put in place to prevent potentially infinite loops from occurring. unsigned int integration_guard = 1000; - std::priority_queue< - EventExecution, - std::vector, - std::greater> delay_queue; + // delay_queue: + // Delayed execution objects which are guaranteed to (eventually) fire at its time. + // Once placed in this queue, an execution is only removed when it's ready to fire. + DelayedExecutionQueue> delay_queue; + + // volatile_queue: + // Delayed execution objects which must be re-evaluated at each root. + // If any volatile executions transition from True -> False, they are removed. + // Otherwise, they are treated just like a delay_queue, by triggering when ready. + std::list volatile_queue; + while (integration_guard > 0 && simulation->current_time < simulation->end_time) { // Compute current propensity values based on existing state. @@ -297,23 +308,43 @@ namespace Gillespy if (event.trigger(t, event_state)) { double delay = event.delay(t, event_state); - if (delay > 0) + if (delay <= 0) + { + // Immediately put EventExecution on "triggered" pile + trigger_queue.emplace(event.get_execution(t, event_state, num_species)); + } + else if (event.is_persistent()) { // Put EventExecution on "delayed" pile - delay_queue.push(event.get_execution(delay, event_state, num_species)); + delay_queue.emplace(event.get_execution(delay, event_state, num_species)); } else { - // Put EventExecution on "triggered" pile - trigger_queue.push(event.get_execution(t, event_state, num_species)); + // Delayed, but must be re-checked on every iteration. + volatile_queue.emplace_back(event.get_execution(delay, event_state, num_species)); } } } + for (auto vol_event = volatile_queue.begin(); vol_event != volatile_queue.end(); ++vol_event) + { + // Execution objects in the volatile queue must remain True until execution. + // Remove any execution objects which transitioned to False before execution. + if (!vol_event->trigger(t, event_state)) + { + vol_event = volatile_queue.erase(vol_event); + } + else if (vol_event->get_execution_time() >= t) + { + trigger_queue.push(*vol_event); + vol_event = volatile_queue.erase(vol_event); + } + } + while (!delay_queue.empty()) { auto &event = delay_queue.top(); - if (event.get_execution_time() < simulation->current_time) + if (event.get_execution_time() < t) { break; } From 49113bb8d09bf5f7d1af6f23542bc15010784849 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Wed, 8 Sep 2021 22:18:07 -0400 Subject: [PATCH 09/19] Root-finder and updates to trigger pipeline - Added methods to `Integrator` class to enable root-finding - Modified trigger pipeline to fire detected events without using trigger calls directly --- .../tau_hybrid_cpp_solver/HybridModel.h | 1 + .../tau_hybrid_cpp_solver/TauHybridSolver.cpp | 85 ++++++++++++------- .../tau_hybrid_cpp_solver/integrator.cpp | 70 ++++++++++++++- .../c_base/tau_hybrid_cpp_solver/integrator.h | 12 ++- 4 files changed, 136 insertions(+), 32 deletions(-) diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h index b1a9c3328..75625e74d 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h @@ -112,6 +112,7 @@ namespace Gillespy } inline double get_execution_time() const { return m_execution_time; } + inline int get_event_id() const { return m_event_id; } bool operator<(const EventExecution &rhs) const; bool operator>(const EventExecution &rhs) const; diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp index 8fb606b4e..637de5906 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp @@ -73,6 +73,10 @@ namespace Gillespy y = y0; } Integrator sol(simulation, y, GPY_HYBRID_RELTOL, GPY_HYBRID_ABSTOL); + if (!events.empty()) + { + sol.use_events(&events); + } // Tau selector initialization. Used to select a valid tau step. TauArgs tau_args = initialize(model, tau_tol); @@ -189,15 +193,20 @@ namespace Gillespy ? simulation->current_time + tau_step : simulation->current_time + std::min(tau_step, increment); + // Vector used to store any continuous events discovered by the root-finder. + // How each integrator event is handled is context-dependent. + std::set roots_found; + // The integration loop continues until a valid solution is found. // Any invalid Tau steps (which cause negative populations) are discarded. sol.save_state(); + do { // Integration Step // For deterministic reactions, the concentrations are updated directly. // For stochastic reactions, integration updates the rxn_offsets vector. - IntegrationResults result = sol.integrate(&next_time); + IntegrationResults result = sol.integrate(&next_time, roots_found); if (sol.status == IntegrationStatus::BAD_STEP_SIZE) { invalid_state = true; @@ -267,7 +276,8 @@ namespace Gillespy sol.restore_state(); tau_step /= 2; next_time = simulation->current_time + tau_step; - } else + } + else { // "Permanently" update the rxn_state and populations. for (int p_i = 0; p_i < num_species; ++p_i) @@ -292,49 +302,59 @@ namespace Gillespy ? integration_guard - 1 : 1000; + // ===== ===== if (!events.empty()) { - double t = simulation->current_time; double *event_state = N_VGetArrayPointer(sol.y); - auto compare = [t, event_state](EventExecution &lhs, EventExecution &rhs) -> bool + auto compare = [next_time, event_state](EventExecution &lhs, EventExecution &rhs) -> bool { - return lhs.priority(t, event_state) < rhs.priority(t, event_state); + return lhs.priority(next_time, event_state) < rhs.priority(next_time, event_state); }; std::priority_queue, decltype(compare)> trigger_queue(compare); - for (auto &event : events) + // Step 1: Check to see if any events are present in the volatile queue. + // If so, those executions must be discarded (double-transition). + for (auto vol_iter = volatile_queue.begin(); vol_iter != volatile_queue.end(); ++vol_iter) { - if (event.trigger(t, event_state)) + auto found_event = roots_found.find(vol_iter->get_event_id()); + // Element in roots_found contains event id; discard + if (found_event != roots_found.end()) { - double delay = event.delay(t, event_state); - if (delay <= 0) - { - // Immediately put EventExecution on "triggered" pile - trigger_queue.emplace(event.get_execution(t, event_state, num_species)); - } - else if (event.is_persistent()) - { - // Put EventExecution on "delayed" pile - delay_queue.emplace(event.get_execution(delay, event_state, num_species)); - } - else - { - // Delayed, but must be re-checked on every iteration. - volatile_queue.emplace_back(event.get_execution(delay, event_state, num_species)); - } + vol_iter = volatile_queue.erase(vol_iter); + } + } + + // Step 2: Evaluate remaining roots as triggers + for (int root : roots_found) + { + Event event = events[root]; + double delay = event.delay(next_time, event_state); + + if (delay <= 0) + { + // Immediately put EventExecution on "triggered" pile + trigger_queue.emplace(event.get_execution(next_time, event_state, num_species)); + } + else if (event.is_persistent()) + { + // Put EventExecution on "delayed" pile + delay_queue.emplace(event.get_execution(delay, event_state, num_species)); + } + else + { + // Delayed, but must be re-checked on every iteration. + volatile_queue.emplace_back(event.get_execution(delay, event_state, num_species)); } } + // Step 3: Process delayed executions that are now ready to fire. + // Both the volatile and non-volatile queue are processed in a similar manner. for (auto vol_event = volatile_queue.begin(); vol_event != volatile_queue.end(); ++vol_event) { // Execution objects in the volatile queue must remain True until execution. // Remove any execution objects which transitioned to False before execution. - if (!vol_event->trigger(t, event_state)) - { - vol_event = volatile_queue.erase(vol_event); - } - else if (vol_event->get_execution_time() >= t) + if (vol_event->get_execution_time() >= next_time) { trigger_queue.push(*vol_event); vol_event = volatile_queue.erase(vol_event); @@ -344,21 +364,26 @@ namespace Gillespy while (!delay_queue.empty()) { auto &event = delay_queue.top(); - if (event.get_execution_time() < t) + if (event.get_execution_time() < next_time) { + // Delay queue is sorted in chronological order. + // As soon as we hit a time that is beyond the current time, + // there is no use in continuing through the queue. break; } trigger_queue.push(event); delay_queue.pop(); } + // Step 4: Process any pending triggers, unconditionally. while (!trigger_queue.empty()) { auto event = trigger_queue.top(); - event.execute(t, event_state); + event.execute(next_time, event_state); trigger_queue.pop(); } } + // ===== ===== // Output the results for this time step. sol.refresh_state(); diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp index 6f5639606..1b06af349 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp @@ -123,6 +123,7 @@ Integrator::~Integrator() N_VDestroy_Serial(y); CVodeFree(&cvode_mem); SUNLinSolFree_SPGMR(solver); + delete[] m_roots; } IntegrationResults Integrator::integrate(double *t) @@ -133,11 +134,62 @@ IntegrationResults Integrator::integrate(double *t) } return { - NV_DATA_S(y), // NV_DATA_S instead? + NV_DATA_S(y), NV_DATA_S(y) + num_species }; } +IntegrationResults Integrator::integrate(double *t, std::set &events) +{ + IntegrationResults results = integrate(t); + + std::vector *integrator_events = data.events; + if (integrator_events != nullptr && !integrator_events->empty()) + { + int num_events = (int) integrator_events->size(); + if (m_roots == nullptr) + { + m_roots = new int[num_events]; + } + + if (validate(this, CVodeGetRootInfo(cvode_mem, m_roots))) + { + for (int event_id = 0; event_id < num_events; ++event_id) + { + // CVode root output can be: -1, 0, or 1 + // +1 : Passes from negative to positive + // -1 : Passes from positive to negative + // 0 : No root found + // (See page 83 of CVODE documentation) + // https://computing.llnl.gov/sites/default/files/cv_guide-5.7.0.pdf + // NOTE: do we need to check root direction? If so, this is the place to do so. + if (m_roots[event_id] < 0 || m_roots[event_id] > 0) + { + std::cerr << " ====== EVENT FIRED AT t=" << *t << std::endl; + events.insert(event_id); + } + } + } + } + + return results; +} +void Integrator::use_events(std::vector *events) +{ + if (events == nullptr + || !validate(this, CVodeRootInit(cvode_mem, (int) events->size(), rootfn))) + { + return; + } + data.events = events; +} + +bool Integrator::has_events() const +{ + return data.events != nullptr && !data.events->empty(); +} + + URNGenerator::URNGenerator() : uniform(0, 1) {} @@ -269,6 +321,22 @@ int Gillespy::TauHybrid::rhs(realtype t, N_Vector y, N_Vector ydot, void *user_d return 0; }; +int Gillespy::TauHybrid::rootfn(realtype t, N_Vector y, realtype *gout, void *user_data) +{ + std::vector &events = *(static_cast(user_data)->events); + realtype *y_t = N_VGetArrayPointer(y); + unsigned long long num_events = events.size(); + + for (int event_id = 0; event_id < num_events; ++event_id) + { + gout[event_id] = events[event_id].trigger(t, y_t) + ? 1.0f + : -1.0f; + } + + return 0; +} + static bool validate(Integrator *integrator, int retcode) { diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h index 968bd582c..33406e3d8 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h @@ -26,8 +26,10 @@ #include #include -namespace Gillespy::TauHybrid +namespace Gillespy { + namespace TauHybrid + { /* IntegratorStatus: represents the runtime state of the integrator. * OK indicates that no errors have occurred. @@ -49,6 +51,7 @@ namespace Gillespy::TauHybrid HybridSimulation *simulation; std::vector *species_state; std::vector *reaction_state; + std::vector *events = nullptr; std::vector concentrations; std::vector populations; @@ -84,6 +87,7 @@ namespace Gillespy::TauHybrid SUNLinearSolver solver; int num_species; int num_reactions; + int *m_roots = nullptr; public: // status: check for errors before using the results. IntegrationStatus status; @@ -114,7 +118,11 @@ namespace Gillespy::TauHybrid void reinitialize(N_Vector y_reset); + void use_events(std::vector *events); + bool has_events() const; + IntegrationResults integrate(double *t); + IntegrationResults integrate(double *t, std::set &events); IntegratorData data; Integrator(HybridSimulation *simulation, N_Vector y0, double reltol, double abstol); @@ -136,5 +144,7 @@ namespace Gillespy::TauHybrid N_Vector init_model_vector(Model &model, URNGenerator urn); int rhs(realtype t, N_Vector y, N_Vector ydot, void *user_data); + int rootfn(realtype t, N_Vector y, realtype *gout, void *user_data); + } } From 1a56c103fa9eefd2492d661cac9b0e416247b6db Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Sun, 3 Oct 2021 19:30:17 -0400 Subject: [PATCH 10/19] Basic direct event trigger detection All triggers are evaluated at the end of every iteration. No root-finding is performed. - Removed calls to `use_events()` in integrator.cpp - Added `trigger_state` to track transition state of each trigger --- .../tau_hybrid_cpp_solver/HybridModel.h | 1 + .../tau_hybrid_cpp_solver/TauHybridSolver.cpp | 98 ++++++++++++------- .../tau_hybrid_cpp_solver/integrator.cpp | 1 - 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h index 75625e74d..2c456538f 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h @@ -59,6 +59,7 @@ namespace Gillespy } inline bool is_persistent() const { return m_use_persist; } + inline bool get_event_id() const { return m_event_id; } EventExecution get_execution(double t, const double *state, int num_state) const; diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp index 637de5906..47fb88db6 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp @@ -68,15 +68,12 @@ namespace Gillespy if (num_trajectories > 0) { y = init_model_vector(model, urn); - } else + } + else { y = y0; } Integrator sol(simulation, y, GPY_HYBRID_RELTOL, GPY_HYBRID_ABSTOL); - if (!events.empty()) - { - sol.use_events(&events); - } // Tau selector initialization. Used to select a valid tau step. TauArgs tau_args = initialize(model, tau_tol); @@ -140,6 +137,21 @@ namespace Gillespy // Otherwise, they are treated just like a delay_queue, by triggering when ready. std::list volatile_queue; + // trigger_state: + // Maps each event to its (current) corresponding event state. + // An event is considered to fire if its trigger() does not match its event state, + // i.e. we have detected a transition from False -> True or True -> False + std::map trigger_state; + for (auto &event : events) + { + // TODO: set to "initial state" rather than trigger value + // With the below implementation, it is impossible for an event to fire at t=t[0]. + trigger_state.insert({ + event.get_event_id(), + event.trigger(simulation->current_time, current_state.data()) + }); + } + while (integration_guard > 0 && simulation->current_time < simulation->end_time) { // Compute current propensity values based on existing state. @@ -189,13 +201,7 @@ namespace Gillespy // Determine what the next time point is. // This will become current_time on the next iteration. // If a retry with a smaller tau_step is deemed necessary, this will change. - next_time = events.empty() - ? simulation->current_time + tau_step - : simulation->current_time + std::min(tau_step, increment); - - // Vector used to store any continuous events discovered by the root-finder. - // How each integrator event is handled is context-dependent. - std::set roots_found; + next_time = simulation->current_time + tau_step; // The integration loop continues until a valid solution is found. // Any invalid Tau steps (which cause negative populations) are discarded. @@ -206,7 +212,7 @@ namespace Gillespy // Integration Step // For deterministic reactions, the concentrations are updated directly. // For stochastic reactions, integration updates the rxn_offsets vector. - IntegrationResults result = sol.integrate(&next_time, roots_found); + IntegrationResults result = sol.integrate(&next_time); if (sol.status == IntegrationStatus::BAD_STEP_SIZE) { invalid_state = true; @@ -313,38 +319,46 @@ namespace Gillespy std::priority_queue, decltype(compare)> trigger_queue(compare); - // Step 1: Check to see if any events are present in the volatile queue. - // If so, those executions must be discarded (double-transition). - for (auto vol_iter = volatile_queue.begin(); vol_iter != volatile_queue.end(); ++vol_iter) + std::set events_found; + // Step 1: Identify any fired event triggers. + for (auto &event : events) { - auto found_event = roots_found.find(vol_iter->get_event_id()); - // Element in roots_found contains event id; discard - if (found_event != roots_found.end()) + bool trigger = event.trigger(next_time, event_state); + if (trigger_state.at(event.get_event_id()) != trigger) { - vol_iter = volatile_queue.erase(vol_iter); + double delay = event.delay(next_time, event_state); + EventExecution execution = event.get_execution(next_time, event_state, num_species); + + // Update trigger state to prevent repeated firings. + trigger_state.find(event.get_event_id())->second = trigger; + events_found.insert(execution.get_event_id()); + if (delay <= 0) + { + // Immediately put EventExecution on "triggered" pile + trigger_queue.push(execution); + } + else if (event.is_persistent()) + { + // Put EventExecution on "delayed" pile + delay_queue.push(execution); + } + else + { + // Delayed, but must be re-checked on every iteration. + volatile_queue.push_back(execution); + } } } - // Step 2: Evaluate remaining roots as triggers - for (int root : roots_found) + // Step 2: Check to see if any events are present in the volatile queue. + // If so, those executions must be discarded (double-transition). + for (auto vol_iter = volatile_queue.begin(); vol_iter != volatile_queue.end(); ++vol_iter) { - Event event = events[root]; - double delay = event.delay(next_time, event_state); - - if (delay <= 0) - { - // Immediately put EventExecution on "triggered" pile - trigger_queue.emplace(event.get_execution(next_time, event_state, num_species)); - } - else if (event.is_persistent()) - { - // Put EventExecution on "delayed" pile - delay_queue.emplace(event.get_execution(delay, event_state, num_species)); - } - else + auto found_event = events_found.find(vol_iter->get_event_id()); + // Element in roots_found contains event id; discard + if (found_event != events_found.end()) { - // Delayed, but must be re-checked on every iteration. - volatile_queue.emplace_back(event.get_execution(delay, event_state, num_species)); + vol_iter = volatile_queue.erase(vol_iter); } } @@ -382,6 +396,14 @@ namespace Gillespy event.execute(next_time, event_state); trigger_queue.pop(); } + + // Step 5: Update any trigger states to reflect the new trigger value. + for (auto &event : events) + { + trigger_state.find(event.get_event_id())->second = event.trigger(next_time, event_state); + } + + std::copy(event_state, event_state + num_species, current_state.begin()); } // ===== ===== diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp index 1b06af349..e71447aaf 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp @@ -165,7 +165,6 @@ IntegrationResults Integrator::integrate(double *t, std::set &events) // NOTE: do we need to check root direction? If so, this is the place to do so. if (m_roots[event_id] < 0 || m_roots[event_id] > 0) { - std::cerr << " ====== EVENT FIRED AT t=" << *t << std::endl; events.insert(event_id); } } From bcbe4d1b382ad39bd11123d5d3041ab45c248471 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Wed, 3 Nov 2021 23:40:06 -0400 Subject: [PATCH 11/19] Basic event toggle functions for `Integrator` Goal is to use new layout where root-finder can search for both events and reactions. - Added `Integrator#use_events` and `Integrator#use_reactions` - Updated `Integrator#integrate` to use new layout --- .../tau_hybrid_cpp_solver/integrator.cpp | 95 ++++++++++++++----- .../c_base/tau_hybrid_cpp_solver/integrator.h | 11 +++ 2 files changed, 82 insertions(+), 24 deletions(-) diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp index e71447aaf..5665b5d7f 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp @@ -142,35 +142,32 @@ IntegrationResults Integrator::integrate(double *t) IntegrationResults Integrator::integrate(double *t, std::set &events) { IntegrationResults results = integrate(t); + unsigned long long num_triggers = data.active_triggers.size(); + unsigned long long num_rxn_roots = data.active_reaction_ids.size(); + unsigned long long root_size = data.active_triggers.size() + data.active_reaction_ids.size(); + int *root_results = new int[root_size]; - std::vector *integrator_events = data.events; - if (integrator_events != nullptr && !integrator_events->empty()) + if (validate(this, CVodeGetRootInfo(cvode_mem, root_results))) { - int num_events = (int) integrator_events->size(); - if (m_roots == nullptr) + unsigned long long root_id; + for (root_id = 0; root_id < num_triggers; ++root_id) { - m_roots = new int[num_events]; + if (root_results[root_id] != 0) + { + std::cerr << "Root-finder found root for event " << root_id << std::endl; + } } - if (validate(this, CVodeGetRootInfo(cvode_mem, m_roots))) + for (; root_id < num_rxn_roots; ++root_id) { - for (int event_id = 0; event_id < num_events; ++event_id) + if (root_results[root_id] != 0) { - // CVode root output can be: -1, 0, or 1 - // +1 : Passes from negative to positive - // -1 : Passes from positive to negative - // 0 : No root found - // (See page 83 of CVODE documentation) - // https://computing.llnl.gov/sites/default/files/cv_guide-5.7.0.pdf - // NOTE: do we need to check root direction? If so, this is the place to do so. - if (m_roots[event_id] < 0 || m_roots[event_id] > 0) - { - events.insert(event_id); - } + std::cerr << "Root-finder found reaction at " << data.active_reaction_ids[root_id] << std::endl; } } } + delete[] root_results; return results; } void Integrator::use_events(std::vector *events) @@ -188,6 +185,48 @@ bool Integrator::has_events() const return data.events != nullptr && !data.events->empty(); } +void Integrator::use_events(const std::vector &events) +{ + data.active_triggers.clear(); + for (auto &event : events) + { + data.active_triggers.emplace_back([event](double t, const double *state) -> double { + return event.trigger(t, state) ? 1.0 : -1.0; + }); + } +} + +void Integrator::use_reactions(const std::vector &reactions) +{ + data.active_reaction_ids.clear(); + for (auto &reaction : reactions) + { + if (reaction.mode == SimulationState::DISCRETE) + { + // Reaction root-finder should only be used on discrete-valued reactions. + // The required IDs are placed into a reference vector and are mapped back out + // when the caller of integrate() retrieves them. + data.active_reaction_ids.push_back(reaction.base_reaction->id); + } + } +} + +void Integrator::use_events(const std::vector &events, const std::vector &reactions) +{ + use_events(events); + use_reactions(reactions); +} + +bool Integrator::enable_root_finder() +{ + unsigned long long root_fn_size = data.active_triggers.size() + data.active_reaction_ids.size(); + return validate(this, CVodeRootInit(cvode_mem, (int) root_fn_size, rootfn)); +} + +bool Integrator::disable_root_finder() +{ + return validate(this, CVodeRootInit(cvode_mem, 0, rootfn)); +} URNGenerator::URNGenerator() @@ -322,15 +361,23 @@ int Gillespy::TauHybrid::rhs(realtype t, N_Vector y, N_Vector ydot, void *user_d int Gillespy::TauHybrid::rootfn(realtype t, N_Vector y, realtype *gout, void *user_data) { - std::vector &events = *(static_cast(user_data)->events); + IntegratorData &data = *static_cast(user_data); + unsigned long long num_triggers = data.active_triggers.size(); + unsigned long long num_reactions = data.active_reaction_ids.size(); realtype *y_t = N_VGetArrayPointer(y); - unsigned long long num_events = events.size(); + realtype *rxn_t = y_t + data.species_state->size(); + realtype *rxn_out = gout + num_triggers; + + unsigned long long trigger_id; + for (trigger_id = 0; trigger_id < num_triggers; ++trigger_id) + { + gout[trigger_id] = data.active_triggers[trigger_id](t, y_t); + } - for (int event_id = 0; event_id < num_events; ++event_id) + unsigned long long rxn_id; + for (rxn_id = 0; rxn_id < num_reactions; ++rxn_id) { - gout[event_id] = events[event_id].trigger(t, y_t) - ? 1.0f - : -1.0f; + rxn_out[rxn_id] = rxn_t[data.active_reaction_ids[rxn_id]]; } return 0; diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h index 33406e3d8..5d5c6e622 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h @@ -52,6 +52,12 @@ namespace Gillespy std::vector *species_state; std::vector *reaction_state; std::vector *events = nullptr; + std::vector> active_triggers; + // Container representing the rootfinder-enabled reactions. + // Each integer at index i represents the reaction id corresponding to rootfinder element i. + // In `rootfn`, this means that gout[i] is the "output" of reaction active_reaction_ids[i]. + // This is used to map the internal reaction number to the actual reaction id. + std::vector active_reaction_ids; std::vector concentrations; std::vector populations; @@ -119,6 +125,11 @@ namespace Gillespy void reinitialize(N_Vector y_reset); void use_events(std::vector *events); + void use_events(const std::vector &events); + void use_events(const std::vector &events, const std::vector &reactions); + void use_reactions(const std::vector &reactions); + bool enable_root_finder(); + bool disable_root_finder(); bool has_events() const; IntegrationResults integrate(double *t); From fb001d38d9b6611f9a7db0ed372e2655e62944a8 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Wed, 17 Nov 2021 00:34:02 -0500 Subject: [PATCH 12/19] Trigger execution for root-finding - Added "trigger pool" data set to track event trigger state - Added "roots found" return parameters to `integrate()` method --- .../tau_hybrid_cpp_solver/TauHybridSolver.cpp | 44 ++++++++++++++++--- .../tau_hybrid_cpp_solver/integrator.cpp | 11 +++-- .../c_base/tau_hybrid_cpp_solver/integrator.h | 2 +- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp index 47fb88db6..9eb3a6584 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp @@ -137,6 +137,11 @@ namespace Gillespy // Otherwise, they are treated just like a delay_queue, by triggering when ready. std::list volatile_queue; + // trigger_pool: + // Set of event IDs whose trigger was detected, where the exact trigger time must be found. + // IDs are removed if either the search interval has passed, or if the trigger has been found. + std::set trigger_pool; + // trigger_state: // Maps each event to its (current) corresponding event state. // An event is considered to fire if its trigger() does not match its event state, @@ -206,13 +211,15 @@ namespace Gillespy // The integration loop continues until a valid solution is found. // Any invalid Tau steps (which cause negative populations) are discarded. sol.save_state(); + // TODO: Fire reactions manually when root-finder is installed. + std::set event_roots, rxn_roots; do { // Integration Step // For deterministic reactions, the concentrations are updated directly. // For stochastic reactions, integration updates the rxn_offsets vector. - IntegrationResults result = sol.integrate(&next_time); + IntegrationResults result = sol.integrate(&next_time, event_roots, rxn_roots); if (sol.status == IntegrationStatus::BAD_STEP_SIZE) { invalid_state = true; @@ -309,7 +316,26 @@ namespace Gillespy : 1000; // ===== ===== - if (!events.empty()) + if (trigger_pool.empty()) + { + double *event_state = N_VGetArrayPointer(sol.y); + for (auto &event : events) + { + if (event.trigger(next_time, event_state) != trigger_state.at(event.get_event_id())) + { + trigger_pool.insert(event.get_event_id()); + } + } + + if (!trigger_pool.empty()) + { + sol.restore_state(); + sol.use_events(events, simulation->reaction_state); + sol.enable_root_finder(); + continue; + } + } + else if (!events.empty()) { double *event_state = N_VGetArrayPointer(sol.y); auto compare = [next_time, event_state](EventExecution &lhs, EventExecution &rhs) -> bool @@ -319,7 +345,6 @@ namespace Gillespy std::priority_queue, decltype(compare)> trigger_queue(compare); - std::set events_found; // Step 1: Identify any fired event triggers. for (auto &event : events) { @@ -331,7 +356,7 @@ namespace Gillespy // Update trigger state to prevent repeated firings. trigger_state.find(event.get_event_id())->second = trigger; - events_found.insert(execution.get_event_id()); + event_roots.insert(execution.get_event_id()); if (delay <= 0) { // Immediately put EventExecution on "triggered" pile @@ -354,9 +379,9 @@ namespace Gillespy // If so, those executions must be discarded (double-transition). for (auto vol_iter = volatile_queue.begin(); vol_iter != volatile_queue.end(); ++vol_iter) { - auto found_event = events_found.find(vol_iter->get_event_id()); + auto found_event = event_roots.find(vol_iter->get_event_id()); // Element in roots_found contains event id; discard - if (found_event != events_found.end()) + if (found_event != event_roots.end()) { vol_iter = volatile_queue.erase(vol_iter); } @@ -395,6 +420,7 @@ namespace Gillespy auto event = trigger_queue.top(); event.execute(next_time, event_state); trigger_queue.pop(); + trigger_pool.erase(event.get_event_id()); } // Step 5: Update any trigger states to reflect the new trigger value. @@ -403,6 +429,12 @@ namespace Gillespy trigger_state.find(event.get_event_id())->second = event.trigger(next_time, event_state); } + // Disable "root search mode" if no more events are found + if (trigger_pool.empty()) + { + sol.disable_root_finder(); + } + std::copy(event_state, event_state + num_species, current_state.begin()); } // ===== ===== diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp index 5665b5d7f..148d2e896 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp @@ -132,6 +132,7 @@ IntegrationResults Integrator::integrate(double *t) { return { nullptr, nullptr }; } + *t = this->t; return { NV_DATA_S(y), @@ -139,7 +140,7 @@ IntegrationResults Integrator::integrate(double *t) }; } -IntegrationResults Integrator::integrate(double *t, std::set &events) +IntegrationResults Integrator::integrate(double *t, std::set &event_roots, std::set &reaction_roots) { IntegrationResults results = integrate(t); unsigned long long num_triggers = data.active_triggers.size(); @@ -154,7 +155,7 @@ IntegrationResults Integrator::integrate(double *t, std::set &events) { if (root_results[root_id] != 0) { - std::cerr << "Root-finder found root for event " << root_id << std::endl; + event_roots.insert((int) root_id); } } @@ -162,7 +163,7 @@ IntegrationResults Integrator::integrate(double *t, std::set &events) { if (root_results[root_id] != 0) { - std::cerr << "Root-finder found reaction at " << data.active_reaction_ids[root_id] << std::endl; + reaction_roots.insert(data.active_reaction_ids[root_id]); } } } @@ -225,7 +226,9 @@ bool Integrator::enable_root_finder() bool Integrator::disable_root_finder() { - return validate(this, CVodeRootInit(cvode_mem, 0, rootfn)); + data.active_triggers.clear(); + data.active_reaction_ids.clear(); + return validate(this, CVodeRootInit(cvode_mem, 0, NULL)); } diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h index 5d5c6e622..00c6efc5d 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h @@ -133,7 +133,7 @@ namespace Gillespy bool has_events() const; IntegrationResults integrate(double *t); - IntegrationResults integrate(double *t, std::set &events); + IntegrationResults integrate(double *t, std::set &event_roots, std::set &reaction_roots); IntegratorData data; Integrator(HybridSimulation *simulation, N_Vector y0, double reltol, double abstol); From ee31ca3ed4767e454edf6b508c3a393b7160cea7 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Thu, 18 Nov 2021 18:33:07 -0500 Subject: [PATCH 13/19] Bug fixes before merge Parameter bug was caused by parameters never been templated as variable (always constants). - Fixed bug where parameter assignments are ignored - Added `variable` member to `SanitizedModel` - Removed `variable` param from `SanitizedModel#get_template`, instead pulls from `self` - Updated docstrings in new integrator methods - Updated root-finder return to apply direct reactions - Fixed parameter overrides not applying - Added array to track which parameters have overrides --- gillespy2/solvers/cpp/build/build_engine.py | 4 +-- gillespy2/solvers/cpp/build/template_gen.py | 5 ++-- .../tau_hybrid_cpp_solver/HybridModel.cpp | 2 ++ .../tau_hybrid_cpp_solver/TauHybridSolver.cpp | 25 +++++++++++++++-- .../tau_hybrid_cpp_solver/integrator.cpp | 16 +---------- .../c_base/tau_hybrid_cpp_solver/integrator.h | 28 +++++++++++++++++-- .../solvers/cpp/c_base/template/template.cpp | 9 ++++-- gillespy2/solvers/cpp/tau_hybrid_c_solver.py | 3 +- 8 files changed, 65 insertions(+), 27 deletions(-) diff --git a/gillespy2/solvers/cpp/build/build_engine.py b/gillespy2/solvers/cpp/build/build_engine.py index 91389f016..5a9423a59 100644 --- a/gillespy2/solvers/cpp/build/build_engine.py +++ b/gillespy2/solvers/cpp/build/build_engine.py @@ -110,14 +110,14 @@ def prepare(self, model: "Union[Model, template_gen.SanitizedModel]", variable=F # If a raw GillesPy2 model was provided, convert it to a sanitized model. if isinstance(model, gillespy2.Model): - model = template_gen.SanitizedModel(model) + model = template_gen.SanitizedModel(model, variable=variable) elif not isinstance(model, template_gen.SanitizedModel): raise TypeError(f"Build engine expected gillespy2.Model or SanitizedModel type: received {type(model)}") # Build the template and write it to the temp directory and remove the sample template_definitions header. template_file = self.template_dir.joinpath(self.template_definitions_name) template_file.unlink() - template_gen.write_definitions(str(template_file), model.get_template(variable=variable)) + template_gen.write_definitions(str(template_file), model.get_template()) custom_definitions = model.get_options() if custom_definitions is not None: options_file = self.template_dir.joinpath(self.template_options_name) diff --git a/gillespy2/solvers/cpp/build/template_gen.py b/gillespy2/solvers/cpp/build/template_gen.py index 24d38e059..0eca20948 100644 --- a/gillespy2/solvers/cpp/build/template_gen.py +++ b/gillespy2/solvers/cpp/build/template_gen.py @@ -39,6 +39,7 @@ class SanitizedModel: def __init__(self, model: Model, variable=False): self.model = model + self.variable = variable self.species: "OrderedDict[str, Species]" = OrderedDict() self.species_names = model.sanitized_species_names() @@ -151,7 +152,7 @@ def use_rate_rule(self, rate_rule: "RateRule") -> "SanitizedModel": log.warning(f"Could not sanitize rate rule formula expression: {rate_rule.formula}") return self - def get_template(self, variable=False) -> "dict[str, str]": + def get_template(self) -> "dict[str, str]": """ Creates a dictionary of C++ macro definitions from the given model. The keys of the dictionary contain the name of the macro definition. @@ -166,7 +167,7 @@ def get_template(self, variable=False) -> "dict[str, str]": results = dict({}) # Get definitions for variables - parameter_definitions = template_def_variables(self, variable) + parameter_definitions = template_def_variables(self, self.variable) results.update(parameter_definitions) # Get definitions for species diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp index fe8be186b..26b7f7d59 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp @@ -57,6 +57,8 @@ namespace Gillespy m_num_variables(num_variables), m_variables(new double[num_variables]) { + std::memcpy(m_state, state, num_state); + std::memcpy(m_variables, variables, num_variables); use_assignments(); } diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp index 9eb3a6584..e3da64367 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp @@ -212,10 +212,13 @@ namespace Gillespy // Any invalid Tau steps (which cause negative populations) are discarded. sol.save_state(); // TODO: Fire reactions manually when root-finder is installed. - std::set event_roots, rxn_roots; + std::set event_roots; + std::set rxn_roots; do { + invalid_state = false; + // Integration Step // For deterministic reactions, the concentrations are updated directly. // For stochastic reactions, integration updates the rxn_offsets vector. @@ -227,10 +230,26 @@ namespace Gillespy // resulting in an early termination of the trajectory. break; } + else if (!rxn_roots.empty()) + { + // "Direct" roots found; these are executed manually + for (unsigned int rxn_i : rxn_roots) + { + // "Fire" a reaction by recording changes in dependent species. + // If a negative value is detected, break without saving changes. + for (int spec_i = 0; spec_i < num_species; ++spec_i) + { + // Unlike the Tau-leaping version of reaction firings, + // it is not possible to have a negative state occur in direct reactions. + population_changes[spec_i] += model.reactions[rxn_i].species_change[spec_i]; + result.reactions[rxn_i] = log(urn.next()); + } + } + continue; + } // The integrator has, at this point, been validated. // Any errors beyond this point is assumed to be a stochastic state failure. - invalid_state = false; // 0-initialize our population_changes array. for (int p_i = 0; p_i < num_species; ++p_i) @@ -418,7 +437,9 @@ namespace Gillespy while (!trigger_queue.empty()) { auto event = trigger_queue.top(); + event.execute(next_time, event_state); + std::cerr << "== AFTER ==" << std::endl; trigger_queue.pop(); trigger_pool.erase(event.get_event_id()); } diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp index 148d2e896..615ec0b1d 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.cpp @@ -140,7 +140,7 @@ IntegrationResults Integrator::integrate(double *t) }; } -IntegrationResults Integrator::integrate(double *t, std::set &event_roots, std::set &reaction_roots) +IntegrationResults Integrator::integrate(double *t, std::set &event_roots, std::set &reaction_roots) { IntegrationResults results = integrate(t); unsigned long long num_triggers = data.active_triggers.size(); @@ -171,20 +171,6 @@ IntegrationResults Integrator::integrate(double *t, std::set &event_roots, delete[] root_results; return results; } -void Integrator::use_events(std::vector *events) -{ - if (events == nullptr - || !validate(this, CVodeRootInit(cvode_mem, (int) events->size(), rootfn))) - { - return; - } - data.events = events; -} - -bool Integrator::has_events() const -{ - return data.events != nullptr && !data.events->empty(); -} void Integrator::use_events(const std::vector &events) { diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h index 00c6efc5d..a0ce3884c 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/integrator.h @@ -124,16 +124,38 @@ namespace Gillespy void reinitialize(N_Vector y_reset); - void use_events(std::vector *events); + /// @brief Make events available to root-finder during integration. + /// The root-finder itself is not activated until enable_root_finder() is called. + /// + /// @param events List of event objects to make available to the root-finder. + /// The trigger functions of all given events are added as root-finder targets. void use_events(const std::vector &events); + + /// @brief Make events and reactions available to root-finder during integration. + /// The root-finder itself is not activated until enable_root_finder() is called. + /// + /// @param events List of event objects to make available to the root-finder. + /// @param reactions List of reaction objects to make available to the root-finder. void use_events(const std::vector &events, const std::vector &reactions); + + /// @brief Make reactions available to root-finder during integration. + /// The root-finder itself is not activated until enable_root_finder() is called. + /// + /// @param reactions List of reaction objects to make available to the root-finder. void use_reactions(const std::vector &reactions); + + /// @brief Installs a CVODE root-finder onto the integrator. + /// Any events or reactions provided by previous calls to use_events() or use_reactions() + /// will cause the integrator to return early, which the integrate() method will indicate. bool enable_root_finder(); + + /// @brief Removes the CVODE root-finder from the integrator. + /// Early returns on root-finder events no longer happen, + /// and the underlying SBML event data and reaction data are removed. bool disable_root_finder(); - bool has_events() const; IntegrationResults integrate(double *t); - IntegrationResults integrate(double *t, std::set &event_roots, std::set &reaction_roots); + IntegrationResults integrate(double *t, std::set &event_roots, std::set &reaction_roots); IntegratorData data; Integrator(HybridSimulation *simulation, N_Vector y0, double reltol, double abstol); diff --git a/gillespy2/solvers/cpp/c_base/template/template.cpp b/gillespy2/solvers/cpp/c_base/template/template.cpp index d5353cf84..68b076aac 100644 --- a/gillespy2/solvers/cpp/c_base/template/template.cpp +++ b/gillespy2/solvers/cpp/c_base/template/template.cpp @@ -30,6 +30,7 @@ namespace Gillespy { static double param_overrides[GPY_PARAMETER_NUM_VARIABLES]; + static bool param_override_mask[GPY_PARAMETER_NUM_VARIABLES]; double populations[GPY_NUM_SPECIES] = GPY_INIT_POPULATIONS; std::vector species_populations( @@ -64,7 +65,9 @@ namespace Gillespy double *variables = new double[GPY_PARAMETER_NUM_VARIABLES]; #define CONSTANT(id, value) - #define VARIABLE(id, value) variables[id] = value; (*num_variables)++; + #define VARIABLE(id, value) variables[id] = param_override_mask[id] \ + ? param_overrides[id] \ + : value; (*num_variables)++; GPY_PARAMETER_VALUES #undef VARIABLE #undef CONSTANT @@ -126,7 +129,9 @@ namespace Gillespy void map_variable_parameters(std::stringstream &stream) { - #define VARIABLE(id, value) stream >> param_overrides[id]; + #define VARIABLE(id, value) \ + stream >> param_overrides[id]; \ + param_override_mask[id] = true; #define CONSTANT(id, value) GPY_PARAMETER_VALUES #undef CONSTANT diff --git a/gillespy2/solvers/cpp/tau_hybrid_c_solver.py b/gillespy2/solvers/cpp/tau_hybrid_c_solver.py index d8a8b336f..069e1fae0 100644 --- a/gillespy2/solvers/cpp/tau_hybrid_c_solver.py +++ b/gillespy2/solvers/cpp/tau_hybrid_c_solver.py @@ -113,7 +113,8 @@ def __create_options(cls, sanitized_model: "SanitizedModel") -> "SanitizedModel" def _build(self, model: "Union[Model, SanitizedModel]", simulation_name: str, variable: bool, debug: bool = False, custom_definitions=None) -> str: - sanitized_model = TauHybridCSolver.__create_options(SanitizedModel(model)) + variable = variable or len(model.listOfEvents) > 0 + sanitized_model = TauHybridCSolver.__create_options(SanitizedModel(model, variable=variable)) for rate_rule in model.listOfRateRules.values(): sanitized_model.use_rate_rule(rate_rule) return super()._build(sanitized_model, simulation_name, variable, debug) From bfe889039f82687e9e1c687e93185b4d7f86e184 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Thu, 18 Nov 2021 23:30:25 -0500 Subject: [PATCH 14/19] Unit tests for events and bug fixes - Added comprehensive test cases for event features - Fixed non-persistent triggers by reordering volatile trigger checks - Fixed delay time checks - Fixed calls to `memcpy` in `EventExecution`'s copy constructor --- .../tau_hybrid_cpp_solver/HybridModel.cpp | 22 ++-- .../tau_hybrid_cpp_solver/TauHybridSolver.cpp | 47 +++++--- test/test_hybrid_c_events.py | 112 ++++++++++++++++++ 3 files changed, 154 insertions(+), 27 deletions(-) create mode 100644 test/test_hybrid_c_events.py diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp index 26b7f7d59..c4ba18cb5 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp @@ -57,31 +57,37 @@ namespace Gillespy m_num_variables(num_variables), m_variables(new double[num_variables]) { - std::memcpy(m_state, state, num_state); - std::memcpy(m_variables, variables, num_variables); + std::memcpy(m_state, state, sizeof(double) * num_state); + std::memcpy(m_variables, variables, sizeof(double) * num_variables); use_assignments(); } EventExecution::EventExecution(const EventExecution &old_event) - : m_execution_time(old_event.m_execution_time), + : m_num_state(old_event.m_num_state), + m_num_variables(old_event.m_num_variables), + m_execution_time(old_event.m_execution_time), m_event_id(old_event.m_event_id), m_assignments(old_event.m_assignments) { if (old_event.m_state != nullptr && m_num_state > 0) { m_state = new double[m_num_state]; - std::memcpy(m_state, old_event.m_state, m_num_state); + std::memcpy(m_state, old_event.m_state, sizeof(double) * m_num_state); } if (old_event.m_variables != nullptr && m_num_variables > 0) { m_variables = new double[m_num_variables]; - std::memcpy(m_variables, old_event.m_variables, m_num_variables); + std::memcpy(m_variables, old_event.m_variables, sizeof(double) * m_num_variables); } } EventExecution::EventExecution(EventExecution &&old_event) noexcept - : m_execution_time(old_event.m_execution_time), + : m_state(old_event.m_state), + m_num_state(old_event.m_num_state), + m_variables(old_event.m_variables), + m_num_variables(old_event.m_num_variables), + m_execution_time(old_event.m_execution_time), m_event_id(old_event.m_event_id), m_assignments(std::move(old_event.m_assignments)) { @@ -107,7 +113,7 @@ namespace Gillespy m_num_state = old_event.m_num_state; m_state = new double[m_num_state]; } - std::memcpy(m_state, old_event.m_state, m_num_state); + std::memcpy(m_state, old_event.m_state, sizeof(double) * m_num_state); } if (old_event.m_variables != nullptr) @@ -118,7 +124,7 @@ namespace Gillespy m_num_variables = old_event.m_num_variables; m_variables = new double[m_num_variables]; } - std::memcpy(m_variables, old_event.m_variables, m_num_variables); + std::memcpy(m_variables, old_event.m_variables, sizeof(double) * m_num_variables); } } diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp index e3da64367..9e6f7d4c5 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp @@ -371,7 +371,7 @@ namespace Gillespy if (trigger_state.at(event.get_event_id()) != trigger) { double delay = event.delay(next_time, event_state); - EventExecution execution = event.get_execution(next_time, event_state, num_species); + EventExecution execution = event.get_execution(next_time + delay, event_state, num_species); // Update trigger state to prevent repeated firings. trigger_state.find(event.get_event_id())->second = trigger; @@ -388,41 +388,51 @@ namespace Gillespy } else { - // Delayed, but must be re-checked on every iteration. - volatile_queue.push_back(execution); - } - } - } + // Search the volatile queue to see if it is already present. + // If it is, the event has "double-fired" and must be erased. + auto vol_iter = volatile_queue.begin(); + while (vol_iter != volatile_queue.end() + && vol_iter->get_event_id() != event.get_event_id()) + { + ++vol_iter; + } - // Step 2: Check to see if any events are present in the volatile queue. - // If so, those executions must be discarded (double-transition). - for (auto vol_iter = volatile_queue.begin(); vol_iter != volatile_queue.end(); ++vol_iter) - { - auto found_event = event_roots.find(vol_iter->get_event_id()); - // Element in roots_found contains event id; discard - if (found_event != event_roots.end()) - { - vol_iter = volatile_queue.erase(vol_iter); + if (vol_iter == volatile_queue.end()) + { + // No match found; this is a new delay trigger, and is therefore valid. + // Delayed, but must be re-checked on every iteration. + volatile_queue.push_back(execution); + } + else + { + // Match found; this is an existing trigger, discard. + volatile_queue.erase(vol_iter); + trigger_pool.erase(event.get_event_id()); + trigger_state.at(event.get_event_id()) = !trigger_state.at(event.get_event_id()); + } + } } } - // Step 3: Process delayed executions that are now ready to fire. + // Step 2: Process delayed, non-persistent executions that are now ready to fire. // Both the volatile and non-volatile queue are processed in a similar manner. for (auto vol_event = volatile_queue.begin(); vol_event != volatile_queue.end(); ++vol_event) { // Execution objects in the volatile queue must remain True until execution. // Remove any execution objects which transitioned to False before execution. - if (vol_event->get_execution_time() >= next_time) + if (vol_event->get_execution_time() < next_time) { trigger_queue.push(*vol_event); vol_event = volatile_queue.erase(vol_event); } } + // Step 3: Process delayed executions, which includes both persistent triggers + // and non-persistent triggers whose execution time has arrived. while (!delay_queue.empty()) { auto &event = delay_queue.top(); - if (event.get_execution_time() < next_time) + if (event.get_execution_time() >= next_time) { // Delay queue is sorted in chronological order. // As soon as we hit a time that is beyond the current time, @@ -439,7 +449,6 @@ namespace Gillespy auto event = trigger_queue.top(); event.execute(next_time, event_state); - std::cerr << "== AFTER ==" << std::endl; trigger_queue.pop(); trigger_pool.erase(event.get_event_id()); } diff --git a/test/test_hybrid_c_events.py b/test/test_hybrid_c_events.py new file mode 100644 index 000000000..f22090bf2 --- /dev/null +++ b/test/test_hybrid_c_events.py @@ -0,0 +1,112 @@ +import unittest +import gillespy2 +from gillespy2 import TauHybridCSolver +import numpy as np + +class EventFeatures(unittest.TestCase): + class BaseEventModel(gillespy2.Model): + def __init__(self, s1, s2, rate): + super().__init__(name="BasicEventModel") + + self.s1 = gillespy2.Species(name="S1", initial_value=s1, mode="continuous") + self.s2 = gillespy2.Species(name="S2", initial_value=s2, mode="continuous") + self.rate = gillespy2.Parameter(name="k1", expression=rate) + self.add_species([self.s1, self.s2]) + self.add_parameter([self.rate]) + self.add_reaction([ + gillespy2.Reaction(name="r1", rate=self.rate, + reactants={self.s1: 1}, + products={self.s2: 1}) + ]) + + def test_event_with_time_trigger(self): + model = EventFeatures.BaseEventModel(s1=0, s2=0, rate=0.0) + event = gillespy2.Event(name="ev1", assignments=[ + gillespy2.EventAssignment(variable=model.s1, expression="100.0"), + gillespy2.EventAssignment(variable=model.rate, expression="1.0") + ], trigger=gillespy2.EventTrigger(expression="t>5")) + model.add_event(event) + + solver = TauHybridCSolver(model=model) + result = model.run(solver=solver)[0] + s1, s2 = result["S1"][-1], result["S2"][-1] + + self.assertGreater(s2, s1, "Expected S2 > S1") + self.assertGreater(s1, 0.0, "Expected S1 > 0") + self.assertAlmostEqual(s1 + s2, 100.0, places=1) + + def test_event_with_species_trigger(self): + model = EventFeatures.BaseEventModel(s1=100, s2=0, rate=10.0) + event = gillespy2.Event(name="ev1", assignments=[ + gillespy2.EventAssignment(variable=model.s1, expression="100.0"), + gillespy2.EventAssignment(variable=model.rate, expression="0.0") + ], trigger=gillespy2.EventTrigger(expression="S1<90")) + model.add_event(event) + + solver = TauHybridCSolver(model=model) + result = model.run(solver=solver)[0] + s1, s2 = result["S1"][-1], result["S2"][-1] + + self.assertEqual(s1, 100, "Expected S1 == 100 (trigger set S1 to 100 and rate to 0") + self.assertGreater(s2, 0, "Expected S2 > 0") + self.assertFalse(np.any(result["S1"] <= 90.0), "Expected S1 > 90 for entire simulation") + + def test_delay_trigger_persistent(self): + model = EventFeatures.BaseEventModel(s1=100, s2=0, rate=1.0) + event1 = gillespy2.Event(name="ev1", assignments=[ + gillespy2.EventAssignment(variable=model.s1, expression="0"), + gillespy2.EventAssignment(variable=model.s2, expression="0"), + gillespy2.EventAssignment(variable=model.rate, expression="0.0") + ], trigger=gillespy2.EventTrigger(expression="S1<60 and S290 and t<3.5", persistent=True), delay="1.0") + model.add_event([event1, event2]) + + solver = TauHybridCSolver(model=model) + result = model.run(solver=solver)[0] + s1, s2 = result["S1"][-1], result["S2"][-1] + + # If delay is working correctly: + # * event1 is never triggered. event1 sets everything to 0. + # If event1 fires, event2 can never fire. + # * event2 is triggered, setting everything to 100 (and rate to 0). + self.assertNotIn(0, [s1, s2], "Non-persistent event fired unexpectedly") + self.assertEqual(s1, 200, "Persistent event failed to fire") + self.assertEqual(s2, 200, "Persistent event failed to fire") + + def test_trigger_priorities(self): + model = EventFeatures.BaseEventModel(s1=100, s2=0, rate=1.0) + event1 = gillespy2.Event(name="ev1", assignments=[ + gillespy2.EventAssignment(variable=model.s1, expression="100"), + gillespy2.EventAssignment(variable=model.s2, expression="100"), + ], trigger=gillespy2.EventTrigger(expression="S1 < 50"), priority="2*t*S1") + event2 = gillespy2.Event(name="ev2", assignments=[ + gillespy2.EventAssignment(variable=model.s1, expression="0"), + ], trigger=gillespy2.EventTrigger(expression="S1 < 50"), priority="t*S1") + model.add_event([event1, event2]) + + solver = TauHybridCSolver(model=model) + result = model.run(solver=solver)[0] + s1, s2 = result["S1"][-1], result["S2"][-1] + + # If priority is working correctly, event2 should ALWAYS fire before event1. + # Proper result is S1 = 0, S2 = 100, so no further reactions are possible. + self.assertEqual(s1, 0, "Events fired in an incorrect order") + self.assertEqual(s2, 100, "Events fired in an incorrect order") + + def test_use_values_from_trigger_time(self): + model = EventFeatures.BaseEventModel(s1=100, s2=0, rate=1.0) + event = gillespy2.Event(name="ev1", assignments=[ + gillespy2.EventAssignment(variable=model.s1, expression="S2"), + gillespy2.EventAssignment(variable=model.rate, expression="0.0"), + ], trigger=gillespy2.EventTrigger(expression="S1 < 60"), delay="1.5", use_values_from_trigger_time=True) + model.add_event(event) + + solver = TauHybridCSolver(model=model) + result = model.run(solver=solver)[0] + s1, s2 = result["S1"][-1], result["S2"][-1] + + self.assertGreater(s2, s1, "Event assignment did not assign values from trigger time") From 356aad0212c0bc645c26d30d777446f5fe4d814e Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Fri, 19 Nov 2021 22:53:10 -0500 Subject: [PATCH 15/19] Initial value templating - Added `initial_value` handling to template gen - Added initial value as macro parameter for `EVENT` --- .../tau_hybrid_cpp_solver/HybridModel.h | 6 ++++ .../tau_hybrid_cpp_solver/hybrid_template.cpp | 31 ++++++++++++++++--- gillespy2/solvers/cpp/tau_hybrid_c_solver.py | 18 ++++++++++- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h index 2c456538f..f667a784c 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h @@ -58,6 +58,11 @@ namespace Gillespy return Event::delay(m_event_id, t, state, Reaction::s_variables.get(), Reaction::s_constants.get()); } + inline bool get_initial_value() const + { + return Event::initial_value(m_event_id); + } + inline bool is_persistent() const { return m_use_persist; } inline bool get_event_id() const { return m_event_id; } @@ -88,6 +93,7 @@ namespace Gillespy const double *variables, const double *constants); static void assign(int event_id, double t, EventOutput output); + static bool initial_value(int event_id); }; class EventExecution diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp index bc026451c..691a4acb4 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/hybrid_template.cpp @@ -56,13 +56,34 @@ namespace Gillespy } + bool Event::initial_value(int event_id) + { + #define INIT_TRUE (1) + #define INIT_FALSE (0) + #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_perist, init) \ + case event_id: return (bool) init; + + switch (event_id) + { + GPY_HYBRID_EVENTS + + default: + return false; + } + + #undef EVENT + #undef INIT_FALSE + #undef INIT_TRUE + } + + bool Event::trigger( int event_id, double t, const double *S, const double *P, const double *C) { - #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ + #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist, init) \ case event_id: return (bool) (trigger); switch (event_id) @@ -82,7 +103,7 @@ namespace Gillespy const double *P, const double *C) { - #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ + #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist, init) \ case event_id: return static_cast(delay); switch (event_id) @@ -102,7 +123,7 @@ namespace Gillespy const double *P, const double *C) { - #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ + #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist, init) \ case event_id: return static_cast(priority); switch (event_id) @@ -125,7 +146,7 @@ namespace Gillespy #define USE_EVAL false #define PERSISTENT true #define IRREGULAR false - #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ + #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist, init) \ events.emplace_back(Event(event_id, use_trigger, use_persist)); GPY_HYBRID_EVENTS #undef EVENT @@ -138,7 +159,7 @@ namespace Gillespy void EventExecution::use_assignments() { m_assignments.clear(); - #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist) \ + #define EVENT(event_id, targets, trigger, delay, priority, use_trigger, use_persist, init) \ case event_id: m_assignments = std::vector(targets); break; switch (m_event_id) diff --git a/gillespy2/solvers/cpp/tau_hybrid_c_solver.py b/gillespy2/solvers/cpp/tau_hybrid_c_solver.py index 069e1fae0..bec20bede 100644 --- a/gillespy2/solvers/cpp/tau_hybrid_c_solver.py +++ b/gillespy2/solvers/cpp/tau_hybrid_c_solver.py @@ -48,6 +48,12 @@ def __create_options(cls, sanitized_model: "SanitizedModel") -> "SanitizedModel" # When event.trigger.persistent == True "PERSISTENT", ] + initial_value_types = [ + # When event.trigger.initial_value == False + "INIT_FALSE", + # When event.trigger.initial_value == True + "INIT_TRUE", + ] species_mode_list = [] for spec_id, species in enumerate(sanitized_model.species.values()): @@ -72,6 +78,7 @@ def __create_options(cls, sanitized_model: "SanitizedModel") -> "SanitizedModel" if event.priority is not None else "0" use_trigger = trigger_mode_types[int(bool(event.use_values_from_trigger_time))] use_persist = persist_types[int(bool(event.trigger.persistent))] + initial_value = initial_value_types[int(bool(event.trigger.value or False))] assignments: "list[str]" = [] for assign in event.assignments: @@ -101,7 +108,16 @@ def __create_options(cls, sanitized_model: "SanitizedModel") -> "SanitizedModel" assign_id += 1 assignments: "str" = " AND ".join(assignments) event_list.append( - f"EVENT({event_id},{{{assignments}}},{trigger},{delay},{priority},{use_trigger},{use_persist})" + f"EVENT(" + f"{event_id}," + f"{{{assignments}}}," + f"{trigger}," + f"{delay}," + f"{priority}," + f"{use_trigger}," + f"{use_persist}," + f"{initial_value}" + f")" ) sanitized_model.options["GPY_HYBRID_SPECIES_MODES"] = " ".join(species_mode_list) From 7f290d80eeec01d6b9112b976837b86338e58b16 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Fri, 19 Nov 2021 23:11:51 -0500 Subject: [PATCH 16/19] Event trigger clean-up and initial values implementation Comp - Moved event execution code to separate `EventList` class - Contains exact same code, just abstracted away - Added check to trigger state at beginning of trajectory - Moved first trajectory value to be after events have been processed --- .../tau_hybrid_cpp_solver/HybridModel.cpp | 137 +++++++++++++ .../tau_hybrid_cpp_solver/HybridModel.h | 26 ++- .../tau_hybrid_cpp_solver/TauHybridSolver.cpp | 189 +++--------------- test/test_hybrid_c_events.py | 16 ++ 4 files changed, 204 insertions(+), 164 deletions(-) diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp index c4ba18cb5..606598013 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.cpp @@ -475,5 +475,142 @@ namespace Gillespy } } + EventList::EventList() + { + Event::use_events(m_events); + + for (auto &event : m_events) + { + // With the below implementation, it is impossible for an event to fire at t=t[0]. + m_trigger_state.insert({ + event.get_event_id(), + event.get_initial_value(), + }); + } + } + + bool EventList::evaluate_triggers(double *event_state, double t) + { + for (auto &event: m_events) + { + if (event.trigger(t, event_state) != m_trigger_state.at(event.get_event_id())) + { + m_trigger_pool.insert(event.get_event_id()); + } + } + + return has_active_events(); + } + + bool EventList::evaluate(double *event_state, int output_size, double t, const std::set &events_found) + { + if (m_events.empty()) + { + return has_active_events(); + } + + auto compare = [t, event_state](EventExecution &lhs, EventExecution &rhs) -> bool + { + return lhs.priority(t, event_state) < rhs.priority(t, event_state); + }; + std::priority_queue, decltype(compare)> + trigger_queue(compare); + + // Step 1: Identify any fired event triggers. + for (auto &event : m_events) + { + bool trigger = event.trigger(t, event_state); + if (m_trigger_state.at(event.get_event_id()) != trigger) + { + double delay = event.delay(t, event_state); + EventExecution execution = event.get_execution(t + delay, event_state, output_size); + + // Update trigger state to prevent repeated firings. + m_trigger_state.find(event.get_event_id())->second = trigger; + if (delay <= 0) + { + // Immediately put EventExecution on "triggered" pile + trigger_queue.push(execution); + } + else if (event.is_persistent()) + { + // Put EventExecution on "delayed" pile + m_delay_queue.push(execution); + } + else + { + // Search the volatile queue to see if it is already present. + // If it is, the event has "double-fired" and must be erased. + auto vol_iter = m_volatile_queue.begin(); + while (vol_iter != m_volatile_queue.end() + && vol_iter->get_event_id() != event.get_event_id()) + { + ++vol_iter; + } + + if (vol_iter == m_volatile_queue.end()) + { + // No match found; this is a new delay trigger, and is therefore valid. + // Delayed, but must be re-checked on every iteration. + m_volatile_queue.push_back(execution); + } + else + { + // Match found; this is an existing trigger, discard. + m_volatile_queue.erase(vol_iter); + m_trigger_pool.erase(event.get_event_id()); + m_trigger_state.at(event.get_event_id()) = !m_trigger_state.at(event.get_event_id()); + } + } + } + } + + // Step 2: Process delayed, non-persistent executions that are now ready to fire. + // Both the volatile and non-volatile queue are processed in a similar manner. + for (auto vol_event = m_volatile_queue.begin(); vol_event != m_volatile_queue.end(); ++vol_event) + { + // Execution objects in the volatile queue must remain True until execution. + // Remove any execution objects which transitioned to False before execution. + if (vol_event->get_execution_time() < t) + { + trigger_queue.push(*vol_event); + vol_event = m_volatile_queue.erase(vol_event); + } + } + + // Step 3: Process delayed executions, which includes both persistent triggers + // and non-persistent triggers whose execution time has arrived. + while (!m_delay_queue.empty()) + { + auto &event = m_delay_queue.top(); + if (event.get_execution_time() >= t) + { + // Delay queue is sorted in chronological order. + // As soon as we hit a time that is beyond the current time, + // there is no use in continuing through the queue. + break; + } + trigger_queue.push(event); + m_delay_queue.pop(); + } + + // Step 4: Process any pending triggers, unconditionally. + while (!trigger_queue.empty()) + { + auto event = trigger_queue.top(); + + event.execute(t, event_state); + trigger_queue.pop(); + m_trigger_pool.erase(event.get_event_id()); + } + + // Step 5: Update any trigger states to reflect the new trigger value. + for (auto &event : m_events) + { + m_trigger_state.find(event.get_event_id())->second = event.trigger(t, event_state); + } + + return has_active_events(); + } } } diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h index f667a784c..b21fd4f45 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/HybridModel.h @@ -22,6 +22,8 @@ #include "tau.h" #include +#include +#include #define GPY_HYBRID_ABSTOL 1e-8 #define GPY_HYBRID_RELTOL 1e-8 @@ -30,7 +32,12 @@ namespace Gillespy { namespace TauHybrid { + class EventExecution; + class Event; + typedef int ReactionId; + template + using DelayedExecutionQueue = std::priority_queue, T>; struct EventOutput { @@ -41,7 +48,24 @@ namespace Gillespy const double *constants; }; - class EventExecution; + class EventList + { + public: + EventList(); + bool evaluate_triggers(double *event_state, double t); + bool evaluate(double *output, int output_size, double t, const std::set &events_found); + inline bool has_active_events() const + { + return !m_trigger_pool.empty(); + } + + private: + std::vector m_events; + std::set m_trigger_pool; + std::map m_trigger_state; + DelayedExecutionQueue> m_delay_queue; + std::list m_volatile_queue; + }; class Event { diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp index 9e6f7d4c5..f3487ac9b 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp @@ -36,8 +36,6 @@ namespace Gillespy { namespace TauHybrid { - template - using DelayedExecutionQueue = std::priority_queue, T>; bool interrupted = false; @@ -86,18 +84,9 @@ namespace Gillespy sol.reinitialize(y0); } - // Initialize each species with their respective user modes. - for (int spec_i = 0; spec_i < num_species; ++spec_i) - { - HybridSpecies *spec = &simulation->species_state[spec_i]; - spec->partition_mode = spec->user_mode == SimulationState::DYNAMIC - ? SimulationState::DISCRETE - : spec->user_mode; - simulation->trajectories[traj][0][spec_i] = spec->base_species->initial_population; - } - // Population/concentration state values for each species. // TODO: change back double -> hybrid_state, once we figure out how that works + EventList event_list; std::vector current_state(num_species); std::vector current_populations(num_species); @@ -108,6 +97,27 @@ namespace Gillespy current_populations[spec_i] = species[spec_i].initial_population; } + // Check for initial event triggers at t=0 (based on initial_value of trigger) + std::set event_roots; + std::set rxn_roots; + if (event_list.evaluate_triggers(current_state.data(), simulation->current_time)) + { + double *event_state = N_VGetArrayPointer(sol.y); + event_list.evaluate(current_state.data(), num_species, simulation->current_time, event_roots); + std::copy(current_state.begin(), current_state.end(), event_state); + sol.refresh_state(); + } + + // Initialize each species with their respective user modes. + for (int spec_i = 0; spec_i < num_species; ++spec_i) + { + HybridSpecies *spec = &simulation->species_state[spec_i]; + spec->partition_mode = spec->user_mode == SimulationState::DYNAMIC + ? SimulationState::DISCRETE + : spec->user_mode; + simulation->trajectories[traj][0][spec_i] = current_state[spec_i]; + } + // SIMULATION STEP LOOP int save_idx = 1; double next_time; @@ -126,37 +136,6 @@ namespace Gillespy // For now, a "guard" is put in place to prevent potentially infinite loops from occurring. unsigned int integration_guard = 1000; - // delay_queue: - // Delayed execution objects which are guaranteed to (eventually) fire at its time. - // Once placed in this queue, an execution is only removed when it's ready to fire. - DelayedExecutionQueue> delay_queue; - - // volatile_queue: - // Delayed execution objects which must be re-evaluated at each root. - // If any volatile executions transition from True -> False, they are removed. - // Otherwise, they are treated just like a delay_queue, by triggering when ready. - std::list volatile_queue; - - // trigger_pool: - // Set of event IDs whose trigger was detected, where the exact trigger time must be found. - // IDs are removed if either the search interval has passed, or if the trigger has been found. - std::set trigger_pool; - - // trigger_state: - // Maps each event to its (current) corresponding event state. - // An event is considered to fire if its trigger() does not match its event state, - // i.e. we have detected a transition from False -> True or True -> False - std::map trigger_state; - for (auto &event : events) - { - // TODO: set to "initial state" rather than trigger value - // With the below implementation, it is impossible for an event to fire at t=t[0]. - trigger_state.insert({ - event.get_event_id(), - event.trigger(simulation->current_time, current_state.data()) - }); - } - while (integration_guard > 0 && simulation->current_time < simulation->end_time) { // Compute current propensity values based on existing state. @@ -211,9 +190,6 @@ namespace Gillespy // The integration loop continues until a valid solution is found. // Any invalid Tau steps (which cause negative populations) are discarded. sol.save_state(); - // TODO: Fire reactions manually when root-finder is installed. - std::set event_roots; - std::set rxn_roots; do { @@ -335,18 +311,9 @@ namespace Gillespy : 1000; // ===== ===== - if (trigger_pool.empty()) + if (!event_list.has_active_events()) { - double *event_state = N_VGetArrayPointer(sol.y); - for (auto &event : events) - { - if (event.trigger(next_time, event_state) != trigger_state.at(event.get_event_id())) - { - trigger_pool.insert(event.get_event_id()); - } - } - - if (!trigger_pool.empty()) + if (event_list.evaluate_triggers(N_VGetArrayPointer(sol.y), next_time)) { sol.restore_state(); sol.use_events(events, simulation->reaction_state); @@ -354,117 +321,13 @@ namespace Gillespy continue; } } - else if (!events.empty()) + else { double *event_state = N_VGetArrayPointer(sol.y); - auto compare = [next_time, event_state](EventExecution &lhs, EventExecution &rhs) -> bool - { - return lhs.priority(next_time, event_state) < rhs.priority(next_time, event_state); - }; - std::priority_queue, decltype(compare)> - trigger_queue(compare); - - // Step 1: Identify any fired event triggers. - for (auto &event : events) - { - bool trigger = event.trigger(next_time, event_state); - if (trigger_state.at(event.get_event_id()) != trigger) - { - double delay = event.delay(next_time, event_state); - EventExecution execution = event.get_execution(next_time + delay, event_state, num_species); - - // Update trigger state to prevent repeated firings. - trigger_state.find(event.get_event_id())->second = trigger; - event_roots.insert(execution.get_event_id()); - if (delay <= 0) - { - // Immediately put EventExecution on "triggered" pile - trigger_queue.push(execution); - } - else if (event.is_persistent()) - { - // Put EventExecution on "delayed" pile - delay_queue.push(execution); - } - else - { - // Search the volatile queue to see if it is already present. - // If it is, the event has "double-fired" and must be erased. - auto vol_iter = volatile_queue.begin(); - while (vol_iter != volatile_queue.end() - && vol_iter->get_event_id() != event.get_event_id()) - { - ++vol_iter; - } - - if (vol_iter == volatile_queue.end()) - { - // No match found; this is a new delay trigger, and is therefore valid. - // Delayed, but must be re-checked on every iteration. - volatile_queue.push_back(execution); - } - else - { - // Match found; this is an existing trigger, discard. - volatile_queue.erase(vol_iter); - trigger_pool.erase(event.get_event_id()); - trigger_state.at(event.get_event_id()) = !trigger_state.at(event.get_event_id()); - } - } - } - } - - // Step 2: Process delayed, non-persistent executions that are now ready to fire. - // Both the volatile and non-volatile queue are processed in a similar manner. - for (auto vol_event = volatile_queue.begin(); vol_event != volatile_queue.end(); ++vol_event) - { - // Execution objects in the volatile queue must remain True until execution. - // Remove any execution objects which transitioned to False before execution. - if (vol_event->get_execution_time() < next_time) - { - trigger_queue.push(*vol_event); - vol_event = volatile_queue.erase(vol_event); - } - } - - // Step 3: Process delayed executions, which includes both persistent triggers - // and non-persistent triggers whose execution time has arrived. - while (!delay_queue.empty()) - { - auto &event = delay_queue.top(); - if (event.get_execution_time() >= next_time) - { - // Delay queue is sorted in chronological order. - // As soon as we hit a time that is beyond the current time, - // there is no use in continuing through the queue. - break; - } - trigger_queue.push(event); - delay_queue.pop(); - } - - // Step 4: Process any pending triggers, unconditionally. - while (!trigger_queue.empty()) - { - auto event = trigger_queue.top(); - - event.execute(next_time, event_state); - trigger_queue.pop(); - trigger_pool.erase(event.get_event_id()); - } - - // Step 5: Update any trigger states to reflect the new trigger value. - for (auto &event : events) - { - trigger_state.find(event.get_event_id())->second = event.trigger(next_time, event_state); - } - - // Disable "root search mode" if no more events are found - if (trigger_pool.empty()) + if (!event_list.evaluate(event_state, num_species, next_time, event_roots)) { sol.disable_root_finder(); } - std::copy(event_state, event_state + num_species, current_state.begin()); } // ===== ===== diff --git a/test/test_hybrid_c_events.py b/test/test_hybrid_c_events.py index f22090bf2..d2317f41d 100644 --- a/test/test_hybrid_c_events.py +++ b/test/test_hybrid_c_events.py @@ -110,3 +110,19 @@ def test_use_values_from_trigger_time(self): s1, s2 = result["S1"][-1], result["S2"][-1] self.assertGreater(s2, s1, "Event assignment did not assign values from trigger time") + + def test_initial_values(self): + model = EventFeatures.BaseEventModel(s1=0, s2=100.0, rate=1.0) + + event = gillespy2.Event(name="ev1", assignments=[ + gillespy2.EventAssignment(variable=model.s1, expression="S2/2"), + ], trigger=gillespy2.EventTrigger(expression="S1==0", initial_value=False)) + model.add_event(event) + + solver = TauHybridCSolver(model=model) + result = model.run(solver=solver) + + s1, s2 = result["S1"][-1], result["S2"][-1] + self.assertAlmostEqual(s1 + s2, 150.0, places=1, msg="Event assignment assigned incorrect value") + self.assertGreater(s2, 100, "Event with initial condition did not fire") + self.assertEqual(result["S1"][0], 50, "Event assignment with initial condition failed to fire at t=0") From 2b56cb121587fbda8721a65ed9ac190835879253 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Mon, 20 Dec 2021 13:02:13 -0500 Subject: [PATCH 17/19] Bug-fix: errant `continue`, mis-ordered initialization - Bug occurred when stochastic event fired - Root-finder list was never cleared - Errant `continue` skipped over state update code - Bug occurred when array is not zero-initialized - Caused compiler-specific bug where stochastic events were impossible to find --- .../tau_hybrid_cpp_solver/TauHybridSolver.cpp | 87 ++++++++++--------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp index f3487ac9b..e28b9f320 100644 --- a/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp +++ b/gillespy2/solvers/cpp/c_base/tau_hybrid_cpp_solver/TauHybridSolver.cpp @@ -206,23 +206,6 @@ namespace Gillespy // resulting in an early termination of the trajectory. break; } - else if (!rxn_roots.empty()) - { - // "Direct" roots found; these are executed manually - for (unsigned int rxn_i : rxn_roots) - { - // "Fire" a reaction by recording changes in dependent species. - // If a negative value is detected, break without saving changes. - for (int spec_i = 0; spec_i < num_species; ++spec_i) - { - // Unlike the Tau-leaping version of reaction firings, - // it is not possible to have a negative state occur in direct reactions. - population_changes[spec_i] += model.reactions[rxn_i].species_change[spec_i]; - result.reactions[rxn_i] = log(urn.next()); - } - } - continue; - } // The integrator has, at this point, been validated. // Any errors beyond this point is assumed to be a stochastic state failure. @@ -240,40 +223,60 @@ namespace Gillespy current_state[spec_i] = result.concentrations[spec_i]; } - // The newly-updated reaction_states vector may need to be reconciled now. - // A positive reaction_state means reactions have potentially fired. - // NOTE: it is possible for a population to swing negative, where a smaller Tau is needed. - for (int rxn_i = 0; rxn_i < num_reactions; ++rxn_i) + if (!rxn_roots.empty()) { - // Temporary variable for the reaction's state. - // Does not get updated unless the changes are deemed valid. - double rxn_state = result.reactions[rxn_i]; - - switch (simulation->reaction_state[rxn_i].mode) + // "Direct" roots found; these are executed manually + for (unsigned int rxn_i : rxn_roots) + { + // "Fire" a reaction by recording changes in dependent species. + // If a negative value is detected, break without saving changes. + for (int spec_i = 0; spec_i < num_species; ++spec_i) + { + // Unlike the Tau-leaping version of reaction firings, + // it is not possible to have a negative state occur in direct reactions. + population_changes[spec_i] += model.reactions[rxn_i].species_change[spec_i]; + result.reactions[rxn_i] = log(urn.next()); + } + } + rxn_roots.clear(); + } + else + { + // The newly-updated reaction_states vector may need to be reconciled now. + // A positive reaction_state means reactions have potentially fired. + // NOTE: it is possible for a population to swing negative, where a smaller Tau is needed. + for (int rxn_i = 0; rxn_i < num_reactions; ++rxn_i) { - case SimulationState::DISCRETE: - while (rxn_state >= 0) + // Temporary variable for the reaction's state. + // Does not get updated unless the changes are deemed valid. + double rxn_state = result.reactions[rxn_i]; + + switch (simulation->reaction_state[rxn_i].mode) { - // "Fire" a reaction by recording changes in dependent species. - // If a negative value is detected, break without saving changes. - for (int spec_i = 0; spec_i < num_species; ++spec_i) + case SimulationState::DISCRETE: + while (rxn_state >= 0) { - population_changes[spec_i] += - model.reactions[rxn_i].species_change[spec_i]; - if (current_state[spec_i] + population_changes[spec_i] < 0) + // "Fire" a reaction by recording changes in dependent species. + // If a negative value is detected, break without saving changes. + for (int spec_i = 0; spec_i < num_species; ++spec_i) { - invalid_state = true; + population_changes[spec_i] += + model.reactions[rxn_i].species_change[spec_i]; + if (current_state[spec_i] + population_changes[spec_i] < 0) + { + invalid_state = true; + } } + + rxn_state += log(urn.next()); } + result.reactions[rxn_i] = rxn_state; + break; - rxn_state += log(urn.next()); + case SimulationState::CONTINUOUS: + default: + break; } - result.reactions[rxn_i] = rxn_state; - break; - - case SimulationState::CONTINUOUS: - default: - break; } } From 6c77fa0127ca5fb3903c86b1e4abfe3eda7f9b8b Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 7 Jan 2022 14:30:21 -0500 Subject: [PATCH 18/19] Added default names for Reactions, Rate Rules, Assignment Rules, Events, and Function Definitions. --- gillespy2/core/assignmentrule.py | 7 ++++++- gillespy2/core/events.py | 7 ++++++- gillespy2/core/functiondefinition.py | 5 ++++- gillespy2/core/raterule.py | 5 ++++- gillespy2/core/reaction.py | 5 ++++- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/gillespy2/core/assignmentrule.py b/gillespy2/core/assignmentrule.py index bbf4e79fe..99131fc45 100644 --- a/gillespy2/core/assignmentrule.py +++ b/gillespy2/core/assignmentrule.py @@ -16,6 +16,8 @@ along with this program. If not, see . """ +import uuid + from gillespy2.core.sortableobject import SortableObject from gillespy2.core.jsonify import Jsonify @@ -36,9 +38,12 @@ class AssignmentRule(SortableObject, Jsonify): """ def __init__(self, variable=None, formula=None, name=None): + if name in (None, ""): + self.name = f'ar{uuid.uuid4()}'.replace('-', '_') + else: + self.name = name self.variable = variable self.formula = formula - self.name = name def __str__(self): return self.variable + ': ' + self.formula diff --git a/gillespy2/core/events.py b/gillespy2/core/events.py index c7c3f0a2e..82bd3564a 100644 --- a/gillespy2/core/events.py +++ b/gillespy2/core/events.py @@ -39,7 +39,12 @@ class EventAssignment(Jsonify): """ - def __init__(self, variable=None, expression=None): + def __init__(self, name=None, variable=None, expression=None): + + if name in (None, ""): + self.name = f'evn{uuid.uuid4()}'.replace('-', '_') + else: + self.name = name self.variable = variable self.expression = expression diff --git a/gillespy2/core/functiondefinition.py b/gillespy2/core/functiondefinition.py index fd18efd32..04670650f 100644 --- a/gillespy2/core/functiondefinition.py +++ b/gillespy2/core/functiondefinition.py @@ -38,7 +38,10 @@ def __init__(self, name="", function=None, args=[]): if function is None: raise TypeError("Function string provided for FunctionDefinition cannot be None") - self.name = name + if name in (None, ""): + self.name = f'fd{uuid.uuid4()}'.replace('-', '_') + else: + self.name = name self.function_string = function self.args = args diff --git a/gillespy2/core/raterule.py b/gillespy2/core/raterule.py index 3b30f5e01..7209da2a9 100644 --- a/gillespy2/core/raterule.py +++ b/gillespy2/core/raterule.py @@ -34,9 +34,12 @@ class RateRule(SortableObject, Jsonify): """ def __init__(self, variable=None, formula='', name=None): + if name in (None, ""): + self.name = f'rr{uuid.uuid4()}'.replace('-', '_') + else: + self.name = name self.formula = formula self.variable = variable - self.name = name def __str__(self): try: diff --git a/gillespy2/core/reaction.py b/gillespy2/core/reaction.py index c2add42c4..9caba420e 100644 --- a/gillespy2/core/reaction.py +++ b/gillespy2/core/reaction.py @@ -77,7 +77,10 @@ def __init__(self, name="", reactants={}, products={}, propensity_function=None, """ # Metadata - self.name = name + if name in (None, ""): + self.name = f'rxn{uuid.uuid4()}'.replace('-', '_') + else: + self.name = name self.annotation = "" # We might use this flag in the future to automatically generate From 84f0afc330dec76d7bf7a2165514ab6bab2309fd Mon Sep 17 00:00:00 2001 From: Bryan Rumsey Date: Fri, 7 Jan 2022 14:36:59 -0500 Subject: [PATCH 19/19] Added missing imports . --- gillespy2/core/events.py | 2 ++ gillespy2/core/functiondefinition.py | 2 ++ gillespy2/core/raterule.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/gillespy2/core/events.py b/gillespy2/core/events.py index 82bd3564a..9954a510d 100644 --- a/gillespy2/core/events.py +++ b/gillespy2/core/events.py @@ -16,6 +16,8 @@ along with this program. If not, see . """ +import uuid + from gillespy2.core.gillespyError import * from gillespy2.core.jsonify import Jsonify diff --git a/gillespy2/core/functiondefinition.py b/gillespy2/core/functiondefinition.py index 04670650f..fb48e266e 100644 --- a/gillespy2/core/functiondefinition.py +++ b/gillespy2/core/functiondefinition.py @@ -16,6 +16,8 @@ along with this program. If not, see . """ +import uuid + from gillespy2.core.sortableobject import SortableObject from gillespy2.core.jsonify import Jsonify diff --git a/gillespy2/core/raterule.py b/gillespy2/core/raterule.py index 7209da2a9..87d02d2dc 100644 --- a/gillespy2/core/raterule.py +++ b/gillespy2/core/raterule.py @@ -16,6 +16,8 @@ along with this program. If not, see . """ +import uuid + from gillespy2.core.sortableobject import SortableObject from gillespy2.core.jsonify import Jsonify