You are here

Developing for jACT-R: Extensions

Developing for jACT-R : Extensions

Sometimes a model needs to interact with a device or system, but a full perceptual interface would be overkill. This is particularly true when the theoretical aspects of the model have nothing to do with embodied interaction. Sometimes you just need to model to have information dumped straight into its head. That's where extensions come in. They provide atheoretic additional functionality.

Overview

Here's the hypothetical situation: The model needs to interface with a networked service and track some variables that will change over time. The extension will provide a buffer tracker containing a chunk of type track-data that contains three slots: time (the variable being tracked), changed (boolean flagging whether the data is new) and requested-at (holds the simulated time that the request was made at).

Module

Because buffers are theoretical model components, an extension cannot (easily) contribute a new buffer. Modules are the default contributor of buffers. First we will create a TrackerModule extending AbstractModule, which will return a BasicBuffer6 from the createBuffers protected method. We'll also ensure that there is a track-data chunk in the buffer via the initialize method.


 

  protected Collection<IActivationBuffer> createBuffers()
  {
    return Collections.singleton((IActivationBuffer) new BasicBuffer6(
        "tracker", this));
  }

  @Override
  public void initialize()
  {
    /*
     * let's make sure that we've got a tracker-data chunk in the buffer
     */
    IActivationBuffer buffer = getModel().getActivationBuffer("tracker");
    try
    {
      IDeclarativeModule decM = getModel().getDeclarativeModule();
      IChunkType chunkType = decM.getChunkType("track-data").get();
      IChunk chunk = decM.createChunk(chunkType, null).get();
      buffer.addSourceChunk(chunk);
    }
    catch (Exception e)
    {
      LOGGER.error("Could not find or create track-data chunk : ", e);
    }
  }


Contributing Chunk-types

So that any model using the module and extension are aware of the buffer and chunk-type, we provide a model snippet (tracker.jactr) that is automatically injected into other models. This is accomplished by providing an IASTParticipant (usually just an extension of BasicASTParticipant), that is registered using Eclipse's extension mechanism (which will be discussed later).

public class TrackerModuleParticipant extends BasicASTParticipant{
  public TrackerModuleParticipant()
  {
    super("org/jactr/examples/tracker/io/tracker.jactr");
    setInstallableClass(TrackerModule.class);
  }
}

MockInterface

The mock network interface provides long running methods to open and close the connection, as well as methods to request new data and to process the results. Because all of these operations are long running (merely calling Thread.sleep()), calling them from the model thread would result in a significant performance penalty. Finally, the interface provides a method hasNewData() which is used to signal whether the data processing method can be called.

TrackerExtension

Finally, we have the extension. This is the code that utilizes the simulated network interface, performs the data requests and processing, and massages the results into the buffer provided by the module. Ideally, we'd connect to the network just before the model starts running, disconnect after termination, and update the buffer contents at the top of each production cycle. This can all be accomplished by using the model listener with an inline executor (discussed previously).

Threading and Performance

As was mentioned in the previous article, when using event listeners you should consider the performance characteristics of your code. If your code is fast and light, or you absolutely need to be notified of an event immediately, you can use the inline executor. This ensures that the thread that fired the event also fires your listener. However, expensive code should usually be processed on a separate executor (possibly the shared background or periodic threads) so that it doesn't get in the way of normal model execution. There is one other consideration when using a separate executor, and that is the frequency of the event. If you've got slow code that fires very frequently (e.g. blocking IO called at the top of each production cycle), it will never be able to keep up with the model. Events will be rapidly queued onto the executor faster than they can be cleared off.

In this particular example, we have very expensive connection management and processing. Performing these operation inline with the model would bring it to a screeching halt (e.g. 100ms blocking IO at the top of each production cycle). However, using a separate executor to process the events would also cause a problem. The executor simply wouldn't be able to handle the flood of events in a timely manner. Instead, we'll use a combination of strategies. The model event listener (listed below) will handle the events inline with the model thread. However, all the actual lengthy processing will be handled by a dedicated executor. Because we only update the buffer if there is new data, the majority of the cycleStarted() methods do nothing.

  public TrackerExtension()
  {
    _modelListener = new ModelListenerAdaptor() {

      /**
       * at each cycle, check to see if the network interface has new data for
       * us to process. If so, execute the processing on the dedicated executor.
       */
      public void cycleStarted(ModelEvent me)
      {
        if (_networkInterface.hasNewData() && !_dataProcessingQueued)
        {
          if (LOGGER.isDebugEnabled())
            LOGGER.debug(String.format("new data is available"));

          _dataProcessingQueued = true;
          _networkExecutor.execute(new Runnable() {
            public void run()
            {
              if (LOGGER.isDebugEnabled())
                LOGGER.debug(String.format("processing data"));

              // lengthy operation
              processData();

              //reset the flag
              _dataProcessingQueued = false;
            }
          });
        }
      }

      /**
       * called on startup, this is our chance to actually connect to the
       * network interface. Again, we do it in a dedicated connection
       */
      public void modelConnected(final ModelEvent me)
      {
        _networkExecutor.execute(new Runnable() {
          public void run()
          {
            try
            {
              if (!_networkInterface.isConnected())
              {
                _networkInterface.connect(_connectionString);
                // make initial request
                _networkInterface.requestUpdate(me.getSimulationTime());
              }
            }
            catch (Exception e)
            {
              // do something
            }
          }
        });
      }

      /**
       * and disconnect on the network thread
       */
      public void modelDisconnected(ModelEvent me)
      {
        _networkExecutor.execute(new Runnable() {
          public void run()
          {
            try
            {
              if (_networkInterface.isConnected())
                _networkInterface.disconnect();
            }
            catch (Exception e)
            {
              // do something smart
            }
            finally
            {
              _networkInterface = null;
            }
          }
        });
      }
    };
  }

  public void install(IModel model)
  {
    _model = model;
    /*
     * first test to see if the tracker module is installed
     */
    TrackerModule tm = (TrackerModule) _model.getModule(TrackerModule.class);
    if (tm == null)
      throw new IllegalExtensionStateException(
          "TrackerModule must be installed");


    _trackerBuffer = _model.getActivationBuffer("tracker");

    /*
     * since network based code often is a massive bottleneck, we want to put it
     * on its own thread. We'll use this executor
     */
    _networkExecutor = Executors.newSingleThreadExecutor();

    /*
     * but we'll queue up processing on the executor based on the model events,
     * which we will handle inline with the model
     */
    _model.addListener(_modelListener, ExecutorServices.INLINE_EXECUTOR);
  }

ChunkUtilities

One thing to notice is that the manipulation of the trace-data chunk in the tracker buffer is being delegated through ChunkUtilities. There are a few caveats when manipulating chunks. If you've just created the chunk and no one has access to it, you can just manipulate the slots returned from IChunk.getSymbolicChunk().getSlots() after casting each slot to an IMutableSlot. If the chunk has made it into declarative memory, it will be marked as encoded and immutable, any attempts to change the values will throw an exception. However, if the chunk is in a buffer it won't be encoded (unless it's the retrieval buffer) and can be manipulated. But to do so, you need to make sure that it is locked to prevent other access. To further ensure safety, it is recommended that you perform the manipulation on the model thread itself. ChunkUtilities will invoke your ChunkUtilities.IChunkModifier on the model thread within the write lock as soon as possible. This is a convenience method for those that don't want to deal with the nitty-gritty details.

    /**
   * hypothetical processing of data.. We take the data and then (safely) modify
   * the chunk that is in the buffer to match
   */
  private void processData()
  {
    final Map<String, Object> data = _networkInterface.getNewData();

    /*
     * to safely update the buffer contents, we must do it on the model thread.
     * we could cache the data locally and use an additional, inline model
     * listener and do the work in cycleStarted(), or use a timed event.
     * ChunkUtilities.modifyLater does the later
     */

    ChunkUtilities.manipulateChunkLater(_trackerBuffer,
        new ChunkUtilities.IChunkModifier() {

          public void modify(IChunk chunk, IActivationBuffer buffer)
          {
            if (LOGGER.isDebugEnabled())
              LOGGER.debug(String.format("Updating buffer"));

            /*
             * we are assuming that the fields in data match 1-1 to the slots of
             * the track-data chunk
             */
            ISymbolicChunk sc = chunk.getSymbolicChunk();
            for (Map.Entry<String, Object> entry : data.entrySet())
            {
              IMutableSlot slot = (IMutableSlot) sc.getSlot(entry.getKey());
              if (slot == null) continue;
              slot.setValue(entry.getValue());
            }
          }
        });

    /*
     * after we've processed the data, let's request an update
     */
    try
    {
      if (LOGGER.isDebugEnabled())
        LOGGER.debug(String.format("requesting update"));


      _networkInterface.requestUpdate(getModel().getAge());
    }
    catch (IOException e)
    {
      // handle this gracefully
    }
  }

Running the Model

The model for this example is incredibly simple. First notice that it installs both the TrackerModule and the TrackerExtension. The TrackerModule, via the TrackerModuleParticipant, injects the chunk-type and buffer into the model. Once the model is built, the TrackerExtension is initialized and the model can start. Two productions will fire: echo-old when there is old data available, and echo-new when there is new data in the buffer. The model will run for ever, so terminate it after you get a feel for what is going on. In the screenshot below you can see what is happening. At 61.95 (simulated) the model makes a request for new data. It gets the response at 74.95 (simulated), but in realtime, only 519ms have actually elapsed. The simulation is able to plow forward in time with no problem, without saturating the network with spurious requests.

200912291044.jpg

Java-specific Nuisances

As we've seen in the past two articles, the META-INF/MANIFEST.MF file needs to be tweaked. First, we export the org.jactr.examples.tracker packages so that they are visible to the core runtime. If you do not do this, you will be plagued by class not found exceptions. Next we provide three Eclipse extensions that allow the runtime to detect the module, extension, and ASTParticipant. For each extension you'll basically be providing a name and class, plus a view extra bits of information. Without this, your model will have compilation errors as the module and extension won't be visible (to your model or anyone else's). Without the ASTParticipant extension defined, your model won't correctly import the injected content.

200912291050.jpg

References