###############################################################################
# Sequential Memory.py - Organize Memory into a Hierarchical Tree Structure
# (C) 2006 David Pierre Leibovitz
#
# All memory items are created sequentially within a context.
# 
#     MemItem
#        isa:   - item - a leaf item (can transmogrify to list?)
#               - list - a list item
#        nodeId: unique sequentially derived identifier number
#               To sequence time, simply find in order.
#               Mem pointers are to this.
#        nodeName: what this item is all about
#        contextId: parent node's nodeId
#        previousId: previous sibling id (cached in buffer, never in memory)
#
#   Focus
#       state
#           wait
#           ?
#       listId
#       lastId
#
# Add a lag1 prediction system:
#   context + current -> next
#
# This version handles duplicates such as the patter: A B A C A D
# So A cannot point back to FIRST, B & C as these chunks would simply get merged.
# Instead, each chunk has a unique ID to prevent merging.
# In fact, we will have to PREVIOUS ponters, one based on word name, and the other on chunk id.
# Eventually, I will try partial matching.
# What about FIRST and LAST? The options are
#   a) we will not search it based on IDs (IDs are unconscious, LAST is not)
#      But then we would have interference among lists? We could only ever
#   b) we will have a listID as well?
#
# A set of words is presented one at a time.
# Inbetween words, actr will rehearse the entire list.
# At the end, it will spit out the entire list.
# Accuracy is measured. It should be "U" shaped with
#    practice and recency effects.
# System must be able to recover from errors.
# Capabilities
# 1) U shaped practice/recency effects
# 2) Be able to say BLANK when uncertain
# 3) Recover from errors
# 4) Able to chunk, i.e., if 3-9-1 repeated across many zTrials, it should become faster
# 5) Able to detect patterns and make guesses on next item
#    - 1 2      -> 3 4
#    - 1 2 1 3  -> 1 4
#    - 2 4      -> 6 8
# 6) long-short word effects
# 7) Handle deletions (missing zPosition 4)
# It should be noted that the purpose of this model is to predict events - a requirement for survival,
# and it would be nice if serial recall abilities follow.
#
# Everything is a list
###############################################################################
"""
1) Implements class SerialRecallModel & SerialRecallEnvironment
2) Runs various serial recall (sequential m_memMod) ACTR based model experiments

Author: David Pierre Leibovitz (C) 2006
"""

# Variables here are considerred as CCM Exploring variables
parDecay        = 0.5
parNoise        = 0.3
parSpread       = True
parSpeech       = True                  # Turn on pretty speech - makes for longer real time in simulations (does not affect simulation time)
parThreshold    = -0.3

import ccm
from ccm.lib.actr import *
import random
from ccm.extras.speech import Speech

log = ccm.log()

class SequentialMemoryModule:
    rootId = "0"                        # nodeId for ROOT node
    m_memModLastId = {}                 # Dictionary to map a node into its last child.
                                        # This memory is not part of ACTR. It is assumed to be hardwired into the sequential memory
                                        # system. It is never requested! Only for linking a new child to its previous sibling!
                                        # Originally, this was passed in as a separate buffer, but it made little sense.
                                        # Assume, sequential memory is its own module with this being built in.
    m_memModIdGen = 0                   # Generator of unique identifiers. Originally passed in as a separate buffer, but should be considerred as
                                        # part of the sequential memory 
                                        # Each list item has a unique ID. The item 'A' can occur multiple time, so this serves to disambiguate.
                                        # ??? Not sure if I ever want to match on this.

    l1f1 = {}                           # Store l1f1 iterator information: (pathName, l1Name, l1Id, f1Name, f1Id)
    
    def newId(self):
        self.m_memModIdGen += 1
        return str(self.m_memModIdGen)

    def lastId(self,nodeId):            # Determine the last node identifier added in a context. ACTR will never do a memory lookup on this,
                                        # But it is usefull to known when we have reahearsed to the end.
        return str(m_memModLastId[int(nodeId)])

class SerialRecallModel:
    """Define the ACTR model (production rules) to handle a serial recall task"""

    env = DirectEnvironment()                                           # Enable this model to interact with an environment
    speech = Speech(voice='LHMICHAEL')                                  # Used for item recall
    innerSpeech = Speech(voice='LHMICHAEL', rate=10, volume=75, timeFactor=0.3) # Used for item rehearsal
    # A timeFactor of 0.2 allows 4 rehearsal
    
    sm = SequentialMemoryModule()

    # Main production system that adds non-predicted items into memory as requested
    # It coordinates all other production systems
    m_ps = Procedural(prefix="m")                                       # The main production system
    m_focus = Buffer(allow_slot_creation=True)                          # Create a (current focus) goal buffer
                                                                        # state:        production state machine
                                                                        # contextId:    current list's identifier
                                                                        # pathId:       current rehearsal iterator
                                                                        # previousId:   for replacing with BLANKS
    # state:wait    --  no production matches this; wait for a command from the environment (look at env buffer)

    m_memBuf = Buffer()                                                 # Create a m_memMod retrival buffer
    m_memMod = Memory(m_memBuf, threshold=parThreshold)                 # Associate retrieval buffer with a particular type of m_memMod system
    m_memBL = DMBaseLevel(m_memMod,decay=parDecay)                           # Enable activation decay and base level learning
    #m_memPM = DMPartial(m_memMod, p='nodeName:9 nodeId:4 previousId:4', default=1) # See PartialMap in pattern.py - ??? needs documentation
    m_memNS = DMNoise(m_memMod, noise=parNoise)

    def init():
        """Initialize the ACT-R model when it is first run(), and everytime it is reset()"""

        m_memMod.add('isa:list nodeId:0 nodeName:ROOT contextId:NONE previousId:NONE')   # Root node in tree
        sm.m_memModLastId[sm.rootId] = "NONE"

        m_focus.set('state:wait')                        # Wait for a command from the environment.
                                                                        # contextId: the current list's identifier.

        l1f1_focus.set('state:wait')                                    # Wait for main PS to tell me what to do
        l1f1_interface.set('state:wait')                                # l1f1 is ready to be commanded
        #l1f1_memMod.add('pathName:dummy l1Name:David l1Id:1 f1Name:Terry f1Id:2')

    ###############################################################################
    # Production State Space
    ###############################################################################

    #   m_focus.state:wait                - wait for a command from the environment. No production matches this!
    #   env.emCmd:newList               - get ready to memorize a new list, can interrupt rehearsal
    #   env.emCmd:newItem               - memorize a new item
    #       m_focus.state:newItemFindLast - find last item before appending new item after
    #       m_focus.state:rehearseStart   - start rehearing the list of items
    #       m_focus.state:rehearse        - rehearse a list item, find the next item
    #   env.emCmd:recall                - recall list of memorized items, can interrupt rehearsal
    #       m_focus.state:recallFindNext  - find first or next item in list

    def m_commandNewList(env            ='emCmd:newList',
                         m_focus        ='state:wait',
                         l1f1_interface ='state:wait'):
        """Start a new list. Note that this command can interrupt recall/rehearsal."""
        
        env.meNewListAck()                                              # Acknowledge command. Reset environmental command state.

        # Create a new empty list.
        nodeId = sm.newId()                                             # New list context
        prevId = sm.m_memModLastId[sm.rootId]
        rootId = sm.rootId
        m_memMod.add('isa:list nodeId:?nodeId nodeName:LIST contextId:?rootId previousId:?prevId')    # ??? should really do an addItem under root
        sm.m_memModLastId[sm.rootId] = nodeId
        sm.m_memModLastId[nodeId] = "NONE"

        # The list is the context for the items within it
        m_focus.modify('contextId:?nodeId')                     # Wait for the next command from the environment
        #m_focus.modify('state:rehearseStart contextId:?nodeId')          # Should I rehearse an empty list?

        env.log.newNode = {"name":"LIST", "id":nodeId, "context":rootId, "previous":prevId}

        # Use the list's identifier as the pathId for adding items via the prediction system
        l1f1_interface.set('state:addStart pathName:NEXT pathId:?nodeId l1Name:LIST l1Id:?nodeId')

    def m_commandNewItem(env            ='emCmd:newItem emItem:?nodeName',  # Interrupt rehearsal/recall, but only when ready
                         m_focus        ='state:wait contextId:?contextId',
                         l1f1_interface ='state:wait'):
        """Memorize a new list item at the end of my list. Note that this command can interrupt recall/rehearsal."""

        env.meNewItemAck()                                              # Acknowledge command. Reset environmental command state.

        nodeId = sm.newId()
        prevId = sm.m_memModLastId[contextId]
        m_memMod.add('isa:leaf nodeName:?nodeName nodeId:?nodeId contextId:?contextId previousId:?prevId')
        sm.m_memModLastId[contextId] = nodeId
        sm.m_memModLastId[nodeId] = "NONE"

        l1f1_interface.set('state:valueAdded pathId:?contextId chosenName:?nodeName chosenId:?nodeId')
        # Should do the add above in a secondary production after this prediction???

        m_focus.modify('state:rehearseStart')

        env.log.newNode = {"name":nodeName, "id":nodeId, "context":contextId, "previous":prevId}

    def m_rehearseStart(m_focus         ='state:rehearseStart contextId:?contextId',
                        l1f1_interface  ='state:wait'):
        """Start rehearsing the list of items - find the first item."""

        # Create a rehearsal identifier
        rehearseId = sm.newId()
        #prevId = sm.m_memModLastId[rootId]
        #m_memMod.add('isa:list nodeName:REHEARSE nodeId:?rehearseId contextId:?rootId previousId:?prevId')
        #sm.m_memModLastId[sm.rootId] = rehearseId
        
        # ??? contextId should be rootId, and we need a mirrodId

        # Find the first item in the list, the next item after after the list's nodeId, i.e., contextId
        l1f1_interface.set('state:predictStart pathName:NEXT pathId:?rehearseId l1Name:LIST l1Id:?contextId')
        m_memBuf.clear()
        m_memMod.request('contextId:?contextId previousId:NONE')        # Find the first item in the list. ??? Don't bother with ID

        m_focus.modify('state:rehearse pathId:?rehearseId')             # Rehearse the (first) item

        env.log.rehearse = {"name":"START", "id":contextId}

    def m_commandRecall(env             ='emCmd:recall',
                        m_focus         ='state:wait contextId:?contextId', # list identifier
                        l1f1_interface  ='state:wait'):
        """Recall the entire list of items - find the first item."""       

        env.meRecallAck()                                               # Acknowledge command. Reset environmental command state.

        # Create a rehearsal identifier
        rehearseId = sm.newId()
        #prevId = sm.m_memModLastId[rootId]
        #m_memMod.add('isa:list nodeName:RECALL nodeId:?rehearseId contextId:?rootId previousId:?prevId')
        #sm.m_memModLastId[sm.rootId] = rehearseId
        
        # Find the first item in the list, the next item after after the list's nodeId, i.e., contextId
        l1f1_interface.set('state:predictStart pathName:NEXT pathId:?rehearseId l1Name:LIST l1Id:?contextId')
        m_memBuf.clear()
        m_memMod.request('contextId:?contextId previousId:NONE')        # Find the first item in the list. ??? Don't bother with ID

        m_focus.modify('state:recall pathId:?rehearseId')               # Recall the (first) item

        env.log.recall = {"name":"START", "id":contextId}

    # Enhance this to decide between BLANKs and end of list?
    def m_rehearseNotFound(m_focus          ='state:rehearse',
                           l1f1_interface   ='state:wait',
                           l1f1_spread      ='nodeId:NONE',             # See m_rehearseInitialBlank
                           m_memMod         ='error:True'):
        """Could not find next item to rehearse. Assume end of list."""

        l1f1_interface.modify('state:predictAbandon')                   # Abandon my prediction requests

        #m_focus.modify('state:rehearseStart')                          # Rehearse again!
        m_focus.modify('state:wait')

        env.log.rehearse = {"name":"END", "id":"N/A"}

    # Enhance this to decide between BLANKs and end of list?
    def m_recallNotFound(m_focus            ='state:recall',
                         l1f1_interface     ='state:wait',
                         l1f1_spread        ='nodeId:NONE',             # see m_recallInitialBlank
                         m_memMod           ='error:True'):
        """Could not find next item to recall. End recall and wait for next command from environment."""

        #if not env.isSilent(): speech.sSay('Forgot where I am!')
        
        env.meRecallDone()                                              # Tell environment we are done. It can now check my responses.
        l1f1_interface.modify('state:predictAbandon')                   # Abandon my prediction requests
        m_focus.modify('state:wait')

        env.log.recall = {"name":"END", "id":"N/A"}

    def m_rehearseInterrupted(env               ='emCmd:?cmd!NONE',     # Enable environment to interrupt rehearsal
                              m_focus           ='state:rehearse',
                              l1f1_interface    ='state:wait'):

        l1f1_interface.modify('state:predictAbandon')                   # Abandon my prediction requests
        m_focus.modify('state:wait')

        env.log.rehearse = {"name":"INTERRUPTED", "id":cmd}
        
    def m_recallInterrupted(env                 ='emCmd:?cmd!NONE',
                            m_focus             ='state:recall',
                            l1f1_interface      ='state:wait'):                

        env.meRecallDone()                                              # Tell environment we are done. It can now check my responses.
        l1f1_interface.modify('state:predictAbandon')                   # Abandon my prediction requests
        m_focus.modify('state:wait')

        env.log.recall = {"name":"INTERRUPTED", "id":cmd}

    def m_rehearse(env              ='emCmd:NONE',                      # Enable Environment to interrupt rehearsal
                   m_focus          ='state:rehearse pathId:?pathId',
                   innerSpeech      ='busy:False',
                   m_memBuf         ='nodeName:?nodeName!BLANK nodeId:?nodeId',  # ??? Assume correct item and list IDs
                   l1f1_interface   ='state:wait'):                
        """Rehearse an item in my list, then find the next item."""

        env.log.rehearse = {"name":nodeName, "id":nodeId}

        silent = env.isSilent()
        innerSpeech.say(nodeName,silent=silent)
        
        m_memMod.add(m_memBuf)                                          # Increase activation value
        l1f1_interface.set('state:valueChosen pathId:?pathId chosenName:?nodeName chosenId:?nodeId')

        m_focus.modify('state:rehearseNext')

    def m_recall(env                ='emCmd:NONE',                      # Enable Environment to interrupt recall
                 m_focus            ='state:recall pathId:?pathId',
                 speech             ='busy:False',
                 m_memBuf           ='isa:leaf nodeName:?nodeName nodeId:?nodeId', # Assume correct item id and list id
                 l1f1_interface     ='state:wait'):                
        """Found the next item to recall. Recall it and find next item to recall."""

        #env.log.recall = (nodeName, nodeId)
        env.log.recall = {"name":nodeName, "id":nodeId}

        silent=env.isSilent()
        speech.say(nodeName, silent=silent)

        env.meRecallItem(nodeName)                                      # Tell environment what I have recalled so it can verify.
        l1f1_interface.set('state:valueChosen pathId:?pathId chosenName:?nodeName chosenId:?nodeId')

        m_focus.modify('state:recallNext')

    def m_rehearseBlank(env             ='emCmd:NONE',                  # Enable Environment to interrupt rehearsal
                        m_focus         ='state:rehearse pathId:?pathId',
                        m_memBuf        ='nodeName:BLANK nodeId:?nodeId',  # ??? Assume correct item and list IDs
                        l1f1_interface  ='state:wait'):                
        """Rehearse an item in my list, then find the next item."""

        env.log.rehearse = {"name":"BLANK", "id":nodeId}

        m_memMod.add(m_memBuf)                                          # Increase activation value
        l1f1_interface.set('state:valueChosen pathId:?pathId chosenName:BLANK chosenId:?nodeId')

        m_focus.modify('state:rehearseNext')

    # Enhance this to decide between BLANKs and end of list?
    def m_rehearseInitialBlank(m_focus          ='state:rehearse pathId:?pathId contextId:?contextId',
                               l1f1_interface   ='state:wait',
                               l1f1_spread      ='nodeId:?nodeId!NONE',
                               m_memMod         ='error:True'):
        """Could not find next item to rehearse, but predictor does. Assume BLANK, but do not say anything."""
        # This production can be seen as shifting of attention from something that is almost forgotten, to other matters ???

        nodeName="BLANK"

        env.log.rehearse = {"name":"BLANK0", "id":nodeId}
        
        m_memMod.add("isa:leaf contextId:?contextId nodeName:?nodeName nodeId:?nodeId previousId:UNKNOWN")   # ??? Get previousId from l1f1_memBuf?
        l1f1_interface.set('state:valueChosen pathId:?pathId chosenName:?nodeName chosenId:?nodeId')

        m_focus.modify('state:rehearseNextAfterBlank previousId:?nodeId')

    def m_recallInitialBlank(m_focus        ='state:recall pathId:?pathId contextId:?contextId',
                             l1f1_interface ='state:wait',
                             l1f1_spread    ='nodeId:?nodeId!NONE',
                             m_memMod       ='error:True'):
        """Could not find next item to recall, but predictor does. Assume BLANK, but do not say anything."""
        # This production can be seen as shifting of attention from something that is almost forgotten, to other matters ???

        env.log.recall = {"name":"BLANK0", "id":nodeId}

        nodeName="BLANK"
        
        silent=env.isSilent()
        speech.say(nodeName, silent=silent)

        env.meRecallItem(nodeName)                                       # Tell environment what I have recalled so it can verify.
        m_memMod.add("isa:leaf contextId:?contextId nodeName:?nodeName nodeId:?nodeId previousId:UNKNOWN")   # ??? Get previousId from l1f1_memBuf?
        l1f1_interface.set('state:valueChosen pathId:?pathId chosenName:?nodeName chosenId:?nodeId')

        m_focus.modify('state:recallNextAfterBlank previousId:?nodeId')

    def m_rehearseNext(m_focus          ='state:rehearseNext contextId:?contextId pathId:?pathId',
                       m_memBuf         ='nodeName:?nodeName nodeId:?nodeId',
                       l1f1_interface   ='state:wait'):

        l1f1_interface.modify('state:predictNext pathId:?pathId')
        m_memBuf.clear()
        m_memMod.request('isa:leaf previousId:?nodeId contextId:?contextId') # Find next item
        m_focus.modify('state:rehearse')                                # Rehearse next item
        
    def m_recallNext(m_focus            ='state:recallNext contextId:?contextId pathId:?pathId',
                     m_memBuf           ='nodeName:?nodeName nodeId:?nodeId',
                     l1f1_interface     ='state:wait'):

        l1f1_interface.modify('state:predictNext pathId:?pathId')
        m_memBuf.clear()
        m_memMod.request('isa:leaf previousId:?nodeId contextId:?contextId') # Find next item
        m_focus.modify('state:recall')                                  # Recall next item

    def m_rehearseNextAfterBlank(m_focus        ='state:rehearseNextAfterBlank contextId:?contextId pathId:?pathId previousId:?nodeId',
                                 l1f1_interface ='state:wait'):

        l1f1_interface.modify('state:predictNext pathId:?pathId')
        m_memBuf.clear()
        m_memMod.request('isa:leaf previousId:?nodeId contextId:?contextId') # Find next item
        m_focus.modify('state:rehearse')                                # Rehearse next item
        
    def m_recallNextAfterBlank(m_focus          ='state:recallNextAfterBlank contextId:?contextId pathId:?pathId previousId:?nodeId',
                               l1f1_interface   ='state:wait'):

        l1f1_interface.modify('state:predictNext pathId:?pathId')
        m_memBuf.clear()
        m_memMod.request('isa:leaf previousId:?nodeId contextId:?contextId') # Find next item
        m_focus.modify('state:recall')                                  # Recall next item

###############################################################################
# Predictor Model: Lag 1 Future 1 (l1f1)
#
# l1f1 Interface
#   0)  <-> state:wait
#           Wait for instructions. The spread activation buffers are clear.
#
#   1)  --> state:predictStart
#           Start a prediction vector and make the first prediction.
#
#       --> pathName:?
#           The name of this vector should be identical across similar runs.
#           Vectors are stored by this name only.
#
#       --> pathId:?
#           A unique identifier for this specific path instance - an iterator id.
#           Used for cacheing values only.
#           Allows for multiple simultaneous walks of this and/or other paths.
#
#       --> l1Name:? l1Id:?
#           Lag1 identifier input to predictor.
#           This uniquely identifies the starting point for all instances of a path.
#           For example, to walk over a list of items, this could be the list's identifier.
#
#       <-- state:wait
#           Prediction complete.
#
#       <-- f1Name:? f1Id:?
#           The predicted output if known.
#           This value is also put into the spread activation buffers.
#           The value NONE implies the end of the list, further iteration is not possible.
#           The value UNKNOWN implies an intermediate unknown value, e.g., for a lag2future2 predictor. Further iteration is possible.
#
#   2)  --> state:predictNext
#           Make the next prediction.
#
#       --> pathId:?            (as above)
#
#       <-- state:wait          (as above)
#
#       <-- f1Name:? f1Id:?              (as above)
#
#   3)  --> state:valueChosen
#           Tell the prediction system what was actually chosen.
#           Clear the spread activation buffers.
#
#       --> pathId:?            (as above)
#
#       --> chosenId:?
#           The actual value chosen, which could be different than predicted!
#
#       <-- state:wait          (as above)
#
#   4)  --> state:predictAbandon
#           Abandon a prediction run.
#           Clear the spread activation buffers.
#
#       --> pathId:?            (as above)
#
#       <-- state:wait          (as above)
#
###############################################################################

    l1f1_ps = Procedural(prefix="l1f1", delay=0.001)   # Lag1 Future1 Predictor production system; very fast
    l1f1_interface = Buffer()               # l1f1 interface to mind/model as described above
    l1f1_focus = Buffer()                   # l1f1 (current focus) goal buffer
                                            # 'state:predict pathId:? nodeId:?' -> 'state:prediction nodeId:?'
                                            # 'state:chose   pathId:? nodeId:?' -> 'state:wait'
                                            # 'state:abandon pathId:? nodeId:?' -> 'state:wait'
    l1f1_memBuf = Buffer()                  # l1f1 memory retrieval buffer
    l1f1_memMod = Memory(l1f1_memBuf, latency=0.001, threshold=-0.3)    # l1f1 m_memMod   ??? likely need differnt thresholds
    #l1f1_memBL = DMBaseLevel(l1f1_memMod, decay=0.5)   # Enable activation decay and base level learning
    l1f1_spread = Buffer()                  # Hold's the prediced node info for spread activation
    if parSpread: m_memSP = DMSpreading(m_memMod, l1f1_spread)
    else:
        class m_memSP: pass

    ###############################################################################
    # l1f1_interface (external interface)
    #   --> state:predictStart
    #       Start a prediction run and make the first prediction.
    #
    #   --> pathName:?
    #       The name of this path should be identical across similar runs.
    #       Path elements are memorized using this name only.
    #
    #   --> pathId:?
    #       A unique identifier for this specific path instance - an iterator id. Use sm.newId().
    #       Used for cacheing values only.
    #       Allows for multiple simultaneous walks of this and/or other paths.
    #
    #   --> l1Name:? l1Id:?
    #       Lag1 identifier input to predictor.
    #       This uniquely identifies the starting point for all instances of a path.
    #       For example, to walk over a list of items, this could be the list's identifier.
    #
    #   <-- state:wait
    #       Prediction complete. Predicted values in spread activation buffers. At this point:
    #       1) --> state:predictAbandon
    #       2) --> state:valueChosen
    #
    # l1f1_spread (external interface)
    #   <-- f1Name:? f1Id:?
    #       The predicted output. If not known, it will be NONE.
    #
    # l1f1_focus (internal interface)
    #   1)  --> state:wait
    #           Ready to make prediction. Validate cache.
    #
    #   2)  --> state:cacheOk
    #           Cache validated. Find the first prediction.
    #
    #   3)  --> state:findPrediction
    #           Find the prediction
    #
    #   4)  --> state:checkPrediction
    #           Check predicted value
    #
    #   5)  <-- state:wait
    ###############################################################################

    def l1f1_predictStart(l1f1_interface                ='state:predictStart pathName:?pathName pathId:?pathId l1Name:?l1Name l1Id:?l1Id',
                          l1f1_focus                    ='state:wait'):
        """Start a prediction run and make the initial prediction."""
        l1f1_spread.clear()
        f1Name = "UNNOWN"
        f1Id = "UNKNOWN"
        sm.l1f1[pathId] = (pathName, l1Name, l1Id, f1Name, f1Id)
        l1f1_focus.set('state:findPrediction')

    def l1f1_addStart(l1f1_interface                    ='state:addStart pathName:?pathName pathId:?pathId l1Name:?l1Name l1Id:?l1Id',
                      l1f1_focus                        ='state:wait'):
        """Start an adding run."""
        l1f1_spread.clear()
        f1Name = "UNNOWN"
        f1Id = "UNKNOWN"
        sm.l1f1[pathId] = (pathName, l1Name, l1Id, f1Name, f1Id)
        l1f1_focus.set('state:wait')
        l1f1_interface.modify('state:wait')

    ###############################################################################
    #   2)  --> state:predictNext
    #           Make the next prediction.
    #
    #       --> pathId:?            (as above)
    #
    #       <-- state:wait          (as above)
    #
    #       <-- f1Name:? f1Id:?              (as above)
    ###############################################################################

    def l1f1_predictNext(l1f1_interface                 ='state:predictNext pathId:?pathId',
                         l1f1_focus                     ='state:wait'):
        l1f1_spread.clear()
        pathName,l1Name,l1Id,f1Name,f1Id = sm.l1f1[pathId]
        l1Name = f1Name     # Shift
        l1Id = f1Id         # Shift
        f1Name = "UNKNOWN"
        f1Id = "UNKNOWN"
        sm.l1f1[pathId] = (pathName, l1Name, l1Id, f1Name, f1Id)
        l1f1_focus.set('state:findPrediction')

    ###############################################################################
    #   3)  --> state:valueChosen
    #           Tell the prediction system what was actually chosen (not NONEs or UNKNOWNs).
    #           Clear the spread activation buffers.
    #
    #       --> pathId:?            (as above)
    #
    #       --> chosenName:? chosenId:?
    #           The actual value chosen, which could be different than predicted!
    #
    #       <-- state:wait          (as above)
    #
    ###############################################################################

    def l1f1_valueChosen(l1f1_interface                 ='state:valueChosen pathId:?pathId chosenName:?chosenName chosenId:?chosenId',
                         l1f1_focus                     ='state:wait'):
        """Inform the prediction system of the value chosen"""
        l1f1_spread.clear()
        pathName,l1Name,l1Id,f1Name,f1Id = sm.l1f1[pathId]
        f1Name = chosenName
        f1Id = chosenId
        l1f1_memMod.add('pathName:?pathName l1Name:?l1Name l1Id:?l1Id f1Name:?f1Name f1Id:?f1Id')
        # If choice differs from prediction, this is likely unrecoverable.
        # It would be recoverable with an f2 predictor.

        sm.l1f1[pathId] = (pathName, l1Name, l1Id, f1Name, f1Id)
        l1f1_focus.set('state:wait')
        l1f1_interface.modify('state:wait')

    def l1f1_valueAdded(l1f1_interface                  ='state:valueAdded pathId:?pathId chosenName:?chosenName chosenId:?chosenId',
                         l1f1_focus                     ='state:wait'):
        """Inform the prediction system of the value chosen"""
        l1f1_spread.clear()
        pathName,l1Name,l1Id,f1Name,f1Id = sm.l1f1[pathId]
        f1Name = chosenName
        f1Id = chosenId
        l1f1_memMod.add('pathName:?pathName l1Name:?l1Name l1Id:?l1Id f1Name:?f1Name f1Id:?f1Id')
        l1Name = f1Name                                 # Shift values and get ready for next add
        l1Id = f1Id
        f1Name = "UNKNOWN"
        f1Id = "UNKNOWN"
        sm.l1f1[pathId] = (pathName, l1Name, l1Id, f1Name, f1Id)
        l1f1_focus.set('state:wait')
        l1f1_interface.modify('state:wait')

    ###############################################################################
    #   4)  --> state:predictAbandon
    #           Abandon a prediction run.
    #           Clear the spread activation buffers.
    #
    #       --> pathId:?            (as above)
    #
    #       <-- state:wait          (as above)
    #
    ###############################################################################

    def l1f1_predictAbandon(l1f1_interface  ='state:predictAbandon pathId:?pathId',
                            l1f1_focus      ='state:wait'):
        """Abandon a prediction run. Clear the spread activation buffers."""
        l1f1_spread.clear()
        del sm.l1f1[pathId]
        l1f1_focus.set('state:wait')
        l1f1_interface.modify('state:wait')

    ###############################################################################
    # Finding Predictions
    #
    # ????l1f1_interface
    #   1)  --> state:findPrediction
    #       <-- state:wait
    ###############################################################################

    def l1f1_findPrediction(l1f1_focus      ='state:findPrediction',
                            l1f1_interface  ='pathId:?pathId'):
        pathName,l1Name,l1Id,f1Name,f1Id = sm.l1f1[pathId]
        l1f1_memBuf.clear()
        l1f1_memMod.request('pathName:?pathName l1Name:?l1Name l1Id:?l1Id') # Get prediction.
        l1f1_focus.set('state:checkPrediction')

    def l1f1_checkPrediction_notFound(l1f1_focus        ='state:checkPrediction',
                                      l1f1_memMod       ='error:True',
                                      l1f1_interface    ='pathId:?pathId'):
        """Prediction not found. Assume NONE. Spread this around."""
        pathName,l1Name,l1Id,f1Name,f1Id = sm.l1f1[pathId]
        f1Name = "NONE"
        f1Id = "NONE"
        sm.l1f1[pathId] = (pathName, l1Name, l1Id, f1Name, f1Id)

        l1f1_spread.set('nodeName:NONE nodeId:NONE')
        m_memSP.strength=0.2
        l1f1_focus.set('state:wait')
        l1f1_interface.modify('state:wait')

    def l1f1_checkPrediction_found(l1f1_focus       ='state:checkPrediction',
                                   l1f1_memBuf      ='f1Name:?f1Name f1Id:?f1Id',
                                   l1f1_interface   ='pathId:?pathId'):
        """Prediction found. Spread it around."""
        pathName,l1Name,l1Id,f1NameOld,f1IdOld = sm.l1f1[pathId]
        sm.l1f1[pathId] = (pathName, l1Name, l1Id, f1Name, f1Id)
        l1f1_spread.set('nodeName:?f1Name nodeId:?f1Id')
        m_memSP.strength=1
        l1f1_focus.set('state:wait')
        l1f1_interface.modify('state:wait')

###############################################################################
# The environment for the experiment
###############################################################################

class SerialRecallEnvironment(ccm.Environment):
    """The environment for running serial recall experiments"""

    # Varables prefixed by "z" are for nice looking logs!

    zTrials = 20                                                         # Maximum number of zTrials
    zTrial = 0                                                           # Trial number (1...zTrials)
    
    zPositions = 6                                                       # Maximum number of word zPositions
    zPosition = 0                                                        # Serial recall word zPosition (0...zPositions-1)
    scoresCorrect  = [0, 0, 0, 0, 0, 0]                                 # Running total score at a given serial zPosition [zPosition-1]
    scoresBlank    = [0, 0, 0, 0, 0, 0]                                 # Running total BLANK score at a given zPosition
    scoresExtra    = 0                                                  # Too many words in recalled list
    scoresMissing  = 0                                                  # Too few words in recalled list
    zRecallDoneInvoked = True                                            # To ensure mind invoked recallDone() method
    
    words = ["David", "Jo", "Nikita", "Keara", "Terry", "Andrea", "Clara", "Sylvain"]
                                                                        # Stimuli set. Handle duplicates later???

    emCmd = "NONE"                                                      # environment -> mind: command to execute (newList, newItem, recall)
    emItem = ""                                                         # environment -> mind: new item word to memorize
    zSilenced = False                                                    # Have we been zSilenced yet? True or False?

    def init(self):
        self.experimentor = Speech(voice='LHMICHELLE')

    def isSilent(self):
        """Determine if speech should be zSilenced."""
        silent = True
        firstLastSpeech = 0
        if (parSpeech): firstLastSpeech = 1
        if self.zTrial<=firstLastSpeech or (self.zTrials - self.zTrial)<firstLastSpeech:
            silent = False
        elif not self.zSilenced:
            self.zSilenced = True
            self.experimentor.sSay("Intermediate speech supressed.")
            
        return silent

    ###############################################################################
    # These methods interface with the modelled mind.
    ###############################################################################

    def emNewList(self):
        """environment -> mind: start a new serial recall zTrial"""
        if not self.zRecallDoneInvoked:
            env.log.emCmdIncomplete = "Previous Recall Incomplete!"
            self.meRecallDone()                                         # update scores & totals
        self.zTrial += 1                                                 # increment zTrial number
        self.zPosition = 0                                               # reset serial recall word item zPosition
        random.shuffle(self.words)                                      # get a random list of words
        #log.newTrial = (self.zTrial, self.words)
        self.emCmd = 'newList'

    def meNewListAck(self):
        """mind -> environment: acknowledge reception of 'newList' command"""
        self.emCmd = 'NONE'

    def emNewItem(self):
        """environment -> mind: memorize a new word item"""
        if self.zPosition < self.zPositions:
            self.emCmd = "newItem"
            self.emItem = self.words[self.zPosition]
        else:
            print "Too many items!!!"
        self.zPosition += 1                                              # increment word/item zPosition number
        
    def meNewItemAck(self):
        """mind -> environment: acknowledge reception of 'newItem' command"""
        self.emCmd = 'NONE'
        self.emItem = ''

    def emRecall(self):
        """environment -> mind: recall the list of word items"""
        self.emCmd = 'recall'
        self.zRecallDoneInvoked = False                                  # to ensure mind invoked recallDone()

    def meRecallAck(self):
        """mind -> environment: acknowledge reception of the 'recall' command"""
        self.emCmd = 'NONE'
        self.zPosition = 0                                               # reset serial recall word/item zPosition

        if not self.isSilent():
            self.experimentor.sSay("Recalling zTrial #"+str(self.zTrial))

    def meRecallItem(self, word):                                       # How to handle BLANKs???
        """mind -> environment: indicate recalled word item"""

        silent = self.isSilent()

        if self.zPosition < self.zPositions:                              # Calculate score
            score = 0                                                   # Assume incorrect word
            if word==self.words[self.zPosition]: score = 1
            if word=="BLANK": self.scoresBlank[self.zPosition] += 1
            self.scoresCorrect[self.zPosition] += score
            log.zRecallWord = (self.zTrial, self.zPosition, word, score)
            if not silent:                                              # These ar not part of the experiment, times not counted
                if score:   self.experimentor.sSay('Correct')
                else:       self.experimentor.sSay('Wrong')
        self.zPosition += 1                                              # increment word item zPosition number

    def meRecallDone(self):
        """mind -> environment: indicate no more words to be recalled"""

        if not self.zRecallDoneInvoked:
            if self.zPosition > self.zPositions:
                self.scoresExtra += 1
            if self.zPosition < self.zPositions:
                self.scoresMissing += 1
                while self.zPosition < self.zPosition:
                    self.meRecallItem("BLANK2")                   # Update scores on missing words
                    
        log.zRecallWord = (self.zTrial, self.zPosition, "DONE", 0)
        self.zRecallDoneInvoked = True

    def emDone(self):                                                   # No ack
        """environment -> mind: done experiment; stop last recall"""
        self.emCmd = 'done'

    ###############################################################################
    # These methods run the experiment.
    ###############################################################################

    def start(self):                                                    # invoked automatically by environment.run()
        """Start the serial recall zTrials"""

        # Introduce the voices.
        if parSpeech:
            env.experimentor.sSay("Hello, I'm the experimentor.")
            env.agent.speech.sSay("Hi, I'm the subject,")
            env.agent.innerSpeech.sSay("and this is my inner voice.")

            env.experimentor.sSay('Are you ready to start this Serial Recall experiment?')
            env.agent.speech.sSay('Yes')
        
        yield self.doTrials()                                           # Propagate yields all the way up
        self.emDone()
        yield 1                                                         # Allow 1 second of rehearsal
        self.displayResults()
        self.stop()

    def doTrials(self):
        """Run a set of serial recall zTrials"""
        while self.zTrial < self.zTrials:
            yield self.doTrial()                                        # Propagate yields all the way up

    def doTrial(self):
        """Run a serial recall zTrial"""
        self.emNewList()
        yield 1                                                         # Wait 1 second after starting a new zTrial/list to match experiment
        
        while self.zPosition < self.zPositions:
            yield self.doWord()                                         # Propagate yields all the way up

        self.emRecall()                                                 # Ask the mind to recall the words.
        yield 13                                                        # Allow 15 seconds to recall entire list before starting another

    def doWord(self):
        """Memorize a word item"""
        self.emNewItem()
        yield 1                                                         # Give the mind 1 second for rehearsal

    def displayResults(self):
        """Log the results"""
        log.scoresCorrect = self.scoresCorrect
        log.scoresBlank   = self.scoresBlank
        log.scoresExtra   = self.scoresExtra
        log.scoresMissing = self.scoresMissing        
        
###############################################################################
# Run the ACT-R model
###############################################################################
if __name__=='__main__':
    mind=ACTR(SerialRecallModel)#, log=log)                             # Create an ACTR based mind
    #mind.m_memMod.log               = log.m_memMod
    #mind.m_memMod.log_activation    = log.m_memMod
    #mind.l1f1_memMod.log            = log.l1f1_memMod
    #mind.l1f1_memMod.log_activation = log.l1f1_memMod

    mind.log.setLevel(None)                                             # Turn of time stamps for productions
    env=SerialRecallEnvironment(log=log)                                # Create the environment
    env.agent=mind

    try:
        env.run()                                                       # Run the experiment, invoke start()
    except KeyboardInterrupt:                                           # Should be part of Environment???
        env.log.keyboard = "Interrupted"
        env.experimentor.sSay('Keyboard interupt!')
        env.displayResults()
        env.stop()
    env.experimentor.sSay('Experiment done. Thank-you!')
    env.agent.speech.sSay('Bye-bye.')

