My previous post described a simple NetLogo example integrated with a NetLogo Java extension, where the Java code implemented a diffusion process on the “patches,” as shown above.
This post will describe how the Java side works (a third post will go into more detail). A “manifest file” (call it manifest.txt
) for the jar file specifies the name of the main class:
Manifest-Version: 1.0
Extension-Name: diffusion
Class-Manager: DiffusionExtension
NetLogo-Extension-API-Version: 5.0
As per the extensions API, this main class defines the reporter and command names, and maps each of them to a special Java class (a routine to read in DiffusionGrid
objects is optional, but is needed if world import/export is to be used):
import org.nlogo.api.*;
public class DiffusionExtension extends DefaultClassManager {
public void load(PrimitiveManager primitiveManager) {
// Usage: diffusion:init-grid world-width world-height min-pxcor min-pycor
primitiveManager.addPrimitive("init-grid", new InitDiffusionGrid());
// Usage: diffusion:set-grid-cell grid pxcor pycor value
primitiveManager.addPrimitive("set-grid-cell", new SetDiffusionGridCell());
// Usage: diffusion:get-grid-cell grid pxcor pycor
primitiveManager.addPrimitive("get-grid-cell", new GetDiffusionGridCell());
// Usage: diffusion:diffusion-step grid factor
primitiveManager.addPrimitive("diffusion-step", new DiffusionStep());
}
public org.nlogo.api.ExtensionObject readExtensionObject(org.nlogo.api.ExtensionManager reader,
String typeName, String value) throws org.nlogo.api.ExtensionException {
return DiffusionGrid.parse(value);
}
}
The class to define a NetLogo reporter (here the init-grid
reporter) contains two parts: one defines the argument and result types, and one performs the actual work. Much of the second part involves unpacking the arguments in a type-correct way. Here the constructor of the DiffusionGrid
class does the real work. Note that if numbers are returned, they must always be of the Java Double
class.
class InitDiffusionGrid extends DefaultReporter {
public Syntax getSyntax() {
return Syntax.reporterSyntax(new int[] {Syntax.NumberType(), Syntax.NumberType(), Syntax.NumberType(), Syntax.NumberType()},
Syntax.WildcardType());
}
public Object report(Argument args[], Context context) throws ExtensionException, LogoException {
int width = args[0].getIntValue();
int height = args[1].getIntValue();
int lox = args[2].getIntValue();
int loy = args[3].getIntValue();
return new DiffusionGrid (width, height, lox, loy);
}
}
For a NetLogo command (here diffusion-step
) the code is similar, except that there is no result type. For extension-defined types like DiffusionGrid
, explicit tests and casts are needed to make sure the argument has the right type:
class DiffusionStep extends DefaultCommand {
public Syntax getSyntax() {
return Syntax.commandSyntax(new int[] {Syntax.WildcardType(), Syntax.NumberType()});
}
public void perform(Argument args[], Context context) throws ExtensionException, LogoException {
Object g = args[0].get();
if (g instanceof DiffusionGrid) {
double diffusionFactor = args[1].getDoubleValue();
((DiffusionGrid) g).step (diffusionFactor);
} else throw new ExtensionException(g + " is not a DiffusionGrid") ;
}
}
The class definition for DiffusionGrid
has two parts. The first defines constructors and methods to do the real work (this are called by the classes associated with reporters and commands):
class DiffusionGrid implements org.nlogo.api.ExtensionObject { // new NetLogo data types defined by extensions must implement this interface
public final int width;
public final int height;
public final int loX;
public final int loY;
private double [] [] grid;
public DiffusionGrid (int w, int h, int loX, int loY) {
this.width = w;
this.height = h;
this.loX = loX;
this.loY = loY;
this.grid = new double [w] [h];
}
public void setValue (int x, int y, double v) { // set one entry in the diffusion grid
grid [x-loX] [y-loY] = v;
}
public double getValue (int x, int y) { // get one entry in the diffusion grid
return grid [x-loX] [y-loY];
}
public void step (double diffusionFactor) { // perform one diffision step
double [] [] temp = new double [width] [height];
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
double a = (i == 0 ? 0 : grid [i-1] [j]);
double b = (i == width-1 ? 0 : grid [i+1] [j]);
double c = (j == 0 ? 0 : grid [i] [j-1]);
double d = (j == height-1 ? 0 : grid [i] [j+1]);
int n = (i == 0 ? 0 : 1) + (i == width-1 ? 0 : 1) + (j == 0 ? 0 : 1) + (j == height-1 ? 0 : 1);
temp [i] [j] = (1 - diffusionFactor) * grid [i] [j] + diffusionFactor * (a + b + c + d) / n;
}
}
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
grid [i] [j] = temp [i] [j];
}
}
}
The second part of the DiffusionGrid
performs housekeeping needed to interface with NetLogo. This includes equality tests and routines to convert to and from text strings:
public boolean equals(Object obj) { // equality test
return this == obj;
}
public String getExtensionName() { // return extension name
return "diffusion";
}
public String getNLTypeName() {
// return type name -- since this extension only defines one type, we don't need to give it a name (ok to just return an empty string)
return "DiffusionGrid";
}
public boolean recursivelyEqual(Object obj) { // should really do an item-by-item comparison here
return this == obj;
}
public static DiffusionGrid parse (String value) { // decode a string into a grid
int k = value.indexOf (":");
String s = value.substring (0, k);
String body = value.substring (k+1);
k = s.indexOf ("x");
int w = Integer.parseInt(s.substring (0, k));
int h = Integer.parseInt (s.substring (k+1));
int lox = -(w/2);
int loy = -(h/2);
DiffusionGrid grid = new DiffusionGrid (w, h, lox, loy);
k = 0;
for (int i = 0; i < w; i++) {
for (int j = 0; j < h; j++) {
grid.setValue (i+lox, j+loy, decode(body.charAt (k++)));
}
}
return grid;
}
private static int encode (double v) { // assume 0..9.9 range, and return a stupidly simplistic text equivalent of a number
return 'A' + ((int) (v * 26.0 / 10));
}
private static double decode (int ch) {
return (ch - 'A') * 10.0 / 26;
}
public String dump(boolean readable, boolean exportable, boolean reference) { // dump a grid to a string
StringBuilder sb = new StringBuilder (width + "x" + height + ":");
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
sb.append ((char) encode(grid [i] [j]));
}
}
return sb.toString ();
}
}
Compilation of the Java (complete file here) must allow access to the jar files defining NetLogo, and must generate code consistent with the JVM in NetLogo (I used -source 1.6 -target 1.6
flags). Creation of the jar file must include the manifest file described above. The resulting diffusion.jar should work with the NetLogo example code posted earlier:
javac -source 1.6 -target 1.6 -classpath ./NetLogo.jar;./scala-library.jar -d classes DiffusionExtension.java
jar cvfm diffusion.jar ./manifest.txt -C classes .
That is all that is needed to make the example work. The third and final post in this series will discuss some more advanced Java issues.
Pingback: Integrating NetLogo and Java: #1 | Scientific Gems
Pingback: Integrating NetLogo and Java: #3 | Scientific Gems
Pingback: NetLogo post summary | Scientific Gems