Beispiel #1
0
    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
Beispiel #2
0
    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
Beispiel #3
0
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
Beispiel #4
0
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)