def start_connect(world, sim_name, sim_config, sim_id, sim_params): """Connect to the already running simulator *sim_name* based on its config entry *sim_config*. Return a :class:`RemoteProcess` instance. Raise a :exc:`~mosaik.exceptions.ScenarioError` if the simulator cannot be instantiated. """ addr = sim_config['connect'] try: host, port = addr.strip().split(':') addr = (host, int(port)) except ValueError: raise ScenarioError('Simulator "%s" could not be started: Could not ' 'parse address "%s"' % (sim_name, sim_config['connect'])) from None proxy = make_proxy(world, sim_name, sim_config, sim_id, sim_params, addr=addr) return proxy
def _assert_async_requests(self, dfg, src_sid, dest_sid): """Check if async. requests are allowed from *dest_sid* to *src_sid* and raise a :exc:`ScenarioError` if not.""" data = { 'src': src_sid, 'dest': dest_sid, } if dest_sid not in dfg[src_sid]: raise ScenarioError( 'No connection from "%(src)s" to "%(dest)s": You need to ' 'connect entities from both simulators and set ' '"async_requests=True".' % data) if dfg[src_sid][dest_sid]['async_requests'] is not True: raise ScenarioError( 'Async. requests not enabled for the connection from ' '"%(src)s" to "%(dest)s". Add the argument ' '"async_requests=True" to the connection of entities from ' '"%(src)s" to "%(dest)s".' % data)
def _check_model_and_meth_names(self, models, api_methods, extra_methods): """Check if there are any overlaps in model names and reserved API methods as well as in them and extra API methods. Raise a :exc:`~mosaik.exception.ScenarioError` if that's the case. """ models = list(models) illegal_models = set(models) & set(api_methods) if illegal_models: raise ScenarioError('Simulator "%s" uses illegal model names: %s' % (self.sid, ', '.join(illegal_models))) illegal_meths = set(models + api_methods) & set(extra_methods) if illegal_meths: raise ScenarioError('Simulator "%s" uses illegal extra method ' 'names: %s' % (self.sid, ', '.join(illegal_meths)))
def validate_api_version(version): """Validate the *version*. Raise a :exc: `ScenarioError` if the version format is wrong or does not match the min requirements. """ try: version_tuple = str(version).split('.') v_tuple = tuple(map(int, version_tuple)) except ValueError: raise ScenarioError('Version parts of %r must be integer' % version) from None if len(v_tuple) != 2: raise ScenarioError('Version must be formated like ' '"major.minor", but is %r' % version) from None if not (v_tuple[0] == API_MAJOR and v_tuple[1] <= API_MINOR): raise ScenarioError('Version must be between %(major)s.0 and ' '%(major)s.%(minor)s' % { 'major': API_MAJOR, 'minor': API_MINOR }) return v_tuple
def start_dockerimage(world, sim_name, sim_config, sim_id, sim_params): """ ... """ print('###### start_dockerimage ######') replacements = { 'addr': '%s:%s' % (world.config['addr'][0], world.config['addr'][1]), 'python': sys.executable, } cmd = sim_config['dockerimage'] % replacements if 'posix' in sim_params.keys(): posix = sim_params.pop('posix') cmd = shlex.split(cmd, posix=posix) else: cmd = shlex.split(cmd, posix=(os.name != 'nt')) cwd = sim_config['cwd'] if 'cwd' in sim_config else '.' # Make a copy of the current env. vars dictionary and update it with the # user provided values (or an empty dict as a default): env = dict(os.environ) env.update(sim_config.get('env', {})) kwargs = { 'bufsize': 1, 'cwd': cwd, 'universal_newlines': True, 'env': env, # pass the new env dict to the sub process } try: proc = subprocess.Popen(cmd, **kwargs) except (FileNotFoundError, NotADirectoryError) as e: # This distinction has to be made due to a change in python 3.8.0. # It might become unecessary for future releases supporting # python >= 3.8 only. if str(e).count(':') == 2: eout = e.args[1] else: eout = str(e).split('] ')[1] raise ScenarioError('Simulator "%s" could not be started: %s' % (sim_name, eout)) from None proxy = make_proxy(world, sim_name, sim_config, sim_id, sim_params, proc=proc) return proxy
def start_proc(world, sim_name, sim_config, sim_id, sim_params): """Start a new process for simulator *sim_name* based on its config entry *sim_config*. Return a :class:`RemoteProcess` instance. Raise a :exc:`~mosaik.exceptions.ScenarioError` if the simulator cannot be instantiated. """ replacements = { 'addr': '%s:%s' % (world.config['addr'][0], world.config['addr'][1]), 'python': sys.executable, } cmd = sim_config['cmd'] % replacements if 'posix' in sim_params.keys(): posix = sim_params.pop('posix') cmd = shlex.split(cmd, posix=posix) else: cmd = shlex.split(cmd, posix=(os.name != 'nt')) cwd = sim_config['cwd'] if 'cwd' in sim_config else '.' # Make a copy of the current env. vars dictionary and update it with the # user provided values (or an empty dict as a default): env = dict(os.environ) env.update(sim_config.get('env', {})) kwargs = { 'bufsize': 1, 'cwd': cwd, 'universal_newlines': True, 'env': env, # pass the new env dict to the sub process } try: proc = subprocess.Popen(cmd, **kwargs) except (FileNotFoundError, NotADirectoryError) as e: raise ScenarioError('Simulator "%s" could not be started: %s' % (sim_name, e.args[1])) from None proxy = make_proxy(world, sim_name, sim_config, sim_id, sim_params, proc=proc) return proxy
def start_dockerfile(world, sim_name, sim_config, sim_id, sim_params): """ ... """ print('###### start_dockerfile ######') addr = sim_config['connect'] try: host, port = addr.strip().split(':') addr = (host, int(port)) except ValueError: raise ScenarioError('Simulator "%s" could not be started: Could not ' 'parse address "%s"' % (sim_name, sim_config['connect'])) from None proxy = make_proxy(world, sim_name, sim_config, sim_id, sim_params, addr=addr) return proxy
def start_inproc(world, sim_name, sim_config, sim_id, sim_params): """Import and instantiate the Python simulator *sim_name* based on its config entry *sim_config*. Return a :class:`LocalProcess` instance. Raise a :exc:`~mosaik.exceptions.ScenarioError` if the simulator cannot be instantiated. """ try: mod_name, cls_name = sim_config['python'].split(':') mod = importlib.import_module(mod_name) cls = getattr(mod, cls_name) except (AttributeError, ImportError, KeyError, ValueError) as err: if sys.version_info.major <= 3 and sys.version_info.minor < 6: detail_msgs = { ValueError: 'Malformed Python class name: Expected "module:Class"', ImportError: 'Could not import module: %s' % err.args[0], AttributeError: 'Class not found in module', } else: detail_msgs = { ValueError: 'Malformed Python class name: Expected "module:Class"', ModuleNotFoundError: 'Could not import module: %s' % err.args[0], AttributeError: 'Class not found in module', } details = detail_msgs[type(err)] raise ScenarioError('Simulator "%s" could not be started: %s' % (sim_name, details)) from None sim = cls() meta = sim.init(sim_id, **sim_params) # "meta" is module global and thus shared between all "LocalProcess" # instances. This may leed to problems if a user modfies it, so make # a deep copy of it for each instance: meta = copy.deepcopy(meta) return LocalProcess(sim_name, sim_id, meta, sim, world)
def start(world, sim_name, sim_id, sim_params): """Start the simulator *sim_name* based on the configuration im *world.sim_config*, give it the ID *sim_id* and pass the parameters of the dict *sim_params* to it. The sim config is a dictionary with one entry for every simulator. The entry itself tells mosaik how to start the simulator:: { 'ExampleSimA': { 'python': 'example_sim.mosaik:ExampleSim', }, 'ExampleSimB': { 'cmd': 'example_sim %(addr)s', 'cwd': '.', }, 'ExampleSimC': { 'connect': 'host:port', }, } *ExampleSimA* is a pure Python simulator. Mosaik will import the module ``example_sim.mosaik`` and instantiate the class ``ExampleSim`` to start the simulator. *ExampleSimB* would be started by executing the command *example_sim* and passing the network address of mosaik das command line argument. You can optionally specify a *current working directory*. It defaults to ``.``. *ExampleSimC* can not be started by mosaik, so mosaik tries to connect to it. The function returns a :class:`mosaik_api.Simulator` instance. It raises a :exc:`~mosaik.exceptions.SimulationError` if the simulator could not be started. Return a :class:`SimProxy` instance. """ try: sim_config = world.sim_config[sim_name] except KeyError: raise ScenarioError('Simulator "%s" could not be started: Not found ' 'in sim_config' % sim_name) # Try available starters in that order and raise an error if none of them # matches: starters = collections.OrderedDict(python=start_inproc, cmd=start_proc, connect=start_connect) for sim_type, start in starters.items(): if sim_type in sim_config: proxy = start(world, sim_name, sim_config, sim_id, sim_params) try: proxy.meta['api_version'] = validate_api_version( proxy.meta['api_version']) return proxy except ScenarioError as se: raise ScenarioError('Simulator "%s" could not be started:' ' Invalid version "%s": %s' % (sim_name, proxy.meta['api_version'], se)) else: raise ScenarioError('Simulator "%s" could not be started: ' 'Invalid configuration' % sim_name)
def connect(self, src, dest, *attr_pairs, async_requests=False, time_shifted=False, initial_data=None): """Connect the *src* entity to *dest* entity. Establish a data-flow for each ``(src_attr, dest_attr)`` tuple in *attr_pairs*. If *src_attr* and *dest_attr* have the same name, you you can optionally only pass one of them as a single string. Raise a :exc:`~mosaik.exceptions.ScenarioError` if both entities share the same simulator instance, if at least one (src. or dest.) attribute in *attr_pairs* does not exist, or if the connection would introduce a cycle in the data-flow (e.g., A → B → C → A). If the *dest* simulator may make asynchronous requests to mosaik to query data from *src* (or set data to it), *async_requests* should be set to ``True`` so that the *src* simulator stays in sync with *dest*. An alternative to asynchronous requests are time-shifted connections. Their data flow is always resolved after normal connections so that cycles in the data-flow can be realized without introducing deadlocks. For such a connection *time_shifted* should be set to ``True`` and *initial_data* should contain a dict with input data for the first simulation step of the receiving simulator. An alternative to using async_requests to realize cyclic data-flow is given by the time_shifted kwarg. If set to ``True`` it marks the connection as cycle-closing (e.g. C → A). It must always be used with initial_data specifying a dict with the data sent to the dest simulator at the first step (e.g. *{‘src_attr’: value}*). """ if src.sid == dest.sid: raise ScenarioError('Cannot connect entities sharing the same ' 'simulator.') if async_requests and time_shifted: raise ScenarioError( 'Async_requests and time_shifted connections ' 'are incongruous methods for handling of cyclic ' 'data-flow. Choose one!') # Expand single attributes "attr" to ("attr", "attr") tuples: attr_pairs = tuple((a, a) if type(a) is str else a for a in attr_pairs) missing_attrs = self._check_attributes(src, dest, attr_pairs) if missing_attrs: raise ScenarioError('At least one attribute does not exist: %s' % ', '.join('%s.%s' % x for x in missing_attrs)) # Time-shifted connections for closing data-flow cycles: if time_shifted: self.shifted_graph.add_edge(src.sid, dest.sid) dfs = self.shifted_graph[src.sid][dest.sid].setdefault( 'dataflows', []) dfs.append((src.eid, dest.eid, attr_pairs)) if type(initial_data) is not dict or initial_data == {}: raise ScenarioError( 'Time shifted connections have to be ' 'set with default inputs for the first step.') # list for assertion of correct assignment: check_attrs = [a[0] for a in attr_pairs] # Set default values for first data exchange: for attr, val in initial_data.items(): if attr not in check_attrs: raise ScenarioError( 'Incorrect attr "%s" in "initial_data".' % attr) self._shifted_cache[0].setdefault(src.sid, {}) self._shifted_cache[0][src.sid].setdefault(src.eid, {}) self._shifted_cache[0][src.sid][src.eid][attr] = val # Standard connections: else: # Add edge and check for cycles and the data-flow graph. self.df_graph.add_edge(src.sid, dest.sid, async_requests=async_requests) if not networkx.is_directed_acyclic_graph(self.df_graph): self.df_graph.remove_edge(src.sid, dest.sid) raise ScenarioError('Connection from "%s" to "%s" introduces ' 'cyclic dependencies.' % (src.sid, dest.sid)) dfs = self.df_graph[src.sid][dest.sid].setdefault('dataflows', []) dfs.append((src.eid, dest.eid, attr_pairs)) # Add relation in entity_graph self.entity_graph.add_edge(src.full_id, dest.full_id) # Cache the attribute names which we need output data for after a # simulation step to reduce the number of df graph queries. outattr = [a[0] for a in attr_pairs] if outattr: self._df_outattr[src.sid][src.eid].extend(outattr)