Replies: 22 comments 2 replies
-
@halfflat : here is the relevant ticket to your question during CNS. Above is just draft from first discussion a year ago. We haven't started any implementation aspects yet but if you have experiences from your implementation and usage, it will be great to have your inputs. |
Beta Was this translation helpful? Give feedback.
-
One question: what responsibility do we have to engage the broader community on the NMODL language description now that other tools (e.g. Arbor) use NMODL? |
Beta Was this translation helpful? Give feedback.
-
In my previous comment I cc'd Sam Yates who is working with Arbor and knows NMODL/Random123 aspects very well. (@bcumming : tagging you as well in case Sam is away during summer vacation). This item is on Friday's meeting agenda so hopefully we will discuss it there. |
Beta Was this translation helpful? Give feedback.
-
Oh that's great. Thanks. Another thing to consider, should it be possible to say |
Beta Was this translation helpful? Give feedback.
-
I don't have a use case for GLOBAL in this context. However it does raise the question about default id1,id2, id3 stream indices and initialization. Naively, 0,0,0 will make all instances generate the same stream, and initialization when the INITIAL block should be at the choice of the user. In other words, perhaps there should be an error or warning if the user does not at least call setseq. Also, setseq(r, ...) fits into standard NMODL function syntax, but it could be made to look more object oriented. |
Beta Was this translation helpful? Give feedback.
-
Another question came to discussion yesterday : Do we need name this interface as |
Beta Was this translation helpful? Give feedback.
-
Thanks for including me in the conversation! We don't yet have a mechanism for specifying randomness in the NMODL that we parse in Arbor, though we will need one shortly for the case of modelling a stochastic ODE. We have though of course been discussing this among ourselves, and we're likely to pursue a less explicit approach, where randomness is included by introducing terms describing stochastic processes. Random123 then is an implementation matter. An aspirational goal is reproducibility across different distributions of the model across ranks and groups; we correspondingly aim to tie each stochastic source to a pseudo-random sequence that is determined entirely by its 'location' in the model definition, together with any run-time seed data given at simulation time. Random123 is naturally a really good fit for this sort of approach. |
Beta Was this translation helpful? Give feedback.
-
I wanted to pick up this topic again.. In the meantime we also had a discussion in the dev meeting (here). Two main questions I am wondering about:
|
Beta Was this translation helpful? Give feedback.
-
I think a special meeting would be useful. Looking at slide 11 of the above dev meeting link. I'm indecisive about whether seed and setseq need to be explicit as opposed to r.start() which itself I can imagine the user may sometimes want (to reproduce an earlier sim) or not want (just continue where it last left off). At any rate it doesn't seem to be relevant in the mod file itself. On the other hand, specification of the distribution does seem relevant within the mod file. I'm also on the fence with regard to whether Random should be in the NEURON block or a block of its own. Not declared in the NEURON block if the random variable is not really managed by the interpreter. @halfflat mentions an intrigueing aspiration: Declaration only of the name and distribution of a random variable in the mod file and everything else is behind the scenes. |
Beta Was this translation helpful? Give feedback.
-
After having met with Michael and other members of the dev team, here is how we think this new language feature should look like: Declaration
where:
Example variable definition:
Three uniformly i.i.d random variables over the range (0,1)
An exponetially distributed random number with parameter lambda=2.0. UseThe random variables can be used in any block of the NMODL file (e.g. Questions
I would really appreciate more feedback (if any) so we can nail down some more details and try out a prototype implementation. |
Beta Was this translation helpful? Give feedback.
-
Sounds good. Some questions:
|
Beta Was this translation helpful? Give feedback.
-
That is correct, but it is also the case that the NEURON block generally means to me what the model wishes to expose to NEURON. Ie. should the random variable be declared as GLOBAL or RANGE as for PARAMETER and ASSIGNED. In that case Are the parameters constants, or can they be the names of parameters. In practice, the negexp distribution always had the parameter 1.0 and the user multiplied the picked value by lambda (which had units of (ms)). Similarly it was generally the case that for the normal distribution, the mean was 0.0 and the variance was 1.0 and the picked value would be x*var + mean, again with consistent units. Not all distributions have this normalization property, e.g. poisson but I hate to drag units into the discussion if it would be possible to say all parameters and pick results are dimensionless. I would vote for default "seed is fixed to a constant value to allow exact reproducibility" but also my opinion is that it is not the business of the model description to specify that kind of higher level user specifiable property I.e. in what way is the random variable not random. Sometimes runs are looking for reproducibility for parallel debugging and sometimes runs are collecting statistically independent results. I guess I would change my vote to say that it is a higher level simulator property and subject to implementation feasibility. Nothing about it can be specified in nmodl and nothing about it is implicit in the nmodl translation. In my opinion a sample implementation can begin and end with Random123 and it suffices to allow distributions of uniform, negexp, and normal. I'd go so far as to say the user can leave out the () and expect the normalized distribution.
This is nice if it is implementable. With respect to cvode, the only safe place to do it is in a BEFORE STEP block. It may be satisfactory to follow the policy of Random.play . See https://neuronsimulator.github.io/nrn/py_doc/programming/math/random.html#Random.play which, in a kind of backhanded |
Beta Was this translation helpful? Give feedback.
-
Backing up a step and speaking for nobody other than myself... My original conception of this was as a way of eliminating a class of VERBATIM block usage by standardizing random specification. I still think this, by itself, would be progress. I love the idea of random variables that know to resample themselves at the beginning of each timestep and then act as constants throughout. That said, I cannot of any analogous case of specifications that have values that change randomly without an explicit assignment, and I could see this as a surprise like you'd get from a global variable that isn't set anywhere you can see. In XPPAUT, an equation could directly invoke e.g. normal(0,1) as in this example:
Since order of equations mostly doesn't matter in XPPAUT, the same number could presumably be reused within a timestep by explicitly assigning it to a variable. We could do the same. The upshot of a function-based approach is that it doesn't require any changes to the parser to allow picking from a (presumably) location-aware random stream. Ideally, each instance pulls from its own stream so that we don't have to worry about what is evaluated in what order (thus avoiding any sort of procedural programming type side effects). Another option that I like less, but just to have it on the table, would be to introduce a bit of object-orientedness into NMODL and allow a Random class that's essentially the same thing as NEURON's Random class. Weak objection: it's possibly tied too closely to NEURON and it's not clear what this would mean in e.g. Arbor. Stronger objection: I'm not sure if this makes NMODL farther away from a declarative format but I don't think it helps. Finally, I think @nrnhines 's point about NET_RECEIVE calling functions vs other function calls is something that a solution would have to address. |
Beta Was this translation helpful? Give feedback.
-
Thanks a lot for the feedback!
Yes, these variables would be implicitly always RANGE, and now that you mention it, actually it does make sense that they are in the NEURON block, because they should be exposed the NEURON as we want to be able to do advanced / user-defined manipulations via the interpreter interface. The user doesn't have to do anything with those variables from the NEURON side, but they can if they wish to (e.g. to set the seed)
I think it should be possible to use constants or parameters. To me this makes the whole random distribution concept much more complete and clean if I can set the distribution with it's parameters at declaration and don't ahve to worry about applying the transformation myself whenever I sample. I "think" it shouldn't be much harder to provide this as part of the specification and it would clean up the NMODL code some more by making such transformations unnecessary.
Absolutely, but we still need to pick one behavior that happens by default. The behavior is not changed in the NMODL code because as you said that is business of the simulator and not the model description. But if in the simulator we don't set anything either, then implicitly we assume that the random number seed is fixed (or not...).
OK, cool, then we begin with uniform, negexp and normal as distributions. I'm wondering if we should make the () optional. The thing is, when this is optional, then the user will have to look up the defaults, arguably it's quite obvious here, but maybe safer to simply ask the user to set them and then it's written down right in the model.
Regarding this: After reading @nrnhines and @ramcdougal 's comments, I think that as a concept it would be nice to have this, but in practice it will be complicated to make it work correctly. Additionally as @ramcdougal points out, this may surprise users quite a bit, since it is not something typically seen. I would say we better keep it simple. Whenever a random variable is accessed, a new value is sampled from the distribution. If we would like to reuse a value to do multiple computations (e.g. in multiple CVODE steps) then we must first do a The idea of using a function syntax instead is interesting too, it would certainly simplify things for us, but at the same time, I find it will be quite wordy, and I sort of like the neat/clean code that random variables would introduce. Would you agree that with above simplification (using the variable always means we are resampling) we get a simple syntax without the semantic ambiguity and complications? It also shouldn't be a problem for the parser, or am I missing something? Thanks again @nrnhines and @ramcdougal for your input! |
Beta Was this translation helpful? Give feedback.
-
That reminds me of an issue I didn't think very consistently about. What does access to the random variable even mean in the interpreter? We do not want it to mean the pick of a new random value. I was thinking in terms of properties such as initialization of the stream (and stream identifiers). And setting distribution parameters for the random variable. That seems very much like an object. But that seems analogous to the There is the edge case of NetStim without a gid in a parallel sim where the NetStim must remain on the same process as its targets (and for CoreNEURON, in the same thread). That complicates the automatic location choice of stream id (often determined by the (single) target the NetStim is connected to (that does have a gid)). |
Beta Was this translation helpful? Give feedback.
-
Exactly my thinking. The interpreter access the object that is controlling the random variables in the model
Not sure I understand this exactly.. do you mean that objects can be created from the interpreter without being declared in the model too? Where would the "live"? Not even sure if my question makes sense.
To me one |
Beta Was this translation helpful? Give feedback.
-
As far as that can be carried out, I prefer it. But there are instance specific properties such as the stream id triple and (of less importance) the current sequence value. And the distribution parameters? The latter two will almost 100% of the time be the same for all instances. ie. By default the current sequence value set to 0 on initialization for all instances (of a specific random variable type). Typical access to mod file variables are on a per instance basis (RANGE as opposed to GLOBAL). This does not And now I can't remember. Were we going to do the same thing for hoc Vector? ie. language support to avoid VERBATIM
Interpreter objects can be created from mod file statements (in VERBATIM blocks). The relevant interface for hoc Vector is
Where in mod VERBATIM Vect* and Object* are replaced by void*. Hoc Random has a few of these also but repick, ids, and set_sequence are the only ones I recall and I don't think the constructor is available. |
Beta Was this translation helpful? Give feedback.
-
If I change nseg on the apical dendrites, would that change the random streams used on the basals? (It'd be best if not.) |
Beta Was this translation helpful? Give feedback.
-
I think the algorithm for converting (cell, section, x, mech, name) -> (id1, id2, id3) should preserve already existing stream id's as much as possible. |
Beta Was this translation helpful? Give feedback.
-
I have an update from Arbor, now that we implemented support for stochastic differential equations. To start, I will refer to some pages in the Arbor docs. General documentation about support for SDEs in Arbor: An illustrated example of its use in a single cell model: Documentation of the NMODL extension that supports this feature: This has been implemented such that a model will generate the same results between runs with the same seed, and it will produce the same results if the number of threads/MPI ranks is varied between runs, and if GPUs or CPU back ends are used. The feature was implemented by Fabian, @boeschf. |
Beta Was this translation helpful? Give feedback.
-
Just to keep the different pieces of information linked in one place. Here's a gist I had put together a while back with some example generated code: |
Beta Was this translation helpful? Give feedback.
-
Update on syntaxWith @pramodk I've looked again over our example code and we found a few improvements (and worked out some of the questions that we had still remaining). I'd like to share with you now the updated proposal of how such variables could look like and how the generated code (with NMODL), along with the python interface, could look like. For simplicity I've abbreviated the
The generated C++ code looks like that: #include <math.h>
#include <coreneuron/utils/randoms/nrnran123.h>
// include all the rest of headers
namespace coreneuron {
/** all mechanism instance variables */
struct NetStim_Instance {
const double* __restrict__ interval;
const double* __restrict__ number;
const double* __restrict__ start;
double* __restrict__ noise;
double* __restrict__ event;
double* __restrict__ on;
double* __restrict__ ispike;
double* __restrict__ v_unused;
double* __restrict__ tsave;
const double* __restrict__ node_area;
// !!! generated for each RANDOM variable to hold the Random123 counter struct
void** __restrict__ rndcntr_r;
void** __restrict__ point_process;
void** __restrict__ tqitem;
};
/** initialize mechanism instance variables */
static inline void setup_instance(NrnThread* nt, Memb_list* ml) {
NetStim_Instance* inst = (NetStim_Instance*) mem_alloc(1, sizeof(NetStim_Instance));
int pnodecount = ml->_nodecount_padded;
Datum* indexes = ml->pdata;
inst->interval = ml->data+0*pnodecount;
inst->number = ml->data+1*pnodecount;
inst->start = ml->data+2*pnodecount;
inst->noise = ml->data+3*pnodecount;
inst->event = ml->data+4*pnodecount;
inst->on = ml->data+5*pnodecount;
inst->ispike = ml->data+6*pnodecount;
inst->v_unused = ml->data+7*pnodecount;
inst->tsave = ml->data+8*pnodecount;
inst->node_area = nt->_data;
// !!!
// generate for each random variable a hidden pointer to its random123 counter struct
inst->rndcntr_r = nt->_vdata;
// !!!
inst->point_process = nt->_vdata;
inst->tqitem = nt->_vdata;
ml->instance = (void*) inst;
}
inline double invl_NetStim(int id, int pnodecount, NetStim_Instance* inst, double* data, const Datum* indexes, ThreadDatum* thread, NrnThread* nt, double v, double mean);
// !!! auto-generated hook to call from hoc/py (cell.init_r(gid, id1, id2)
// takes in hoc/py 2 or 3 parameters, this is random123 specific but we hide that fact
inline int init_r_NetStim(int id, int pnodecount, NetStim_Instance* inst, double* data, const Datum* indexes, ThreadDatum* thread, NrnThread* nt, double v) {
int ret_init_r = 0;
nrnran123_State** pv = (nrnran123_State**)(&inst->rndcntr_r[indexes[2*pnodecount + id]]);
if (*pv) {
nrnran123_deletestream(*pv);
*pv = nullptr;
}
if (ifarg(3)) {
*pv = nrnran123_newstream3((uint32_t)*getarg(1), (uint32_t)*getarg(2), (uint32_t)*getarg(3));
}else if (ifarg(2)) {
*pv = nrnran123_newstream((uint32_t)*getarg(1), (uint32_t)*getarg(2));
}
return ret_init_r;
}
// !!!
inline double invl_NetStim(int id, int pnodecount, NetStim_Instance* inst, double* data, const Datum* indexes, ThreadDatum* thread, NrnThread* nt, double v, double mean) {
double ret_invl = 0.0;
if (mean <= 0.0) {
mean = .01;
}
if (inst->noise[id] == 0.0) {
ret_invl = mean;
} else {
// !!!
// generate the appropriate call to the random number function possibly
// with the necessary transformation into the requested distribution
// Alternatively, add a little helper function sample(r), which contain the actual call
// to nrnran123.
// !!!
ret_invl = (1.0 - inst->noise[id]) * mean + inst->noise[id] * mean * nrnran123_negexp((nrnran123_State*)inst->rndcntr_r[indexes[2*pnodecount + id]])r;
}
return ret_invl;
}
/** initialize channel */
void nrn_init_NetStim(NrnThread* nt, Memb_list* ml, int type) {
int nodecount = ml->nodecount;
int pnodecount = ml->_nodecount_padded;
const int* __restrict__ node_index = ml->nodeindices;
double* __restrict__ data = ml->data;
const double* __restrict__ voltage = nt->_actual_v;
Datum* __restrict__ indexes = ml->pdata;
ThreadDatum* __restrict__ thread = ml->_thread;
setup_instance(nt, ml);
NetStim_Instance* __restrict__ inst = (NetStim_Instance*) ml->instance;
if (_nrn_skip_initmodel == 0) {
int start = 0;
int end = nodecount;
#pragma ivdep
for (int id = start; id < end; id++) {
inst->tsave[id] = -1e20;
double v = 0.0;
// !!! the NMODL Random123 API could be translated into this (or this comes from VERBATIM)
nrnran123_setseq((nrnran123_State*)inst->rndcntr_r[indexes[2*pnodecount + id]], 0, 0);
inst->on[id] = 0.0;
inst->ispike[id] = 0.0;
}
}
}
} Finally, here's how RANDOM variables are initialized (ie. seeded): from neuron import h
pc = h.ParallelContext()
h.dt = 1.0/32
def test_bbcore():
from neuron import coreneuron
coreneuron.enable = True
ncell = 10
cells = []
gids = range(pc.id(), ncell, pc.nhost()) # round robin
for gid in gids:
pc.set_gid2node(gid, pc.id())
cell = h.NetStim()
# !!! new way of initializing the seed for any RANDOM variable
cell.init_rng(cell.r, gid, 2, 3)
cells.append(cell)
pc.spike_record(-1, tvec, idvec)
h.CVode().cache_efficient(1)
pc.set_maxstep(10)
h.finitialize(-65)
pc.psolve(10)
if __name__ == "__main__":
test_bbcore() @nrnhines , @ramcdougal (and others) : What do you think? |
Beta Was this translation helpful? Give feedback.
-
Random123 variables could be treated in NEURON as first class citizen. A modfile using such random variables could look like this:
Beta Was this translation helpful? Give feedback.
All reactions