0% found this document useful (0 votes)
98 views

Programmers Guide PDF

Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
98 views

Programmers Guide PDF

Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 28

Programmers Guide

April 25, 2011

www.neurodynamix.net

Introduction ..................................................................................................................................................... 3
1.1
How NeuroDynamix II Works................................................................................................................ 3
1.2
Use of Java .............................................................................................................................................. 3
1.3
About This Guide.................................................................................................................................... 3
Parts of a Model .............................................................................................................................................. 4
2.1
Overview ................................................................................................................................................. 4
2.2
A Simple Model ...................................................................................................................................... 4
2.3
Parameters ............................................................................................................................................... 5
2.4
Hidden Data ............................................................................................................................................ 7
2.5
Variables ................................................................................................................................................. 8
2.6
Constructor.............................................................................................................................................. 8
compute() method.................................................................................................................................. 9
2.7
Complex Models ........................................................................................................................................... 10
3.1
Arrays Of Parameters ............................................................................................................................ 10
3.2
Matrices of Parameters ......................................................................................................................... 11
3.3
Structures of Parameters ....................................................................................................................... 12
3.4
Arrays Of Structured Parameters .......................................................................................................... 14
3.5
The Stimulator ...................................................................................................................................... 16
3.6
Arrays and Matrices of Variables ......................................................................................................... 17
Packaging a Model........................................................................................................................................ 17
4.1
Creating a JAR file................................................................................................................................ 17
4.2
The default lesson ................................................................................................................................. 17
4.3
Help files ............................................................................................................................................... 18
4.4
Multiple models in a single JAR file .................................................................................................... 18
Advanced Topics .......................................................................................................................................... 18
5.1
Permutations ......................................................................................................................................... 18
5.2
Custom Parameter Panels ..................................................................................................................... 20
5.3
Visualization ......................................................................................................................................... 23
5.3.1
Overriding paintComponent()....................................................................................................... 24
5.3.2
using child components................................................................................................................. 24
References ..................................................................................................................................................... 25
6.1
Java Language Resources ..................................................................................................................... 27
6.2
Swing Resources ................................................................................................................................... 27
6.3
Java2D Resources ................................................................................................................................. 27

1 Introduction
1.1 How NeuroDynamix II Works
NeuroDynamix II is a real-time framework for computing numbers iteratively and graphing the results. Realtime means that the computations are intended to be modified interactively, as the output data is being
produced, so that the results of various modifications can be seen instantly. Although designed as a teaching aid
for neurobiology, NeuroDynamix II is actually a general-purpose tool which can be used in a variety of
applications. This guide will describe the process of developing a computational model in general terms, and
does not require knowledge of neurobiology.
To write your own data generation functions in NeuroDynamix II, you develop a model. A model consists of
input parameters, a compute() method, and output variables. Parameters can be thought of as knobs on a radio
(which might control balance, volume, treble, and bass) and variables are the speakers connected to the radio.
The circuitry in the radio is the compute() method.
In mathematical terms, given the formula
y = a * sin(t * b + c) + d
a, b, c, and d are parameters. y, which appears on the left side of the equal sign, is a variable. t is a special
case: this is the value which ranges from 0 to infinity as the compute() method is iteratively called.

1.2 Use of Java


Because NeuroDynamix II is written in Java, models must also be written in Java. Java was chosen as a
platform for NeuroDynamix II because it includes several favorable features:
Portable across Windows, Unix, and Macintosh platforms which are in common use at colleges and
universities
Provides a portable, convenient way to include additional code (models) at runtime
Development tools are widely and freely available to model developers
Thanks to the flexibility of reflection, models are POJOs that need to meet very few requirements.
NeuroDynamix II uses Java 5.0 features and therefore requires at least JDK 1.5 or equivalent.

1.3 About This Guide


This guide is a programmers guide and assumes a basic familiarity with the Java language. To write a model
without any frills, you do not need to be familiar with any Java GUI components (such as AWT and Swing) or
web technologies (such as J2EE). A familiarity with the Math API will be useful if you require trigonometric or
exponential functions, and some advanced techniques require Swing and Java2D programming.
This book features the following formatting:
Italics introduce a new term
Courier Text indicates code you are to type literally. If quotes are included, type them too
Code examples will generally omit comments, package and import statements for brevity.
We will use the terms parameters and variables in the NeuroDynamix II sense: as elements of a model. When
we talk about functions we will mean this in the mathematical sense.

When discussing Java programs, we will use unambiguous terms like arguments, fields, data, and methods.

2 Parts of a Model
2.1 Overview
As described earlier, a model is written in Java and describes parameters, variables, and a compute() method.
Nothing else is required to implement a model! In the Advanced Topics section we will discuss additional
techniques for coding models.
The technical requirements for a model are quite modest:
The model is a class that extends com.pitchphork.neurodynamix.models.Model
The model class must be public
The model class must define a public inner class called Variables
The model class must include a compute() method:
public void compute(double t, double elapsed, Variables v, Variables o)

Here are some related details which may guide you in writing your model class:
A model class can have any name
A model class can be in any package (but we recommend every model should be in its own package)
A model does not need a constructor, but if it does, the constructor should have no arguments (see the
Permutations section for an exception)
A model should restrict itself to the NeuroDynamix II API. In particular, a model should not attempt to
use the Java SE API (printing, graphics, sound, reflection, network, etc) as these are likely to interfere
with the operation of NeuroDynamix II. (The NeuroDynamix II API includes a facility for adding
arbitrary graphics output to your model described in the Visualization section)

2.2 A Simple Model


Heres a simple model that implements
y = a * sin(t * b + c) + d

Lets get right to the code.


package com.pitchphork.neurodynamix.model.sample.onesine;
import com.pitchphork.neurodynamix.model.*;
@Version("1.0")
@Name("One Sine Wave")
@Description("An example of a model, using sine waves")
public class OneSine extends Model {
// Declare output variables
public class Variables {
public double y;
};
// Define input
public double a
public double b
public double c
public double d

parameters
= 1;
= 2;
= 4;
= 6;

public void compute(double t, double elapsed, Variables v, Variables o) {


v.y = a * Math.sin(b*t + c) + d;

}
}

As you can see, weve written a class called OneSine that extends
com.pitchphork.neurodynamix.model.Model.
public class OneSine extends Model {

! Java Note

All models must directly extend com.pitchphork.neurodynamix.models.Model. When first initialized,


NeuroDynamix II scans all JAR files for classes that extend Model.
After packaging the class in a JAR file and adding it to NeuroDynamix IIs library list, the class will appear in
the list of available models:

Note that NeuroDynamix reads the name, version, and a brief description of the model from the annotations:
@Version("1.0")
@Name("One Sine Wave")
@Description("An example of a model, using sine waves")

NeuroDynamix II compares the model version to the version stamp in lesson files so that it can warn users when
a lesson file was built with a different version of the model than the model currently in use.

2.3 Parameters
Once loaded, the Parameters window of NeuroDynamix II looks like:

How did NeuroDynamix II know that a, b, c, and d are parameters? Because they are public, defined members
of the OneSine class. Any members of the model class are parameters if they meet the following criteria:
They are public
They never appear on the left-hand side of an equal sign
They are one of the following data types:
o double (appear as Spinners)
o boolean (appear as checkboxes)
o String (appear as editable fields)
o enum (appear as drop-down lists)

o
o
o
o

arrays of the above types


two-dimensional arrays of the above types
Objects with fields only of the above types
Arrays of Objects with fields only of the above types

Some other feature of parameters:


Parameters should be initialized; this becomes the default value (unless it is overridden by a lesson)
public double a = 5;
public String name = Jon;
public boolean allowNegative = true;
enum PrimaryColor {Green, Red, Blue }
public PrimaryColor background = PrimaryColor.Green;

!Java Note

One important note is that Java allows enums and Strings to have a null value, but NeuroDynamix II does
not allow these values. Take care to always initialize your enums and Strings.
Heres a silly model that demonstrates all four simple parameter types:
:
:
public class ParameterDemo extends Model {
enum Flavor {
Chocolate, Vanilla, Strawberry
}
public
public
public
public

String yourName =
Flavor iceCream =
double fahrenheit
boolean married =

public class
public
public
public
public
}

"Ferdinand";
Flavor.Chocolate;
= 98.6;
true;

Variables {
double nameLength;
double delicious;
double celcius;
double averageLifespan;

public synchronized void compute(double t, double elapsed, Variables v,


Variables o) {
v.nameLength = yourName.length();
switch (iceCream)
case Chocolate:
v.delicious
break;
case Vanilla:
v.delicious
break;
case Strawberry:
v.delicious
break;
}

{
= 10;

= 2;

= -5;

v.celcius = (5.0 / 9.0) * (fahrenheit - 32.0);


if (married)
v.averageLifespan = 77.6;
else
v.averageLifespan = 69.2;

}
}

2.4 Hidden Data


If your computation requires temporary scratch data, they are best stored as local variables within the
computation. There are cases where local variables arent desirable, however:
The scratch data needs to be preserved across invocations of compute()
The large amount of scratch data requires too much time to allocate and garbage-collect
As weve seen, public fields in the model class are treated as parameters. However, you can store scratch
data in private and protected fields. In our OneSine model, suppose we wanted d to constantly increase
instead of being a user-controlled parameter. Then we might do something like this:
:
:
// Define input parameters
public double a = 1;
public double b = 2;
public double c = 4;
private double d = 6;
public void compute(double t, double elapsed, Variables v, Variables o) {
v.y = a * Math.sin(b*t + c) + d;
d = d + 1;
}
:
:

Now d will not appear on the Parameters window; instead it will increase automatically with every iteration
of the compute() method. Also, since d is private, it will not be saved in the Lesson and will always revert to 6
when the model is loaded or reset.

! Note

Why cant we do this for parameters?


public double d;
d = d + 1;

// d is a parameter

Because it breaks the second rule of parameters: they must never appear on the left-hand side of the equals
sign. Only NeuroDynamix II is allowed to modify parameters (on behalf of a user, via the Parameters
window, or in response to loading a lesson). If your model modifies a parameter, users will not see the
change in the Parameters window, and therefore the model output will be unexpected.

! Note

Theres another reason the above code is a bad idea:


d = d + 1;

Why? Because compute() method isnt necessarily called at regular intervals of t. So d will not necessarily
increase at a steady rate over time it will depend on the computation interval chosen by the user. A better
choice would be:
d = d + elapsed;

// elapsed is the simulation time, in seconds

2.5 Variables
Every model must declare an inner class called Variables.
:
:
public class OneSine extends Model {
// Declare output variables
public class Variables {
public double y;
};
:
:

This does not actually define any data; it only declares the Variables class. NeuroDynamix II is responsible for
allocating instances of Variables before calling compute(). After NeuroDynamix II has called your compute()
method a thousand time, there might be a thousand instances of Variables held by NeuroDynamix II to store the
results (this is not actually how it is done, but conceptually this holds true). For a class to be the Variables class
it must meet the following technical requirements
It is public
It is called Variables
It is an inner class of the model class
Its fields must only be of the following data types:
o

double

o arrays of double
o two-dimensional arrays of double
Some other features of the Variables class:
Since the fields are set by the compute() method:
o It does not need a constructor
o The scalar fields do not need to be initialized
o Array fields (such as arrays of doubles) must be allocated, but do not need to be initialized
It may have private fields, but this is not recommended as the values of these fields may be modified
by NeuroDynamix II arbitrarily
It may have methods, but this is not necessary
If it has a constructor, the constructor must be the default constructor (with no arguments) and it must be
public

2.6 Model Constructor


A model is instantiated when a lesson is first loaded, and subsequently every time the model is reset.
The model class may have a constructor, although generally it is not necessary. Thats because all fields can be
initialized using other means:
Parameters will be initialized by lessons or directly (public double k = 5)
Scratch data can be initialized directly (private double j = 10)

Variables do not require initialization because they are computed by the compute() method

The Permutations section will give an example of a situation where a constructor is necessary. If you are not
using permutations, but still wish to have a constructor, it must meet the following criteria:
It must be the default constructor, with no arguments
It must be public
If the constructor throws an exception, NeuroDynamix II will cancel the loading of the model, and will notify
the user of the exception.

2.7

compute() method

The signature of the compute method is


public void compute(double t, double elapsed, Variables v, Variables o)

The arguments of compute() are:


t
Contains the current time t, in seconds. Since time may advance in fractions of a second, this is a
floating-point number. The time here is simulation time which may not advance at the same rate
as real-world time, since the user can speed up or slow down the simulation.
elapsed Contains the number of seconds elapsed since the previous call to compute(). The saves your model
from having to store the previous time t, if it depends on integration steps.
v
Refers to an instance of Variables that compute() should fill in with new values. You can never
count on the values v contains before you fill them in.
o
Refers to the previous instance of Variables that compute() filled in with values. The first time
compute() is ever called, o will contain an instance of Variables that will be initialized with
default values (including default values you included in your declaration of Variables, and its
constructor).

! Java Note

You might notice something funny about the compute() method. Since every model will have its own inner
Variables class, the compute() method cannot be overriding a an ancestor. This method is located by
NeuroDynamix via reflection, and not by calling an abstract function in the base Model class. Because
NeuroDynamix II was originally written under the JDK 1.4, generics were not available.
As an aside, it is unlikely that future revisions of NeuroDynamix II will use generics to solve this problem.
Updating the compute() signature to use generics will be difficult, because the inner Variables class cannot
be used as a generic type parameter in the outer class. The Variables class could be moved out of the model
class to be a sibling class, but this would create its own set of namespace collision problems.

The job of compute() is simply to assign values to all the members of v. For simple models like our OneModel,
this can be done without using the values in o. Lets consider an example that might use o: a model that
generates a value that creeps up and down based on random noise.
:
:
public class Wanderer extends Model {
public double staggerFactor = 1;
public class Variables {

public double vagrant;


}
public synchronized void compute(double t, double elapsed, Variables v, Variables o) {
v.vagrant = o.vagrant + (Math.random() - 0.5) * staggerFactor;
}
}

In this case the new value v.vagrant is based on the previous value, o.vagrant. The first time compute() is ever
called, o.vagrant will be zero because this is the default value for double fields. If we wanted our random line
to start at 100, we would have to specify a default value for vagrant like this:
public class Variables {
public double vagrant = 100;
}

Now the value of vagrant will always reset to 100 when the model is reset, because the reset will initialize a
new instance of Wanderer, and the first time compute is called, o will be a fresh instance of Variables.
Therefore the value of o.vagrant will be 100.
Since Variables is an inner class of Wanderer, we could even do something a little more subtle like:
public class Wanderer extends Model
public double staggerFactor =
public double startingPoint =
public class Variables {
public double vagrant =
}

{
1;
-100;
startingPoint;

public synchronized void compute(double t, double elapsed, Variables v, Variables o) {


v.vagrant = o.vagrant + (Math.random() - 0.5) * staggerFactor;
}
}

Now every time the model is reset, the initial value of o.vagrant will be whatever startingPoint was at the time
the the model was reset! This is because a reset creates a new instance of Variables.

3 Complex Models
Weve started with some simple models with just a few parameters. But a model may require many parameters
in more complicated structures.

3.1 Arrays Of Parameters


Its not unusual to need an array of parameters. NeuroDynamix II supports arrays of doubles, booleans, Strings
and enums only; any other data type is unsupported. Heres a model which generates a skyline of rolling hills
using the composition of three sine waves:
public class RollingHills extends Model {
public double frequency[] = new double[3];
public class Variables {
public double skyline;
}

public synchronized void compute(double t, double elapsed, Variables v,


Variables o) {
v.skyline = 0;
for (int j = 0; j < frequency.length; j++) {
v.skyline += Math.sin(t * frequency[j]);
}
}
}

When running this model, the Parameters window will have two tabs:

The frequency tab shows us our frequency array as a two-column table, with a column for the index and a
column for the value. Because the model contains no non-array parameters, the Parameters tab is empty. The
Parameters tab only contains simple parameters. Array parameters always get their own tabs.

3.2 Matrices of Parameters


NeuroDynamix II supports two-dimensional arrays of doubles, booleans, Strings and enums in a similar
manner. Lets modify our RollingHills to have 9 frequency parameters, in a 3x3 matrix:
public class RollingHills extends Model {
public double frequency[][] = {
new double[3],
new double[3],
new double[3]
};
public class Variables {
public double skyline;
}
public synchronized void compute(double t, double elapsed, Variables v,
Variables o) {
v.skyline = 0;
for (int j = 0; j < frequency.length; j++) {
for (int k = 0; k < frequency[j].length; k++) {

v.skyline += Math.sin(t * frequency[j][k]);


}
}
}
}

The tab in the Parameters window looks like:

One thing youll notice about the code above is the clumsiness of the initialization of frequency.
public double frequency[][] = {
new double[3],
new double[3],
new double[3]
};

Because initializing matrices of doubles is a common operation in models, Model provides a convenience
method:
public double[][] makeDoubleMatrix(int rows, int columns, double value)

Using this, we can initialize frequency as a 3x3 matrix with the value 0 in each element:
public double frequency[][] = this.makeDoubleMatrix(3,3,0);

! Note
NeuroDynamix II does not support more than two dimensions in an array of parameters.

3.3 Structures of Parameters


Now suppose you just have simple parameters (scalar doubles, booleans, Strings and enums), but you have so
many that you want to separate them into two tabs. As we have seen, arrays and matrices of doubles get their
own tabs. But any object that contains only scalar doubles, booleans, Strings and enums also gets its own tab.
Lets consider a more complicated model of two runners in a race, which takes into account stats like height,
weight, and shoes for each runner.
public class TwoRacers extends Model {
public class RunnerStats {
public boolean sprinter = false;
public boolean nikes = false;
public boolean chokes = false;
public double heightFeet = 5.8;
public double weightPounds = 140;
};
public double finishLine = 100;
public RunnerStats stats1 = new RunnerStats();
public RunnerStats stats2 = new RunnerStats();
public class Variables {

public double runner1 = 0;


public double runner2 = 0;
}
public synchronized void compute(double t, double elapsed, Variables v,
Variables o) {
v.runner1 = computeStep(stats1, o.runner1);
v.runner2 = computeStep(stats2, o.runner2);
if (v.runner1 > finishLine && v.runner2 > finishLine) {
v.runner1 = 0;
v.runner2 = 0;
}
}
protected double computeStep(RunnerStats stats, double location) {
double resistance = stats.heightFeet * stats.weightPounds / 10;
double power = 1;
if (stats.sprinter) {
if (location < 100)
power *= 2;
else
power /= 2;
}
if (finishLine - location < 10 && stats.chokes)
power /= 3;
if (stats.nikes)
power *= 1.1;
double stepSize = Math.random() * power / resistance;
return location + stepSize;
}
}

The first part of the model defines a new class called RunnerStats which describes parameters that each runner
will need. This definition does not actually create any data. Only later in the model, when we declare instances
of RunnerStats, are actual parameters created:
public RunnerStats stats1 = new RunnerStats();
public RunnerStats stats2 = new RunnerStats();

Each of these complex parameters gets its own tab:

One refinement to the TwoRunners model would be to move the computeStep() method into the RunnerStats
class itself:
:
:
public class
public
public
public
public
public

RunnerStats {
boolean sprinter = false;
boolean nikes = false;
boolean chokes = false;
double heightFeet = 5.8;
double weightPounds = 140;

protected double computeStep(double location) {

double resistance = heightFeet * weightPounds / 10;


double power = 1;
if (sprinter) {
if (location < 100)
power *= 2;
else
power /= 2;
}
if (finishLine - location < 10 && chokes)
power /= 3;
if (nikes)
power *= 1.1;
double stepSize = Math.random() * power / resistance;
return location + stepSize;
}
};

This makes the computeStep() method a little more elegant. It doesnt affect the display of the Parameters
tabs because NeuroDynamix II only pays attention to member fields, and not methods. If we wanted, we could
even move RunnerStats into a separate .java file, making it a reusable class rather than an inner class bound to
TwoRacers.

3.4 Arrays Of Structured Parameters


Lets take our own advice and move RunnerStats to a separate .java file:
:
:
public class
public
public
public
public
public

RunnerStats {
boolean sprinter = false;
boolean nikes = false;
boolean chokes = false;
double heightFeet = 5.8;
double weightPounds = 140;

protected double computeStep(double location, double finishLine) {


double resistance = heightFeet * weightPounds / 10;
double power = 1;
if (sprinter) {
if (location < 100)
power *= 2;
else
power /= 2;
}
if (finishLine - location < 10 && chokes)
power /= 3;
if (nikes)
power *= 1.1;
double stepSize = Math.random() * power / resistance;
return location + stepSize;
}
};

Now our TwoRunners class is much tidier:


:
:
public class TwoRacers extends Model {
public double finishLine = 100;
public RunnerStats stats1 = new RunnerStats();

public RunnerStats stats2 = new RunnerStats();


public class Variables {
public double runner1 = 0;
public double runner2 = 0;
}
public synchronized void compute(double t, double elapsed, Variables v,
Variables o) {
v.runner1 = stats1.computeStep(o.runner1, finishLine);
v.runner2 = stats2.computeStep(o.runner2, finishLine);
if (v.runner1 > finishLine && v.runner2 > finishLine) {
v.runner1 = 0;
v.runner2 = 0;
}
}
}

Now lets create a new model with a large number of racers. Your first impulse might be to expand the model
like:
public RunnerStats
public RunnerStats
public RunnerStats
public RunnerStats
:
:

stats1
stats2
stats3
stats4

=
=
=
=

new
new
new
new

RunnerStats();
RunnerStats();
RunnerStats();
RunnerStats();

In this case, each stats would get its own tab. But it would make more sense to make an array of RunnerStats.
NeuroDynamix II supports arrays of Objects, so lets try writing a model called ManyRacers using an array.
public class ManyRacers extends Model {
private static final int NUM_RACERS = 4;
public double finishLine = 100;
public RunnerStats stats[] = {
new RunnerStats(),
new RunnerStats(),
new RunnerStats(),
new RunnerStats(),
};
public class Variables {
public double runner[] = new double[NUM_RACERS];
}
public synchronized void compute(double t, double elapsed, Variables v,
Variables o) {
for ( int j = 0; j < stats.length; j++)
v.runner[j] = stats[j].computeStep(o.runner[j], finishLine);
for ( int j = 0; j < stats.length; j++)
if ( v.runner[j] <= finishLine ) return;
for ( int j = 0; j < stats.length; j++)
v.runner[j] = 0;
}
}

When using arrays of Objects, the array appears as a single tab arranged in a table:

One last improvement would be to tighten up our initialization of stats:


public RunnerStats stats[] = {
new RunnerStats(),
new RunnerStats(),
new RunnerStats(),
new RunnerStats(),
};

It is tempting to initialize the array with:


public RunnerStats stats[] = new RunnerStats[NUM_RACERS];

But remember that in Java, this would only create an array of null references, not an array of allocated objects.
Model includes a convenience method for filling in an empty array:
public <T> T[] fillArray(T[] emptyArray, Object... initargs)

This seemingly complex function allows for the initialization of an empty array of objects, with an optional list
of arguments for the constructor. Since RunnerStats has no constructor, we can initialize our array with:
public RunnerStats stats[] = this.fillArray(new RunnerStats[4]);

If RunnerStats had a constructor like RunnerStats(boolean, double) then we would initialize our array like:
public RunnerStats stats[] = this.fillArray(new RunnerStats[4], true, 5.6);

3.5 The Stimulator


Because it is common to want a waveform such as a sine wave, triangle wave or sawtooth wave to be part of
your model, NeuroDynamix II provides such a mechanism for you in the form of the Stimulator. The
Stimulator supports several wave forms and provides easy controls for adjusting frequency, amplitude, period,
and offers support for echo and train repetitions. Lets rewrite our OneSine model using the Stimulator:
public class OneStim extends Model {
// Declare output variables
public class Variables {
public double y;
};
// Define input parameters
Stimulator stim = new Stimulator();
public void compute(double t, double elapsed, Variables v, Variables o) {
v.y = stim.compute(t);
}
}

As you can see, the Stimulator has taken care of all the details. The controls for the Stimulator appear in the
Parameters window as a separate tab:

By now, you should have some idea of how the Stimulator is implemented. It is, in fact, just a structure
type of Parameter just like our RunnerStats example earlier. Stimulator.compute() is just a method just
like RunnerStats.computeStep() was. You can easily invent your own Stimulator-type classes to reuse code
across your models.

3.6 Arrays and Matrices of Variables


In the ManyRacers example, we glossed over one detail: arrays of variables. As you can see in the example
above, for the first time we have included an array of doubles in our Variables class:
public class Variables {
public double runner[] = new double[NUM_RACERS];
}

NeuroDynamix II support both arrays and two-dimensional matrices of doubles in the Variables class.

4 Packaging a Model
4.1 Creating a JAR file
Models are packaged into standard JAR files. A Model called com.mycompany.ndmodels.supermodel should
be stored in the JAR file according to the standard convention:
com\mycompany\ndmodels\supermodel\SuperModel.class
com\mycompany\ndmodels\supermodel\SuperModel$Variables.class

Note that the Variables inner class gets its own class file and, since it is in the same package as its outer class,
appears in the same directory of the JAR file.
Although JAR files generally have a Manifest.mf file, NeuroDynamix II does not use it. Model JAR files do
not need to be signed.

4.2 The default lesson


When a model is opened in the Model browser, NeuroDynamix II will look for a default lesson to be loaded.
The lesson must appear in the same directory of the JAR file as the model class, and must have the name
default.ndl.
com\mycompany\ndmodels\supermodel\SuperModel.class

com\mycompany\ndmodels\supermodel\SuperModel$Variables.class
com\mycompany\ndmodels\supermodel\default.ndl

If there is no default lesson, then the model will be opened with the default values of parameters defined in the
model itself, and with no windows open.

4.3 Help files


A model may have help files available through the Help menu of NeuroDynamix II. The help files must be in
HTML format.

Model help: should be called index.html and should be a sort of home page for the model help.
Appropriate information might be diagrams, background information, and pseudocode for the model.
About the model: should be called about.html and should contain authorship and copyright information
for the model

Both index.html and about.html must be in the same JAR directory as the model class. These HTML pages
may refer to images, CSS files, audio files and links to other HTML pages (which can be in the JAR file or
external links to web pages).
com\mycompany\ndmodels\supermodel\SuperModel.class
com\mycompany\ndmodels\supermodel\SuperModel$Variables.class
com\mycompany\ndmodels\supermodel\index.html
com\mycompany\ndmodels\supermodel\about.html
com\mycompany\ndmodels\supermodel\logo.jpg
:
:

4.4 Multiple models in a single JAR file


For ease of use, multiple models may be packaged in a single JAR file. In order to include default lessons and
help files for each model, we recommend that each model be assigned to its own package.

5 Advanced Topics
5.1 Permutations
In our ManyRacers model, we specified the number of racers in a constant value:
private static final int NUM_RACERS = 4;

If we want to change the number of racers, we must recompile the model. But it is frequently desirable to let
the user choose the size of the model themselves, without recompiling. NeuroDynamix II accommodates this
scenario with the notion of permuting a model. When the user first opens a permutable model, they are
prompted for some initial permutation arguments which determine how the model behaves. To make a model
permutable:
make a public constructor with one or more int permutation arguments
parameters that depend on the permutation arguments (such as arrays of parameters) should be
initialized in the constructor
if variables depend on the permutation arguments (such as arrays of variables), store the value of the
permutation arguments in a private field
Lets rewrite our ManyRacers model to be permutable:
public class ManyRacers extends Model {

private int NUM_RACERS = 4;


public double finishLine = 100;
public RunnerStats stats[];
public class Variables {
public double runner[] = new double[NUM_RACERS];
}
public ManyRacers(int num_racers) {
this.NUM_RACERS = num_racers;
stats = this.fillArray(new RunnerStats[NUM_RACERS]);
}
public synchronized void compute(double t, double elapsed, Variables v,
Variables o) {
for ( int j = 0; j < stats.length; j++)
v.runner[j] = stats[j].computeStep(o.runner[j], finishLine);
for ( int j = 0; j < stats.length; j++)
if ( v.runner[j] <= finishLine ) return;
for ( int j = 0; j < stats.length; j++)
v.runner[j] = 0;
}
}

The changes we have made to make the model permutable are:


NUM_RACERS is no longer a (static final) constant, but a private field
we initialize stats[] in the constructor (after NUM_RACERS is known)
we created a constructor with an int argument
When opened, this model prompts for the value of num_racers:

Obviously, it would be preferable to prompt the user with a sensible question rather than unknown value; to
do this we can annotate the constructor argument with the @Prompt annotation:
public ManyRacers(@Prompt("Number of racers") int num_racers) {
this.NUM_RACERS = num_racers;
stats = this.fillArray(new RunnerStats[NUM_RACERS]);
}

The default value of the permutation argument comes from the lesson being loaded (including the default
lesson), otherwise it will default to 0.

The constructor may have multiple permutation arguments, and they may each be annotated with the @Prompt
annotation. The only requirement is that the arguments are int.

5.2 Custom Parameter Panels


Earlier we saw that the Stimulator is just a regular structure of parameters. We could design our own, called
CustomStimulator like this:
public class CustomStimulator {
static final long serialVersionUID = 0;
// private state variables
enum Waveform {Flat, Sine, Sawtooth, Triangle, Square}
public Waveform wave = Waveform.Sine;
public
public
public
public
public

boolean stimulate = true;


double amplitude = 1.0;
double frequency = 1.0;
double interval = 3.0;
double squareDuration = 0.1; // square waves only

public boolean train = false;


public double trainDuration = 0.75;
public boolean echo = false;
public double echoDelay = 0.1;
public double compute(double t) {
// compute code goes here
}
}

Now we can use CustomStimulator in a model:


public class CustomDemo extends Model {
public CustomStimulator stim = new CustomStimulator();
public class Variables {
public double v;
}
public synchronized void compute(double t, double elapsed, Variables v,
Variables o) {
v.v = stim.compute(t);
}
}

The CustomStimulator appears in the Parameter window as this tab:

If you compare the layout of the CustomStimulators tab to that of the regular Stimulator, you will see that
the Stimulators tab is much more cleanly laid out. Thats because CustomStimulator is relying on
NeuroDynamix IIs default layout strategy, which is to simply list the parameters from left-to right in three
columns, and top-to-bottom. However, a Java programmer familiar with Swing GUI programming can develop
their own custom tab, which is simply a JComponent. To supply a custom JComponent for its tab, the class
(remember, we are talking about a class which meets the criteria described in the Structures of Parameters
section) must implement the CustomParameters interface, which requires one member method:
public JComponent getCustomPanel(AbstractParameterBranch pset)

The argument of type AbstractParameterBranch allows the JComponent to access the model parameters by
name using AbstractParameterBranchs getLeaf() method:
public Parameter getLeaf(String pid)

The returned Parameter object gives you access to the parameters value. Each allowed scalar type has a
subclass of Parameter for accessing it:
Scalar type
double
boolean
enum
String

Subclass of Parameter
DoubleParameter
BooleanParameter
EnumParameter
StringParameter

In the simplest case, to get the value of the amplitude parameter you can write:
DoubleParameter ampParameter = (DoubleParameter)pset.getLeaf("amplitude");
double value = (Double)ampParameter.getValue(null);

Lets make a custom tab for CustomStimulator that uses a JSlider to set the amplitude parameter. To do
that well need to determine amplitudes min, max, and current values in order to initialize our JSlider.
Well make our tab be a subclass of JPanel:
public class CSPanel extends JPanel {
JSlider ampSlider;
DoubleParameter ampParameter;
public CSPanel(AbstractParameterBranch pset) {
ampParameter = (DoubleParameter)pset.getLeaf("amplitude");
ampParameter.getSpinnerModel().addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
double oldValue = ampSlider.getValue();
Double value = (Double)ampParameter.getSpinnerModel().getValue();
if ( value.doubleValue() != oldValue ) ampSlider.setValue(value.intValue());
}
});

Double min = (Double)ampParameter.getSpinnerModel().getMinimum();


Double max = (Double)ampParameter.getSpinnerModel().getMaximum();
Double value = (Double)ampParameter.getSpinnerModel().getValue();
ampSlider = new JSlider(JSlider.HORIZONTAL, min.intValue(), max.intValue(),
value.intValue());
ampSlider.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
double value = ampSlider.getValue();
double oldValue = (Double)ampParameter.getSpinnerModel().getValue();
if ( oldValue != value ) ampParameter.setValue(value);
}
});
this.add(ampSlider);
}
}

Note that DoubleParameter gives us access to amplitudes min and max values through the
getSpinnerModel() interface.
A JSlider is actually a bad choice for setting a double, because JSliders only set integer values. JSpinners
are a more natural choice, and are the default for NeuroDynamix II. In fact, DoubleParameters
getSpinnerModel() makes it very easy to create a JSpinner to control a double parameter. We can rewrite
the above constructor to use a JSpinner by reducing it to:
public CSPanel(AbstractParameterBranch pset) {
ampParameter = (DoubleParameter)pset.getLeaf("amplitude");
ampSpinner = new JSpinner(ampParameter.getSpinnerModel());
this.add(ampSpinner);
}

The other subclasses of Parameter offer similar interfaces to easily create JCheckboxes, JComboBox, and
JTextfields.
When making a subclass of JPanel (as opposed to subclassing JComponent) it is extremely common to create
the above components based on a Parameters true class. To facilitate this scenario even further,
NeuroDynamix II offers a subclass of JPanel with a convenience function:
protected JComponent createComponent(Parameter p)
createComponent returns a component (JCheckbox, JSpinner, etc) that is appropriate for the parameter type

(DoubleParameter, BooleanParameter, etc). The returned component includes a text label identifying the
parameter. This is the function that the default Parameter pane uses.
As you can see, writing a custom Parameter panel can be somewhat involved. When not using the default
component types (JSpinners, JTextfields, etc) the most important thing is to connect the state of the GUI
component to the state of the Parameter in both directions. Remember that a user can have multiple Parameter
windows open, and parameter changes in one window should propegate to all windows. Each Parameter type
has a model for doing this: a SpinnerModel for doubles, a ChangeListener for booleans, a ComboBoxModel
for enums, and a Document for Strings.

!Java Note

When sending and receiving change notifications with a Parameter, it is vital that you avoid endless
feedback loops. In our example above, we compared the old value of the parameter with the new value,
and if they are equal, we discard the change with no further action.
One final note on this topic. The Parameters tab of the Parameters window does not belong to a structure
parameter; instead it collects the scalar parameters of the model itself. In order to customize the Parameters
tab, the model class itself should override the getCustomPanel() method itself. The base class, Model,
already implements the CustomParameters interface!

5.3 Visualization
In addition to graphing variables in the Scope, a model can also provide a window containing a custom,
arbitrary rendering of the models state, called a visualization of the model. This rendering can reflect the status
of both variables and parameters, which means that it must be repainted frequently to animate the visualization.
To offer a visualization, a model must implement
public Visualization<Variables> getVisualization()
Visualization<Variables> is a JPanel with three additional methods:
boolean notifyUpdate()

Variables getVariables()
void init()

override this to be notified in order to prepare for a


redraw of the visualization. Should return true if the
visualization needs an explicit repaint()
call this to find out the values of the current Variables
this is called by the constructor; it is useful for adding
components to an anonymous inner subclass of
visualization

notifyUpdate() is called before the panel is repainted. It gives the visualization a chance to make adjustments
to its child components if necessary. If those adjustments (say, JLabel.setText()) are sufficient to repaint the
visualization, then notifyUpdate() should return false to avoid a spurious repaint. Returning true will
cause the entire visualization to be repainted.

NeuroDynamix II does not call notifyUpdate() every time compute() is called in the model. In fact,
notifyUpdate() will never be called unless variables and parameters are marked as relevant to the
visualization, with the @Visualized annotation. For example, if we were writing a visualization for OneSine,
and we wanted the visualization to be updated every time parameter b is changed we would write this:
public double a;
@Visualized public double b;
public double c;
public double d;

This avoids spurious repaints which could affect overall performance. This is even more true for fields in
Variables, which also must be marked with @Visualized if changes to the variable must trigger a repaint of
the visualization.
public class Variables {
@Visualized public double y;
};

Even if a variable changes with every call to compute(), the visualization will not be repainted on every
compute(). compute() may be called thousands of times a second, but typically notifyUpdate() will only be
called a few times per second, for performance reasons.
There are two approaches for building a visualization:
override the visualizations paintComponent() method, using Graphics2D to render the contents
fill the visualization with child components, and update their contents whenever notifyUpdate() is
called

5.3.1

Overriding paintComponent()

In the simplest case, just override paintComponent() to create a custom component. To add a simple
visualization to OneSine, to show a red circle which fades to black depending on the value of y, something like
this is sufficient:
@Override
public Visualization<Variables> getVisualization() {
return new Visualization<Variables>() {
@Override
protected void paintComponent(Graphics g) {
Variables v = getVariables();
if ( v == null ) return;
double red = (v.y - d) / (2*a) + 0.5;
Color color = new Color((float)red, 0, 0);
g.setColor(color);
g.fillOval(0, 0, getWidth(), getHeight());
}
};
}

The usual rules for Swing component programming apply here, and it is advisable to specify a desired size for
the visualization by overriding getMinimumSize():
@Override
public Dimension getMinimumSize() {
return new Dimension(200, 300);
}

5.3.2 using child components


The other option is to fill your visualization with child components. Then you can change their contents in the
notifyUpdate() method. In most cases, the child components will repaint themselves when adjusting their
values, so you can supress a global repaint of your component by returning false from notifyUpdate().
Heres an example visualization for our OneSine model that shows the formula being used. Note how we
override init() to add formula to our anonymous inner class!

@Override
public Visualization<Variables> getVisualization() {
Visualization<Variables> vis = new Visualization<Variables>() {
protected JLabel formula;
@Override

public boolean notifyUpdate() {


formula.setText("y=" + a + "*sin(" +b+ "t+" + c + ")+" + d);
return false; // formula.setText forces repaint of formula
}
public void init() {
formula = new JLabel();
this.add(formula);
}
};
return vis;
}

This is stylistically different than the method of overriding paintComponent(). For one thing, the size of the
visualization is determined by the sizes of its children. And the call to formula.setText() forces a repaint of
formula, which means that we do not require a repaint of the whole visualization so we return false from
notifyUpdate().

6 The Development Process


6.1 Setting Up NeuroDynamix For Development
In NDX,
go to ToolsPreferences
choose the Development Tab
Set the JDK Home to point to the JAVA_HOME of your JDK 1.6 (download this from
http://www.oracle.com/technetwork/java/javase/downloads/index.html if you dont have it)
Set the NeuroDynamix API to point to the JAR file of the NeuroDynamix API file (if you dont have
this file it can be downloaded from http://www.neurodynamix.net/dist)

Press OK to save your changes

6.2 Enter Development Mode

Lets suppose you want to develop a model called OneModel.

Create a subdirectory of your src directory called OneModel


Choose FileDevelop
Navigate to the new OneModel subdirectory
Choose the name for a new Java file called OneModel.java (so this file will be
src\OneModel\OneModel.java)

When you press Open, OneModel.java will be created as a template, along with a template index.html
and about.html file; you will be prompted for these file creations:

ND will automatically compile the template model; the model folder will now contain these files:

6.3 Develop
Your source can be edited in Notepad or, if you have a favorite IDE like Eclipse or JBuilder, you can edit your
code in those environments (if you use an IDE, then you should add NeuroDynamix.jar to your build path so
that the IDE can resolve references to the ND API).

6.4 Build and Publish


In Development mode, ND will have a new menu called Develop.
DevelopOpen Default Lesson and DevelopSave Default Lesson allow you to package a default
lesson with your model (in practice the default lesson is rarely used)
DevelopBuild will compile your Java source and package the resulting class files, along with any
HTML files and default models, into a JAR file.
DevelopPublish will send your model to the NeuroDynamix model repository so that it can be used by
other users. Those other users will need a Lesson file to access the model, which you will need to
provide to them separately via e-mail or posting on a website; when they open the Lesson file
NeuroDynamix will automatically download the current version of your model from neurodynamix.net.

7 References
7.1 Java Language Resources
Some basics of the language and its data types:
http://java.sun.com/docs/books/tutorial/java/nutsandbolts/index.html

7.2 Swing Resources


A detailed tutorial on Java Swing (for writing Visualizations and custom parameter tabs)
http://java.sun.com/docs/books/tutorial/uiswing/index.html

7.3 Java2D Resources


A detailed discussion of Java2D (for writing custom Visualizations with their own paint routines)
http://java.sun.com/docs/books/tutorial/2d/index.html

8 Glossary
Model
Parameters
Variables

Reflection
POJO
Functions
Arguments
Fields
Methods
Inner class
Constructor
Signed JAR files
Default.ndl
Permutation
Visualization
permutation arguments
anonymous inner subclass

You might also like