Overview

The Process you've created can now be controlled, generating a dynamic behaviour dependent on the parameters we pass to it. Processes of this sort can be described as "source" processes, because their output is derived entirely from their initial state - that is, they don't take any inputs. Most processes you write will want to take some input, at least. Let's try this, now.

(Aside) Adding a second process to the system

This sort of stuff is really covered elsewhere (in Developing Systems), so we're not going to dwell on it. We're just going to add a source of numeric data to the system, that we can use as an input to our process. We're then going to link its output to the input of our process, calling the input Port "drive".

Modify your test script as follows, to add the source.

M Source Code (against 995)
... cls = 'dev/quickstart/1199'; sys = sys.addprocess('neuron', cls, fS, state);
% add source
state = [];
state.data = 1;
state.repeat = true;
state.ndims = 1;
sys = sys.addprocess('source', cls, fS, state);
 
% link source to our process
sys = sys.link('source>out', 'neuron<drive');
% execution exe = brahms_execution; ...

You can go ahead and run this immediately, without modifying your process. Nothing will be different - the process has been presented this input, but it has ignored it, and this is quite acceptable in BRAHMS.

Validating an input is present

Modify your source file as follows to check that an input named "drive" was presented, and store it amongst our state. Since we don't need to see our inputs to create our outputs, we can safely do all our input validation on the last call to EVENT_INIT_CONNECT. Run your test script afterwards to check it all works OK.

C++ Source Code (against 1199)
/* Private member data of the process goes here. You can use this to store your parameters and process state between calls to event(). */
// an (numeric) input port
numeric::Input input;
// an (numeric) output port numeric::Output output;
C++ Source Code (against 1199)
// on last call if (event->flags & F_LAST_CALL) {
// attach an input called "drive"
input.attach(hComponent, "drive");
}

Once this is working, you might try temporarily changing the test script to call the input something else (e.g. neuron<other), and you'll see what happens if the correct input isn't presented.

Note that through using the numeric::Input interface, the correct class of the input (std/data/numeric) is automatically validated for us.

Validating input Structure

Often, a process will handle inputs with many different structures (number of dimensions, dimensions themselves, even different Numeric Types). Our process, however, is going to insist on a simple real, scalar, DOUBLE-type input as the current drive to its cell membrane. Modify your source file as shown.

C++ Source Code (against 1199)
// attach an input called "drive" input.attach(hComponent, "drive");
// validate structure
input.validateStructure(TYPE_REAL | TYPE_DOUBLE, Dims(1));

Reading input

OK, so we've validated that (a) we got an input called "drive", (b) it was a std/data/numeric input, and (c) it had the expected structure (real scalar double, in this case). Let's now use the content of the input to drive our membrane. Like writing output, we read input on each call to EVENT_RUN_SERVICE, and we have to resolve the port to a data object to read it. Modify your source file as follows, and run the test script.

C++ Source Code (against 1199)
case EVENT_RUN_SERVICE: {
// get a pointer to contents of input (actual numeric data)
DOUBLE* d = (DOUBLE*) input.getContent();
// run dynamics DOUBLE fS = sampleRateToRate(time->sampleRate); DOUBLE lambda = exp(-1.0/(tau * fS)); currentMembranePotential *= lambda;
// now use the input data to drive the membrane
currentMembranePotential += d[0] / fS;
...

With the input driving the membrane, you should get a plot that looks something like this.

Whoops. We're not driving this neuron very hard, I don't think (though, notice it's running a little hotter than before)...

New dynamics

Let's finish the job on the dynamics, by detecting threshold-crossing (membrane > 1.0), resetting the membrane, and generating a spike on the second output channel. Modify your source code as follows.

C++ Source Code (against 1199)
// now use the input data to drive the membrane currentMembranePotential += d[0] / fS;
// handle threshold crossing
DOUBLE spike = 0.0;
if (currentMembranePotential > 1.0)
{
currentMembranePotential = 0.0;
spike = 1.0;
}
// get a pointer to the port's contents (actual numeric data) DOUBLE* p = (DOUBLE*) output.getContent(); // it's got two elements, so let's write them both p[0] = currentMembranePotential;
p[1] = spike;

Let's also make a modification to the parameters in our test script so that the behaviour is a bit more interesting.

M Source Code (against 995)
% add source state = [];
state.data = 25;
state.repeat = true; ...

And let's update our plot commands so we can see the spiking output as well as the membrane potential.

M Source Code (against 995)
% show output figure(1) t = ((1:length(out.neuron.out))-1)/fS;
clf
hold on
plot(t, out.neuron.out(1, :))
plot(t, out.neuron.out(2, :) + 1.2, 'r')
axis([0 exe.stop -0.5 2.5])
xlabel('time (seconds)'); ylabel('membrane potential (unitless)')

Ta-da! Our neuron is doing its thing.

Now - try changing the sample rate of the execution. You should find that sample rates all the way down to 25Hz still result in oscillatory behaviour, and, of course, you can raise the sample rate as high as you like and just increase the accuracy of the integration, though you'll notice things starting to slow down a bit if you go much above 1MHz.

More than one input

Finally, we're going to add a second input, just to illustrate how it's done. This neurons a bit unrealistic at the moment, so let's add some current injection noise. There's a random number generator in the Standard Library, at std/random/numeric, so we'll add an instance of that to the system, parametrize it appropriately, and link it to our neuron.

M Source Code (against 995)
... % link source to our process sys = sys.link('source>out', 'neuron<drive');
% add RNG
state = [];
state.dims = 1;
state.dist = 'normal';
state.pars = [0 50];
state.complex = false;
seed = 42;
sys = sys.addprocess('rng', cls, fS, state, seed);
 
% link RNG to our process
sys = sys.link('rng>out', 'neuron<noise');
% execution exe = brahms_execution; ...

Next, we'll have to modify our source file to validate this input, read it, and use it as an additional input to the membrane.

C++ Source Code (against 1199)
// an (numeric) input port numeric::Input input;
// an (numeric) input port
numeric::Input noiseInput;
// an (numeric) output port numeric::Output output;
C++ Source Code (against 1199)
... // validate structure input.validateStructure(TYPE_REAL | TYPE_DOUBLE, Dims(1));
// attach and validate noise input
noiseInput.attach(hComponent, "noise");
noiseInput.validateStructure(TYPE_REAL | TYPE_DOUBLE, Dims(1));
}
C++ Source Code (against 1199)
// get a pointer to contents of input (actual numeric data) DOUBLE* d = (DOUBLE*) input.getContent();
// get a pointer to contents of noise input (actual numeric data)
DOUBLE* n = (DOUBLE*) noiseInput.getContent();
// run dynamics DOUBLE fS = sampleRateToRate(time->sampleRate); DOUBLE lambda = exp(-1./(tau * fS)); currentMembranePotential *= lambda; // now use the input data to drive the membrane
currentMembranePotential += d[0] / fS + n[0] / fS;

Nice. We've a realistic (ahem...) neuron, taking two inputs (current drive and membrane noise), maintaining internal dynamics, and outputting a two-element output for other Processes, or the user, to view.

Notes
  • We've used the Standard Library RNG, and hard-coded the seed, so your plot should actually look identical to the one displayed here.
  • In addition, it should look identical regardless of which language you have authored in. Perhaps unsurprisingly, but hey.

Where do I go from here?

You should note that it is not necessary to name inputs. You can, rather, review what inputs you were passed by index instead. In addition, you can offer input Sets, if required, but this is not something you should adopt unless there is a real need - most processes don't need Sets. See Using Sets under Advanced Topics.

To find out more about how you can interact with your inputs, review the documentation for SystemMLInterface.