Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New pixel time offsets and cycle times #58

Merged
merged 41 commits into from
Jul 28, 2020
Merged

Conversation

alessandrofelder
Copy link
Collaborator

This PR will handle differences between LabView versions in storing/computing pixel time offsets and cycle time.

flake8 didn't like that empty line
assertions and assigning of pixel time offsets still needs to be adapted
to newer 231 version.

also, pandas reading the roi data as uint16 causes rounding errors,
which mess with using z_start as key for the ROI plane
Copy link
Contributor

@ageorgou ageorgou left a comment

Choose a reason for hiding this comment

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

Some small comments. I haven't looked at the code for the computation in the newer version yet but it's looking good so far!

Comment on lines 601 to 611
timings = LabViewTimingsPre2018(file_path)
self.cycle_relative_times = timings.pixel_time_offsets
self.cycle_time = timings.cycle_time

def read_cycle_relative_times_v231(self, file_path, roi_path):
timings = LabViewTimings231(file_path,
roi_path=roi_path,
n_cycles_per_trial=self.imaging_info.cycles_per_trial,
n_trials=len(self.trial_times))
self.cycle_relative_times = timings.pixel_time_offsets
self.cycle_time = timings.cycle_time
Copy link
Contributor

Choose a reason for hiding this comment

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

Since these are quite similar, it may be better to have a single method read_cycle_relative_times which takes a path rather than a file name:

def read_cycle_relative_times(self, path):
    file_path = ...
    if version is old:
        timings = LabViewTimingsPre2018(file_path)
    else if version is new:
        roi_path = ...
        timings = LabViewTimings231(file_path, roi_path)
    self.cycle_relative_times = ...

Or we could even have the Timings classes take a path in their constructor, but that makes them less general (and makes testing harder).

Copy link
Contributor

Choose a reason for hiding this comment

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

We should also update the docstring to replace the specific filename with something more descriptive that applies to both versions.

class LabViewTimingsPre2018(LabViewTimings):

def __init__(self, relative_times_path):
self._read_relative_times_file(relative_times_path)
Copy link
Contributor

Choose a reason for hiding this comment

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

This (same for the other subclass) should call the base LabViewTimings constructor using super(), to ensure the object is properly initialised. In this case it doesn't really matter because the base class constructor only initialises some attributes which will be overwritten later!

In fact, since they both call _read_relative_times_file as the first step, that should maybe go into the base class __init__ that both subclasses call - unless we think that other subclasses would not.


def _read_relative_times_file(self, file_path):
raw_data = pd.read_csv(file_path, names=('RelativeTime', 'CycleTime'),
sep='\t', dtype=np.float64) / 1e6
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe a note here that we're converting to seconds for consistency? It's in the docstring of the NWBFile's method but worth moving/adding here too, I think.

Comment on lines 47 to 48
# in 3D, in which case the ==0 assertion may become imprecise and we would need a tolerance to compare with a
# double value.
Copy link
Contributor

Choose a reason for hiding this comment

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

Good to have the future changes in mind! We could use math.isclose or np.isclose for checking against 0 in that case.

# the synthetic file has
# 2 trials, each of which has 3 cycles and 4 rois a 5 lines
# and a few zero lines as may be expected "in the wild"
# first cycle ofs trial 1 and trial 2 take 1300.4 and 1200.4 nanoseconds, respectively
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
# first cycle ofs trial 1 and trial 2 take 1300.4 and 1200.4 nanoseconds, respectively
# first cycle of trial 1 and trial 2 take 1300.4 and 1200.4 nanoseconds, respectively

add data structure that stores pixel time offsets by ROI to timings
class
write pixel time offsets for first cycle in new labview
remove old datastructure completely, rename some indices
addresses initial review comments
 - hide versioning inside read_cycle_relative_times
 - call timings base class constructor
 - add comment
 - typo
Copy link
Contributor

@ageorgou ageorgou left a comment

Choose a reason for hiding this comment

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

Looking good. What's left is recording the times for the other cycles in the NWBFile?

It may make sense to move the computation out of add_rois and into the LabViewTimings subclasses. I was thinking they would have a get_roi_offsets(roi_number) method or something similar that the NwbFile would call. Then all the row calculations etc could happen inside the new class, allowing for less branching in the main file. That should also let us write tests more easily (e.g. get all the times for a ROI).

Or would the timings objects be missing something they need to compute the offsets themselves? I suppose they need the dwell time... we could pass that in, or could return the offset of each row in the ROI rather than the whole rectangle.

@@ -591,24 +586,34 @@ def add_stimulus(self):
self.nwb_file.add_stimulus(TimeSeries(**attrs))
self._write()

def read_cycle_relative_times_pre2018(self, file_path):
def read_cycle_relative_times(self, folder_path):
"""Read the 'Single cycle relative times.txt' file and store the values in memory.
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably update this to be more generic. Something like: (roughly)

"""Read the files containing relative times and store the values in memory.

Depending on the version of software used, the file is named either (...) or (...), and in newer versions this can also require reading the ROI.dat file.
Note that while the file has...
"""

Comment on lines 604 to 605
self.cycle_relative_times = timings.pixel_time_offsets
self.cycle_time = timings.cycle_time
Copy link
Contributor

Choose a reason for hiding this comment

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

These can go outside the if since they're shared in both branches.
(I now see that one of the fields is named differently (pixel_time_offsets_*by_roi*), but maybe that's by accident?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yea, you're right - that is a mess-up by yours truly :)

pixel_time_offsets=pixel_time_offsets,
dimensions=dimensions,
Copy link
Contributor

Choose a reason for hiding this comment

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

This confused me, but it's just changing the order of the arguments, right? Nothing else is going on?

pixel_time_offsets will be moved from segmentation to roi data
this allows more flexibility to vary the dimensions and the shape of the
pixel_time_offsets between ROIs
Comment on lines 725 to 732
all_attrs = dict(ts_attrs)
all_attrs.update(data_attrs)
roi_with_offsets = ROISeriesWithPixelTimeOffsets(name=ts_name,
data=data,
pixel_time_offsets=pixel_time_offsets,
rate=1.0/self.cycle_time,
**all_attrs)
self.nwb_file.add_acquisition(roi_with_offsets)
Copy link
Contributor

Choose a reason for hiding this comment

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

This can possibly use self._add_time_series_data(kind=ROISeriesWithPixelTimeOffsets, ...)?

Comment on lines 68 to 69
'relative to the starting time/timestamp of a '
'ROISeriesWithPixelTimeOffsets.',
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it relative to the starting time (= time of the cycle?) or to the start of the trial?

moved within-line pixel time offset computation inside timings class
making the interface more unified among subclasses
consistent API across versions unifies handling of pixel_time_offsets
adapt roi extension to shape of pre2018 versions
pixel_time_offsets completely removed from ImageSegmentation in all
versions to form part of ROI image data.
alessandrofelder and others added 7 commits July 22, 2020 14:59
Before this, there was an issue when planes had more than one ROI.
This fixes it for now but we should find a better solution.
The newest release (2.0.0) adds some attributes and allows
slightly different types, resulting in changes to the signatures.
pynwb is still using the older versions too, even in the most
recent release (1.3.3).
Copy link
Contributor

@ageorgou ageorgou left a comment

Choose a reason for hiding this comment

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

This looks basically done, but with some (generally small) comments below.

Some improvements can wait for another PR. I'll leave a comment about that.

Comment on lines 68 to 69
'relative to the starting time/timestamp of a '
'ROISeriesWithPixelTimeOffsets.',
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be changed to "starting time of a trial" (can run a quick check to confirm first).

name='pixel_time_offsets',
shape=[(None, None), (None, None, None), (None, None, None, None)],
neurodata_type_def='PixelTimeOffsets')
silverlab_roi_image_specs = NWBGroupSpec(doc='documentation of this class goes here',
Copy link
Contributor

Choose a reason for hiding this comment

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

We should put something there :)

@@ -583,15 +586,37 @@ def add_stimulus(self):
self.nwb_file.add_stimulus(TimeSeries(**attrs))
self._write()

def read_cycle_relative_times(self, file_path):
def read_cycle_relative_times(self, folder_path):
"""Read the 'Single cycle relative times.txt' file and store the values in memory.
Copy link
Contributor

Choose a reason for hiding this comment

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

See the previous comment about this.

Comment on lines 606 to 607
self.raw_pixel_time_offsets = timings.pixel_time_offsets
self.cycle_time = timings.cycle_time
Copy link
Contributor

Choose a reason for hiding this comment

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

These lines should go outside the if as they're shared.

Comment on lines +618 to +619
else:
raise ValueError('Unsupported LabView version for timings {}.'.format(self.labview_version))
Copy link
Contributor

Choose a reason for hiding this comment

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

This is fine for now, but if you want to avoid it, we can add a method to the (abstract) Timings class which creates an object of the appropriate type based on the version we pass it (similar to how the abstract Header class works, if I remember right). If we pass the wrong version (or the wrong arguments for that version), we can throw the error there.

This can wait until later, however.

Comment on lines 2 to 5
1 3 133200 888 70 100 0 76 105 0 0 0 5 512 1 45.1 0
2 3 133200 888 80 200 1 86 205 1 0 0 5 512 1 45.2 0
3 3 133200 888 90 300 2 96 305 2 0 0 5 512 1 45.3 0
4 3 133200 888 100 400 3 106 405 3 0 0 5 512 1 45.4 0
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as in the other test file :)

@@ -0,0 +1,8 @@
400.0 12345.000
Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, does this work even without the header line? Interesting!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, none of the pre2018 data that I have seen have headers here. The column designation is inferred from the order and added in the code.



def test_pixel_time_offsets_for_roi_v231(synthetic_timings_v231):
roi_0_offsets = synthetic_timings_v231.pixel_time_offsets[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

We should check the other ROI too; this is the recent error that we had to work around in 661aca6.

assert roi_0_offsets.shape == expected_shape
assert roi_0_offsets[0][0][0] == expected_first_cycle_first_row_offset
assert roi_0_offsets[0][4][0] == expected_first_cycle_last_row_offset
assert roi_0_offsets[5][0][0] == expected_last_cycle_first_row_offset
Copy link
Contributor

Choose a reason for hiding this comment

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

We're already checking the size earlier, but if we want to be clearer that we're checking the last cycle, I'd put roi_0_offsets[-1][0][0].



def test_pixel_time_offsets_for_roi_pre2018(synthetic_timings_pre2018):
roi_0_offsets = synthetic_timings_pre2018.pixel_time_offsets[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

As in the other test, we should check the other ROIs too. Perhaps the first and last line of each?

@ageorgou ageorgou marked this pull request as ready for review July 24, 2020 14:58
@ageorgou
Copy link
Contributor

ageorgou commented Jul 24, 2020

Once the above changes are made, it would be good to compare with the existing analysis code if possible.

There are other possible improvements but better left for another round, since this is already quite a lot! For example:

  • Try compressing the pixel data, either in the same way as the manifold and image data, or through a method of the new class (ideally, only store the row offsets and the dwell time, and have a method that computes the full array).

  • Use a "factory method" for creating an LabViewTimings of the right subclass.

  • How we check for pointing mode in LabViewTimings

  • The interface of LabViewTimings (is a dict acceptable, or should we have a method for getting the values for a particular ROI?) This may particularly help with...

  • How we refer to ROIs, especially when we retrieve the timings for them (improve the temporary solution from 661aca6)

  • How we build up the offsets array in LabViewTimings231 (as commented in the review)

  • The location of the test input files: now that we have a lot of them, should we put them in separate folders?

  • AF Edit: as mentioned in one of the @ageorgou 's comments above, it would also be good to avoid a loop over all cycles in timings.py if possible. Noting here so everything is in one place.

@alessandrofelder alessandrofelder self-assigned this Jul 27, 2020
improve docstring
move common code outside if-else clauses
consistent passing of arguments
add docstring to abstract timings class
make comment more precise
clarify confusing variable name
remove unnecessary comment (see #60)
@alessandrofelder alessandrofelder merged commit 3f8ed12 into new-labview Jul 28, 2020
@alessandrofelder alessandrofelder deleted the new-timings branch July 28, 2020 11:34
ageorgou added a commit that referenced this pull request Aug 19, 2020
Also includes changes due to #58 (moving pixel time offsets).
Otherwise the only changes are to the spec and the new LabVIEW
version, plus the datatype of the BodyCam image series.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants