def __init__(self, env, callback=None, log_folder=None): '''Create simulation for previously set up environment. :param env: fully initialized environment with agents already set :type env: :py:class:`~creamas.core.environment.Environment` :param callable callback: function to call after each simulation step :parat str log_folder: folder to log simulation information ''' self._env = env self._callback = callback self._age = 0 self._order = 'alphabetical' self._name = 'sim' self._start_time = time.time() self._step_start_time = None self._step_processing_time = 0.0 self._processing_time = 0.0 self._end_time = None # List of agents that have not been triggered for current step. self._agents_to_act = [] if type(log_folder) is str: self.logger = ObjectLogger(self, log_folder, add_name=False, init=True) else: self.logger = None
def __init__(self, environment, resources=0, name=None, log_folder=None, log_level=logging.DEBUG): super().__init__(environment) self._age = 0 self._env = environment self._max_res = resources self._cur_res = resources self._R = [] self._W = [] self._A = [] self._D = {} self._connections = [] self._attitudes = [] if type(name) is str and len(name) > 0: self.__name = name else: self.__name = self.addr if type(log_folder) is str: self.logger = ObjectLogger(self, log_folder, add_name=True, init=True, log_level=log_level) else: self.logger = None
class Simulation(): '''Base class for iterative simulations. In each simulation step calls :py:meth:`~creamas.core.agent.CreativeAgent.act` for each agent in simulation environment. ''' @classmethod def create(self, agent_cls=None, n_agents=10, agent_kwargs={}, env_cls=Environment, env_kwargs={}, callback=None, conns=0, log_folder=None): '''Convenience function to create simple simulations. Method first creates environment, then instantiates agents into it with give arguments, and finally creates simulation for the environment. :param agent_cls: class for agents, or list of classes. If list, then **n_agents** and **agent_kwargs** are expected to be lists also. :param n_agents: amount of agents for simulation, or list of amounts :param agent_kwargs: keyword arguments passed to agents at creation time, or list of keyword arguments. :param env_cls: environment class for simulation :type env_cls: :py:class:`~creamas.core.environment.Environment` :param dict env_kwargs: keyword arguments passed to environment at creation time :param callable callback: optional callable to call after each simulation step :param conns: Create **conns** amount of initial (random) connections for agents in the simulation environment. :param str log_folder: folder for possible logging. This overwrites *log_folder* keyword argument from **agent_kwargs** and **env_kwargs**. ''' assert issubclass(env_cls, Environment) assert (callback is None or hasattr(callback, '__call__')) if hasattr(agent_cls, '__iter__'): for e in agent_cls: assert issubclass(e, CreativeAgent) else: assert issubclass(agent_cls, CreativeAgent) env = env_cls.create(**env_kwargs) agents = [] if hasattr(agent_cls, '__iter__'): for i in range(len(n_agents)): agent_kwargs[i]['environment'] = env agent_kwargs[i]['log_folder'] = log_folder agents = agents + [agent_cls[i](**agent_kwargs[i]) for e in range(n_agents[i])] else: agent_kwargs['environment'] = env agent_kwargs['log_folder'] = log_folder agents = [agent_cls(**agent_kwargs) for e in range(n_agents)] if conns > 0: env.create_initial_connections(n=conns) return Simulation(env, callback, log_folder) def __init__(self, env, callback=None, log_folder=None): '''Create simulation for previously set up environment. :param env: fully initialized environment with agents already set :type env: :py:class:`~creamas.core.environment.Environment` :param callable callback: function to call after each simulation step :parat str log_folder: folder to log simulation information ''' self._env = env self._callback = callback self._age = 0 self._order = 'alphabetical' self._name = 'sim' self._start_time = time.time() self._step_start_time = None self._step_processing_time = 0.0 self._processing_time = 0.0 self._end_time = None # List of agents that have not been triggered for current step. self._agents_to_act = [] if type(log_folder) is str: self.logger = ObjectLogger(self, log_folder, add_name=False, init=True) else: self.logger = None @property def name(self): '''Name of the simulation.''' return self._name @property def env(self): '''Environment for the simulation. Must be a subclass of :py:class:`~creamas.core.environment.Environment`. ''' return self._env @property def age(self): '''Age of the simulation.''' return self._age @property def callback(self): '''Callable to be called after each simulation step for any extra bookkeeping, etc.. Should accept one parameter: *age* that is current simulation age. ''' return self._callback @property def order(self): '''Order in which agents are run. Possible values: * alphabetical: agents are sorted by name * random: agents are shuffled Changing the order while iteration is unfinished will take place in the next iteration. ''' return self._order @order.setter def order(self, order): assert order in ['alphabetical', 'random'] self._order = order def _get_order_agents(self): agents = self.env.get_agents(address=True) if self.order == 'alphabetical': return sorted(agents) shuffle(agents) return agents def _init_step(self): '''Initialize next step of simulation to be run.''' self._age += 1 self.env.age = self._age self._log(logging.INFO, "") self._log(logging.INFO, "\t***** Step {:0>4} *****". format(self.age)) self._log(logging.INFO, "") self._agents_to_act = self._get_order_agents() self._step_processing_time = 0.0 self._step_start_time = time.time() def _finalize_step(self): '''Finalize simulation step after all agents have acted for the current step. ''' t = time.time() if self._callback is not None: self._callback(self.age) t2 = time.time() self._step_processing_time += t2 - t self._log(logging.INFO, "Step {} run in: {:.3f}s ({:.3f}s of " "actual processing time used)" .format(self.age, self._step_processing_time, t2 - self._step_start_time)) self._processing_time += self._step_processing_time def async_steps(self, n): assert len(self._agents_to_act) == 0 for _ in range(n): self.async_step() def async_step(self): '''Progress simulation by running all agents once asynchronously. ''' assert len(self._agents_to_act) == 0 self._init_step() t = time.time() tasks = [asyncio.ensure_future(self.env.trigger_act(addr)) for addr in self._agents_to_act] aiomas.run(until=asyncio.gather(*tasks)) self._agents_to_act = [] self._step_processing_time = time.time() - t self._finalize_step() def steps(self, n): '''Progress simulation with given amount of steps. Can not be called when some of the agents have not acted for the current step. :param int n: amount of steps to run ''' assert len(self._agents_to_act) == 0 for _ in range(n): self.step() def step(self): '''Progress simulation with single step. Can not be called when some of the agents have not acted for the current step. ''' assert len(self._agents_to_act) == 0 self.next() while len(self._agents_to_act) > 0: self.next() def next(self): '''Trigger next agent to :py:meth:`~creamas.core.CreativeAgent.act` in the current step. ''' # all agents acted, init next step t = time.time() if len(self._agents_to_act) == 0: self._init_step() agent = self._agents_to_act.pop(0) aiomas.run(until=self.env.trigger_act(agent)) t2 = time.time() self._step_processing_time += t2 - t # all agents acted, finalize current step if len(self._agents_to_act) == 0: self._finalize_step() def finish_step(self): '''Progress simulation to the end of the current step.''' while len(self._agents_to_act) > 0: self.next() def _log(self, level, msg): if self.logger is not None: self.logger.log(level, msg) def end(self, folder=None): '''End simulation and destroy the current simulation environment.''' ret = self.env.destroy(folder=folder) self._end_time = time.time() self._log(logging.INFO, "Simulation run with {} steps took {:.3f}s to " "complete, while actual processing time was {:.3f}s." .format(self.age, self._end_time - self._start_time, self._processing_time)) return ret
class CreativeAgent(aiomas.Agent): '''Base class for all creative agents. All agents share certain common attributes: :ivar ~creamas.core.agent.CreativeAgent.env: The environment where the agent lives. :ivar int max_res: Agent's resources per step, 0 if agent has unlimited resources. :ivar int cur_res: Agent's current resources. :ivar list ~creamas.core.agent.CreativeAgent.R: rules agent uses to evaluate artifacts :ivar list ~creamas.core.agent.CreativeAgent.W: Weight for each rule in **R**, in [-1,1]. :ivar list A: Artifacts the agent has created so far :ivar dict D: Domain knowledge, other agents' artifacts seen by this agent :ivar list connections: Other agents this agent knows :ivar list attitudes: Attitude towards each agent in **connections**, in [-1,1] :ivar str ~creamas.core.agent.CreativeAgent.name: Name of the agent. defaults to A<n>, where n is the agent's number in environment. Agent's name must be unique within its environment. :ivar ~creamas.core.agent.CreativeAgent.age: Age of the agent ''' def __init__(self, environment, resources=0, name=None, log_folder=None, log_level=logging.DEBUG): super().__init__(environment) self._age = 0 self._env = environment self._max_res = resources self._cur_res = resources self._R = [] self._W = [] self._A = [] self._D = {} self._connections = [] self._attitudes = [] if type(name) is str and len(name) > 0: self.__name = name else: self.__name = self.addr if type(log_folder) is str: self.logger = ObjectLogger(self, log_folder, add_name=True, init=True, log_level=log_level) else: self.logger = None @property def age(self): '''Age of the agent.''' return self._age @age.setter def age(self, i): self._age = i @property def name(self): '''Human readable name of the agent. Must be unique in agent's environment. Agent should not change its name during its lifetime.''' return self.__name def sanitized_name(self): import re a = re.split("[:/]", self.name) return "_".join([i for i in a if len(i) > 0]) @name.setter def name(self, name): self.__name = name @property def env(self): '''The environment where the agent lives. Must be a subclass of :py:class:`~creamas.core.environment.Environment`.''' return self._env @property def R(self): '''Rules agent uses to evaluate artifacts. Each rule in **R** is expected to be a callable with single parameter, the artifact to be evaluated. Callable should return a float in [-1,1], where 1 means that rule is very prominent in the artifact, and 0 that there is none of that rule in the artifact, and -1 means that the artifact shows traits opposite to the rule. .. note:: If used other way than what is stated above, override :py:meth:`~creamas.core.agent.CreativeAgent.extract`. ''' return self._R @property def W(self): '''Weights for features. Each weight should be in [-1,1].''' return self._W @property def A(self): '''Artifacts created so far by the agent.''' return self._A @property def D(self): '''Domain knowledge accumulated by this agent. Dictionary of agents and their artifacts. ''' return self._D @property def max_res(self): '''Maximum resources for the agent per act. If 0, agent has unlimited resources. If maximum resources are set below current resources, current resources are capped to new maximum resources. ''' return self._max_res @max_res.setter def max_res(self, value): if value < 0: value = 0 self._max_res = value if self._cur_res > self._max_res: self._cur_res = self._max_res @property def cur_res(self): '''Agent's current resources. Capped to maximum resources.''' return self._cur_res @cur_res.setter def cur_res(self, value): if value > self._max_res: value = self._max_res if value < 0: value = 0 self._cur_res = value @property def connections(self): '''Connections to the other agents in the **env**.''' return self._connections @property def attitudes(self): '''Attitudes towards agents in **connections**.''' return self._attitudes def qualname(self): '''Get qualified name of this class. ''' return "{}:{}".format(self.__module__, self.__class__.__name__) def get_attitude(self, agent): '''Get attitude towards agent in **connections**. If agent is not in **connections**, returns None. ''' try: ind = self._connections.index(agent) return self._attitudes[ind] except: return None def set_attitude(self, agent, attitude): '''Set attitude towards agent. If agent is not in **connections**, adds it. ''' assert (attitude >= -1.0 and attitude <= 1.0) try: ind = self._connections.index(agent) self._attitudes[ind] = attitude except: self.add_connection(agent, attitude) def set_weight(self, rule, weight): '''Set weight for rule in **R**, if rule is not in **R**, adds it. ''' if not (issubclass(rule.__class__, Rule) or issubclass(rule.__class__, RuleLeaf)): raise TypeError("Rule to set weight ({}) is not subclass " "of {} or {}.".format(rule, Rule, RuleLeaf)) assert (weight >= -1.0 and weight <= 1.0) try: ind = self._R.index(rule) self._W[ind] = weight except: self.add_rule(rule, weight) def get_weight(self, rule): '''Get weight for rule. If rule is not in **R**, returns None.''' if not (issubclass(rule.__class__, Rule) or issubclass(rule.__class__, RuleLeaf)): raise TypeError("Rule to get weight ({}) is not subclass " "of {} or {}.".format(rule, Rule, RuleLeaf)) try: ind = self._R.index(rule) return self._W[ind] except: return None def add_artifact(self, artifact): '''Add artifact to **A**.''' if not issubclass(artifact.__class__, Artifact): raise TypeError("Artifact to add ({}) is not {}." .format(artifact, Artifact)) self._A.append(artifact) def add_rule(self, rule, weight): '''Add rule to **R** with initial weight. :param rule: rule to be added :type rule: `~creamas.core.rule.Rule` :param float weight: initial weight for the rule :raises TypeError: if rule is not subclass of :py:class:`Rule` :returns: true if rule was successfully added, otherwise false :rtype bool: ''' if not (issubclass(rule.__class__, Rule) or issubclass(rule.__class__, RuleLeaf)): raise TypeError("Rule to add ({}) is not subclass of {} or {}." .format(rule.__class__, Rule, RuleLeaf)) if rule not in self._R: self._R.append(rule) self._W.append(weight) return True return False def remove_rule(self, rule): '''Remove rule from **R** and its corresponding weight from **W**. :param rule: rule to remove :type rule: `~creamas.core.rule.Rule` :raises TypeError: if rule is not subclass of :py:class:`Rule` :returns: true if rule was successfully removed, otherwise false :rtype bool: ''' if not (issubclass(rule.__class__, Rule) or issubclass(rule.__class__, RuleLeaf)): raise TypeError("Rule to remove ({}) is not subclass of {} or {}." .format(rule.__class__, Rule, RuleLeaf)) try: ind = self._R.index(rule) del self._R[ind] del self._W[ind] return True except: return False def add_connection(self, agent, attitude=0.0): '''Added agent to current **connections** with given initial attitude. Does nothing if agent is already in **connections**. :param agent: agent to be added :type agent: :py:class:`~creamas.core.agent.CreativeAgent` :param attitude: initial attitude towards agent, in [-1, 1] :type attitude: float ''' if not issubclass(agent.__class__, CreativeAgent): raise TypeError("Agent to add in connections ({}), was not " "subclass of {}" .format(agent, CreativeAgent)) if agent not in self._connections: self.connections.append(agent) self.attitudes.append(attitude) return True return False def remove_connection(self, agent): '''Remove agent from current connections.''' if not issubclass(agent.__class__, CreativeAgent): raise TypeError("Agent to remove from connections ({}), was " "not subclass of {}" .format(agent, CreativeAgent)) try: ind = self._connections.index(agent) del self._connections[ind] del self._attitudes[ind] return True except: return False async def connect(self, addr): '''Connect to agent in given address. ''' remote_agent = await self.env.connect(addr) return remote_agent async def random_connection(self): '''Connect to random agent from current **connections**. .. note:: This is an async method that should be awaited. :returns: connected remote agent :rtype: :py:class:`~creamas.core.agent.CreativeAgent` ''' r_agent = choice(self._connections) remote_agent = await self.env.connect(r_agent.addr) return remote_agent def publish(self, artifact): '''Publish artifact to agent's environment. :param artifact: artifact to be published :type artifact: :py:class:`~creamas.core.artifact.Artifact` ''' self.env.add_artifact(artifact) self._log(logging.DEBUG, "Published {} to domain.".format(artifact)) def refill(self): '''Refill agent's resources to maximum.''' self._cur_res = self._max_res def evaluate(self, artifact): r'''Evaluate artifact with agent's current rules and weights. :param artifact: artifact to be evaluated :type artifact: :py:class:`~creamas.core.artifact.Artifact` :returns: agent's evaluation of the artifact, in [-1,1], and framing. In this basic implementation framing is always *None*. :rtype: tuple Actual evaluation formula is: .. math:: e(A) = \frac{\sum_{i=1}^{n} r_{i}(A)w_i} {\sum_{i=1}^{n} \lvert w_i \rvert}, where :math:`r_{i}(A)` is the :math:`i` th rule's evaluation on artifact :math:`A`, and :math:`w_i` is the weight for rule :math:`r_i`. ''' s = 0 w = 0.0 if len(self.R) == 0: return 0.0, None for i in range(len(self.R)): s += self.R[i](artifact) * self.W[i] w += abs(self.W[i]) if w == 0.0: return 0.0, None return s / w, None async def ask_opinion(self, agent, artifact): '''Ask agent's opinion about artifact. .. note:: This is an async method that should be awaited. :param agent: agent which opinion is asked :type agent: :py:class:`~creamas.core.agent.CreativeAgent` :param object artifact: artifact to be evaluated :returns: agent's evaluation of the artifact :rtype: float ''' remote_agent = await self.container.connect(agent.addr) pkl = pickle.dumps(artifact) ret = await remote_agent.evaluate_pickle(pkl) ev = pickle.loads(ret) return ev @aiomas.expose async def act(self): '''Trigger agent to act. **Dummy method, override in subclass.** :raises NotImplementedError: if not overridden in subclass .. note:: This is an async method that should be awaited. ''' raise NotImplementedError('Override in subclass.') def validate_candidates(self, candidates): '''Validate list of candidate artifacts. ''' return candidates @aiomas.expose def vote(self, candidates): '''Rank artifact candidates based on agent's own *evaluate*-method. :param candidates: list of :py:class:`~creamas.core.artifact.Artifact` objects to be ranked :returns: ordered list of (candidate, evaluation)-tuples ''' ranks = [(c, self.evaluate(c)[0]) for c in candidates] ranks.sort(key=operator.itemgetter(1), reverse=True) return ranks @aiomas.expose async def get_older(self): '''Age agent by one simulation step.''' self._age = self._age + 1 def _log(self, level, msg): if self.logger is not None: self.logger.log(level, msg) @aiomas.expose def close(self, folder=None): '''Perform any bookkeeping needed before closing the agent. **Dummy implementation, override in subclass if needed.** ''' pass def __str__(self): return self.__repr__() def __repr__(self): return "{}({})".format(self.__class__.__name__, self.name)