We are in early beta, and may not have tested Walnut in your browser/OS combination yet, so it may not look perfect. Thanks for your patience! ×
We are in early beta, and the documentation may be incomplete and/or have missing links. Thanks for your patience! ×

Defining Worlds

One of the goals of Walnut is to allow users to define different worlds with more or less arbitrary rules and simulate specific problems on them.

A world is an environments where agents live which defines rules of interaction. In each world, it is possible to specify an initial set of conditions and goals for which agents are the solutions. A world has an spatial context and a set of rules of interactions among agents and the coupling between each agent with the spatial context. A performance measure is sometimes part of the world and sometimes part of the problem.

Example: The Light Bulb World

Let us build an Walnut a very simple world with a light bulb that can be toggled on or off as an initial example. There is an agent called the "lamplighter". Before going into the Walnut notation, this is an informal description of the world:

  • There is no intrinsic performance. Different problems in this world may choose different performance measures (which can represent goals like "keep the light on", "keep it off", "make sure it is off at a certain time step", etc.)
  • The environment with the lamplighter has a light bulb that can be turned on or off.
  • The only actuator is a push button; the lamplighter can decide to push it, or leave it as it is. The button toggles the light bulb when pushed, nothing happens if it is not pushed.
  • The agent has a light sensor, which returns True or False. True means that the light bulb is turned on, False means that it is off.

This does not make a very interesting problem from the AI perspective, but it is simple enough to give a first demonstration how to work with Walnut

The worlds tab

Once you log in, you'll see a few tabs, one of them labeled “Worlds”. There you can see a list of the worlds you have defined and you have a “Create new world” button allowing you to define a new one.

This button takes you to a screen where you can give your world a name (let's use “The Light Bulb World” for this one), and allows you to write in the specification for your world

Defining the State Structure

To model and simulate the state we need some kind of description of what information is carried in the environment. We have to specify a data structure able to hold that information. Walnut supports and uses type declarations for this purpose. There are several built-in types which are comparable to basic types in other languages (booleans, numbers, strings), some built-in data structures (lists/arrays and mappings/dictionaries), and some ability to define custom types (tuples, multi-field structures, enumerations, trees). This should be familiar to you if you have programmed in a statically typed language like C or Java; they will look specially familiar if you have programmed in ML or Haskell (the type system in Walnut is a very small and simple subset of Haskell and follows its notation)

For our light bulb example, all the state of the world can be represented with a truth value describing if the light bulb is on. In Walnut we write that as

# True means light bulb on, False means light bulb off
state: Boolean

(enter this code in the “World definition” box)

The state is a predefined keyword, and the colon in Walnut can always be read as “has the type”; in this case “state has the type Boolean”. The type Boolean is built-in and represent truth values (which can be written as the built-in constants True and False). Note that here we are describing what are the possible values for the state, the actual value will be set and changed during the simulation.

The # marker indicates that the text until the end of line is a comment and will be ignored by Walnut.

There are different possible ways to represent the world state (we could have used a number with 0 for off and 1 for on, or a string saying "on" or "off"), but for this simple example the above will do fine.

An important convention shown here is that variable names in Walnut (like state) have names in lowercase, and type names (like Boolean) have names starting in uppercase.

Roles

The description for each agent must describe sensors and actuators. Walnut also introduces the notion of “role”, because in some worlds there are “kinds of” agents, each with different sensors, actuators, and rules for interacting with the world. Games and sports like tennis and chess tend to be mostly symmetrical (with minor differences like which side of the court each agent is in, or if it moves the white or black pieces), but some environments have different kinds of agents which we call roles. A driving environment can have drivers and pedestrians; the classical video game Pac-man has a "pacman" agent and three "ghost" agents, with some similar actions (move through a maze), but interacting differently with the world (the ghosts do not eat pills while pacman does, the ghosts have to chase pacman but pacman has to avoid them, etc.). In Walnut we must first define the roles and then assign a role for each agent.

For symmetrical worlds where all agents are similar, you will have a single role. In particular, in single-agent environments as our example, we will have a single role too. We will call it “Lamplighter” (role names start with an uppercase letter too)

role Lamplighter
    sensor = state
    action: Boolean
    actuator_for True
        do { state = not state }
    actuator_for False
        do { }

Let us go over this definition line by line:

  1. The first line just defines the role name. as mentioned above, role names have to start with an uppercase letter
  2. The second line of a role always starts with sensor =. At the right of the equal sign you can write any expression, which might depend on the state. This expression will be evaluated every time that the agent takes a perception of the world. In this case, the sensor perceives the whole state, so if the light bulb is on, the state will be True, and the sensor will perceive the value True. That is the value that will be received by the agent program, mapped to the programming language of the agent (if the agent program is JavaScript code, a Boolean value will map to the JavaScript values true and false).
  3. The third line always start with action: and describes the type of agent actions (remember that the colon in Walnut always means “has the type”). In our case the agent can choose to push the button or not, so a Boolean value is useful here too: True will mean “the agent chooses to push the button” and False will mean “the agent does not push the button”. Again, this is a type, the actual action value will be chosen by the agent each time it acts.
  4. The rest of the lines describes actuators following the pattern actuator_for pattern do effect, which means “When the action looks like pattern, apply the effect to the world”. You can define as many actuators as you need. The pattern can be a literal value, which matches when the action is exactly that; we will see later some other things that can be done with patterns. The effect is a small “program” that can have side effects, typically modifying the state. As you can see here, touching the button flips the state to its negated value, and not touching it does nothing.

Additionally, please note that you can add comments anywhere in the source text.

The code above shows a valid and usable world in Walnut; you can save it now. However to run a simulation of it we still need a problem definition and an agent program.

Problem Design

Problems in Walnut are descriptions which specify a situation that agents have to solve. This involves describing how is the world set up when the simulation starts, who are the agents (which role they have and which program drives their behavior), and what is the goal (the performance measure) of each.

When creating a new problem (you can do that from the world details), you specify a name for it, and a timeout for the simulation. The problem we will define can be named “Start with the light bulb off and make sure that at time step 10, it is on”. The simulation timeout is a time limit for the simulation, measured in number of actions taken; sometimes this is required if you have worlds they can run indefinitely. You can leave at its default right now.

The problem we will define is “Start with the light bulb off and make sure that at time step 10, it is on”. We will also force the the simulation to stop at time step 10.

The “performance function” is an expression that should return a number, and is evaluated at each time step for each agent. Higher values should mean that the agent is doing better. For this problem we will use if time == 10 and state then 1 else 0. This means that at time 10 the agent performance will be 1 if the lightbulb is on. It will be 0 otherwise. Note that we use an if condition then true-value else false-value, which returns one of the values depending on the value of the condition (similar to the cond?v1:v2 operator in C/Java/JavaScript).

The "end_condition" is an expression that should return True or False and is evaluated at each time step. If it evaluates to True, the simulation stops at that time step. It can depend on several values including the state and the current time step. For our example, we care about the performance when time == 10 and after that time the simulation is not relevant, so that's the end condition

The “Agents” section allows adding and removing agents. Each agent defines a unique identifier used as an identification, specifies its role (one of the roles given in the world, and one of the available agent programs. Our problem will have a single agent: Click on “Add Agent”, give it the id of “joe_lamplighter” (ids should be lowercase, no spaces), and pick one of the example programs. This world has only one role available so it will be already selected for you.

Finally, set the initial state to true, meaning that the lamp will be initially on.

How the Simulation Runs

We will assume here that you use the agent provided with the Walnut examples. If you want to create your own agents please check the Agent Design guide. You can run the simulator on a problem by clicking on the “run” or “save and run” buttons when viewing the problem.

When you run the simulator the world state is created in the initial state defined in the problem, and the agent programs are started. The simulator sends perceptions and expects actions from the agents in turns (the order can be redefined but the default is to go round-robin between all agents, sending a perception and waiting for an action). The perception is calculated by evaluating the sensor expression as defined in the agent role. The returned action is matched against the agent actuators, and the effects are applied for those actuators that match. Each time a perception is sent or an action received, the time step is increased.

For each time step increase, the end_condition is checked. If it evaluates to True, or the step counter in the simulator reaches the step limit (set when starting it) the simulation stops. If any erroneous value is produced (for example, a sensor trying to do an erroneous operation like diving by 0), or received (for example, the agent sending an action value which is not from the given action type), the simulation is also stopped.

All the progress of this process is recorded into a database. This record will be called “trace”. For each time step, the trace contains:

  • The state of the world
  • Which agents did what (act or perceive) in each order
  • The perception/action sent
  • If the simulation is still running or not
  • If it stopped, the reason why it did stop (reached step limit, reached end condition, error)

A simulation gets also an identifier assigned that later can be used to look it up in the front end and pick it for further visualization.

Visualizing Results

If you run a simulation or pick a previously ran one, it will open for visualization. Without describing how to represent this world, you will see a white area (called the "canvas") with no information. However, the steps bar at the right already shows useful information about the perceptions and and actions taken by the agent.

By editing the visualization definition, you can add different "components" to the canvas that display information related to the trace, typically to the current time step (controlled by the playback controls on the top). Once an interesting visualization is created you can save it for later reuse.

If we like to add a simple title to our visualization we can add

text {
    label = "Light bulb World!"
}

The text is the component type. Each component accepts several attributes that determine their appearance. The text component has a label which is the message displayed. If you would like to move this text component to the top of the canvas, you can set its v_align (vertical alignment) attribute to 0.0 (0.0 means top, 1.0 means bottom).

You can see how this is displayed clicking on the “Visualize” button. Then you can click on the “Edit” button that appears to go back to the editor. Now try this instead:

text {
    label = "Light bulb World!"
    v_align = 0.0
}
/static/doc/images/image_0000.png

Let us now add some display of the state of the world. we want to show a glowing light bulb when the light bulb is on, and a dark light bulb when it is off. To do so, we will include another component to our visualization; we will use a “sprite”. Components of type sprite can show an image; they have an "image" attribute which should be a URL for an image to display. Add this to the bottom of your visualization definition

sprite {
    image = if state then ":full_moon:" else ":new_moon:"
    width = 0.1
    height = 0.1
}

You will note that the image attribute is being set to an expression that depends on the state. When moving with the playback controls, this expression is evaluated and the canvas is updated, so you will be able to play back and display at what times the light bulb is on and at what times it is not. The value for the image attribute can be:

  • The example above uses emoji icons, entered as :icon_name:. Emoji is a standard set of general purpose icons, used in several texting/messaging applications like WhatsApp or Google Hangouts.
  • You can also upload custom images (see the “Sprites” control at the bottom right of the visualization screen), and use them by setting the image attribute to img:name, where name is the name you entered when uploading the picture.
  • You can use a full url (something like http://example.com/some-image.png), to use an arbitrary resource in the internet.
/static/doc/images/image_0001.png

Some images are low resolution ones so we scale it to a small size to avoid scaling artifacts. Note that a sprite will use the full canvas by default. The width/height values used mean 0.1 × canvas size, i.e. 10% of the canvas linear size.

You can include as many components as you like. They are drawn in the order that you specify them in the visualization definition, so the last ones will clobber the first ones if they are in overlapping positions. You can set any attributes with expressions which depend on the state and some other variables.

You can check the Reference Document to see what is the whole range of available visualization components and their valid attributes. For example, you can set “x” and “y” attributes to move the sprite around the canvas. Note that the canvas has the center in (0, 0), it measures 1×1, and the axis grows to the bottom right (the usual computer orientation, different to the usual mathematical orientation of axis growing to the top right). So, the top left corner is (-0.5, -0.5) and the bottom right is (+0.5, 0.5)

Walnut Basic Values, Types, and Expressions

To enable modeling of rich, interesting worlds, Walnut allows operating with different kind of values and data structures.

The most basic types of values in Walnut are:

  • Boolean: The values True and False, used in the last example.
  • Number: Real numbers (implemented as double precision floating point). You can write constants of these values as "42", "-3.14", "6.02e23". The formal syntax is the same used in JavaScript
  • String: Character strings (with Unicode support). String constants are enclosed in double quotes, and you can escape special characters with a backslash (\", \\, \n, etc.). Again, the syntax mimics JavaScript (but only double quotes are allowed)

An additional basic type available is Null. This type has a single value also called Null (When you see Null you have to figure out if it is a type or a value by context). It does not have any special meaning, but it is useful to have as a placeholder when a value is needed. For example, if you have an agent which has no sensors, a good way to model its sensor expression is defining it as Null

Any two values can be compared with the == and != operators. Equality only happens when both values are of the same type and exactly the same (there are no “magic” rules like "1==False" or "0"==0). You also can operate between numbers with the normal arithmetic operators (+, -, *, /), modulus (%). Comparison operators (<, >, <=, >=) can be used between numbers, or between strings (but not values of different types). The usual boolean operations (and, or and not) are also available between booleans. The logical operators are short-circuited, which means that they are well defined in cases like "x != 0 and 1/x == y" (where a zero value for x might make "1/x" undefined). Finally, we have already shown you the "if condition then x1 else x2", which evaluates and returns x1 when the condition is True, and x2 when the condition is False. Expression grouping with parenthesis also works in the usual way.

If any expression is invalid (division by 0, incompatible types used, etc.) in the world or problem definition, the simulation is stopped and the problem reported. If any error occurs evaluating an expression used in the visualization, a warning message is shown and the relevant attribute is not set, but the rest of the visualization keeps working

Example: Vacuum-cleaner World

In this section we will build a more interesting world example to showcase some additional capabilities of Walnut. This section will cover a variant of the "Vacuum-cleaner world" as defined in AIMA section 2.1.

The world as defined in AIMA has a vacuum cleaner robot that can move on two locations, each on a left/right orientation with respect to the other. Each location may be clean or dirty. The agent can move left, move right, clean the dirt, or do nothing. The agent knows where it is, and if the current square is dirty or not.

We will generalize the original problem a bit, to allow any number of locations, as long as they are reachable by Left/Right movements.

We could use Boolean typed variables to represent the state of a location, but this time we will choose a more expressive way to describe the cell state, so we can use symbolic constants Clean and Dirty instead of the less descriptive values True and False. To do so, we will add the following auxiliary definition at the top of our world definition

type LocationStatus = Dirty | Clean

This declaration defines the new LocationStatus type which we can use in the rest of our world definition. It also defines two labels, Dirty and Clean, which are distinct constants of type LocationStatus. We will say that Dirty and Clean are the “constructors” of the LocationStatus type, because they are the only way to build values of this type. Constructors also need to have a name starting with an uppercase letter.

The definition above describes the status of a single location, but the state of this world has many locations, sequentially arranged. Thus, in order to represent the state of the world, we need a linear data structure. Walnut has support for lists, that are similar to JavaScript arrays or Java vectors. To express the type of a list you need to wrap the element type in square brackets. So the type [String] describes lists where every element is a String and [Boolean] describes lists where every element is a Boolean. In this case, the state is a list of LocationStatus.

Now, we can now now define the type for the global state of the world as

state: [LocationStatus]

This sentence should be read as “the global state is a list of LocationStatus”. Strictly speaking, the state of the world has also the current location of the cleaning robot, which will be modeled as an integer number. It is typical when defining worlds, that some portion of the state is global, but another part of it is related to the agent (or the agents, in a multi-agent world). Walnut allows making this distinction, by adding state variables to the role definition,

role CleaningRobot(location: Number)

If you compare this to the previous example (the light bulb world), you will note that we have added a (location: Number) to the role definition. This means that agents with the role CleaningRobot will have a state variable labeled location (you can use any name here), and in this case, that variable has a Number type. You can actually define many variables, separated by commas.

Note that state variables are defined per-role. In many problems, agents for each role has different state variables. Having state variables defined per role allows us specifying clearly which variables are relevant to each role. Also, having this part of the state tied to the agent and not to the global state makes it easier to reuse this world in multi-agent problems (i.e. a swarm of collaborating cleaning robots), where the state for each agent is replicated appropriately (i.e, each cleaner bot has its own position).

Now let us go to the role definition. The cleaner robot knows two things: its current location, and the status (dirty or clean) of its location. So the sensor readout should somehow reflect both things. We need some kind of structure to represent this pair of perception values. The best way to do it is going back to the top of the world description and adding a new type

type Perception = SensorReading(status: LocationStatus, position: Number)

This line defines a type called Perception, but also a constructor called SensorReading. What is new here is that the constructor has two arguments (called “fields”), one of them called status which receives values of the LocationStatus type, and the other called position which receives numeric values. The constructor can be seen as a function that returns Perception``s, where every different pair of arguments produce a different value. If you have ``p a value of type Perception, you can also write p.status and p.position to refer to the values that were originally used to build p. You can also write p[0] and p[1] to refer to the two fields although the first form is recommended.

With this definition in hand, we can go back to the role definition and add the sensor,

role CleaningRobot(location: Number)
    sensor = SensorReading(status=state[agent.location], position=agent.location)

Note that we are using the state variable as before, but we are also using an agent variable. This variable is always defined when evaluating a sensor and allows us access to the state variables for the current agent. That means that agent.location will be the current location of the agent perceiving the world. Given that state is a list, state[agent.location] is the status (Dirty or Clean) at the location of the agent (in general l[i] evaluates to the i-th element of the list l, counting from zero).

Now we can define the action type as

role CleaningRobot(location: Number)
    ...
    action: Left | Right | Suck | Wait

Again, note that we are using | to define a type as a choice of constructors. There is no need to give a name to the type in a type section at the top as we did before. We could add it if we liked, but typically you only do it when it adds clarity to your definition. In this case, we are just using an anonymous type which can be built through 4 constructors.

Once this is done, we can define the actuators for moving

role CleaningRobot(location: Number)
    ...
    action: Left | Right | Suck | Wait
    actuator_for Left
        do { agent.location = agent.location - 1}
    actuator_for Right
        do { agent.location = agent.location + 1}

There is a problem with this definition, which is that an ill-behaved agent could move to an invalid location (outside the limits of the world). There are two possible approaches for solving this. One of them is making the Left and Right operations equivalent to Wait when done at the edge of the world. This can be defined in this way,

role CleaningRobot(location: Number)
    ...
    action: Left | Right | Suck | Wait
    actuator_for Left
        do {
            if agent.location > 0 {
                agent.location = agent.location - 1
            }
        }
    actuator_for Right
        do {
            if agent.location + 1 < len(state) {
                agent.location = agent.location + 1
            }
        }

There are a few new things here: The first one is the use of if statements in the actuator definition. You can also see the use of the built-in function len (which accepts a list and returns its length). Also note that here we are not modifying the global state, but a state for the agent which is acting.

In some cases, this approach is not a good model of the world. In abstract board games like chess, if an agent tries to do an invalid move it can not be replaced by another "default" move sensibly. Walnut allows adding to each actuator a condition that specifies if the action is valid. For our vacuum cleaner world, we could write it like

role CleaningRobot(location: Number)
    ...
    action: Left | Right | Suck | Wait
    actuator_for Left
        valid_when agent.location > 0
        do { agent.location = agent.location - 1 }
    actuator_for Right
        valid_when agent.location + 1 < len(state)
        do { agent.location = agent.location + 1 }

The valid_when clause of an actuator defines a condition that must be true for the action to be valid. If an agent sends an invalid action, the simulation is stopped (it is considered a “bug” in the agent code), and the aborted simulation is recorded in the trace.

Choosing between both styles is a matter of modeling. When you need to be strict on the agents, or there is no reasonable default for an invalid action, you will probably want the valid_when clause. In situations where the agent does not have enough information to decide if an action is valid or not, you will be forced to the first style: if the cleaning robot did not have a location sensor, or had one that is not 100% accurate, it would be impossible to write a correct agent program for the second world definition. Other way to see it is that the first world models an action “try to go left” instead of “go left”, and trying is always valid (even if you do not succeed). In many cases like this one, you have both options. For this example we will choose the latter variant.

Now we can add another missing actuator

role CleaningRobot(location: Number)
    ...
    action: Left | Right | Suck | Wait
    ...
    actuator_for Suck
        do { state[agent.location] = Clean }

As you can see Suck is always valid, and it forces the state of the current location to Clean.

The only missing actuator is the one for Wait. Actually, it is possible to leave it undefined. If an action sent does not match any of the actuators given, it will have no effect on the world state.

For this world, we might want to add default definitions for the performance function and end conditions. To do so, you can define after the role definitions

performance = sum([1 for status in state if status == Clean])
end_condition = all([status == Clean for status in state])

This performance function counts the number of clean locations; and the end conditions finishes the simulation when all locations are clean. These are defaults that can be overridden by a problem definition. They are added here because they sound like reasonable defaults for many problems.

We will now analyze both expressions so you can know better how they work. The sum built-in function takes a list of numbers and adds them (i.e., sum([1,3,5,7]) returns 16). The notation [value for var in list if condition] is a practical way to generate lists called “list comprehension”. It iterates on each element for list, assigns it to a given variable var, and checks if the condition (which might depend on var) is True. In this case, it computes the value and appends it in a resulting list. The condition might be omitted. In the performance function above, the comprehension is checking the status of each cell (in state), mapping it to a variable called status (for status), checking if it is clean (if status == Clean), and appending a number 1 to a resulting list. When there are 3 clean cells, the resulting list will be [1, 1, 1], so its sum will be 3. The all built-in takes a list of booleans and returns True when all of its items are True. The comprehension will go over the state and generate a list of True values for the cells that are clean and False for those which are not.

The final world definition looks like

type LocationStatus = Dirty | Clean
type Perception = SensorReading(status: LocationStatus, location: Number)

state: [LocationStatus]

role CleaningRobot(location: Number)
    sensor = SensorReading(status=state[agent.location], location=agent.location)
    action: Left | Right | Suck | Wait
    actuator_for Left
        valid_when agent.location > 0
        do { agent.location = agent.location - 1 }
    actuator_for Right
        valid_when agent.location + 1 < len(state)
        do { agent.location = agent.location + 1 }
    actuator_for Suck
        do { state[agent.location] = Clean }

performance = sum([1 for status in state if status == Clean])
end_condition = all([status == Clean for status in state])

Note: there are many other additional built-ins; you can see the complete list at the Reference Document (Built-ins section).

Some Notation Tricks

The following section summarizes some practical tips on notation and some shorthands that can be useful. You can skip this if you just want an overview, but they can be helpful to write simpler definitions if you use Walnut a lot.

It is frequent to desire a constructor for a type with some fields, like the one we wrote for the Perception type above, and not being very interested in the type itself. In our last example, you may note that we never used the Perception type, we just needed the SensorReading constructor. In those cases, you can as a shorthand write just the following

type SensorReading(status: LocationStatus, location: Number)

Note that this is very similar to our previous definition, but we omitted the Perception =. This will work perfectly fine for our previous example world. This definition actually defines a type called SensorReading in addition to the SensorReading constructor (as it happened for Null, the same name is used both for the type and the constructor). Actually, it is a shorthand for the equivalent

type SensorReading = SensorReading(status: LocationStatus, location: Number)

This kind of declaration can only be used when you have only one constructor for the type.

Additionally, we can reduce verbosity in the vacuum world example by writing the sensor definition

sensor = SensorReading(state[agent.location], agent.location)

What we changed here is the omission of the status= and location= in the constructor call. You can do that as long as you pass the parameters in the same order as they were defined. Both alternatives are mutually exclusive: you can pass all parameters by location, or add a label (like status=) to all of them. If you use the labels you can put the arguments in any order.

In fact, if you are not interested in the field names at all, another valid way to declare the type is

type SensorReading(LocationStatus, Number)

In that case you can also use positional arguments when calling the constructor, and the only way to access the fields of a SensorReading value s is writing s[0] and s[1]. However, unless the meaning of the constructor arguments are obvious, it is recommended to add labels to it.

A Vacuum-cleaner World Problem

Defining a problem for this world is not very different to what we did for the light bulb world, but we will highlight here the differences that you will find in the problem editor.

If you add an agent, you will note that the Role selector also allows you to set the initial values of agent state for the role, in this case the initial location.

The initial state can also be edited here. Controls for editing are slightly different in this case because we have a list; you can add, remove, and move elements around using the handlebars on the left.

If you want to use the default performance function and/or end condition in the problem, just omit them and you will get the ones defined in the world. Otherwise, just add an alternative definition. For example, if you would like the simulation to end when the second location is clean, no matter what happens elsewhere, you could set the “End condition” of the problem to

state[1] == Clean

That will ignore the previous definition of end condition, but keep the performance.

Visualizing the Vacuum-cleaner world

The visualization definition for this world brings some new problems. In the light bulb world we had a fixed number of elements to display, but here we have an arbitrary number of locations. The Walnut visualization definition has some support to displaying easily things that are contained in grids.

Try putting the following in your visualization definition

grid {
    dimension = [10, 4]
}

You will see a grid displayed on the canvas. That is because we added a grid component of width 10 (columns) and height 4 (rows). Our problem has 5 locations, but instead of hard-coding that into our visualization, we can just say that to display our world we need a grid as wide as the state, and 1 row tall

grid {
    dimension = [len(state), 1]
}

By default a grid fills its whole container (the canvas), so in our case this will make our cells look disproportionately tall. We can make the grid smaller so cells are square. You can put a fixed number for height (like 0.2, remember that height is measured in relative units), or we can keep the size relative to the canvas by doing

grid {
    dimension = [len(state), 1]
    height = 1 / len(state)
}

Now we would like to display the current location of the agent with a sprite. The agent location is part of the agent state, which is available in the trace through the agents variable. Agent is actually a mapping from agent ids to the agent state. The cleaning robot location, if you are using the problem definition described before, can be expressed as agents.cleanbot_1.location. That value will be an integer from 0 to 4 (in our problem we assume 5 locations). Mapping that to the canvas coordinate system (centered in 0,0, measuring 1×1) is not too hard, but somewhat cumbersome. What we can do instead, is put the sprite inside the grid.

grid {
    ...

    sprite {
        # A vacuum cleaner is represented by a recycling symbol
        image = ":recycle:"
        x = agents.cleanbot_1.location
        y = 0
    }
}

As you can see Walnut allows putting components inside one another. The effect of doing this is that the inner component is placed with respect to the coordinate system of the outer component. Grids define a coordinate system where (0, 0) is the center of the top-left cell, and (9, 3) is the center of the cell in the 10th-column, 4th row (not 9th and 3rd because counting is done from 0). That allows you to easily locate items in a grid. Also note that the sprite is automatically fitted to the cell, because by default a sprite measures 1×1, but the coordinate unit inside a grid is the size of the cell instead of the whole canvas. Also note that you are not limited to grid alignment; if you want to display something right over the line between the cells (0, 0) and (1, 0), you can just give it coordinates (0.5, 0)

All components in Walnut can be nested, and define their own coordinate system. Most simple components just define a coordinate system with the (0, 0) in their center and the coordinate system and the coordinate unit equal to their size (just like the canvas). For example, if you want to add text over the agent sprite displaying its position numerically you can add text inside the sprite with

grid {
    ...

    sprite {
        ...
        text {
            label = agents.cleanbot_1.location
            h_align = 0
            v_align = 0
        }
    }
}

Here, we have hard-coded the display definition to details of our problem. If someone builds another problem changing the name of the agent, this visualization will not work. When somebody builds a problem with multiple cleaning robots, only the one called “cleanbot_1” will be shown. We can solve both problems using a new visualization language construct; we will replace the sprite component above as follows:

grid {
    ...

    sprite for a in agents {
        image = ":recycle:"
        x = agents[a].location
        y = 0
        text { label = a }
    }
}

The for a in agents added along the sprite component means that actually one sprite will be created for each element inside the agents variable (which contains by default the list of agent ids). Moreover, it will set the value of a to the agent identifier of each agent, and you can use that variable in the attributes and sub-components. So agents[a].location means the location of the agent being shown. We added a text label with a to each agent so we can see their names along the picture and know who is who.

This kind of looping can be added to any component. We could for example tag the dirty cells with an X by doing

grid {
    ...

    text for status in state {
        label = if status == Dirty then "X" else ""
        x = loop.counter
    }
}

The loop variable used here is defined in every loop, and has several attributes; one of them is counter with the number of the current loop iteration, but there are others like booleans first and last, a revcounter (counting how many values are left), and a few others.

There is another way to apply loops that is useful when you need to do it on the whole grid, which is using the cell component. A single cell component typically goes inside a grid component, but it is rendered once per cell in the grid. While rendering it, a position variable is defined with two fields called x and y containing the coordinates. The cell attributes control its background color and borders (which is useful to draw “walls” in a map, checkerboards, etc), and as any component, you can nest elements inside it (if you want to add text or sprites to a cell). Try for example the definition

grid {
    dimension = [len(state), 1]
    height = 1 / len(state)

    cell {
        color = if state[position.x] == Dirty then "gray" else "white"
    }

    sprite for a in agents {
        image = "/static/img/vacuum.png"
        x = agents[a].location
        y = 0
        text { label = a }
    }
}

This definition draws dirty cells in gray background, and clean cells in white background. Note that here we use both looping constructs. It is normally easier to use cell when the iteration is more natural to do on every cell, and the <elem> for ... in ... construct when it is easier to iterate on other list and then compute the position of the elements being displayed.

Make sure the cell element is before the sprite, otherwise the cell color will be painted over the sprite, and you will not see the sprite.

Contexts

Every expression, either in the world definition, problem definition, or visualization definition is evaluated having some useful variables already set This group of set variables will be called a “context”. Here we detail the content of the context in different places of the system:

All contexts contain:

  • state: the global state
  • agents: a dictionary in which each key is an identifier (string) of an agent, and the value is the internal state (Walnut value) of that agent
  • config: an object containing all the specified configurations

The contexts for the sensor, actuator and performance expressions also contain those variables, plus the following agent-specific ones:

  • agent_id: the id (string) of the current agent
  • agent: the state of the current agent
  • last_performance: the last measured performance (number) for the current agent

The visualization interface sets many additional variables in the context:

  • time: contains the current count of events (the current time step)
  • performances: a mapping from agent ids to their last measured performances
  • sensors: a mapping from agent ids to their last perception.
  • current_agent: The agent that is acting/perceiving at this time step, or null (for the final state)
  • event: A string describing what is happening (action, perception, end condition fulfilled, error, etc)
  • agent_meta-data: a mapping from agent ids to their last meta-data recorded. See Agent Design for details about recording agent meta-data from the agent program

Patterns

On the examples we have written, we have mentioned about the possibility of matching actions with patterns to have different actuators. In the initial examples we matched constant values, and in the last example we matched any value and captured into a variable. In this section we will discuss some additional way to write patterns that improve our ability to filter actions.

We have already seen that patterns can do two things: the first is selecting (accepting or rejecting) some values for others; the second is capturing values into variables. In the vacuum cleaner world, the Suck pattern “accepts” Suck values but rejects any other (for example a Left value). In the Tic-tac-toe world the p pattern accepts any value, and captures it into a variable called p

In general, you can use any constant (like Suck, 3, Point(2, 3) or ["red", "blue"] to accept only that value and reject any other. And you can use any name to capture the value into a variable with that name. This are the examples we have seen and the simplest ones.

You can also use a constructor call with patterns as arguments to build a pattern that matches any value built from that constructor, and where the arguments match the inner patterns. So the pattern Point(x=px, y=0) matches points where the y coordinate is 0, and captures the x coordinate into the variable px. This is useful, for example in the following role definition

role Mover(posx: Number, posy: Number)
    ...
    action: Move(deltax: Number, deltay: Number) | Wait
    actuator_for Move(dx, dy)
        do {
            x = x + dx
            y = y + dy
        }
    actuator_for Wait ...

It is also possible to match lists with patterns for the elements. The pattern [1, 2, x] matches any 3-element list starting with 1, 2, and captures the third item into the variable x. Using a star in a list pattern allows capturing sub-lists, for example [0, *l, -1] accepts only lists starting with a 0, ending with a -1, and captures all the middle elements into a variable called l. So if you use that pattern to match [0, 2, 7, -1], the match will accept, and l will get the list [2, 7] assigned to it.

Other Uses of Patterns

Patterns are not only used in actuators. List comprehensions use patterns too after the for keyword. For example, in the Tic-tac-toe example if you want for some reason the average x coordinate of played tokens, you can write

sum([t.position.x for t in state]) / len (state)

And the t there is a pattern. You can also write the equivalent

sum([x for Token(l, Point(x, y)) in state]) / len (state)

Note that a list comprehension will fail to evaluate if any element in the list is rejected by the pattern.

Comprehensions on components in the visualization language can also use patterns. You can for example display the tokens in the Tic-tac-toe game in a grid using

text for Token(l, Point(x, y)) in state {
    x = x
    y = y
    label = l
}

Multiple Matches

When defining actuators using patterns, it is possible that more than one pattern matches the action. In that case, Walnut applies all the matching actuators, in the order that they were defined. All validation conditions must be satisfied in that case. That is useful among other things to include state changes that apply to many actions. For example, let us consider a variant of the vacuum cleaner world where the agent also has a battery, moving or cleaning consumes one unit of power, and moving to a charging unit at a certain position recharges the power to full We could have something like the following

role CleaningRobot(location: Number, power: Number)
    ...
    action: Left | Right | Suck | Wait
    actuator_for Left
        valid_when agent.location > 0 and agent.power > 0
        do { agent.location = agent.location - 1 }
    actuator_for Right
        valid_when agent.location + 1 < len(state) and agent.power > 0
        do { agent.location = agent.location + 1 }
    actuator_for Suck
        valid_when agent.power > 0
        do { state[agent.location] = Clean }
    actuator_for a
        do {
            # discharge if action is not a wait
            if a != Wait then { agent.power = agent.power - 1 }
            # recharge if at charging station
            if agent.location == config.charger_location then {
                agent.power = config.max_battery_power
            }
        }

Note that the last actuators always match, and takes care in a single place of calculating the agent power. That makes the definition simpler than adding the power decreases in every action, and the position checks after every move.

Customizing Agent Alternation

Of the examples we have seen, only Tic-tac-toe is typically multi-agent; the others can be used in multi-agent problems (and perhaps it would not be strange to have a vacuum cleaner world with many cleaning robots).

Given that our worlds are sequential with discrete time, it is relevant to define in which orders the agents will take their actions. Walnut defaults to a round robin policy, following the order specified in the problem definition. However, the order may be customized to adopt more complex rules.

Let us model for example a world where each agent decides on his action who acts next. The next agent must exist and be different to the current one.

We could write the following

# state.next_agent is the agent_id of the next acting agent
state: Relay(next_agent: String)

role RelayActor

    # The action is the id of the next actor
    action: String
    actuator_for id
        valid when id != agent_id and id in agents
        do { state.next_agent = id}

Now we want to tell Walnut that it should choose the next actor from the state instead of using its default round robin policy. To do so, add the following to the world definition, right after the roles

agents_alternation = [state.next_agent]

The agents_alternation function always should return a list. When Walnut needs to decide who will act for the first time, it evaluates this function, and then goes over the list making each agent there take a perception, and then an action. When the list is exhausted, the function is reevaluated and the new list iterated. The default return value for this function when it is not defined is the list of agent ids as set in the problem definition, which produces the standard round robin behavior. However, this allow to define any arbitrary rule to handle alternation

More about Types

In this section we will try to expand our knowledge about modeling in Walnut. In the last example we saw a glimpse of how type definitions increase expressiveness and readability. Here we will describe some useful features that go beyond that example.

Dictionaries

Up to this point we have described basic types (strings, numbers, booleans, and the null type), lists, and custom types built by constructors. The only missing Walnut data structure is the dictionary. The dictionary (called also “map”) in other languages, is a finite association of keys to values. Walnut dictionaries can only use strings as keys, but a uniform type as value. The type is specified as Dict {T} where T is the value type. For example Dict {[Number]} maps each string key to a list of numbers. If d has that type, you can evaluate d.somekey or d["somekey"] to get to the value attached to the string "somekey"; looking up for a non-present key is an evaluation error. You can check for the presence of a key with the in operator, used as "somekey" in d. It is also possible to set/update dictionary values for a key from actuators with the syntax d.somekey = newvalue or d["somekey"] = newvalue.

TODO: dictionary literals

Lists

We have already seem lists in action, but it is useful to add some details about what you can do with them. Our example accessed and modified items, and we have shown how to specify lists literally ([1, 2, 10]) or by comprehensions. You can also compare lists with relational operators, as longs as the item types as comparable, the comparison is done lexicographically, so [1,2,1] < [1,3] < [2,1]. The + operator (but not -) can be applied to lists of the same type, and concatenates them. The * operator can be used with a list and a number, and repeats the list (i.e., ["a","b"]*3 returns ["a","b","a","b","a","b"]). Finally, the elem in list operation returns True if elem is present in list.

The Universal Type

It may be possible on occasions that we need to specify values of unknown types, and it is not easy to tell which. In that case, it is possible to specify a type as "?" which is called the “universal type”. All the values we have seen are from the universal type. So it is possible to write for a role action: [?] (list of anything), and then [2, "Hello", Null] will be a valid action. It is not usually necessary to use the universal type, but it is useful to know about it when you need more flexibility. Note that in any case, the values themselves have one of the other types, and any invalid operation on them or between them will produce a failure.

Custom Types

The ability of creating constructor based types adds a wide range of modeling possibilities. We saw some examples above, where we used many field-less constructors as enumerations, or one constructor as a structure/tuple.

When declaring a custom type you can use one or many constructors, and any of them can have the number of fields you prefer. So you can for example write

type OptionalString = Option(String) | Default

to represent a place when you can use a string, or leave the default. If you want values that can hold either one string or one number, but not both you can declare

type StringOrNumber = Str(String) | Num(Number)

which then allows you to use values like Str("hello") and Num(7) as values of the same type.

There are a lot of possible combinations, for example

type Point2D = Cartesian(x: Number, y: Number) | Polar(rho: Number, theta:number)

As we shown on the example, one type declaration can use another custom type like this,

type Point2D = Cartesian(x: Number, y: Number) | Polar(rho: Number, theta:number)
type Path = [Point2D]

But something that the example did not show, is that type declarations can be recursive. For example, If you want to model binary trees, with Boolean label on the leafs you can define

type BinaryTree = Leaf(Boolean) | Node(left: BinaryTree, right: BinaryTree)

Or for generic trees with string labels in any node,

type Tree = Node(label: String, children: [Tree])

As a final important note, constructors are tied to a single type. That forbids you to use the same constructor name in two different type declarations (named or anonymous). So it would not be valid Walnut code to write

type SensorReading(status: Clean | Dirty, location: Number)

state: [Clean | Dirty]

Because the Clean and Dirty constructors are being used in two different types. In these cases, giving the type a name and then using the name is not only useful but also required.