brian-team/brian2 — pull request #343 by mstimberg (2014-10-08 16:52:34+00:00)
Title: Indices generalization

[This is on top of the `doc_improvements` branch]

This branch implements a new "objects as indices" paradigm that allows more compact syntax (briefly mentioned in #300). Closes #301

I still have to add/update the documentation (mostly developer docs) for this, but the general idea is the following:

Objects that somehow represent a subset of other objects can be used for indexing. Some examples
```Python
# State variables, synaptic connectivity
G = NeuronGroup(10, 'v:volt')
exc = G[:5]
inh = G[5:]
S = Synapses(G, G, 'w:1')
S.connect(exc, exc)
S.w[exc, exc] = exc_w
S.connect(inh, exc)
S.w[inh, exc] = inh_w

# monitors
syn_mon_1 = StateMonitor(S, 'w', record=S[exc, :])
syn_mon_2 = StateMonitor(S, 'w', record=True)
# ...
plot(syn_mon_1.t/ms, syn_mon_1.w[S[exc, 0]])
plot(syn_mon_1.t/ms, syn_mon_2.w['w>0'])
```

This removes the need to call `indices` to get the synaptic indices. It also removes all need (I think) for `index`, `compartments` and `compartment` for `SpatialNeuron`/`Morphology`. @romainbrette can you have a look at the updated compartmental examples and test whether it all works as expected?

There are some subtleties to this:
* The context for strings always depends on the object, not on the index, e.g. the interpretation of `i` and `j`. This means (contrary to what I said before in #300): in `S.w[group1, group2] = "(1+cos(i-j))*2*nS"`, the `i` and `j` will not be relative to `group1` and `group2`, but to source and target of `S`. This makes sense to me, since we do not shift the context when we have numerical indices either. You can have indices relative to the two groups only by creating `S` as `S = Synapses(group1, group2, ...)`.
* Related to the above: the context for the string expressions when accessing the monitor is still the full original group, i.e. if your string expression returns synapses that were not recorded, you'll get an error.
* The internal interface works by creating indices from these objects, shifting them by an offset if the "context object" is already shifted. For example, from above, `inh` will always be interpreted as `5...9`. But if the `Synapses` is created e.g. from `Synapses(exc, inh, ...)` instead of `Synapses(G, G, ...)` then it will be offset to give `0...4`.
* Given that simple implementation, the user could do weird things and pass arbitrary objects for indexing, e.g. groups that a `Synapses` object does not know about, etc. I think that's ok for now, though, you could do the same mistake when explicitly calling `.indices[...]`.
* For reasons of internal consistency, the semantics of one-dimensional indexing into synaptic state variables changed. Before, it was implemented as for 2d arrays, i.e. `S.w[idcs]` was equivalent to `S.w[idcs, :]`. We didn't document this anywhere, though. I think the current approach makes sense, there will always be situations where you directly need to index with synaptic indices (even more useful when #263 is implemented). Also it is kind of nice if `S.w[:][idcs]` is equivalent to `S.w[idcs]`, which it wasn't before.
* The `compartment` method had a bit of an odd semantics where for a cylinder of 10 compartments of 10um each, using multiples of 10um would always give you the compartment up to that point, but 0um would also give you the first compartment. I think it is more consistent to use a `[x, x+length)` semantic for compartments, no? This is the way it is implement currently, in one of the examples 100um was used to access the last compartment, this now has to be changed to e.g. 99um or (which is now possible) just use `-1` as for any sequence.

I think this should be the last big change before our release, apart from the open pull requests only a bit of renaming (e.g. `compile_project` --> `project`, see #300) is missing.