Part 2: Adding a Sensor in ASTRA

This guide is the second part in a series of guides on how to link an agent to an environment. Typically, this is implemented through sensing (perceiving the state of the environment) and acting (effecting the environment). Support for creating sensors and actions in ASTRA is provided through the notion of a module. The first part of this series dealt with how to create custom actions in ASTRA. This part focuses on how to sense the environment using sensors.

Generally, the use of sensors is not recommended in ASTRA. Sensors can be quite inefficient because they typically poll the environment for changes. This requires that each agent with a sensor be constantly active. As an alternative, ASTRA also includes a custom event model that, when coupled with support for custom terms and formulae, makes sensors redundant.

What you’ll build

We will adapt the simple light switch program that was developed in part 1. The completed source code for this prior version of the code can be downloaded from the ASTRA Gitlab Repository.

The code implements a module that models a light switch and provides the ability for the agent to turn the light switch on or off as required. The actual behaviour shown here will be the agent flashing the light 3 times (turning it on / off repeatedly 3 times).

What you’ll need

  • About 15 minutes
  • A favorite text editor or IDE
  • JDK 1.8 or later
  • Maven 3.3+

Creating a project from scratch

In a project directory of your choosing, create the following directory structure (for example, on *nix systems, type mkdir -p src/main/astra):

|-- src
|    |-- main
|          |-- astra
|-- pom.xml

Now create the maven build file (pom.xml):

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>examples</groupId>
    <artifactId>module-sensors</artifactId>
    <version>1.4.0</version>

    <parent>
        <groupId>com.astralanguage</groupId>
        <artifactId>astra-base</artifactId>
        <version>1.4.0</version>
    </parent>

    <build>
        <defaultGoal>clean compile astra:deploy</defaultGoal>
        <plugins>
            <plugin>
                <groupId>com.astralanguage</groupId>
                <artifactId>astra-maven-plugin</artifactId>
                <version>1.4.0</version>
            </plugin>
        </plugins>
    </build>
</project>

You should change the groupId and artifactId to reflect your own project.

Creating a project from an archetype

To use the maven archtetype mechanism to create an ASTRA project, go to a folder of your choice, and type in the following command:

mvn archetype:generate \
    -DarchetypeGroupId=com.astralanguage \
    -DarchetypeArtifactId=astra-archetype \
    -DarchetypeVersion=1.0.2 \
    -DgroupId=examples \
    -DartifactId=module-sensors \
    -Dversion=0.1.0

You should change the groupId and artifactId to reflect your own project.

Source Code

The full source code for this guide can be downloaded from the ASTRA Gitlab repository.

Starting Point

The starting point for this guide is the code created in the first part of the series which explored how to create custom actions in ASTRA. The source code for the Switch module is given below. As can be seen, the state of the switch is managed through the updateBeliefs() method, which is called when the module is initialized or whenever the flip() action is performed because this changes the state of the switch.

package example;

public class Switch extends Module {
  private boolean on = false;
  private Predicate belief;

  public void setAgent(Agent agent) {
    super.setAgent(agent);
    updateBeliefs();
  }

  @ACTION
  public boolean flip() {
    on = !on;
    updateBeliefs();
    return true;
  }

  private void updateBeliefs() {
    if (belief != null) {
      agent.beliefs().dropBelief(belief);
    }
    belief = new Predicate("switch", new Term[] {
      Primitive.newPrimitive(on ? "on":"off")
    });
    agent.beliefs().addBelief(belief);
  }
}

A limitation of this approach is that changes to the state are driven by the actions of the agent. In many environments, the agent does not have such absolute control and the state may change independent of the agents behaviour. For example, an agent monitoring a thermostat to measure ambient temperature does not how control of how it changes (although such an agent may have an ability to influence it through some indirect means, such as a heater). Sensors are designed to cater for such scenarios because the separate state update from action.

Changing this code to introduce a sensor does not require a significant amount of work. The main idea of a sensor is similar to that used in the updateBeliefs() method but with the difference that the agent updates its state constantly rather than only doing so when it acts. As is standard for most agent implementations, ASTRA is designed to fire all of its sensors at the start of each iteration of the agent interpreter cycle. A method is identified as a sensor if: it is annotated with @SENSOR, it is public and it has no return value.

package example;

public class Switch extends Module {
  private boolean on = false;
  private boolean lastOn = true;
  private Predicate belief;

  @ACTION
  public boolean flip() {
    on = !on;
    return true;
  }

  @SENSOR
  public void switchSensor() {
    if (on != lastOn) {
      if (belief != null) {
        agent.beliefs().dropBelief(belief);
      }
      belief = new Predicate("switch", new Term[] {
        Primitive.newPrimitive(on ? "on":"off")
      });
      agent.beliefs().addBelief(belief);
      lastOn = on;
    }
  }
}

As can be seen in the above code, the setAgent(...) method is no longer required and the code to update the state is no longer part of the flip() action. A signifcant change to the code for updating the state is the introduction of the lastOn field. The purpose of this additional field is to track the previous state of the switch so that the state is updated only when the switch is flipped. Initially, lastOn is set to true while on is set as false. This causes the sensor to update the agents state the first time the sensor is fired. After this, the state is only updated each time the flip() action is performed.

While the above sensor code is implemented in a single method, multiple sensors can be defined in each module and across multiple modules.

Using the Module

The code provided in the first part of the series stays the same for this guide. For completeness, it is given again below.

agent Main {
  module Switch switch;
  module Console console;

  types switch {
    formula switch(string);
  }

  rule +!main(list args) {
    !flash(3);
    console.println("FINISHED");
  }

  rule +!flash(int N) : N > 0 {
    !switch("on");
    !switch("off");
    !flash(N-1);
  }

  rule +!flash(int N) {}

  rule +!switch(string state) : ~switch(state) {
    switch.flip();
    wait(switch(state));
  }

  rule +!switch(string state) {
    console.println("should not happen");
  }

  rule +switch(string state) {
    console.println("switch=" + state);
  }
}

To compile and run the resulting program, simply type:

$ mvn

The output should again look something like this:

[main]switch=off
[main]switch=on
[main]switch=off
[main]switch=on
[main]switch=off
[main]switch=on
[main]switch=off
[main]FINISHED

Summary

This guide has introduced the concept of a sensor as it can be implemented within a module. It has built on the first part of the series which focused on how to create custom actions. As indicated at the start of this guide, sensors are no longer the preferred way of updating the agents state for ASTRA. Instead, we recommend using a combination of custom terms or formulae and custom events. How to do this is described in Part 3: Implementing your own Events and Part 4: Defining Custom Terms and Formulae respectively.