You are here

Developing for jACT-R: Instrumentation

Developing for jACT-R: Instrumentation

One of the key benefits of modeling is that it allows us to instrument and inspect internal states that are not otherwise accessible in the real world. While most psychological measures are behavioral, sometimes we want to record data from deeper within the model. The IInstrument interface allows us to do just that.

Instruments

Instruments are the bits of code that inspect the state of the model. They are intended to be light-weight and not influence the execution of the model in anyway. A prime example of an instrument is the Model Recorder which can be used to save a copy of the model pre- and post-execution. These are the tools that are accessible in the run configuration's Instruments tab:

200910221415.jpg

This example will walk you through the implementation of a simple instrument that tracks the average interval between two specified production firings.

Events and Listeners

Before diving into the details of the example instrument, it is worth discussing how most instruments (and most jACT-R code in general) are implemented. Where the canonical Lisp ACT-R implementation relies upon hooks to extend behavior, jACT-R uses events and registered listeners. Virtually every theoretically and computationally relevant element in jACT-R fires off events to interested listeners. A partial sampling includes:

Most of the event sources support both synchronous and asynchronous event notification by requiring you to specify an Executor for each listener. This basically allows you to control what thread the event will be processed on. If you need to receive the event immediately and on the same thread that issued the event, you can use ExecutorServices.INLINE_EXECUTOR. However, instruments should not do this unless absolutely necessary. Synchronous event handling runs the risk of negatively impacting model execution speed (and in some cases, logical flow). Instead, instruments should use a separate executor. By default the runtime provides a background executor which is a low-priority, shared thread (ExecutorServices.getExecutor(ExecutorServices.BACKGROUND)).

 

ProductionIntervalTimer

 

The example starts by implementing two interfaces: IInstrument and IParameterized. The first marks it as an instrument and has a contract for three methods: install, uninstall, and initialize. The second allows the instrument to be customized at runtime using named parameter values. The most important piece here is the install method in which we attach a listener to the procedural module. One thing to note: if a runtime includes multiple models, only one instrument will be instantiated and its install & uninstall methods will be called for each model.


  public ProductionIntervalTimer()
  {
    /*
     * when the procedural module fires events, this will be called. We're only
     * interested in the productionFired event, all the others are Noops.
     */
    _proceduralListener = new ProceduralModuleListenerAdaptor() {
      public void productionFired(ProceduralModuleEvent pme)
      {
        /*
         * pass on the production (actually the IInstantiation) and
         * the simulated time that the event occured at.
         */
        checkProduction(pme.getProduction(), pme.getSimulationTime());
      }
    };
  }

  /**
   * called when the instrument is installed, this is the time to attach any
   * event listeners
   */
  public void install(IModel model)
  {
    /*
     * When the procedural module does something important, it fires off an
     * event. We can register a listener for that event quite simply. But we
     * need to consider how that even will be delivered. It can come
     * synchronously (on the model thread as the event occurs) or asynchronously
     * (to be delievered later). Since this is an instrument, which should not
     * effect model execution, we will attach asynchronously
     */
    IProceduralModule procMod = model.getProceduralModule();


    /*
     * the background executor is a low priority shared thread that is often
     * used for listeners. If we wanted to listen synchronously we could use
     * ExecutorServices.INLINE_EXECUTOR
     */
    procMod.addListener(_proceduralListener, ExecutorServices
        .getExecutor(ExecutorServices.BACKGROUND));
  }

When a production is fired, the listener is asynchronously notified on the background thread and checks to see if the production is the start or stop production and handles the timer logic accordingly.


  /**
   * test the production that fired to see if it start or stop and adjust the
   * clock accordingly
   * 
   * @param firedProduction
   * @param firingTime
   */
  private void checkProduction(IProduction firedProduction, double firingTime)
  {
    // timer hasn't been started, check for start
    if (Double.isNaN(_startTime))
    {
      if (_startProductionName.matcher(
          firedProduction.getSymbolicProduction().getName()).matches())
        _startTime = firingTime;
    }
    else
    {
      //timer has started
      if (_stopProductionName.matcher(
          firedProduction.getSymbolicProduction().getName()).matches())
      {
        _sampleTimes.addValue(firingTime - _startTime);
        _startTime = Double.NaN;
      }
    }
  }

When the model execution completes, uninstall is called, so we use that opportunity to dump the statistics.


  /**
   * after the model has run and before it is cleaned up, instruments are
   * uninstalled. we'll output the result here
   */
  public void uninstall(IModel model)
  {
    /*
     * detach the listener
     */
    model.getProceduralModule().removeListener(_proceduralListener);


    System.out.println(String.format(
        "Average time between productions : %.2f (%.2f)", _sampleTimes
            .getMean(), _sampleTimes.getStandardDeviation()));
  }

The start and stop production name regular expression patterns are defined using the setParameter method defined by the IParameterized interface.


  /**
   * we set the start/stop regex patterns here
   */
  public void setParameter(String key, String value)
  {
    if(START_PATTERN.equalsIgnoreCase(key))
      _startProductionName = Pattern.compile(value);
    else if(STOP_PATTERN.equalsIgnoreCase(key))
      _stopProductionName = Pattern.compile(value);
  }

Running the Model

Included in this example is the unit 1 semantic model. You'll want to create a run configuration for it and enable the Production Interval Timer instrument (as seen in the image at the top of the article). Run the model and in the console you should see the output of the instrument.

Java-specific Nuisances

As we saw in the prior article, there needs to be some configuration of the META-INF/MANIFEST.MF file, which manages all the meta information for the project. As with the last example, we need to make sure that the instrument's package is exported to the runtime. That will allow the runtime to see, create and install the instrument. However, that won't tell the IDE about the existence of your instrument.

In order to make your instrument visible to the IDE, you need to provide an Eclipse extension (not to be confused with a jACT-R extension). From the manifest file, you can select the Extensions tab, click on add and select org.jactr.instruments. From there you will specify the name and class of the instrument, a description, and provide default parameter names and values. Once this is done, the IDE will be able to see your instrument and it will be accessible in the run configuration's Instruments tab.

200910221515.jpg
 
 

References