jakdot / pyactr

Python package for ACT-R cognitive models
GNU General Public License v3.0
134 stars 37 forks source link

Multiple Agents #54

Open BastianMannerow opened 2 weeks ago

BastianMannerow commented 2 weeks ago

Hey! Thanks a lot for the package. Do you have any idea, how I'm able to let multiple agents interact with each other in the same environment simultaniously? Working on my thesis at the moment.

jakdot commented 2 weeks ago

Hi,

Thanks for your interest in pyactr.

pyactr was not built with this functionality in mind. However, I think it is possible to mimic multiple agents interacting in one environment. The following code should work. I am also attaching a trace from running this model with extra explanation on the behavior of this model in some detail. trace.txt

The code simulates two agents, both interacting with the screen on which letters A, B and C are printed one after the other. Agent 1 has to press the key A or C when the letter A or C appears on the screen. If Agent 1 does so, the screen will advance to the next letter. If the letter B appears on the screen, Agent 2 has to press the key B, and the screen advances then to the next letter. If Agent 1 presses A or C when B appears on the screen, nothing happens, and if Agent 2 presses B when A or C are on the screen, nothing happens.

The environments are not completely shared, but the trick to get it to work is to let the environment advance for one agent whenever the other agent made it advance. See the part in the scope of while True at the final part of the code. Most crucial bits are two lines under the "update stimulus" comments. This is a bit of a hack, I had to access some private instance variables to update the simulation from outside of it. At least for this case, it works but I cannot promise it will not break in more complicated cases.

"""
Demo for multiple agents.

This code shows how one can build an environment with multiple agents in which actions of each agent affects the environment for them both.

There are two agents. AG1 should press keys A and C when those letters appear on the screen. AG2 should press B when that letter appears on the screen.

Pressing the right key moves the screen from one letter to the next.

pyactr was not designed with the functionality of supporting multiagents in mind. So the package has to be hacked a bit. 

First, sim.run() does not work. You have to go through events manually (see lines at while True how that is done). But that's fine, any serious modeling should do that, instead of using the running function.

Second, in the two lines following the comment "# update stimulus for the other agent..." we have to go into the internal setup of event simulation and manually update the values there. It's not nice and it's likely it will break in more complicated cases. Please, use this code with caution.

Third, there is some inherent asymmetry in the agents. If agent 1 cannot fire any rules whatsoever, the simulation would stop even if agent 2 has some actions ready. This could be avoided with some extra code (e.g., specify that break from the loop only happens after you check both/all agents).

"""

import string
import random
import simpy

import pyactr as actr

stimuli = ['A', 'B', 'C']
text = [{1: {'text': stimuli[0], 'position': (100,100)}}, {2: {'text': stimuli[1], 'position': (100,100)}}, {3: {'text': stimuli[2], 'position': (100,100)}}]

# environments
environ = actr.Environment(focus_position=(100,100))
environproc = environ.environment_process

# chunk types
actr.chunktype("chunk", "value")
actr.chunktype("read", "state")
actr.chunktype("image", "img")
actr.makechunk(nameofchunk="start", typename="chunk", value="start")
actr.makechunk(nameofchunk="start", typename="chunk", value="start")
actr.makechunk(nameofchunk="attend_let", typename="chunk", value="attend_let")
actr.makechunk(nameofchunk="response", typename="chunk", value="response")
actr.makechunk(nameofchunk="done", typename="chunk", value="done")

# rules
encode_letter="""
        =g>
        isa     read
        state   start
        =visual>
        isa     _visual
        value  =letter
        ==>
        =g>
        isa     read
        state   respond
        +g2>
        isa     image
        img     =letter"""

respond_toA = """
        =g>
        isa     read
        state   respond
        =g2>
        isa     image
        img     A
        ?manual>
        state   free
        ==>
        =g>
        isa     read
        state   start
        =g2>
        isa     image
        img     empty
        +manual>
        isa     _manual
        cmd     'press_key'
        key     A"""

respond_toC = """
        =g>
        isa     read
        state   respond
        =g2>
        isa     image
        img     C
        ?manual>
        state   free
        ==>
        =g>
        isa     read
        state   done
        =g2>
        isa     image
        img     empty
        +manual>
        isa     _manual
        cmd     'press_key'
        key     C"""

dontrespond_toB = """
        =g>
        isa     read
        state   respond
        =g2>
        isa     image
        img     B
        ?manual>
        state   free
        ==>
        =g>
        isa     read
        state   start"""

dontrespond_toA = """
        =g>
        isa     read
        state   respond
        =g2>
        isa     image
        img     A
        ?manual>
        state   free
        ==>
        =g>
        isa     read
        state   start"""

respond_toB = """
        =g>
        isa     read
        state   respond
        =g2>
        isa     image
        img     B
        ?manual>
        state   free
        ==>
        =g>
        isa     read
        state   done
        =g2>
        isa     image
        img     empty
        +manual>
        isa     _manual
        cmd     'press_key'
        key     B"""

class Agent:
    """
    Create agent for environment. Delay specifies delay in creating imaginal buffer (called g2 here).
    delay is used for illustration, just to differentiate between agents.
    """

    def __init__(self, environment, delay):

        self.agent = actr.ACTRModel(environment=environment, motor_prepared=True)
        self.agent.goal.add(actr.chunkstring(name="reading", string="""
        isa     read
        state   start"""))
        self.agent.productionstring(name="encode_letter", string=encode_letter)
        g2 = self.agent.set_goal("g2")
        g2.delay=delay

# 2 agents with different speed of encoding goals
magent1 = Agent(environ, delay=0.2).agent
magent2 = Agent(environ, delay=0.4).agent

# 2 agents differ in their rules - see the description at the start of this script
magent1.productionstring(name="respond_toA", string=respond_toA)
magent1.productionstring(name="respond_toC", string=respond_toC)
magent1.productionstring(name="dontrespond_toB", string=dontrespond_toB)
magent2.productionstring(name="dontrespond_toA", string=dontrespond_toA)
magent2.productionstring(name="respond_toB", string=respond_toB)

# We start the simulation for both agents
agent1_sim = magent1.simulation(realtime=False, environment_process=environproc, stimuli=text, triggers=stimuli, times=3)
agent2_sim = magent2.simulation(realtime=False, environment_process=environproc, stimuli=text, triggers=stimuli, times=3)

# this is used for internal updates of environment
old_stimulus1, old_stimulus2 = None, None

# The following loop runs the whole simulation

while True:

    # store current stimulus
    try:
        old_stimulus1 = agent1_sim._Simulation__env.stimulus.copy()
    except AttributeError:
        pass

    try:
        # do one simulation step
        agent1_sim.step()

        # print event
        print("AGENT1, ", agent1_sim.current_event)

        # update stimulus for the other agent if they changed due to the event agent 1 carried
        if old_stimulus1 and old_stimulus1 != agent1_sim._Simulation__env.stimulus:
            agent2_sim._Simulation__environment_activate.succeed(value=(agent1_sim._Simulation__env.trigger, agent1_sim._Simulation__pr.env_interaction))

    # if the schedule is empty, the agent has no rules to do and the action is stopped
    except simpy.core.EmptySchedule:
        break

    # switch to the other agent who should work as long as its internal time is smaller than agent 1 - the timing does not always show well in the trace because of internal time updates in pyactr, which are not always visible to the user
    while agent1_sim.show_time() > agent2_sim._Simulation__simulation.peek():

        # store stimuli /triggers
        try:
            old_stimulus2 = agent2_sim._Simulation__env.stimulus.copy()
        except AttributeError:
            pass

        try:
            # do one simulation step
            agent2_sim.step()

            # print event
            print("AGENT2, ", agent2_sim.current_event)

            # update stimulus for the other agent if they changed due to the event agent 2 carried
            if old_stimulus2 and old_stimulus2 != agent2_sim._Simulation__env.stimulus:
                agent1_sim._Simulation__environment_activate.succeed(value=(agent2_sim._Simulation__env.trigger, agent2_sim._Simulation__pr.env_interaction))
                break

        # if the schedule is empty, the agent has no rules to do and the action is stopped
        except simpy.core.EmptySchedule:
            break