My strategy is to divide the tasks up according to current injection amplitude so that each task is assigned a single amplitude to test with a specified number of repeats. Each repeat generates a random number from a normal distribution and sets that to the stimulation delay. The voltage output is collected into a matrix and the max value is tested. Thus each task posted to the bulletin board does the following:
1. saves a unique data file with data for a specific amp
2. returns the max depol to the master process
#1 is pretty straight-forward; it should work as long as there isn't a conflict over file access (and that shouldn't happen if we handle our files correctly). #2 is where using the PC class requires some special attention.
The way the demo works, is that each task packs its IClamp amplitude and max depolarization into a message, which is then posted with key value grabbed from hoc_ac_. The master task then pulls the message and unpacks the two values.
In case this might be of use to anybody else, I've included a full listing.
Code: Select all
// test ParallelContext class
/////////////////////////////
// file/directory handling; loading standard run library
strdef homedir, libdir, nrndir, buffdir
homedir = getcwd()
nrndir = neuronhome()
if (unix_mac_pc() == 3) {
sprint(buffdir,"%s\\lib\\hoc\\",nrndir)
} else {
sprint(buffdir,"%s/lib/hoc/",nrndir)
}
chdir(buffdir)
xopen("stdrun.hoc")
chdir(homedir)
/////////////////////////////
// parameters
TSTOP = 1000 // msec
DT = 0.1 // msec
V_INIT = -70 // mV
integrator = 1 // 0 -> fixed time step; 1 -> adaptive time step
ATOL = 0.01
MEAN = 0 // mean rand IClamp delay (msec)
VARIANCE = 5 // variance rand IClamp delay (msec)
REPEATS = 3
SEED = TSTOP/1000+ATOL+MEAN+VARIANCE-REPEATS // seed for RNG
AMP_START = 0.01 // IClamp starting amp (nA)
AMP_STOP = 0.03
AMP_STEP = 0.01
/////////////////////////////
// simulation setup
objref cvode
cvode = new CVode()
cvode.active(integrator)
cvode.atol(ATOL)
/////////////////////////////
// cell setup
create patch
objref recsec
forall {
L = 10
diam = 10
nseg = 1
insert pas
Ra = 150
e_pas = V_INIT
}
patch recsec = new SectionRef()
/////////////////////////////
// electrophys setup
objref stim
recsec.sec stim = new IClamp(0.5)
stim.del = TSTOP/2
stim.dur = 10
stim.amp = 0
/////////////////////////////
// setup simulation run
objref xeven, yeven, tvec, vvec
xeven = new Vector()
xeven.indgen(0,TSTOP,DT)
obfunc run_simulation() {
steps_per_ms = 1/DT
dt = DT
tstop = TSTOP
v_init = V_INIT
tvec = new Vector()
vvec = new Vector()
recsec.sec cvode.record(&v(0.5),vvec,tvec)
yeven = new Vector(xeven.size())
run() // calls stdrun library procedure; includes standard init() procedure
yeven.interpolate(xeven,tvec,vvec) // generate vector of data points with evenly-spaced time intervals
return yeven
}
/////////////////////////////
// setup ParallelContext
objref pc
pc = new ParallelContext()
print "number of hosts: ", pc.nhost(), "\thost id: ", pc.id()
objref rand, data_buffer, data_matrix, file
strdef filename
data_matrix = new Matrix(TSTOP/DT+1,REPEATS+1) // 0th column is time vector; the ith data vector goes into the ith+1 column
data_buffer = new Vector()
rand = new Random(SEED)
rand.normal(MEAN,VARIANCE)
func main() { local key, i, num_repeats, amplitude, MX
MX = -1e9
key = hoc_ac_
num_repeats = $1
amplitude = $2
print "host: ", pc.id(), "\tkey: ", key, "\trepeats: ", num_repeats, "\tamplitude: ", amplitude
stim.amp = amplitude
for i = 0,num_repeats-1 {
stim.del = TSTOP/2 + rand.repick()
data_buffer = new Vector()
data_buffer = run_simulation()
data_matrix.setcol(i+1,data_buffer)
if (data_buffer.max() > MX) {
MX = data_buffer.max()
}
}
data_matrix.setcol(0,xeven) // put time vector into 0th column
sprint(filename,"%f.data",amplitude)
file = new File()
file.wopen(filename)
data_matrix.fprint(0,file,"%f\t","\n")
file.close()
pc.pack(amplitude)
pc.pack(MX)
pc.post(key)
return key
}
/////////////////////////////
// perform parallel simulations
pc.runworker()
objref z, maxV
z = new Vector()
z.indgen(AMP_START,AMP_STOP,AMP_STEP)
maxV = new Vector(z.size())
for i = 0,z.size()-1 {
pc.submit("main",REPEATS,z.x[i])
}
while ((id = pc.working()) != 0) {
pc.look_take(id)
amp = pc.upkscalar()
mx = pc.upkscalar()
index = z.indwhere("==",amp)
maxV.x[index] = mx
}
pc.done()
print "max depols: "
maxV.printf()
It seems it is generally a good idea to avoid using hoc_ac_ because of its volatility, but the demo seems to work properly. From the documentation, it appears that using pc.submit with an explicit userid *excludes* the ability to unpack input arguments, and this was a restriction that I don't want to impose if I can avoid it. (To quote the documentation, "If there is no explicit userid, then the args (after the function name) are saved locally and can be unpacked when the corresponding working call returns" implies to me that if there is an explicit userid, than this cannot be done.) The indwhere() statement is a little ugly and I'd like to achieve the same result as using a userid with pc.submit, but without the restriction. I can change the FOR and WHILE loops like this to explicitly supply what is in effect a userid as the first argument to the main() procedure:
Code: Select all
func main() { local key, i, num_repeats, amplitude, MX
MX = -1e9
key = $1
num_repeats = $2
amplitude = $3
...
pc.pack(amplitude)
pc.pack(MX)
pc.post(key)
return key
}
for i = 0,z.size()-1 {
pc.submit("main",i,REPEATS,z.x[i])
}
while (pc.working()) {
key = pc.retval()
pc.look_take(key)
amp = pc.upkscalar()
mx = pc.upkscalar()
maxV.x[key] = mx
}
1.) For what I'm trying to do here, are there any subtle (or maybe not subtle) issues that make one method of master/worker communications superior to the other? There seem to be several ways to do this communication and it's not clear to me when one method should be preferred over another.
2.) What benefit is there to supplying an explicit userid to pc.submit other than the fact that one can get its value with a call to pc.userid(), given that doing so might restrict unpacking arguments and the fact that one can get nearly the same effect by using an additional argument to supply a "userid"? Also please note that I have attempted to avoid writing to "global" variables from within the task functions and my task doesn't submit child tasks, so I should avoid the issues discussed in the pc.working() documentation.
In the end, it seems that the safest, most powerful, and efficient strategy is to supply a "userid" as an argument to the task function: it has almost all of the benefits of having a userid (except that it can't be referenced by pc.userid()), doesn't restrict packing/unpacking arguments, and allows easier "dereferencing" in the pc.working() loop at a cost of a minimal amount of overhead.