Programmers Guide PDF
Programmers Guide PDF
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.
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)
parameters
= 1;
= 2;
= 4;
= 6;
}
}
As you can see, weve written a class called OneSine that extends
com.pitchphork.neurodynamix.model.Model.
public class OneSine extends Model {
! Java Note
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
!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;
{
= 10;
= 2;
= -5;
}
}
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
// 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
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;
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
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
! 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 {
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;
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.
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.
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.
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();
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;
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.
RunnerStats {
boolean sprinter = false;
boolean nikes = false;
boolean chokes = false;
double heightFeet = 5.8;
double weightPounds = 140;
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:
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);
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.
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.
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.
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
:
:
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 {
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.
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());
}
});
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()
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);
}
@Override
public Visualization<Variables> getVisualization() {
Visualization<Variables> vis = new Visualization<Variables>() {
protected JLabel formula;
@Override
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().
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).
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
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