def build(root: "str", model: Model, solver: "str") -> "CProfiler": build = BuildEngine(output_dir=root) definitions = { "CXXFLAGS": "-pg -std=c++14 -O0", "CFLAGS": "-pg -O0", } # Prepare the simulation executable. build.prepare(model) exe = build.build_simulation(simulation_name=solver, definitions=definitions) return CProfiler(root_dir=pathlib.Path(root), exe_path=exe)
class TestBuildEngine(unittest.TestCase): test_model = create_dimerization() # List of solver names. # Each entry represents the Makefile target for a C++ solver. solver_names = [ "ssa", "ode", "tau_leap", ] def setUp(self) -> None: # Disable the gillespy2 cache. gillespy2.cache_enabled = False self.build_engine = BuildEngine() self.tmp_dir = self.build_engine.prepare(self.test_model, variable=False) def tearDown(self) -> None: self.build_engine.clean() if self.tmp_dir is not None and os.path.exists(self.tmp_dir): shutil.rmtree(self.tmp_dir, ignore_errors=True) def test_default_layout(self): """ Ensure that with the default, cache-less output, the template and obj files are subdirectories of the temp directory. This is to ensure that they get cleaned up when the temp directory is cleaned up. """ # This test should be attempted with each solver target. for solver_name in self.solver_names: with self.subTest(solver=solver_name): self.build_engine.build_simulation(solver_name) template_dir = self.build_engine.template_dir obj_dir = self.build_engine.obj_dir self.assertTrue( str(template_dir.resolve()).startswith( str(Path(self.tmp_dir).resolve())), "Template directory was not placed in temp directory when cache is disabled" ) self.assertTrue( str(obj_dir.resolve()).startswith(str( Path(self.tmp_dir).resolve())), "Dependency obj directory was not placed in temp directory when cache is disabled" ) # Ensure that the dependencies are cleaned up when .clean() is called. self.build_engine.clean() self.assertFalse( template_dir.exists(), "Template directory was not cleaned up when cache is disabled") self.assertFalse( obj_dir.exists(), "Dependency obj directory was not cleaned up when cache is disabled" ) def test_clean(self): """ Ensure that with the default, cache-less output, the output files are cleaned up. """ # Test clean-up with manual call to .clean() self.build_engine.clean() self.assertFalse(os.path.exists(self.tmp_dir), "Build engine output directory not cleaned up.") def test_template_file(self): """ Ensure that the resulting template file exists and was prepared correctly. """ template_file = self.build_engine.template_dir.joinpath( self.build_engine.template_definitions_name) self.assertTrue(template_file.exists(), "Simulation template header file could not be found") # Test to ensure the contents of the output template_file contains ONLY macro definitions, # nothing else. with open(template_file) as template: for line in template.readlines(): self.assertTrue( line.startswith("#"), "Output template file contains an invalid definition.") def test_build_output(self): """ Ensure that the default, cache-less output has expected behavior. Should result in an expected definitions file and an executable simulation. """ simulation_file = self.build_engine.output_dir.joinpath( "GillesPy2_Simulation.exe" if os.name == "nt" else "GillesPy2_Simulation.out") for solver_name in self.solver_names: with self.subTest(solver=solver_name): self.build_engine.build_simulation(solver_name) self.assertTrue(simulation_file.exists(), "Simulation executable could not be found") self.assertTrue(os.access(str(simulation_file), os.X_OK), "Simulation output is not executable")
class CSolver: """ This class implements base behavior that will be needed for C++ solver implementees. :param model: The Model to simulate. :type model: Model :param output_directory: The working output directory. :type output_directory: str :param delete_directory: If True then the output_directory will be deleted upon completetion. :type delete_directory: bool :param resume: Resume data from a previous simulation run. :param variable: Indicates whether the simulation should be variable. :type variable: bool """ rc = 0 def __init__(self, model: Model = None, output_directory: str = None, delete_directory: bool = True, resume=None, variable: bool = False): if model is None: raise gillespyError.SimulationError("A model is required to run the simulation.") if len(BuildEngine.get_missing_dependencies()) > 0: raise gillespyError.SimulationError( "Please install/configure 'g++' and 'make' on your system, to ensure that GillesPy2 C solvers will run properly." ) if platform.system() == "Windows" and " " in gillespy2.__file__: raise gillespy2Error.SimulationError("GillesPy2 does not support spaces in its path on Windows systems.") self.delete_directory = False self.model = copy.deepcopy(model) self.resume = resume self.variable = variable self.build_engine: BuildEngine = None # Validate output_directory, ensure that it doesn't already exist if isinstance(output_directory, str): output_directory = os.path.abspath(output_directory) if os.path.exists(output_directory): raise gillespyError.DirectoryError( f"Could not write to specified output directory: {output_directory}" " (already exists)" ) self.output_directory = output_directory self.delete_directory = delete_directory if self.model is not None: self._set_model() self.is_instantiated = True def __del__(self): if self.build_engine is None: return if not self.delete_directory: return self.build_engine.clean() def _build(self, model: "Union[Model, SanitizedModel]", simulation_name: str, variable: bool, debug: bool = False) -> str: """ Generate and build the simulation from the specified Model and solver_name into the output_dir. :param model: The Model to simulate. :type model: gillespy2.Model :param simulation_name: The name of the simulation to execute. :type simulation_name: str :param variable: If True the simulation will be variable, False if not. :type variable: bool :param debug: Enables or disables debug behavior. :type debug: bool """ # Prepare the build workspace. if self.build_engine is None or self.build_engine.get_executable_path() is None: self.build_engine = BuildEngine(debug=debug, output_dir=self.output_directory) self.build_engine.prepare(model, variable) # Compile the simulation, returning the path of the executable. return self.build_engine.build_simulation(simulation_name) # Assume that the simulation has already been built. return self.build_engine.get_executable_path() def _run_async(self, sim_exec: str, sim_args: "list[str]", decoder: SimDecoder, timeout: int = 0): """ Run the target executable simulation as async. :param sim_exec: The executable simulation to run. :type sim_exec: str :param sim_args: The arguments to pass on simulation run. :type sim_args: list[str] :param decoder: The SimDecoder instance that will handle simulation output. :type decoder: SimDecoder :returns: A future which represents the currently executing run_simulation job. """ executor = ThreadPoolExecutor() return executor.submit(self._run, sim_exec, sim_args, decoder, timeout) def _run(self, sim_exec: str, sim_args: "list[str]", decoder: SimDecoder, timeout: int = 0, display_args: dict = None) -> int: """ Run the target executable simulation. :param sim_exec: The executable simulation to run. :type sim_exec: str :param sim_args: The arguments to pass on simulation run. :type sim_args: list[str] :param decoder: The SimDecoder instance that will handle simulation output. :type decoder: SimDecoder :param display_args: The kwargs need to setup the live graphing :type display_args: dict :returns: The return_code of the simulation. """ # Prefix the executable to the sim arguments. sim_args = [sim_exec] + sim_args # nt and *nix require different methods to force shutdown a running process. if os.name == "nt": proc_kill = lambda sim: sim.send_signal(signal.CTRL_BREAK_EVENT) platform_args = { "creationflags": subprocess.CREATE_NEW_PROCESS_GROUP, "start_new_session": True } else: proc_kill = lambda sim: os.killpg(sim.pid, signal.SIGINT) platform_args = { "start_new_session": True } live_grapher = [None] if display_args is not None: live_queue = queue.Queue(maxsize=1) def decoder_cb(curr_time, curr_state, trajectory_base=decoder.trajectories): try: old_entry = live_queue.get_nowait() except queue.Empty as err: pass curr_state = {self.species[i]: curr_state[i] for i in range(len(curr_state))} entry = ([curr_state], [curr_time], trajectory_base) live_queue.put(entry) decoder.with_callback(decoder_cb) from gillespy2.core.liveGraphing import ( LiveDisplayer, CRepeatTimer, valid_graph_params ) valid_graph_params(display_args['live_output_options']) live_grapher[0] = LiveDisplayer(**display_args) display_timer = CRepeatTimer( display_args['live_output_options']['interval'], live_grapher[0].display, args=(live_queue, display_args['live_output_options']['type']) ) timeout_event = [False] with subprocess.Popen(sim_args, stdout=subprocess.PIPE, **platform_args) as simulation: def timeout_kill(): timeout_event[0] = True proc_kill(simulation) timeout_thread = threading.Timer(timeout, timeout_kill) reader_thread = threading.Thread(target=decoder.read, args=(simulation.stdout,)) if timeout > 0: timeout_thread.start() try: reader_thread.start() if display_args is not None: display_timer.start() reader_thread.join() except KeyboardInterrupt: if live_grapher[0] is not None: display_timer.pause = True proc_kill(simulation) finally: if live_grapher[0] is not None: display_timer.cancel() timeout_thread.cancel() return_code = simulation.wait() reader_thread.join() if timeout_event[0]: return SimulationReturnCode.PAUSED return self._handle_return_code(return_code) def _make_args(self, args_dict: "dict[str, str]") -> "list[str]": """ Convert a dictionary of key, value pairs into a valid Popen argument list. Note: Do not prefix a key with `-` as this will be handled automatically. :param args_dict: A dictionary of named arguments. :type args_dict: dict[str, str] :returns: A formatted list of arguments. """ args_list = [] for key, value in args_dict.items(): args_list.extend([f"--{key}", str(value)]) return args_list def _format_output(self, trajectories: numpy.ndarray): # Check the dimensionality of the input trajectory. if not len(trajectories.shape) == 3: raise gillespyError.ValidationError("Could not format trajectories, input numpy.ndarray is not 3-dimensional.") # The trajectory count is the first dimention of the input ndarray. trajectory_count = trajectories.shape[0] self.result = [] # Begin iterating through the trajectories, copying each dimension into simulation_data. for trajectory in range(trajectory_count): # Copy the first index of the third-dimension into the simulation_data dictionary. data = { "time": trajectories[trajectory, :, 0] } for i in range(len(self.species)): data[self.species[i]] = trajectories[trajectory, :, i + 1] self.result.append(data) return self.result def _handle_return_code(self, return_code: "int") -> "SimulationReturnCode": """ Default return code handler; determines whether the simulation succeeded or failed. Intended to be overridden by solver subclasses, which handles solver-specific return codes. Does nothing if the return code checks out, otherwise raises an error. :param return_code: Return code returned by a simulation. :type return_code: int """ if return_code == 33: return SimulationReturnCode.PAUSED if return_code == 0: return SimulationReturnCode.DONE raise gillespyError.ExecutionError("Error encountered while running simulation C++ file " f"(return code: {int(return_code)})") def _make_resume_data(self, time_stopped: int, simulation_data: numpy.ndarray, t: int): """ If the simulation was paused then the output data needs to be trimmed to allow for resume. In the event the simulation was not paused, no data is changed. """ # No need to create resume data if the simulation completed without interruption. # Note that, currently, some C++ solvers write 0 out as the "time stopped" by default. # This is likely to change in the future. if not time_stopped < t or time_stopped == 0: return simulation_data # Find the index of the time step value which is closest to the time stopped. cutoff = numpy.searchsorted(simulation_data[-1]["time"], float(time_stopped)) if cutoff < 2: log.warning('You have paused the simulation too early, and no points have been calculated past' ' initial values. A graphic display will not produce expected results.') # Break off any extraneous data which goes past the cutoff time. # Any data in this case is assumed to be untrusted. for entry_name, entry_data in simulation_data[-1].items(): simulation_data[-1][entry_name] = entry_data[:cutoff] return simulation_data def _set_model(self, model=None): if model is not None: self.model = copy.deepcopy(model) self._build(self.model, self.target, self.variable, False) self.species_mappings = self.model.sanitized_species_names() self.species = list(self.species_mappings.keys()) self.parameter_mappings = self.model.sanitized_parameter_names() self.parameters = list(self.parameter_mappings.keys()) self.reactions = list(self.model.listOfReactions.keys()) self.result = [] self.rc = 0 def _update_resume_data(self, resume: Results, simulation_data: "list[dict[str, numpy.ndarray]]", time_stopped: int): """ Modify the simulation output to continue from a previous Results object. Does not handle the case where the simulation was interrupted again. """ # No need to update the resume data if there is no previous data, or not enough. if resume is None or len(resume["time"]) < 2: return simulation_data resume_time = float(resume["time"][-1]) increment = resume_time - float(resume["time"][-2]) # Replace the simulation's timespan to continue where the Results object left off. simulation_data[-1]["time"] = numpy.arange(start=(resume_time), stop=(resume_time + time_stopped + increment), step=increment) for entry_name, entry_data in simulation_data[-1].items(): # The results of the current simulation is treated as an "extension" of the resume data. # As such, the new simulation output is formed by joining the two end to end. new_data = numpy.concatenate((resume[entry_name], entry_data[1:]), axis=None) simulation_data[-1][entry_name] = new_data return simulation_data def _validate_resume(self, t: int, resume): """ Validate `resume`. An exception will be thrown if resume['time'][-1] is > t. """ if resume is None: return if t < resume["time"][-1]: raise gillespyError.ExecutionError( "'t' must be greater than previous simulations end time, or set in the run() method as the " "simulations next end time" ) def _validate_kwargs(self, **kwargs): """ Validate any additional kwargs passed to the model. If any exist, warn the user. """ if len(kwargs) == 0: return for key, val in kwargs.items(): log.warning(f"Unsupported keyword argument for solver {self.name}: {key}") def _validate_seed(self, seed: int): if seed is None: return None if not isinstance(seed, int): seed = int(seed) if seed <= 0: raise gillespyError.ModelError("`seed` must be a postive integer.") return seed def _validate_variables_in_set(self, variables, values): for var in variables.keys(): if var not in values: raise gillespyError.SimulationError(f"Argument to variable '{var}' is not a valid variable. Variables must be model species or parameters.") def _validate_type(self, value, typeof: type, message: str): if not type(value) == typeof: raise gillespyError.SimulationError(message)