How to analyse a (Stytra) embedded tail tracking experiment

In this tutorial we show how to use bouter to analyse data that was aquired with Stytra.

First, some imports:

[1]:
from bouter import EmbeddedExperiment
from bouter import ASSETS_PATH

Given a folder containing data from a Stytra experiment, it’s very easy to instantiate an Experiment object. Here we use example bouter data, otherwise replace the path with the folder to your data:

[2]:
experiment_data_path = ASSETS_PATH / "embedded_dataset"

# Initialize the experiment:
exp = EmbeddedExperiment(experiment_data_path)

The Experiment class

Experiment objects are objects that behaves like dictionariers with extended magical properties.

They have keys, reflecting the Stytra metadata:

[3]:
print(exp.keys())
dict_keys(['camera_params', 'general', 'gui', 'stimulus', 'behavior'])

And looking inside these keys, we found Stytra hierarchical data log:

[4]:
print(exp["general"]["animal"])
{'age': 8, 'comments': '', 'dish_diameter': '30', 'embedded': True, 'genotype': 'TL', 'id': 10, 'screened': 'not', 'species': 'Danio Rerio', 'treatment': ''}

In addition, there are some values (properties) that are computed from the metadata, invoked in this way:

[5]:
# sampling dt of the behavioral tracking
exp.behavior_dt
[5]:
0.003343767676767584

There’s several useful properties. E.g.

  • Fish id from folder:

[6]:
exp.fish_id
[6]:
'181115_f10'
  • Unique id for the experiment:

[7]:
exp.full_name
[7]:
'181115_f10_192316'
  • Protocol name:

[8]:
exp.protocol_name
[8]:
'closed_open_loop'
  • Simplified protocol parameters dict:

[9]:
exp.protocol_parameters
[9]:
{'grating_cycle': 10,
 'grating_duration': 4.0,
 'inter_stim_pause': 0,
 'n_repeats': 1,
 'post_pause': 0.0,
 'pre_pause': 0.0}
  • Start and end times of each stimulus in the log:

[10]:
(exp.stim_start_times, exp.stim_end_times)
[10]:
(array([0.002]), array([600.004995]))

The behavioural logs

The bulk of the behavioural data is contained in the stytra log files. With properties you can look at the logs of the experiments, returned as pd.DataFrames; depending on what experiment you run you’ll find for example:

  • Experiment.stimulus_log: the dynamic log of stytra stimulus

[11]:
exp.stimulus_log.head()
[11]:
closed loop 1D_vel closed loop 1D_base_vel closed loop 1D_gain closed loop 1D_lag closed loop 1D_fish_swimming t
0 -10.000000 -10.0 1.0 0.0 0.0 0.026302
1 -10.000000 -10.0 1.0 0.0 0.0 0.070306
2 -10.000000 -10.0 1.0 0.0 0.0 0.104309
3 -10.000000 -10.0 1.0 0.0 0.0 0.158315
4 1.554264 -10.0 1.0 0.0 1.0 0.189318
  • Experiment.behavior_log: the dynamic log of the tracked tail (or eye, position, etc.):

[12]:
exp.behavior_log.head()
[12]:
tail_sum theta_00 theta_01 theta_02 theta_03 theta_04 theta_05 theta_06 theta_07 theta_08 ... theta_16 theta_17 theta_18 theta_19 theta_20 theta_21 theta_22 theta_23 theta_24 t
0 0.009258 0.054718 0.006608 -0.003954 0.002449 0.009792 0.007008 -0.012253 0.001501 0.001586 ... -0.000696 0.003783 0.029968 0.078343 -0.015539 0.032789 0.030664 0.018611 0.051973 0.000299
1 0.007062 0.054792 0.006414 -0.003654 0.002591 0.009731 0.006824 -0.012492 0.001748 0.001355 ... 0.000460 0.002993 0.030286 0.075214 -0.014672 0.033336 0.030252 0.016750 0.051518 0.004300
2 0.007431 0.054551 0.006468 -0.003410 0.003069 0.009150 0.007100 -0.012553 0.001517 0.001845 ... 0.000749 0.003484 0.031352 0.073967 -0.013793 0.031789 0.030658 0.016382 0.052068 0.007300
3 0.006643 0.054923 0.006485 -0.003741 0.002603 0.009605 0.006857 -0.012986 0.001931 0.002025 ... 0.000263 0.003009 0.030150 0.075574 -0.014799 0.032864 0.031805 0.016760 0.051292 0.010300
4 0.007127 0.054769 0.006760 -0.004054 0.002216 0.010527 0.007025 -0.012684 0.001756 0.001611 ... -0.000001 0.003914 0.029962 0.073601 -0.014057 0.033337 0.030134 0.016966 0.051691 0.014301

5 rows × 27 columns

Filter the tail log

Sometimes, Stytra will mess the tracking of the last tail segments. Luckily, this can be fixed using a custom interpolation function. It uses n=continue_curvature points to average the difference of consecutive segments and fill the nan values using the same incremental difference. This in general is safe as long as the tail tracking was not completely off.

To use it, before using the behavior log, run the following:

[13]:
exp.reconstruct_missing_segments(continue_curvature=4)
exp.behavior_log.head()
[13]:
tail_sum theta_00 theta_01 theta_02 theta_03 theta_04 theta_05 theta_06 theta_07 theta_08 ... theta_17 theta_18 theta_19 theta_20 theta_21 theta_22 theta_23 theta_24 t missing_n
0 0.009258 0.054718 0.006608 -0.003954 0.002449 0.009792 0.007008 -0.012253 0.001501 0.001586 ... 0.003783 0.029968 0.078343 -0.015539 0.032789 0.030664 0.018611 0.051973 0.000299 0
1 0.007062 0.054792 0.006414 -0.003654 0.002591 0.009731 0.006824 -0.012492 0.001748 0.001355 ... 0.002993 0.030286 0.075214 -0.014672 0.033336 0.030252 0.016750 0.051518 0.004300 0
2 0.007431 0.054551 0.006468 -0.003410 0.003069 0.009150 0.007100 -0.012553 0.001517 0.001845 ... 0.003484 0.031352 0.073967 -0.013793 0.031789 0.030658 0.016382 0.052068 0.007300 0
3 0.006643 0.054923 0.006485 -0.003741 0.002603 0.009605 0.006857 -0.012986 0.001931 0.002025 ... 0.003009 0.030150 0.075574 -0.014799 0.032864 0.031805 0.016760 0.051292 0.010300 0
4 0.007127 0.054769 0.006760 -0.004054 0.002216 0.010527 0.007025 -0.012684 0.001756 0.001611 ... 0.003914 0.029962 0.073601 -0.014057 0.033337 0.030134 0.016966 0.051691 0.014301 0

5 rows × 28 columns

Extract and analyse bouts

To analyse bouts, we will need to:

  1. Compute vigor

  2. Detect bouts

  3. (Optional) Extract bout statistics

1. Compute vigor: use a rolling standard deviation to infer fish speed. After calling the function, a exp._behavior_log["vigor"] column is added to our exp._behavior_log dataframe.

vigor_duration_s is the window for the rolling std in s; the default 0.05 is the lab standard.

[14]:
exp.compute_vigor(vigor_duration_s=0.05)
exp.behavior_log.head()
[14]:
tail_sum theta_00 theta_01 theta_02 theta_03 theta_04 theta_05 theta_06 theta_07 theta_08 ... theta_18 theta_19 theta_20 theta_21 theta_22 theta_23 theta_24 t missing_n vigor
0 0.009258 0.054718 0.006608 -0.003954 0.002449 0.009792 0.007008 -0.012253 0.001501 0.001586 ... 0.029968 0.078343 -0.015539 0.032789 0.030664 0.018611 0.051973 0.000299 0 NaN
1 0.007062 0.054792 0.006414 -0.003654 0.002591 0.009731 0.006824 -0.012492 0.001748 0.001355 ... 0.030286 0.075214 -0.014672 0.033336 0.030252 0.016750 0.051518 0.004300 0 NaN
2 0.007431 0.054551 0.006468 -0.003410 0.003069 0.009150 0.007100 -0.012553 0.001517 0.001845 ... 0.031352 0.073967 -0.013793 0.031789 0.030658 0.016382 0.052068 0.007300 0 NaN
3 0.006643 0.054923 0.006485 -0.003741 0.002603 0.009605 0.006857 -0.012986 0.001931 0.002025 ... 0.030150 0.075574 -0.014799 0.032864 0.031805 0.016760 0.051292 0.010300 0 NaN
4 0.007127 0.054769 0.006760 -0.004054 0.002216 0.010527 0.007025 -0.012684 0.001756 0.001611 ... 0.029962 0.073601 -0.014057 0.033337 0.030134 0.016966 0.051691 0.014301 0 NaN

5 rows × 29 columns

2. Detect bouts: threshold the vigor to find consecutive periods of continuous swimming. # This will return an array with the index (not the time) for the detected bouts start and end points.

vigor_threshold is the threshold on the vigor in a.u.; 0.1 is a reasonable value that will work in 95% of cases.

[15]:
exp.get_bouts(vigor_threshold=0.1)
[15]:
array([[  40,  236],
       [ 377,  584],
       [ 717,  887],
       [1015, 1174]])

3. Extract bouts properties: Infer some properties of the detected bouts. It Returns a dataframe with bout properties.

directionality_duration is a window in seconds to compute directionality idx; lab standard is 0.07 s.

[16]:
exp.get_bout_properties(directionality_duration=0.07)
[16]:
t_start duration peak_vig med_vig bias bias_total n_pos_peaks n_neg_peaks
0 0.133313 0.654065 1.934675 0.012229 -0.087974 -0.026151 15 15
1 1.257425 0.689069 2.929249 0.025798 0.901800 0.169295 15 14
2 2.390538 0.567057 3.014080 0.036779 0.900419 0.223960 13 12
3 3.384638 0.529053 3.910690 0.029872 0.745277 0.130034 11 13
[ ]: