示例#1
0
 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)
示例#2
0
    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')
示例#3
0
    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')
示例#4
0
 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
示例#5
0
 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)
示例#6
0
    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))
示例#7
0
    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)
示例#8
0
    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])
示例#9
0
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
示例#10
0
 def test_is_active(self):
     fast_logger = FastLogger(self.fixture_logger, self.fixture_level.name)
     self.assertTrue(fast_logger.is_active())
示例#11
0
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))
示例#12
0
 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')
示例#13
0
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
示例#14
0
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)