Skip to content

Conversation

@apdavison
Copy link
Member

@apdavison apdavison commented May 19, 2020

The goal of this is to complete the implementation of #456. Please take a look, and let me know if this fits your understanding of what was agreed, and if you see any difficulties with this approach.

Most of the necessary changes to core are done, I think, io has not been touched yet.

An important decision is whether we keep ChannelIndex and Unit for one major release, but deprecate them, or whether we remove them in the same release where we add View/Group. The advantages of keeping them are that (i) people can test the old and new approaches side-by-side, (ii) existing files in NIX format can be read, converted by the user, and rewritten with the new objects (otherwise NIXIO will have to be convert things automatically, which may not be easy). The disadvantages are the increased complexity and possible confusion of having two ways to do the same thing in Neo at the same time.

@pep8speaks
Copy link

pep8speaks commented May 19, 2020

Hello @apdavison! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

Line 110:9: E741 ambiguous variable name 'l'
Line 115:41: E262 inline comment should start with '# '
Line 116:39: E262 inline comment should start with '# '
Line 116:100: E501 line too long (106 > 99 characters)
Line 123:44: E201 whitespace after '{'
Line 145:25: E117 over-indented
Line 146:100: E501 line too long (107 > 99 characters)
Line 147:93: E262 inline comment should start with '# '
Line 148:82: E262 inline comment should start with '# '

Line 241:62: W292 no newline at end of file

Line 132:1: W293 blank line contains whitespace

Line 145:28: E222 multiple spaces after operator

Line 786:12: E713 test for membership should be 'not in'
Line 914:100: E501 line too long (112 > 99 characters)
Line 931:100: E501 line too long (114 > 99 characters)

Line 212:30: E226 missing whitespace around arithmetic operator

Line 115:9: E265 block comment should start with '# '
Line 116:9: E265 block comment should start with '# '
Line 166:36: E261 at least two spaces before inline comment
Line 166:36: E262 inline comment should start with '# '
Line 169:9: E265 block comment should start with '# '
Line 170:9: E265 block comment should start with '# '

Line 318:17: E265 block comment should start with '# '
Line 318:100: E501 line too long (102 > 99 characters)

Line 64:9: E265 block comment should start with '# '
Line 65:9: E265 block comment should start with '# '

Line 1022:17: E222 multiple spaces after operator
Line 1062:17: E222 multiple spaces after operator

Line 19:1: E302 expected 2 blank lines, found 1
Line 70:100: E501 line too long (101 > 99 characters)

Line 159:33: E126 continuation line over-indented for hanging indent

Comment last updated at 2020-11-06 12:41:05 UTC

@apdavison apdavison added this to the future milestone May 19, 2020
@mdenker
Copy link
Member

mdenker commented May 20, 2020

Thanks, Andrew, I will go over this PR over the next days. I don't have an ad-hoc opinion on whether to keep or drop the old classes yet.

@samuelgarcia
Copy link
Contributor

Hi Andrew.
Waooohh. You did it. You are our heroe.

I also try to have a look before the end of the week end.
I think it is important to discuss/validate this important PR during a short period.

After everyone in core team have read this, maybe we could plan a very short call to accelerate discussion, what do you think ?

@samuelgarcia
Copy link
Contributor

I will start to read your PR.

Some feeling right now (before reading) :

  • I can help to make change at IO level
  • I would vote for hard break (no ChannelIndex in next release) even if it is painfull
  • I don't think it is more diffulct to make NIX compatible with previous format than having still
    ChannelIndex
  • @achilleas-k should have a strong weight in this choice (slow/hard transition)
  • I can have the opposite opinion tomorow

@achilleas-k
Copy link
Contributor

I haven't had a good look at the PR but I skimmed through it yesterday. I agree with Samuel that it might not be that difficult to read old NIX files into the new format. I think an important part of the decision is whether the user or the IO should control the old-to-new conversion.

  • If the ChannelIndex is kept for a transition period, it's up to the user to convert those ChannelIndex objects to one of the new ones when they read a file.
  • If the ChannelIndex is removed, the IO would have to decide how to read those objects from old files. I don't know how straightforward this would be.

I suppose in the latter case it could be accompanied by a warning informing the user that "N ChannelIndex objects have been converted to View". Then it's up to the user to create a different kind of object if they need to (assuming View isn't the only replacement type).

That's just some initial thoughts I had on the decision. I understand why you would want to avoid having the complexity of keeping around the old object types alongside the new ones, especially if it might lead to users mixing old with new classes while they work with the transition version.

for i, spiketrain in enumerate(spiketrains):
unit = Group(spiketrain, name=f"Neuron #{i + 1}")
units.append(unit)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this example would more clear with sevral segment.
a unit is more easy to understand across segment. No ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is just a direct translation of the previous one, it's probably not the best example for the new features.

We should write new examples when we rewrite the documentation.

segment.spiketrains.append(spiketrain)
spiketrain.segment = segment
# assign each spiketrain to a given neuron
current_group = next(iter_group)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this example.
Maybe the use of next.cycle is a bit confusion for a python beginner.
Maybe an old school and less elegant indexing would be more explicit.
No ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but I propose to leave it for now since we plan to rewrite the documentation (and examples) anyway.

_container_child_objects = ('Segment', 'Group')
_single_parent_objects = ('Block',)

def __init__(self, *objects, name=None, description=None, file_origin=None, **annotations):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, I am old school but I would expect objects an iterable and not *objects here.

A standard use case will be

a_list = []
for i in myloop:
    a_list.append(an_obbect)   

# This is more intuitive a biginer
g = Group(a_list)

# if I understand correctly you propose this
g = Group(*a_list)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree

neo/core/view.py Outdated
if self.index.dtype == np.bool: # convert boolean mask to integer index
if self.index.size != self.obj.shape[-1]:
raise ValueError("index size does not match number of channels in signal")
self.index = np.arange(self.obj.shape[-1])[self.index]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or self.index, = np.nonzero(self.index)

Copy link
Member Author

@apdavison apdavison Jul 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

@samuelgarcia
Copy link
Contributor

Hi andrew.
I made some comments in the code directly.

My main feelings:

Group
super cool and totally fit the need. I totally validate.
The signature have to be discuss maybe.

View
I am more dibutative with the view object.
I am not sure it will be so usefull for long term usage.
I known we already discuss with Julia and Michael.
One object attached to the tree only to handle a subset of channel would make user code more heavy than dealing directly with the index vector in my opignon.
And about the semantic, in numpy a view is an array iself that is a lazy representation of a subarray, it is a bit different concept finally.

Let Julia, Michael, Achilleas have a look and we could maybe plan a short meeting, no ?

@JuliaSprenger
Copy link
Member

@apdavison Thanks for going ahead and starting this!

Regarding the deprecation of ChannelIndex objects I think it would be better to have a consistent solution across IOs. Currently this affects KlustaKwikIO NixIO NeoMatlabIO NSDFIO and PickleIO as all of these can read and write Blocks. Also we should consider that neo structures are not only generated by the IOs, but might also be generated by custom user scripts that might currently rely on the ChannelIndex object. Updating such preprocessing to the new Group and View objects might imply a lot of tideous work for users. When abruptly removing ChannelIndex objects, the output of such preprocessing could only be automatically updated using two different neo versions, one for saving the preprocessing result using the old ChannelIndex mechansim and one for loading it with a later version of Neo. This is quite cumbersome and potentially resource consuming.

Therefore I think we should provide a function to convert any given neo structure based on ChannelIndexes to one based on Groups and Views. This way we have a well defined mapping between the old structures and the new ones. However, this would require the ChannelIndex object to be present in the next version(s) of neo.

Besides these thoughts regarding ChannelIndexes, I did not have the time yet to test the proposed implementation yet, but will do so soon!

Copy link
Member

@dizcza dizcza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixing the issues in #456 comes at the cost of introducing more abstractions that you call Group and View. I cannot judge how hard things were in the past with ChannelIndex, but I find these changes useful. Here are my suggestions:

  • the name "View" is very ambiguous; I'd rename it to AnalogSignalView or at least SignalView;
  • before I came upon "the Unit class has been deprecated in favor of Group", it was confusing to read unit = Group(). Then I thought OK, Group is basically grouping by a unit id. I'd rename Group to UnitGroup or GroupedByUnit (like the result of a pandas dataframe.group_by()) because essentially what you call Group is NOT a physical identity but a result of the analysis that neuron's activations, something physical and yet unobserved, are diluted into several channels. You call it Group as if it's another physical identity like Segment or Block. It's rather a mask (but please don't call it Mask).
  • one way to build these relationships is by hand (by user), another - an automatic call, something like segment.to_groups(), assuming that each AnalogSignal and SpikeTrain are somehow already linked to channel indices. This will remove the burden for users of switching to the new API if you leave ChannelIndex deprecated instead of complete removal.

Consider another example. An analog signal has information in its annotations that it belongs to channel "2" (is there such kind of annotations available?), and there is a spike train with the annotation that it also belongs to channel "2". It'd be naturally then to split a segment into a list of UnitGroups transparently for the user just by parsing the annotations for each analog signal and spike train.

None

*Recommended attributes/properties*:
:objects: (Neo object) Objects with which to pre-populate the :class:`Group`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to specify of which type: only AnalogSignal and SpikeTrain, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, a Group can contain any Neo object.

@apdavison
Copy link
Member Author

I've made the agreed changes.

What do you think about "ChannelView" instead of "View"?

@samuelgarcia
Copy link
Contributor

@apdavison : I think that I am able to push some change into your branch. Is it OK for you ?

@apdavison apdavison modified the milestones: future, 0.9.0 Jul 7, 2020
@JuliaSprenger
Copy link
Member

JuliaSprenger commented Oct 19, 2020

I suggest to start a list of practical todos here, so we keep an overview of what is missing before this PR can be merged:

  • NixIO can not yet write nested Group objects (implemented in f2fd5da)
  • Unittests are still failing
  • Represent units as groups (BasefromrawIO)
  • Resolve merge conflicts with master
  • Assert that structure of neo.io.nixio.NixIO and neo.io.nixio_fr.NixIO are compatible and identical
  • ChannelView can only link to BaseSignal objects, but no ProxyObjects (see https://github.com/apdavison/python-neo/blob/4f27b20be2d1917869ced85ef697a70a7d3dc8f3/neo/core/view.py#L46). Should be fixed in the same way as 4f27b20. Solved via 8e73267
  • NixIO is attempting to write ChannelViews multiple times if linked by different Groups, e.g. this minimal example results in a DuplicateName Exception (Fixed in 8e73267).
import neo
import quantities as pq

# creating neo structure with one instance of each object type required
bl = neo.Block(name='1')
bl.groups.append(neo.Group(name='2', allowed_types=[neo.ChannelView]))
anasig=neo.AnalogSignal([1,2,3],units=pq.dimensionless, sampling_rate=1*pq.Hz, name='4')
bl.groups[0].add(neo.ChannelView(anasig, [0], name='3'))

# segment is required for nixio to save the dataobjects first
bl.segments.append(neo.Segment(name='5'))
bl.segments[0].analogsignals.append(anasig)

# create a 2nd group to reference the same ChannelView
bl.groups.append(neo.Group(name=6, allowed_types=[neo.ChannelView], objects=bl.groups[0].channelviews))

io = neo.NixIO('testfile.nix', 'ow')
io.write_block(bl)
io.close()

Feel free to extend this list.

@samuelgarcia
Copy link
Contributor

Hi all.
The 3 brainware IO are easy to fix for the ChannelIndex stuff.
I will do it now.

@apdavison
Copy link
Member Author

apdavison commented Nov 5, 2020

A question about "Represent units as groups (BasefromrawIO)" for @samuelgarcia and @JuliaSprenger :

In the master branch, in BaseFromRaw, SpikeTrains are put into Units, and these Units are attached to ChannelIndexes, but those ChannelIndexes do not seem to link to any AnalogSignals.

i.e. raw IOs do not support use-case 2 for ChannelIndex

  1. for spike sorting of extracellular signals, where spikes may be recorded on more than one recording channel, and the ChannelIndex can be used to associate each Unit with the group of recording channels from which it was obtained.

Is that correct? Don't any of the file formats contain the information necessary for this association?

@JuliaSprenger
Copy link
Member

JuliaSprenger commented Nov 5, 2020

Is that correct? Don't any of the file formats contain the information necessary for this association?

I would hope that this association is possible via the channel ids. Or are there systems that use different channel_ids for continuously sampled data and spiketrain data coming from the same electrode? What do you think @samuelgarcia?
If this is association is not possible in some cases, then I would suggest to still implement it for the other cases, as this is a key information needed for working with spiketrains and LFP data.

@samuelgarcia
Copy link
Contributor

@apdavison :
in some old school file format spiketrain belong to a channel.
The rawio at the moment to not handle this and so the ChannelIndex was used only for grouping Unit and not linking to channels. And it have never been handle by an IO for experimental format (with higer level level format like nix, yes)
In short the like ChannelIndex were never used in io even before rawio layer for those file format.

ChannelIndex was more an object to reconstruct this with spike sorting tools but quasi impossible to handle at file reader level (or too much work). In my opinion this relationship between a SpikeTrain and some channel is less and less usefull in the spike sorting work with dense probe.
A SpikeTrain have a "template" waveform on all (at least many) channel. Then if you put a threhold on the amplitude of this waveform then you have a sparse "template" and in that case you have indeed a relationship between a spiketrain and some channels but this relationship itself is not important. What is important is the template waveform itself (sparse or not) and the position channel wise of the extremum amplitude or even better the soma location on the probe.

In short this object can be usefull but it is not a raw input from the data.
It is some thing we infer from data with processing but do not come from raw experimental.

# Conflicts:
#	neo/core/analogsignal.py
#	neo/io/basefromrawio.py
#	neo/io/nixio.py
#	neo/test/coretest/test_analogsignal.py
#	neo/test/iotest/test_axographio.py
#	neo/test/iotest/test_exampleio.py
@apdavison
Copy link
Member Author

thanks @samuelgarcia and @JuliaSprenger

so it seems that rawio has never had full support for Unit. In that case, I think the new basefromrawio that Samuel wrote is essentially feature-equivalent with the previous version. We can try to add more functionality, but perhaps after the release.

I've now fixed the conflicts with master, and the tests seem to be passing, so I think this is ready to merge. I suggest we merge this now, and then make any remaining changes that are needed before the release with PRs onto master.

@apdavison apdavison changed the title [WIP] Replace ChannelIndex and Unit by View and Group Replace ChannelIndex and Unit by View and Group Nov 6, 2020
@JuliaSprenger
Copy link
Member

Hi @samuelgarcia & @apdavison. Ok, then we postpone the unit-channel linking enhancement discussion to another release and I will merge this PR now. Thanks for the amount of work put into the new concept. I hope this is the last structural change we need for representing channel relations.

@samuelgarcia
Copy link
Contributor

Champagne!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants