def test_get_level(self): for log_level in LogLevel: handler = StdOutHandler(name="handler_{}".format(log_level), level=log_level) logger = Logger("logger_{}".format(log_level), handler=handler) fast_logger = FastLogger(logger, 'info') self.assertEqual(fast_logger.get_level(), log_level)
def __init__(self, name, start_time=0, **kwargs): """ Initialize a SimulationObject. Create its event queue, initialize its name, and set its start time. Args: name (:obj:`str`): the object's unique name, used as a key in the dict of objects start_time (:obj:`float`, optional): the earliest time at which this object can execute an event kwargs (:obj:`dict`): which can contain: event_time_tiebreaker (:obj:`str`, optional): used to break ties among simultaneous events; must be unique across all instances of an `ApplicationSimulationObject` class; defaults to `name` """ self.name = name self.time = start_time self.num_events = 0 self.simulator = None if 'event_time_tiebreaker' in kwargs and kwargs[ 'event_time_tiebreaker']: self.event_time_tiebreaker = kwargs['event_time_tiebreaker'] else: self.event_time_tiebreaker = name self.debug_logs = core.get_debug_logs() self.fast_debug_file_logger = FastLogger( self.debug_logs.get_log('de_sim.debug.file'), 'debug') self.fast_plot_file_logger = FastLogger( self.debug_logs.get_log('de_sim.plot.file'), 'debug')
def test_active_logger(self): for log_level in LogLevel: active = FastLogger.active_logger(self.fixture_logger, log_level.name) self.assertEqual(active, self.fixture_level <= log_level) with self.assertRaises(ValueError): FastLogger(self.fixture_logger, 'not a level')
def __init__(self, shared_state=None): if shared_state is None: self.shared_state = [] else: self.shared_state = shared_state self.debug_logs = core.get_debug_logs() self.fast_debug_file_logger = FastLogger(self.debug_logs.get_log('de_sim.debug.file'), 'debug') self.fast_plotting_logger = FastLogger(self.debug_logs.get_log('de_sim.plot.file'), 'debug') # self.time is not known until a simulation starts self.time = None self.simulation_objects = {} self.event_queue = EventQueue() self.event_counts = Counter() self.__initialized = False
def __init__(self, id, dynamic_model, reactions, species, dynamic_compartments, local_species_population): """ Initialize a dynamic submodel """ super().__init__(id) self.id = id self.dynamic_model = dynamic_model self.reactions = reactions self.rates = np.full(len(self.reactions), np.nan) self.species = species self.dynamic_compartments = dynamic_compartments self.local_species_population = local_species_population self.fast_debug_file_logger = FastLogger( debug_logs.get_log('wc.debug.file'), 'debug') self.fast_debug_file_logger.fast_log( f"DynamicSubmodel.__init__: submodel: {self.id}; " f"reactions: {[reaction.id for reaction in reactions]}", sim_time=self.time)
def test_fast_log(self): with CaptureOutput(relay=True) as capturer: fast_logger = FastLogger(self.fixture_logger, 'info') fast_logger.fast_log('msg') self.assertFalse(capturer.get_text()) with CaptureOutput(relay=False) as capturer: fast_logger = FastLogger(self.fixture_logger, self.fixture_level.name) message = 'hi mom' fast_logger.fast_log(message) self.assertTrue(capturer.get_text().endswith(message))
def test_config(self): debug_config = core.get_debug_logs_config( cfg_path=('tests', 'fixtures/config/debug.default.cfg')) debug_log_manager = DebugLogsManager().setup_logs(debug_config) file_logger = debug_log_manager.logs['de_sim.debug.testing.file'] self.assertFalse(FastLogger(file_logger, 'debug').active) self.assertTrue(FastLogger(file_logger, 'info').active) console_logger = debug_log_manager.logs['de_sim.debug.testing.console'] self.assertFalse(FastLogger(console_logger, 'debug').active) self.assertFalse(FastLogger(console_logger, 'info').active) self.assertTrue(FastLogger(console_logger, 'warning').active) debug_config = core.get_debug_logs_config() debug_log_manager = DebugLogsManager().setup_logs(debug_config) file_logger = debug_log_manager.logs['de_sim.debug.file'] self.assertFalse(FastLogger(file_logger, 'debug').active)
def test_suspend_restore_logging(self): debug_logs = core.get_debug_logs() existing_levels = self.suspend_logging(self.log_names) # suspended for log_name in self.log_names: fast_logger = FastLogger(debug_logs.get_log(log_name), 'debug') self.assertEqual(fast_logger.get_level(), LogLevel.exception) self.restore_logging_levels(self.log_names, existing_levels) level_by_logger = {} for logger, handler_levels in existing_levels.items(): min_level = LogLevel.exception for handler, level in handler_levels.items(): if level < min_level: min_level = level level_by_logger[logger] = min_level # restored for log_name in self.log_names: fast_logger = FastLogger(debug_logs.get_log(log_name), 'debug') self.assertEqual(fast_logger.get_level(), level_by_logger[log_name])
class SimulationEngine(object): """ A simulation engine General-purpose simulation mechanisms, including the simulation scheduler. Architected as an OO simulation that could be parallelized. `SimulationEngine` contains and manipulates global simulation data. SimulationEngine registers all simulation objects types and all simulation objects. Following `simulate()` it runs the simulation, scheduling objects to execute events in non-decreasing time order; and generates debugging output. Attributes: time (:obj:`float`): the simulations's current time simulation_objects (:obj:`dict` of :obj:`SimulationObject`): all simulation objects, keyed by name shared_state (:obj:`list` of :obj:`object`, optional): the shared state of the simulation, needed to log or checkpoint the entire state of a simulation; all objects in `shared_state` must implement :obj:`SharedStateInterface` debug_logs (:obj:`wc_utils.debug_logs.core.DebugLogsManager`): the debug logs fast_debug_file_logger (:obj:`FastLogger`): a fast logger for debugging messages fast_plotting_logger (:obj:`FastLogger`): a fast logger for trajectory data for plotting event_queue (:obj:`EventQueue`): the queue of future events event_counts (:obj:`Counter`): a counter of event types num_events_handled (:obj:`int`): the number of events in a simulation sim_config (:obj:`SimulationConfig`): a simulation run's configuration sim_metadata (:obj:`SimulationMetadata`): a simulation run's metadata author_metadata (:obj:`AuthorMetadata`): information about the person who runs the simulation, if provided by the simulation application measurements_fh (:obj:`_io.TextIOWrapper`, optional): file handle for debugging measurements file mem_tracker (:obj:`pympler.tracker.SummaryTracker`, optional): a memory use tracker for debugging """ # Termination messages NO_EVENTS_REMAIN = " No events remain" END_TIME_EXCEEDED = " End time exceeded" TERMINATE_WITH_STOP_CONDITION_SATISFIED = " Terminate with stop condition satisfied" # number of rows to print in a performance profile NUM_PROFILE_ROWS = 50 def __init__(self, shared_state=None): if shared_state is None: self.shared_state = [] else: self.shared_state = shared_state self.debug_logs = core.get_debug_logs() self.fast_debug_file_logger = FastLogger(self.debug_logs.get_log('de_sim.debug.file'), 'debug') self.fast_plotting_logger = FastLogger(self.debug_logs.get_log('de_sim.plot.file'), 'debug') # self.time is not known until a simulation starts self.time = None self.simulation_objects = {} self.event_queue = EventQueue() self.event_counts = Counter() self.__initialized = False def add_object(self, simulation_object): """ Add a simulation object instance to this simulation Args: simulation_object (:obj:`SimulationObject`): a simulation object instance that will be used by this simulation Raises: :obj:`SimulatorError`: if the simulation object's name is already in use """ name = simulation_object.name if name in self.simulation_objects: raise SimulatorError("cannot add simulation object '{}', name already in use".format(name)) simulation_object.add(self) self.simulation_objects[name] = simulation_object def add_objects(self, simulation_objects): """ Add many simulation objects into the simulation Args: simulation_objects (:obj:`iterator` of :obj:`SimulationObject`): an iterator of simulation objects """ for simulation_object in simulation_objects: self.add_object(simulation_object) def get_object(self, simulation_object_name): """ Get a simulation object instance Args: simulation_object_name (:obj:`str`): get a simulation object instance that is part of this simulation Raises: :obj:`SimulatorError`: if the simulation object is not part of this simulation """ if simulation_object_name not in self.simulation_objects: raise SimulatorError("cannot get simulation object '{}'".format(simulation_object_name)) return self.simulation_objects[simulation_object_name] def get_objects(self): """ Get all simulation object instances in the simulation """ # TODO(Arthur): make this reproducible # TODO(Arthur): eliminate external calls to self.simulator.simulation_objects return self.simulation_objects.values() def delete_object(self, simulation_object): """ Delete a simulation object instance from this simulation Args: simulation_object (:obj:`SimulationObject`): a simulation object instance that is part of this simulation Raises: :obj:`SimulatorError`: if the simulation object is not part of this simulation """ # TODO(Arthur): is this an operation that makes sense to support? if not, remove it; if yes, # remove all of this object's state from simulator, and test it properly name = simulation_object.name if name not in self.simulation_objects: raise SimulatorError("cannot delete simulation object '{}', has not been added".format(name)) simulation_object.delete() del self.simulation_objects[name] def initialize(self): """ Initialize a simulation Call `send_initial_events()` in each simulation object that has been loaded. Raises: :obj:`SimulatorError`: if the simulation has already been initialized """ if self.__initialized: raise SimulatorError('Simulation has already been initialized') for sim_obj in self.simulation_objects.values(): sim_obj.send_initial_events() self.event_counts.clear() self.__initialized = True def init_metadata_collection(self, sim_config): """ Initialize a simulation metatdata object Call just before a simulation starts, so that correct clock time of start is recorded Args: sim_config (:obj:`SimulationConfig`): information about the simulation's configuration (start time, maximum time, etc.) """ if self.author_metadata is None: author = AuthorMetadata() else: author = self.author_metadata run = RunMetadata() run.record_ip_address() run.record_start() # obtain repo metadaa, if possible simulator_repo = None try: simulator_repo, _ = get_repo_metadata(repo_type=RepoMetadataCollectionType.SCHEMA_REPO) except ValueError: pass self.sim_metadata = SimulationMetadata(simulation_config=sim_config, run=run, author=author, simulator_repo=simulator_repo) def finish_metadata_collection(self): """ Finish metatdata collection """ self.sim_metadata.run.record_run_time() if self.sim_config.output_dir: SimulationMetadata.write_dataclass(self.sim_metadata, self.sim_config.output_dir) def reset(self): """ Reset this `SimulationEngine` Delete all objects and reset any prior initialization. """ for simulation_object in list(self.simulation_objects.values()): self.delete_object(simulation_object) self.event_queue.reset() self.__initialized = False def message_queues(self): """ Return a string listing all message queues in the simulation Returns: :obj:`str`: a list of all message queues in the simulation and their messages """ now = "'uninitialized'" if self.time is not None: now = f"{self.time:6.3f}" data = [f'Event queues at {now}'] for sim_obj in sorted(self.simulation_objects.values(), key=lambda sim_obj: sim_obj.name): data.append(sim_obj.name + ':') rendered_eq = self.event_queue.render(sim_obj=sim_obj) if rendered_eq is None: data.append('Empty event queue') else: data.append(rendered_eq) data.append('') return '\n'.join(data) @staticmethod def _get_sim_config(time_max=None, sim_config=None, config_dict=None): """ External simulate interface Legal combinations of the three parameters: 1. Just `time_max` 2. Just `sim_config`, which will contain an entry for `time_max` 3. Just `config_dict`, which must contain an entry for `time_max` Other combinations are illegal. Args: time_max (:obj:`float`, optional): the time of the end of the simulation sim_config (:obj:`SimulationConfig`, optional): the simulation run's configuration config_dict (:obj:`dict`, optional): a dictionary with keys chosen from the field names in :obj:`SimulationConfig`; note that `config_dict` is not a `kwargs` argument Returns: :obj:`SimulationConfig`: a validated simulation configuration Raises: :obj:`SimulatorError`: if no arguments are provided, or multiple arguments are provided, or `time_max` is missing from `config_dict` """ num_args = 0 if time_max is not None: num_args += 1 if sim_config is not None: num_args += 1 if config_dict: num_args += 1 if num_args == 0: raise SimulatorError('time_max, sim_config, or config_dict must be provided') if 1 < num_args: raise SimulatorError('at most 1 of time_max, sim_config, or config_dict may be provided') # catch common error generated when sim_config= is not used by SimulationEngine.simulate(sim_config) if isinstance(time_max, SimulationConfig): raise SimulatorError(f"sim_config is not provided, sim_config= is probably needed") # initialize sim_config if it is not provided if sim_config is None: if time_max is not None: sim_config = SimulationConfig(time_max) else: # config_dict must be initialized if 'time_max' not in config_dict: raise SimulatorError('time_max must be provided in config_dict') sim_config = SimulationConfig(**config_dict) sim_config.validate() return sim_config SimulationReturnValue = namedtuple('SimulationReturnValue', 'num_events profile_stats', defaults=(None, None)) def simulate(self, time_max=None, sim_config=None, config_dict=None, author_metadata=None): """ Run a simulation See `_get_sim_config` for constraints on arguments Args: time_max (:obj:`float`, optional): the time of the end of the simulation sim_config (:obj:`SimulationConfig`, optional): the simulation run's configuration config_dict (:obj:`dict`, optional): a dictionary with keys chosen from the field names in :obj:`SimulationConfig` author_metadata (:obj:`AuthorMetadata`, optional): information about the person who runs the simulation Returns: :obj:`SimulationReturnValue`: a :obj:`namedtuple` which contains a) the number of times any simulation object executes `_handle_event()`, which may be smaller than the number of events sent, because simultaneous events at one simulation object are handled together, and b), if `sim_config.profile` is set, a :obj:`pstats.Stats` instance containing the profiling statistics Raises: :obj:`SimulatorError`: if the simulation has not been initialized, or has no objects, or has no initial events, or attempts to execute an event that violates non-decreasing time order """ self.sim_config = self._get_sim_config(time_max=time_max, sim_config=sim_config, config_dict=config_dict) self.author_metadata = author_metadata if self.sim_config.output_dir: measurements_file = core.get_config()['de_sim']['measurements_file'] self.measurements_fh = open(os.path.join(self.sim_config.output_dir, measurements_file), 'w') print(f"de_sim measurements: {datetime.now().isoformat(' ')}", file=self.measurements_fh) profile = None if self.sim_config.profile: # profile the simulation and return the profile object with tempfile.NamedTemporaryFile() as file_like_obj: out_file = file_like_obj.name locals = {'self': self} cProfile.runctx('self._simulate()', {}, locals, filename=out_file) if self.sim_config.output_dir: profile = pstats.Stats(out_file, stream=self.measurements_fh) else: profile = pstats.Stats(out_file) profile.sort_stats('tottime').print_stats(self.NUM_PROFILE_ROWS) else: self._simulate() if self.sim_config.output_dir: self.measurements_fh.close() return self.SimulationReturnValue(self.num_events_handled, profile) def run(self, time_max=None, sim_config=None, config_dict=None, author_metadata=None): """ Alias for simulate """ return self.simulate(time_max=time_max, sim_config=sim_config, config_dict=config_dict, author_metadata=author_metadata) def _simulate(self): """ Run the simulation Returns: :obj:`int`: the number of times a simulation object executes `_handle_event()`. This may be smaller than the number of events sent, because simultaneous events at one simulation object are handled together. Raises: :obj:`SimulatorError`: if the simulation has not been initialized, or has no objects, or has no initial events, or attempts to start before the start time in `time_init`, or attempts to execute an event that violates non-decreasing time order """ if not self.__initialized: raise SimulatorError("Simulation has not been initialized") if not len(self.get_objects()): raise SimulatorError("Simulation has no objects") if self.event_queue.empty(): raise SimulatorError("Simulation has no initial events") _object_mem_tracking = False if 0 < self.sim_config.object_memory_change_interval: _object_mem_tracking = True # don't import tracker unless it's being used from pympler import tracker self.mem_tracker = tracker.SummaryTracker() # set simulation time to `time_init` self.time = self.sim_config.time_init # error if first event occurs before time_init next_time = self.event_queue.next_event_time() if next_time < self.sim_config.time_init: raise SimulatorError(f"Time of first event ({next_time}) is earlier than the start time " f"({self.sim_config.time_init})") # set up progress bar self.progress = SimulationProgressBar(self.sim_config.progress) # write header to a plot log # plot logging is controlled by configuration files pointed to by config_constants and by env vars self.fast_plotting_logger.fast_log('# {:%Y-%m-%d %H:%M:%S}'.format(datetime.now()), sim_time=0) self.num_events_handled = 0 self.log_with_time(f"Simulation to {self.sim_config.time_max} starting") try: self.progress.start(self.sim_config.time_max) self.init_metadata_collection(self.sim_config) while True: # use the stop condition if self.sim_config.stop_condition is not None and self.sim_config.stop_condition(self.time): self.log_with_time(self.TERMINATE_WITH_STOP_CONDITION_SATISFIED) self.progress.end() break # if tracking object use, record object and memory use changes if _object_mem_tracking: self.track_obj_mem() # get the earliest next event in the simulation # get parameters of next event from self.event_queue next_time = self.event_queue.next_event_time() next_sim_obj = self.event_queue.next_event_obj() if float('inf') == next_time: self.log_with_time(self.NO_EVENTS_REMAIN) self.progress.end() break if self.sim_config.time_max < next_time: self.log_with_time(self.END_TIME_EXCEEDED) self.progress.end() break self.time = next_time # error will only be raised if an object decreases its time if next_time < next_sim_obj.time: raise SimulatorError("Dispatching '{}', but event time ({}) " "< object time ({})".format(next_sim_obj.name, next_time, next_sim_obj.time)) # dispatch object that's ready to execute next event next_sim_obj.time = next_time self.log_with_time(" Running '{}' at {}".format(next_sim_obj.name, next_sim_obj.time)) next_events = self.event_queue.next_events() for e in next_events: e_name = ' - '.join([next_sim_obj.__class__.__name__, next_sim_obj.name, e.message.__class__.__name__]) self.event_counts[e_name] += 1 next_sim_obj.__handle_event_list(next_events) self.num_events_handled += 1 self.progress.progress(next_time) except SimulatorError as e: raise SimulatorError('Simulation ended with error:\n' + str(e)) self.finish_metadata_collection() return self.num_events_handled def track_obj_mem(self): """ Write memory use tracking """ def format_row(values, widths=(60, 10, 16)): widths_format = "{{:<{}}}{{:>{}}}{{:>{}}}".format(*widths) return widths_format.format(*values) if self.num_events_handled % self.sim_config.object_memory_change_interval == 0: heading = f"\nMemory use changes by SummaryTracker at event {self.num_events_handled}:" if self.sim_config.output_dir: print(heading, file=self.measurements_fh) data_heading = ('type', '# objects', 'total size (B)') print(format_row(data_heading), file=self.measurements_fh) # mem_values = obj_type, count, mem for mem_values in sorted(self.mem_tracker.diff(), key=lambda mem_values: mem_values[2], reverse=True): row = [str(val) for val in mem_values] print(format_row(row), file=self.measurements_fh) else: print(heading) self.mem_tracker.print_diff() def log_with_time(self, msg): """ Write a debug log message with the simulation time. """ self.fast_debug_file_logger.fast_log(msg, sim_time=self.time) def provide_event_counts(self): """ Provide the simulation's categorized event counts Returns: :obj:`str`: the simulation's categorized event counts, in a tab-separated table """ rv = ['\t'.join(['Count', 'Event type (Object type - object name - event type)'])] for event_type, count in self.event_counts.most_common(): rv.append("{}\t{}".format(count, event_type)) return '\n'.join(rv) def get_simulation_state(self): """ Get the simulation's state """ # get simulation time state = [self.time] # get the state of all simulation object(s) sim_objects_state = [] for simulation_object in self.simulation_objects.values(): # get object name, type, current time, state state_entry = (simulation_object.__class__.__name__, simulation_object.name, simulation_object.time, simulation_object.get_state(), simulation_object.render_event_queue()) sim_objects_state.append(state_entry) state.append(sim_objects_state) # get the shared state shared_objects_state = [] for shared_state_obj in self.shared_state: state_entry = (shared_state_obj.__class__.__name__, shared_state_obj.get_name(), shared_state_obj.get_shared_state(self.time)) shared_objects_state.append(state_entry) state.append(shared_objects_state) return state
def test_is_active(self): fast_logger = FastLogger(self.fixture_logger, self.fixture_level.name) self.assertTrue(fast_logger.is_active())
class DynamicSubmodel(ApplicationSimulationObject): """ Provide generic dynamic submodel functionality All submodels are implemented as subclasses of `DynamicSubmodel`. Instances of them are combined to make a multi-algorithmic model. Attributes: id (:obj:`str`): unique id of this dynamic submodel and simulation object dynamic_model (:obj:`DynamicModel`): the aggregate state of a simulation reactions (:obj:`list` of :obj:`Reaction`): the reactions modeled by this dynamic submodel rates (:obj:`np.array`): array to hold reaction rates species (:obj:`list` of :obj:`Species`): the species that participate in the reactions modeled by this dynamic submodel, with their initial concentrations dynamic_compartments (:obj:`dict` of :obj:`str`, :obj:`DynamicCompartment`): the dynamic compartments containing species that participate in reactions that this dynamic submodel models, including adjacent compartments used by its transfer reactions local_species_population (:obj:`LocalSpeciesPopulation`): the store that maintains this dynamic submodel's species population fast_debug_file_logger (:obj:`FastLogger`): a fast logger for debugging messages """ def __init__(self, id, dynamic_model, reactions, species, dynamic_compartments, local_species_population): """ Initialize a dynamic submodel """ super().__init__(id) self.id = id self.dynamic_model = dynamic_model self.reactions = reactions self.rates = np.full(len(self.reactions), np.nan) self.species = species self.dynamic_compartments = dynamic_compartments self.local_species_population = local_species_population self.fast_debug_file_logger = FastLogger( debug_logs.get_log('wc.debug.file'), 'debug') self.fast_debug_file_logger.fast_log( f"DynamicSubmodel.__init__: submodel: {self.id}; " f"reactions: {[reaction.id for reaction in reactions]}", sim_time=self.time) # The next 2 methods implement the abstract methods in ApplicationSimulationObject def send_initial_events(self): pass # pragma: no cover GET_STATE_METHOD_MESSAGE = 'object state to be provided by subclass' def get_state(self): return DynamicSubmodel.GET_STATE_METHOD_MESSAGE # At any time instant, event messages are processed in this order # TODO(Arthur): cover after MVP wc_sim done event_handlers = [(message_types.GetCurrentProperty, 'handle_get_current_prop_event')] # pragma: no cover # TODO(Arthur): cover after MVP wc_sim done messages_sent = [message_types.GiveProperty] # pragma: no cover def get_compartment_masses(self): """ Get the mass (g) of each compartment Returns: :obj:`dict`: dictionary that maps the ids of compartments to their masses (g) """ return { id: comp.mass() for id, comp in self.dynamic_compartments.items() } def get_species_ids(self): """ Get ids of species used by this dynamic submodel Returns: :obj:`list`: ids of species used by this dynamic submodel """ return [s.id for s in self.species] def get_species_counts(self): """ Get a dictionary of current species counts for this dynamic submodel Returns: :obj:`dict`: a map: species_id -> current copy number """ species_ids = set(self.get_species_ids()) return self.local_species_population.read(self.time, species_ids) def get_num_submodels(self): """ Provide the number of submodels Returns: :obj:`int`: the number of submodels """ return self.dynamic_model.get_num_submodels() def calc_reaction_rate(self, reaction): """ Calculate a reaction's current rate The rate is computed by eval'ing the reaction's rate law, with species populations obtained from the simulations's :obj:`LocalSpeciesPopulation`. Args: reaction (:obj:`Reaction`): the reaction to evaluate Returns: :obj:`float`: the reaction's rate """ rate_law_id = reaction.rate_laws[0].id rate = self.dynamic_model.dynamic_rate_laws[rate_law_id].eval( self.time) self.fast_debug_file_logger.fast_log( f"DynamicSubmodel.calc_reaction_rate: " f"rate of reaction {rate_law_id} = {rate}", sim_time=self.time) return rate def calc_reaction_rates(self): """ Calculate the rates for this dynamic submodel's reactions Rates are computed by eval'ing rate laws for reactions used by this dynamic submodel, with species populations obtained from the simulations's :obj:`LocalSpeciesPopulation`. This assumes that all reversible reactions have been split into two forward reactions, as is done by `wc_lang.transform.SplitReversibleReactionsTransform`. Returns: :obj:`np.ndarray`: a numpy array of reaction rates, indexed by reaction index """ for idx_reaction, rxn in enumerate(self.reactions): if rxn.rate_laws: self.rates[idx_reaction] = self.calc_reaction_rate(rxn) if self.fast_debug_file_logger.is_active(): msg = 'DynamicSubmodel.calc_reaction_rates: reactions and rates: ' + \ str([(self.reactions[i].id, self.rates[i]) for i in range(len(self.reactions))]) self.fast_debug_file_logger.fast_log(msg, sim_time=self.time) return self.rates # These methods - enabled_reaction, identify_enabled_reactions, execute_reaction - are used # by discrete time submodels like SsaSubmodel and the SkeletonSubmodel. def enabled_reaction(self, reaction): """ Determine whether the cell state has adequate species counts to run a reaction Indicate whether the current species counts are large enough to execute `reaction`, based on its stoichiometry. Args: reaction (:obj:`Reaction`): the reaction to evaluate Returns: :obj:`bool`: True if `reaction` is stoichiometrically enabled """ for participant in reaction.participants: species_id = participant.species.gen_id() count = self.local_species_population.read_one( self.time, species_id) # 'participant.coefficient < 0' determines whether the participant is a reactant is_reactant = participant.coefficient < 0 if is_reactant and count < -participant.coefficient: return False return True def identify_enabled_reactions(self): """ Determine which reactions have adequate species counts to run Returns: :obj:`np.array`: an array indexed by reaction number; 0 indicates reactions without adequate species counts """ enabled = np.full(len(self.reactions), 1) for idx_reaction, rxn in enumerate(self.reactions): if not self.enabled_reaction(rxn): enabled[idx_reaction] = 0 return enabled def execute_reaction(self, reaction): """ Update species counts to reflect the execution of a reaction Called by discrete submodels, like SSA. Counts are updated in the :obj:`LocalSpeciesPopulation` that stores them. Args: reaction (:obj:`Reaction`): the reaction being executed Raises: :obj:`DynamicMultialgorithmError:` if the species population cannot be updated """ adjustments = {} for participant in reaction.participants: species_id = participant.species.gen_id() if not species_id in adjustments: adjustments[species_id] = 0 adjustments[species_id] += participant.coefficient try: self.local_species_population.adjust_discretely( self.time, adjustments) except DynamicSpeciesPopulationError as e: raise DynamicMultialgorithmError( self.time, "dynamic submodel '{}' cannot execute reaction: {}: {}".format( self.id, reaction.id, e)) # TODO(Arthur): cover after MVP wc_sim done def handle_get_current_prop_event(self, event): # pragma: no cover not used """ Handle a GetCurrentProperty simulation event. Args: event (:obj:`de_sim.event.Event`): an `Event` to process Raises: DynamicMultialgorithmError: if an `GetCurrentProperty` message requests an unknown property """ property_name = event.message.property_name if property_name == distributed_properties.MASS: ''' # TODO(Arthur): rethink this, as, strictly speaking, a dynamic submodel doesn't have mass, but its compartment does self.send_event(0, event.sending_object, message_types.GiveProperty, message=message_types.GiveProperty(property_name, self.time, self.mass())) ''' raise DynamicMultialgorithmError( self.time, "Error: not handling distributed_properties.MASS") else: raise DynamicMultialgorithmError( self.time, "Error: unknown property_name: '{}'".format(property_name))
def __init__(self): self.event_heap = [] self.debug_logs = core.get_debug_logs() self.fast_debug_file_logger = FastLogger( self.debug_logs.get_log('de_sim.debug.file'), 'debug')
class EventQueue(object): """ A simulation's event queue Stores a `SimulationEngine`'s events in a heap (also known as a priority queue). The heap is a 'min heap', which keeps the event with the smallest `(event_time, sending_object.name)` at the root in heap[0]. This is implemented via comparison operations in `Event`. Thus, all entries with equal `(event_time, sending_object.name)` will be popped from the heap adjacently. `schedule_event()` costs `O(log(n))`, where `n` is the size of the heap, while `next_events()`, which returns all events with the minimum `(event_time, sending_object.name)`, costs `O(mlog(n))`, where `m` is the number of events returned. Attributes: event_heap (:obj:`list`): a `SimulationEngine`'s heap of events debug_logs (:obj:`wc_utils.debug_logs.core.DebugLogsManager`): a `DebugLogsManager` """ def __init__(self): self.event_heap = [] self.debug_logs = core.get_debug_logs() self.fast_debug_file_logger = FastLogger( self.debug_logs.get_log('de_sim.debug.file'), 'debug') def reset(self): """ Empty the event queue """ self.event_heap = [] def len(self): """ Size of the event queue Returns: :obj:`int`: number of events in the event queue """ return len(self.event_heap) def schedule_event(self, send_time, receive_time, sending_object, receiving_object, message): """ Create an event and insert in this event queue, scheduled to execute at `receive_time` Simulation object `X` can sends an event to simulation object `Y` by invoking `X.send_event(receive_delay, Y, message)`. Args: send_time (:obj:`float`): the simulation time at which the event was generated (sent) receive_time (:obj:`float`): the simulation time at which the `receiving_object` will execute the event sending_object (:obj:`SimulationObject`): the object sending the event receiving_object (:obj:`SimulationObject`): the object that will receive the event; when the simulation is parallelized `sending_object` and `receiving_object` will need to be global identifiers. message (:obj:`SimulationMessage`): a `SimulationMessage` carried by the event; its type provides the simulation application's type for an `Event`; it may also carry a payload for the `Event` in its attributes. Raises: :obj:`SimulatorError`: if `receive_time` < `send_time`, or `receive_time` or `send_time` is NaN """ if math.isnan(send_time) or math.isnan(receive_time): raise SimulatorError( "send_time ({}) and/or receive_time ({}) is NaN".format( receive_time, send_time)) # Ensure that send_time <= receive_time. # Events with send_time == receive_time can cause loops, but the application programmer # is responsible for avoiding them. if receive_time < send_time: raise SimulatorError( "receive_time < send_time in schedule_event(): {} < {}".format( receive_time, send_time)) if not isinstance(message, SimulationMessage): raise SimulatorError( "message should be an instance of {} but is a '{}'".format( SimulationMessage.__name__, type(message).__name__)) event = Event(send_time, receive_time, sending_object, receiving_object, message) # As per David Jefferson's thinking, the event queue is ordered by data provided by the # simulation application, in particular the tuple (event time, receiving object name). # See the comparison operators for Event. This achieves deterministic and reproducible # simulations. heapq.heappush(self.event_heap, event) def empty(self): """ Is the event queue empty? Returns: :obj:`bool`: return `True` if the event queue is empty """ return not self.event_heap def next_event_time(self): """ Get the time of the next event Returns: :obj:`float`: the time of the next event; return infinity if no event is scheduled """ if not self.event_heap: return float('inf') next_event = self.event_heap[0] next_event_time = next_event.event_time return next_event_time def next_event_obj(self): """ Get the simulation object that receives the next event Returns: :obj:`SimulationObject`): the simulation object that will execute the next event, or `None` if no event is scheduled """ if not self.event_heap: return None next_event = self.event_heap[0] return next_event.receiving_object def next_events(self): """ Get all events at the smallest event time destined for the object whose name sorts earliest Because multiple events may occur concurrently -- that is, have the same simulation time -- they must be provided as a collection to the simulation object that executes them. Handle 'ties' properly. That is, since an object may receive multiple events with the same event_time (aka receive_time), pass them all to the object in a list. Returns: :obj:`list` of :obj:`Event`: the earliest event(s), sorted by message type priority. If no events are available the list is empty. """ if not self.event_heap: return [] events = [] next_event = heapq.heappop(self.event_heap) now = next_event.event_time receiving_obj = next_event.receiving_object events.append(next_event) # gather all events with the same event_time and receiving_object while (self.event_heap and now == self.next_event_time() and receiving_obj == self.next_event_obj()): events.append(heapq.heappop(self.event_heap)) if 1 < len(events): # sort events by message type priority, and within priority by message content # thus, a sim object handles simultaneous messages in priority order; # this costs O(n log(n)) in the number of event messages in events receiver_priority_dict = receiving_obj.get_receiving_priorities_dict( ) events = sorted(events, key=lambda event: (receiver_priority_dict[ event.message.__class__], event.message)) for event in events: self.log_event(event) return events def log_event(self, event): """ Log an event with its simulation time Args: event (:obj:`Event`): the Event to log """ msg = "Execute: {} {}:{} {} ({})".format( event.event_time, type(event.receiving_object).__name__, event.receiving_object.name, event.message.__class__.__name__, str(event.message)) self.fast_debug_file_logger.fast_log(msg, sim_time=event.event_time) def render(self, sim_obj=None, as_list=False, separator='\t'): """ Return the content of an `EventQueue` Make a human-readable event queue, sorted by non-decreasing event time. Provide a header row and a row for each event. If all events have the same type of message, the header contains event and message fields. Otherwise, the header has event fields and a message field label, and each event labels message fields with their attribute names. Args: sim_obj (:obj:`SimulationObject`, optional): if provided, return only events to be received by `sim_obj` as_list (:obj:`bool`, optional): if set, return the `EventQueue`'s values in a :obj:`list` separator (:obj:`str`, optional): the field separator used if the values are returned as a string Returns: :obj:`str`: String representation of the values of an `EventQueue`, or a :obj:`list` representation if `as_list` is set """ event_heap = self.event_heap if sim_obj is not None: event_heap = list( filter(lambda event: event.receiving_object == sim_obj, event_heap)) if not event_heap: return None # Sort the events in non-decreasing event time (receive_time, receiving_object.name) sorted_events = sorted(event_heap) # Does the queue contain multiple message types? message_types = set() for event in event_heap: message_types.add(event.message.__class__) if 1 < len(message_types): break multiple_msg_types = 1 < len(message_types) rendered_event_queue = [] if multiple_msg_types: # The queue contains multiple message types rendered_event_queue.append(Event.header(as_list=True)) for event in sorted_events: rendered_event_queue.append( event.render(annotated=True, as_list=True)) else: # The queue contain only one message type # message_type = message_types.pop() event = sorted_events[0] rendered_event_queue.append(event.custom_header(as_list=True)) for event in sorted_events: rendered_event_queue.append(event.render(as_list=True)) if as_list: return rendered_event_queue else: table = [] for row in rendered_event_queue: table.append(separator.join(elements_to_str(row))) return '\n'.join(table) def __str__(self): """ Return event queue members as a table """ rv = self.render() if rv is None: return '' return rv
class SimulationObject(object): """ Base class for simulation objects. SimulationObject is a base class for all simulations objects. It provides basic functionality: the object's name (which must be unique), its simulation time, a queue of received events, and a send_event() method. Attributes: name (:obj:`str`): this simulation object's name, which is unique across all simulation objects handled by a `SimulationEngine` time (:obj:`float`): this simulation object's current simulation time event_time_tiebreaker (:obj:`str`): the least significant component of an object's 'sub-tme' priority, which orders simultaneous events received by different instances of the same `ApplicationSimulationObject` num_events (:obj:`int`): number of events processed simulator (:obj:`int`): the `SimulationEngine` that uses this `SimulationObject` debug_logs (:obj:`wc_utils.debug_logs.core.DebugLogsManager`): the debug logs """ LOG_EVENTS = config['de_sim']['log_events'] def __init__(self, name, start_time=0, **kwargs): """ Initialize a SimulationObject. Create its event queue, initialize its name, and set its start time. Args: name (:obj:`str`): the object's unique name, used as a key in the dict of objects start_time (:obj:`float`, optional): the earliest time at which this object can execute an event kwargs (:obj:`dict`): which can contain: event_time_tiebreaker (:obj:`str`, optional): used to break ties among simultaneous events; must be unique across all instances of an `ApplicationSimulationObject` class; defaults to `name` """ self.name = name self.time = start_time self.num_events = 0 self.simulator = None if 'event_time_tiebreaker' in kwargs and kwargs[ 'event_time_tiebreaker']: self.event_time_tiebreaker = kwargs['event_time_tiebreaker'] else: self.event_time_tiebreaker = name self.debug_logs = core.get_debug_logs() self.fast_debug_file_logger = FastLogger( self.debug_logs.get_log('de_sim.debug.file'), 'debug') self.fast_plot_file_logger = FastLogger( self.debug_logs.get_log('de_sim.plot.file'), 'debug') def add(self, simulator): """ Add this object to a simulation. Args: simulator (:obj:`SimulationEngine`): the simulator that will use this `SimulationObject` Raises: :obj:`SimulatorError`: if this `SimulationObject` is already registered with a simulator """ if self.simulator is None: # TODO(Arthur): reference to the simulator is problematic because it means simulator can't be GC'ed self.simulator = simulator return raise SimulatorError( "SimulationObject '{}' is already part of a simulator".format( self.name)) def delete(self): """ Delete this object from a simulation. """ # TODO(Arthur): is this an operation that makes sense to support? if not, remove it; if yes, # remove all of this object's state from simulator, and test it properly self.simulator = None def send_event_absolute(self, event_time, receiving_object, message, copy=False): """ Send a simulation event message with an absolute event time. Args: event_time (:obj:`float`): the absolute simulation time at which `receiving_object` will execute the event receiving_object (:obj:`SimulationObject`): the simulation object that will receive and execute the event message (:obj:`SimulationMessage`): the simulation message which will be carried by the event copy (:obj:`bool`, optional): if `True`, copy the message before adding it to the event; set `False` by default to optimize performance; set `True` as a safety measure to avoid unexpected changes to shared objects Raises: :obj:`SimulatorError`: if `event_time` < 0, or if the sending object type is not registered to send messages with the type of `message`, or if the receiving simulation object type is not registered to receive messages with the type of `message` """ if math.isnan(event_time): raise SimulatorError("event_time is 'NaN'") if event_time < self.time: raise SimulatorError( "event_time ({}) < current time ({}) in send_event_absolute()". format(round_direct(event_time, precision=3), round_direct(self.time, precision=3))) # Do not put a class reference in a message, as the message might not be received in the # same address space. # To eliminate the risk of name collisions use the fully qualified classname. # TODO(Arthur): wait until after MVP # event_type_name = most_qual_cls_name(message) event_type_name = message.__class__.__name__ # check that the sending object type is registered to send the message type if not isinstance(message, SimulationMessage): raise SimulatorError( "simulation messages must be instances of type 'SimulationMessage'; " "'{}' is not".format(event_type_name)) if message.__class__ not in self.__class__.metadata.message_types_sent: raise SimulatorError( "'{}' simulation objects not registered to send '{}' messages". format(most_qual_cls_name(self), event_type_name)) # check that the receiving simulation object type is registered to receive the message type receiver_priorities = receiving_object.get_receiving_priorities_dict() if message.__class__ not in receiver_priorities: raise SimulatorError( "'{}' simulation objects not registered to receive '{}' messages" .format(most_qual_cls_name(receiving_object), event_type_name)) if copy: message = deepcopy(message) self.simulator.event_queue.schedule_event(self.time, event_time, self, receiving_object, message) self.log_with_time("Send: ({}, {:6.2f}) -> ({}, {:6.2f}): {}".format( self.name, self.time, receiving_object.name, event_time, message.__class__.__name__)) def send_event(self, delay, receiving_object, message, copy=False): """ Send a simulation event message, specifing the event time as a delay. Args: delay (:obj:`float`): the simulation delay at which `receiving_object` should execute the event receiving_object (:obj:`SimulationObject`): the simulation object that will receive and execute the event message (:obj:`SimulationMessage`): the simulation message which will be carried by the event copy (:obj:`bool`, optional): if `True`, copy the message before adding it to the event; set `False` by default to optimize performance; set `True` as a safety measure to avoid unexpected changes to shared objects Raises: :obj:`SimulatorError`: if `delay` < 0 or `delay` is NaN, or if the sending object type is not registered to send messages with the type of `message`, or if the receiving simulation object type is not registered to receive messages with the type of `message` """ if math.isnan(delay): raise SimulatorError("delay is 'NaN'") if delay < 0: raise SimulatorError("delay < 0 in send_event(): {}".format( str(delay))) self.send_event_absolute(delay + self.time, receiving_object, message, copy=copy) @staticmethod def register_handlers(subclass, handlers): """ Register a `SimulationObject`'s event handler methods. The simulation engine vectors execution of a simulation message to the message's registered event handler method. The priority of message execution in an event containing multiple messages is determined by the sequence of tuples in `handlers`. These relationships are stored in an `ApplicationSimulationObject`'s `metadata.event_handlers_dict`. Each call to `register_handlers` re-initializes all event handler methods. Args: subclass (:obj:`SimulationObject`): a subclass of `SimulationObject` that is registering the relationships between the simulation messages it receives and the methods that handle them handlers (:obj:`list` of (`SimulationMessage`, `function`)): a list of tuples, indicating which method should handle which type of `SimulationMessage` in `subclass`; ordered in decreasing priority for handling simulation message types Raises: :obj:`SimulatorError`: if a `SimulationMessage` appears repeatedly in `handlers`, or if a method in `handlers` is not callable """ for message_type, handler in handlers: if message_type in subclass.metadata.event_handlers_dict: raise SimulatorError( "message type '{}' appears repeatedly".format( most_qual_cls_name(message_type))) if not callable(handler): raise SimulatorError( "handler '{}' must be callable".format(handler)) subclass.metadata.event_handlers_dict[message_type] = handler for index, (message_type, _) in enumerate(handlers): subclass.metadata.event_handler_priorities[message_type] = index @staticmethod def register_sent_messages(subclass, sent_messages): """ Register the messages sent by a `SimulationObject` subclass Calling `register_sent_messages` re-initializes all registered sent message types. Args: subclass (:obj:`SimulationObject`): a subclass of `SimulationObject` that is registering the types of simulation messages it sends sent_messages (:obj:`list` of :obj:`SimulationMessage`): a list of the `SimulationMessage` type's which can be sent by `SimulationObject`'s of type `subclass` """ for sent_message_type in sent_messages: subclass.metadata.message_types_sent.add(sent_message_type) def get_receiving_priorities_dict(self): """ Get priorities of message types handled by this `SimulationObject`'s type Returns: :obj:`dict`: mapping from message types handled by this `SimulationObject` to their execution priorities. The highest priority is 0, and higher values have lower priorities. Execution priorities determine the execution order of concurrent events at a `SimulationObject`. """ return self.__class__.metadata.event_handler_priorities def _SimulationEngine__handle_event_list(self, event_list): """ Handle a list of simulation events, which may contain multiple concurrent events This method's special name ensures that it cannot be overridden, and can only be called from `SimulationEngine`. Attributes: event_list (:obj:`list` of :obj:`Event`): the `Event` message(s) in the simulation event Raises: :obj:`SimulatorError`: if a message in `event_list` has an invalid type """ self.num_events += 1 if self.LOG_EVENTS: # write events to a plot log # plot logging is controlled by configuration files pointed to by config_constants and by env vars for event in event_list: self.fast_plot_file_logger.fast_log(str(event), sim_time=self.time) # iterate through event_list, branching to handler for event in event_list: try: handler = self.__class__.metadata.event_handlers_dict[ event.message.__class__] handler(self, event) except KeyError: # pragma: no cover # unreachable because of check that receiving sim # obj type is registered to receive the message type raise SimulatorError( "No handler registered for Simulation message type: '{}'". format(event.message.__class__.__name__)) @property def class_event_priority(self): """ Get the event priority of this simulation object's class Returns: :obj:`int`: the event priority of this simulation object's class """ return self.__class__.metadata.class_priority def render_event_queue(self): """ Format an event queue as a string Returns: :obj:`str`: return a string representation of the simulator's event queue """ return self.simulator.event_queue.render() def log_with_time(self, msg): """ Write a debug log message with the simulation time. """ self.fast_debug_file_logger.fast_log(msg, sim_time=self.time)