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

Add complete session start time to FicTrac #598

Merged
merged 9 commits into from
Oct 17, 2023
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# (Upcoming)

### Features
Added `session_start_time` extraction to `FicTracDataInterface`. [PR #598](https://github.com/catalystneuro/neuroconv/pull/598)


### Fixes
Remove `starting_time` reset to default value (0.0) when adding the rate and updating the `photon_series_kwargs` or `roi_response_series_kwargs`, in `add_photon_series` or `add_fluorescence_traces`. [PR #595](https://github.com/catalystneuro/neuroconv/pull/595)

Expand All @@ -17,6 +22,7 @@ Remove `starting_time` reset to default value (0.0) when adding the rate and upd




# v0.4.4

### Features
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import importlib.util
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

import numpy as np
from pynwb.behavior import CompassDirection, SpatialSeries
from pynwb.file import NWBFile

# from ....basetemporalalignmentinterface import BaseTemporalAlignmentInterface TODO: Add timing methods
from ....basedatainterface import BaseDataInterface
from ....tools import get_module, get_package
from ....tools import get_module
from ....utils import FilePathType, calculate_regular_series_rate


Expand All @@ -22,6 +23,9 @@ class FicTracDataInterface(BaseDataInterface):
"visual fixation",
]

# The full header can be found in:
# https://github.com/rjdmoore/fictrac/blob/master/doc/data_header.txt

data_columns = [
"frame_counter",
"rotation_delta_x_cam",
Expand Down Expand Up @@ -56,7 +60,7 @@ def __init__(
verbose: bool = True,
):
"""
Interface for writing fictract files to nwb.
Interface for writing FicTrac files to nwb.

Parameters
----------
Expand All @@ -72,17 +76,9 @@ def __init__(

def get_metadata(self):
metadata = super().get_metadata()
from datetime import datetime

config_file = self.file_path.parent / "fictrac_config.txt"
if config_file.exists():
self._config_file = parse_fictrac_config(config_file)
date_string = self._config_file["build_date"]
date_object = datetime.strptime(date_string, "%b %d %Y")

metadata["NWBFile"].update(
session_start_time=date_object,
)
session_start_time = extract_session_start_time(self.file_path)
metadata["NWBFile"].update(session_start_time=session_start_time)

return metadata

Expand All @@ -102,21 +98,29 @@ def add_to_nwbfile(

import pandas as pd

fictrac_data_df = pd.read_csv(self.file_path, sep=",", header=None, names=self.data_columns)
# The first row only contains the session start time and invalid data
fictrac_data_df = pd.read_csv(self.file_path, sep=",", skiprows=1, header=None, names=self.data_columns)

# Get the timestamps
timestamps_milliseconds = fictrac_data_df["timestamp"].values
timestamps = timestamps_milliseconds / 1000.0
rate = calculate_regular_series_rate(series=timestamps) # Returns None if it is not regular
write_timestamps = True

# Note: The last values of the timestamps look very irregular for the sample file in catalyst neuro gin repo
# The reason, most likely, is that FicTrac is relying on OpenCV to get the timestamps from the video
# In my experience, OpenCV is not very accurate with the timestamps at the end of the video.
rate = calculate_regular_series_rate(series=timestamps) # Returns None if the series is not regular
if rate:
write_timestamps = False

processing_module = get_module(nwbfile=nwbfile, name="Behavior")
starting_time = timestamps[0]
else:
write_timestamps = True

# All the units in FicTrac are in radians, the radius of the ball required to transform to
# Distances is not specified in the format
compass_direction_container = CompassDirection(name="FicTrac")
compass_direction_container = CompassDirection(
name="FicTrac"
) # TODO: this is just for the heading angle. I will change this to Position in a subsequent PR.
# Position and SpatialSeries nested into it

# Add rotation delta from camera
rotation_delta_cam_columns = [
Expand Down Expand Up @@ -148,6 +152,7 @@ def add_to_nwbfile(
spatial_seriess_kwargs["timestamps"] = timestamps
else:
spatial_seriess_kwargs["rate"] = rate
spatial_seriess_kwargs["starting_time"] = starting_time

spatial_series = SpatialSeries(**spatial_seriess_kwargs)
compass_direction_container.add_spatial_series(spatial_series)
Expand Down Expand Up @@ -182,6 +187,7 @@ def add_to_nwbfile(
spatial_seriess_kwargs["timestamps"] = timestamps
else:
spatial_seriess_kwargs["rate"] = rate
spatial_seriess_kwargs["starting_time"] = starting_time

spatial_series = SpatialSeries(**spatial_seriess_kwargs)
compass_direction_container.add_spatial_series(spatial_series)
Expand Down Expand Up @@ -216,6 +222,7 @@ def add_to_nwbfile(
spatial_seriess_kwargs["timestamps"] = timestamps
else:
spatial_seriess_kwargs["rate"] = rate
spatial_seriess_kwargs["starting_time"] = starting_time

spatial_series = SpatialSeries(**spatial_seriess_kwargs)
compass_direction_container.add_spatial_series(spatial_series)
Expand Down Expand Up @@ -250,6 +257,7 @@ def add_to_nwbfile(
spatial_seriess_kwargs["timestamps"] = timestamps
else:
spatial_seriess_kwargs["rate"] = rate
spatial_seriess_kwargs["starting_time"] = starting_time

spatial_series = SpatialSeries(**spatial_seriess_kwargs)
compass_direction_container.add_spatial_series(spatial_series)
Expand All @@ -274,6 +282,7 @@ def add_to_nwbfile(
spatial_seriess_kwargs["timestamps"] = timestamps
else:
spatial_seriess_kwargs["rate"] = rate
spatial_seriess_kwargs["starting_time"] = starting_time

spatial_series = SpatialSeries(**spatial_seriess_kwargs)
compass_direction_container.add_spatial_series(spatial_series)
Expand All @@ -300,6 +309,7 @@ def add_to_nwbfile(
spatial_seriess_kwargs["timestamps"] = timestamps
else:
spatial_seriess_kwargs["rate"] = rate
spatial_seriess_kwargs["starting_time"] = starting_time

spatial_series = SpatialSeries(**spatial_seriess_kwargs)
compass_direction_container.add_spatial_series(spatial_series)
Expand All @@ -321,6 +331,7 @@ def add_to_nwbfile(
spatial_seriess_kwargs["timestamps"] = timestamps
else:
spatial_seriess_kwargs["rate"] = rate
spatial_seriess_kwargs["starting_time"] = starting_time

spatial_series = SpatialSeries(**spatial_seriess_kwargs)
compass_direction_container.add_spatial_series(spatial_series)
Expand Down Expand Up @@ -351,6 +362,7 @@ def add_to_nwbfile(
spatial_seriess_kwargs["timestamps"] = timestamps
else:
spatial_seriess_kwargs["rate"] = rate
spatial_seriess_kwargs["starting_time"] = starting_time

spatial_series = SpatialSeries(**spatial_seriess_kwargs)
compass_direction_container.add_spatial_series(spatial_series)
Expand Down Expand Up @@ -379,6 +391,7 @@ def add_to_nwbfile(
spatial_seriess_kwargs["timestamps"] = timestamps
else:
spatial_seriess_kwargs["rate"] = rate
spatial_seriess_kwargs["starting_time"] = starting_time

spatial_series = SpatialSeries(**spatial_seriess_kwargs)
compass_direction_container.add_spatial_series(spatial_series)
Expand All @@ -401,14 +414,37 @@ def add_to_nwbfile(

else:
spatial_seriess_kwargs["rate"] = rate
spatial_seriess_kwargs["starting_time"] = starting_time

spatial_series = SpatialSeries(**spatial_seriess_kwargs)
compass_direction_container.add_spatial_series(spatial_series)

# Add the compass direction container to the processing module
processing_module = get_module(nwbfile=nwbfile, name="Behavior")
processing_module.add_data_interface(compass_direction_container)


def extract_session_start_time(file_path: FilePathType) -> datetime:
"""
Lazily extract the session start datetime from a FicTrac data file.

In FicTrac the column 22 in the data has the timestamps which are given in milliseconds since the epoch.

The epoch in Linux is 1970-01-01 00:00:00 UTC.
"""
with open(file_path, "r") as file:
# Read the first data line
first_line = file.readline()

# Split by comma and extract the timestamp (the 22nd column)
utc_timestamp = float(first_line.split(",")[21]) / 1000.0 # Transform to seconds

utc_datetime = datetime.utcfromtimestamp(utc_timestamp).replace(tzinfo=timezone.utc)

return utc_datetime


# TODO: Parse probably will do this in a simpler way.
def parse_fictrac_config(filename) -> dict:
"""
Parse a FicTrac configuration file and return a dictionary of its parameters. See the
Expand Down
6 changes: 5 additions & 1 deletion tests/test_on_data/test_behavior_interfaces.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import unittest
from datetime import datetime
from datetime import datetime, timezone
from pathlib import Path

import numpy as np
Expand Down Expand Up @@ -37,6 +37,10 @@ class TestFicTracDataInterface(DataInterfaceTestMixin, unittest.TestCase):

save_directory = OUTPUT_PATH

def check_extracted_metadata(self, metadata: dict):
Copy link
Member

Choose a reason for hiding this comment

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

Actually, before merge, can we get a test for the new starting time functionality? Or is the example data irregular?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is irregular in the example : (

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I plan to add the timing methods soon, that will be tested there.

Copy link
Member

@CodyCBakerPhD CodyCBakerPhD Oct 17, 2023

Choose a reason for hiding this comment

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

It would be possible to do this once it becomes a Temporally aligned interface; the ability to override those fetched timestamps, you could override it to something regular in a test

expected_session_start_time = datetime(2023, 7, 24, 9, 30, 55, 440600, tzinfo=timezone.utc)
assert metadata["NWBFile"]["session_start_time"] == expected_session_start_time


class TestVideoInterface(VideoInterfaceMixin, unittest.TestCase):
data_interface_cls = VideoInterface
Expand Down