xarray-behave 0.35.5__tar.gz → 0.35.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/.github/workflows/publish.yaml +18 -16
  2. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/PKG-INFO +2 -4
  3. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/README.md +1 -2
  4. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/build_env.yml +0 -6
  5. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/conda/xarray-behave/meta.yaml +11 -24
  6. xarray-behave-0.35.8/condarc.yml +9 -0
  7. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/pyproject.toml +1 -1
  8. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/__init__.py +1 -1
  9. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/annot.py +16 -1
  10. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/app.py +12 -10
  11. xarray-behave-0.35.8/src/xarray_behave/io/samplestamps/__init__.py +6 -0
  12. xarray-behave-0.35.8/src/xarray_behave/io/samplestamps/samplestamps.py +114 -0
  13. xarray-behave-0.35.8/src/xarray_behave/io/samplestamps/utils.py +181 -0
  14. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/loaders.py +1 -1
  15. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/xarray_behave.py +122 -32
  16. xarray-behave-0.35.5/conda/xarray-behave/conda_build_config.yaml +0 -4
  17. xarray-behave-0.35.5/condarc.yml +0 -3
  18. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/.gitignore +0 -0
  19. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/LICENSE +0 -0
  20. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/conda/xarray-behave/bld.bat +0 -0
  21. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/conda/xarray-behave/build.sh +0 -0
  22. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/doc/demo.ipynb +0 -0
  23. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/doc/demo_behavioral_features.ipynb +0 -0
  24. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/doc/demo_behavioral_features_large_group.ipynb +0 -0
  25. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/doc/ncb.mplstyle +0 -0
  26. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/setup.py +0 -0
  27. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/event_utils.py +0 -0
  28. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/__init__.py +0 -0
  29. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/audio_player.py +0 -0
  30. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/das.py +0 -0
  31. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/formbuilder.py +0 -0
  32. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/das_make.yaml +0 -0
  33. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/das_predict.yaml +0 -0
  34. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/das_train.yaml +0 -0
  35. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/envelope_computation.yaml +0 -0
  36. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/export_for_das.yaml +0 -0
  37. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/from_dir.yaml +0 -0
  38. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/from_file.yaml +0 -0
  39. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/from_zarr.yaml +0 -0
  40. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/spec_freq.yaml +0 -0
  41. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/icon.png +0 -0
  42. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/table.py +0 -0
  43. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/utils.py +0 -0
  44. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/view_dialog.py +0 -0
  45. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/views.py +0 -0
  46. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/gui/widgets.py +0 -0
  47. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/io/__init__.py +0 -0
  48. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/io/annotations.py +0 -0
  49. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/io/annotations_manual.py +0 -0
  50. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/io/audio.py +0 -0
  51. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/io/balltracks.py +0 -0
  52. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/io/movieparams.py +0 -0
  53. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/io/poses.py +0 -0
  54. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/io/timestamps.py +0 -0
  55. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/io/tracks.py +0 -0
  56. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/src/xarray_behave/metrics.py +0 -0
  57. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/tests/test_annot.py +0 -0
  58. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/tests/test_assemble.py +0 -0
  59. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/tests/test_assemble_metrics.py +0 -0
  60. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/tests/test_imports.py +0 -0
  61. {xarray-behave-0.35.5 → xarray-behave-0.35.8}/tests/test_io.py +0 -0
@@ -19,11 +19,8 @@ jobs:
19
19
  strategy:
20
20
  fail-fast: False
21
21
  matrix:
22
- python-version: ['3.10']
23
- os: [ubuntu-latest, windows-latest, macOS-13, macOS-14]
24
- include:
25
- - python-version: 3.9
26
- os: windows-latest
22
+ python-version: ['3.10', '3.11', '3.12']
23
+ os: [ubuntu-latest, windows-latest, macOS-14]
27
24
  defaults: # https://github.com/marketplace/actions/setup-miniconda#use-a-default-shell
28
25
  run:
29
26
  shell: bash -l {0}
@@ -33,21 +30,26 @@ jobs:
33
30
  uses: conda-incubator/setup-miniconda@v3
34
31
  with:
35
32
  auto-update-conda: true
36
- miniforge-variant: Mambaforge
37
- miniforge-version: latest
38
- channels: conda-forge,ncb,anaconda,defaults
39
- python-version: 3.11
40
- mamba-version: "*"
33
+ channels: conda-forge,ncb
34
+ python-version: 3.12
41
35
  auto-activate-base: false
42
36
  activate-environment: anaconda-client-env
43
37
  environment-file: build_env.yml
44
38
  condarc-file: condarc.yml
45
- use-mamba: true
46
- use-only-tar-bz2: true # IMPORTANT: This needs to be set for caching to work properly!
47
- - name: Build and upload to anaconda
39
+ - name: Build
48
40
  run: |
49
- conda mambabuild conda/xarray-behave -c conda-forge -c ncb -c anaconda --python ${{ matrix.python-version }} --user ncb --token $ANACONDA_TOKEN
41
+ mkdir ./build
42
+ conda build conda/xarray-behave --no-test --no-anaconda-upload --python ${{ matrix.python-version }} --output-folder ./build
43
+ - name: Test
44
+ run: |
45
+ conda deactivate
46
+ conda create -n xb_test python=${{ matrix.python-version }} xarray-behave -c ./build -c local -c conda-forge -c ncb -y
47
+ conda activate xb_test
48
+ python -c "import xarray_behave; print(xarray_behave.__version__)"
49
+ python -c "import xarray_behave.gui.app"
50
+ # xb --help
51
+ - name: Upload
52
+ run: |
53
+ conda build conda/xarray-behave --no-test --python ${{ matrix.python-version }} --output-folder ./build --user ncb --token ${{ secrets.ANACONDA_TOKEN }}
50
54
  env:
51
55
  ANACONDA_TOKEN: ${{ secrets.ANACONDA_TOKEN }}
52
-
53
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: xarray-behave
3
- Version: 0.35.5
3
+ Version: 0.35.8
4
4
  Summary: xarray tools for behavioral data.
5
5
  Author-email: Jan Clemens <clemensjan@googlemail.com>
6
6
  Requires-Python: >3.6
@@ -15,7 +15,6 @@ Requires-Dist: flammkuchen
15
15
  Requires-Dist: rich
16
16
  Requires-Dist: dask
17
17
  Requires-Dist: toolz
18
- Requires-Dist: samplestamps
19
18
  Requires-Dist: pysoundfile
20
19
  Requires-Dist: opencv-python-headless
21
20
  Requires-Dist: pyside6 ; extra == "gui"
@@ -38,8 +37,7 @@ Provides-Extra: gui
38
37
  Install a working conda installation (see [here](https://docs.conda.io/en/latest/miniconda.html)).
39
38
 
40
39
  ```shell
41
- conda install mamba -c conda-forge -n base -y
42
- mamba create -c conda-forge -c anaconda -c ncb -n xb python=3.9 xarray-behave
40
+ conda create -c conda-forge -c ncb -n xb python=3.12 xarray-behave
43
41
  ```
44
42
 
45
43
  See `demo.ipynb` for usage examples.
@@ -3,8 +3,7 @@
3
3
  Install a working conda installation (see [here](https://docs.conda.io/en/latest/miniconda.html)).
4
4
 
5
5
  ```shell
6
- conda install mamba -c conda-forge -n base -y
7
- mamba create -c conda-forge -c anaconda -c ncb -n xb python=3.9 xarray-behave
6
+ conda create -c conda-forge -c ncb -n xb python=3.12 xarray-behave
8
7
  ```
9
8
 
10
9
  See `demo.ipynb` for usage examples.
@@ -1,15 +1,9 @@
1
1
  name: anaconda-client-env
2
2
  channels:
3
3
  - conda-forge
4
- - anaconda
5
- - ncb
6
- - defaults
7
4
  dependencies:
8
5
  - conda
9
- - mamba
10
6
  - anaconda-client
11
- - boa
12
7
  - conda-build
13
- - conda-verify
14
8
  - pip
15
9
  - git
@@ -16,46 +16,33 @@ requirements:
16
16
  host:
17
17
  - python {{ python }}
18
18
  - pip
19
- - numpy=1.23
20
19
  run:
21
20
  - python {{ python }}
22
21
  - defopt=6.3
23
22
  - rich
24
23
  - flammkuchen
25
24
  - h5py
26
- - librosa>0.8
27
- - matplotlib
28
- - pandas
29
- - scipy>=1.9
30
- - peakutils
31
25
  - pyyaml
32
- - scikit-learn
33
26
  - zarr
34
27
  - numba
35
28
  - xarray
36
29
  - dask
37
- - conda-forge::pyside2
38
- # - conda-forge::pyside2 # [linux or osx or arm64]
39
- # - conda-forge::pyside6 # [win and py>39]
40
- # - conda-forge::pyside2 # [osx or arm64]
41
- # - pyside2 # [win and py==39]
30
+ - librosa>0.8
31
+ - pysoundfile
32
+ - python-sounddevice
33
+ - matplotlib
34
+ - colorcet
35
+ - pandas
36
+ - scipy>=1.9
37
+ - scikit-image
38
+ - scikit-learn
39
+ - peakutils
40
+ - pyside6
42
41
  - pyqtgraph>0.12
43
42
  - qtpy
44
43
  - superqt
45
- - rich
46
- - colorcet
47
- - python-sounddevice
48
- - scikit-image
49
44
  - ffmpeg
50
45
  - pyvideoreader
51
- - samplestamps>=0.6
52
- - pysoundfile
53
-
54
- test:
55
- imports:
56
- - xarray_behave
57
- commands:
58
- - xb --help [ not win ]
59
46
 
60
47
  about:
61
48
  home: "http://github.com/janclemenslab/xarray-behave"
@@ -0,0 +1,9 @@
1
+ anaconda_upload: yes
2
+ always_yes: yes
3
+ changeps1: no
4
+ channel_priority: flexible
5
+ channels:
6
+ - conda-forge
7
+ - ncb
8
+ conda-build:
9
+ pkg_format: 2
@@ -11,7 +11,7 @@ classifiers = ["License :: OSI Approved :: MIT License"]
11
11
  dynamic = ["version", "description"]
12
12
  requires-python = ">3.6"
13
13
  dependencies = ['numpy', 'scipy', 'xarray', 'h5py', 'zarr', 'flammkuchen', 'rich',
14
- 'dask', 'toolz', 'samplestamps', 'pysoundfile', 'opencv-python-headless']
14
+ 'dask', 'toolz', 'pysoundfile', 'opencv-python-headless']
15
15
  readme = "README.md"
16
16
 
17
17
  [project.optional-dependencies]
@@ -1,6 +1,6 @@
1
1
  """xarray tools for behavioral data."""
2
2
 
3
- __version__ = "0.35.5"
3
+ __version__ = "0.35.8"
4
4
 
5
5
  from .xarray_behave import assemble, assemble_metrics, load, save
6
6
  import os
@@ -87,7 +87,7 @@ class Events(UserDict):
87
87
  ):
88
88
  if possible_event_names is None:
89
89
  possible_event_names = []
90
- unique_names = list(set(names))
90
+ unique_names = list(np.unique(names)) # use `np.unique` instead of `set` to have reproducible order of names for now
91
91
  unique_names.extend(possible_event_names)
92
92
  dct = {name: [] for name in unique_names}
93
93
 
@@ -548,6 +548,18 @@ class Events(UserDict):
548
548
  self[name] = np.delete(self[name], indices, axis=0)
549
549
  return indices
550
550
 
551
+ def sort(self, names: Optional[List[str]] = None):
552
+ """Sort annotations by start time.
553
+
554
+ Args:
555
+ names (Optional[List[str]], optional): _description_. Defaults to None.
556
+ """
557
+ if names is None:
558
+ names = self.names
559
+
560
+ for name in names:
561
+ self[name] = self[name][np.argsort(self[name][:, 0]), :]
562
+
551
563
  def find_next(self, t: float, names: Optional[List[str]] = None):
552
564
  """Find event starting after `t` of type in names.
553
565
 
@@ -564,7 +576,9 @@ class Events(UserDict):
564
576
  names = self.names
565
577
 
566
578
  nxt = []
579
+
567
580
  for name in names:
581
+ self.sort([name])
568
582
  cmp = self[name][:, 0] > t
569
583
  if np.any(cmp):
570
584
  nxt.append(self[name][np.argmax(cmp), 0])
@@ -588,6 +602,7 @@ class Events(UserDict):
588
602
 
589
603
  nxt = []
590
604
  for name in names:
605
+ self.sort([name])
591
606
  cmp = self[name][:, 1] < t
592
607
  if np.any(cmp):
593
608
  nxt.append(self[name][np.argmin(cmp) - 1, 0])
@@ -2084,24 +2084,26 @@ class PSV(MainWindow):
2084
2084
  self.update_frame()
2085
2085
 
2086
2086
  def set_prev_cuepoint(self, qt_keycode):
2087
- if self.edit_only_current_events: # of the currently active type
2088
- names = [self.current_event_name]
2089
- else: # of any type
2090
- names = self.event_times.names
2091
-
2087
+ # if self.edit_only_current_events: # of the currently active type
2088
+ # names = [self.current_event_name]
2089
+ # else: # of any type
2090
+ # names = self.event_times.names
2091
+ names = [self.current_event_name]
2092
2092
  t = (self.t0 - 1) / self.fs_song
2093
2093
  nxt = self.event_times.find_prev(t, names)
2094
+
2094
2095
  if nxt is not None:
2095
2096
  self.t0 = nxt * self.fs_song
2096
2097
 
2097
2098
  def set_next_cuepoint(self, qt_keycode):
2098
- if self.edit_only_current_events: # of the currently active type
2099
- names = [self.current_event_name]
2100
- else: # of any type
2101
- names = self.event_times.names
2102
-
2099
+ # if self.edit_only_current_events: # of the currently active type
2100
+ # names = [self.current_event_name]
2101
+ # else: # of any type
2102
+ # names = self.event_times.names
2103
+ names = [self.current_event_name]
2103
2104
  t = (self.t0 + 1) / self.fs_song
2104
2105
  nxt = self.event_times.find_next(t, names)
2106
+
2105
2107
  if nxt is not None:
2106
2108
  self.t0 = nxt * self.fs_song
2107
2109
 
@@ -0,0 +1,6 @@
1
+ """For converting between timestamps."""
2
+
3
+ __version__ = "0.6.0"
4
+
5
+
6
+ from .samplestamps import *
@@ -0,0 +1,114 @@
1
+ """Tools for converting between samples and time stamps.
2
+
3
+ glossary:
4
+ densely stamped - each sample is consecutively time stamped
5
+ sparsely stamped - only selected samples are time stamped - need to provide sample numbers
6
+ """
7
+
8
+ from . import utils
9
+
10
+
11
+ class SampStamp:
12
+ """Converts between frames and samples."""
13
+
14
+ def __init__(
15
+ self,
16
+ sample_times,
17
+ frame_times=None,
18
+ sample_numbers=None,
19
+ frame_numbers=None,
20
+ frame_samples=None,
21
+ sample_times_offset=0,
22
+ frame_times_offset=0,
23
+ auto_monotonize=True,
24
+ ):
25
+ """Get converter.
26
+
27
+ Args:
28
+ sample_times(np.ndarray)
29
+ frame_times(np.ndarray)
30
+ sample_number(np.ndarray)
31
+ frame_number(np.ndarray)
32
+ frame_samples
33
+ sample_times_offset(float)
34
+ frame_times_offset(float)
35
+ auto_monotonize(bool)
36
+ """
37
+ # we want:
38
+ # samples -> frames
39
+ # samples -> times
40
+ # frames -> samples
41
+ # frames -> times
42
+
43
+ # we need:
44
+ # (samples, times) from DAQ, (frames, times) from video timestamps
45
+ # (samples, times) from DAQ, (samples, frames) from movieframes
46
+ # (samples, frames) from movieframes, (frames, times) from video timestamps (ignore edge case)
47
+
48
+ # generate dense x_number arrays
49
+ if sample_numbers is None and sample_times is not None:
50
+ sample_numbers = range(sample_times.shape[0])
51
+ if frame_numbers is None and frame_times is not None:
52
+ frame_numbers = range(frame_times.shape[0])
53
+
54
+ # correct for offsets
55
+ sample_times += sample_times_offset
56
+ if frame_times is not None:
57
+ frame_times += frame_times_offset
58
+
59
+ if auto_monotonize:
60
+ if sample_times is not None:
61
+ sample_times = utils.monotonize(sample_times)
62
+ sample_numbers = sample_numbers[: sample_times.shape[0]]
63
+ if frame_times is not None:
64
+ frame_times = utils.monotonize(frame_times)
65
+ frame_numbers = frame_numbers[: frame_times.shape[0]]
66
+ if frame_samples is not None:
67
+ frame_samples = utils.monotonize(frame_samples)
68
+ # frame_numbers = frame_numbers[:frame_times.shape[0]]
69
+
70
+ # get all interpolators for re-use
71
+ if sample_numbers is not None and sample_times is not None:
72
+ self.samples2times = utils.interpolator(sample_numbers, sample_times)
73
+ if frame_times is None and frame_samples is not None:
74
+ frame_times = self.samples2times(frame_samples)
75
+ # self.frame_times = self.samples2times(frame_samples)
76
+
77
+ if frame_numbers is not None and frame_times is not None:
78
+ self.frames2times = utils.interpolator(frame_numbers, frame_times)
79
+ if frame_times is not None and sample_numbers is not None:
80
+ self.times2samples = utils.interpolator(sample_times, sample_numbers)
81
+ if frame_times is not None and frame_numbers is not None:
82
+ self.times2frames = utils.interpolator(frame_times, frame_numbers)
83
+
84
+ if frame_samples is not None:
85
+ self.samples2frames = utils.interpolator(frame_samples, frame_numbers, fill_value="extrapolate")
86
+
87
+ def frame(self, sample):
88
+ """Get frame number from sample number."""
89
+ return self.times2frames(self.sample_time(sample))
90
+
91
+ def sample(self, frame):
92
+ """Get sample number from frame number."""
93
+ return self.times2samples(self.frame_time(frame))
94
+
95
+ def frame_time(self, frame):
96
+ """Get time of frame number."""
97
+ return self.frames2times(frame)
98
+
99
+ def sample_time(self, sample):
100
+ """Get time of sample number."""
101
+ return self.samples2times(sample)
102
+
103
+
104
+ class SimpleStamp(SampStamp):
105
+ """If all you need is conversion between sampling rates."""
106
+
107
+ def __init__(self, sampling_rate: float):
108
+
109
+ self.sampling_rate = sampling_rate
110
+ self.samples2times = utils.SampleInterpolator(self.sampling_rate)
111
+ self.times2samples = utils.SampleInterpolator(1 / self.sampling_rate)
112
+
113
+ self.frames2times = self.samples2times
114
+ self.times2frames = self.times2samples
@@ -0,0 +1,181 @@
1
+ import numpy as np
2
+ import operator as op
3
+ from datetime import datetime
4
+ import scipy.interpolate
5
+
6
+
7
+ def ismonotonous(x, direction="increasing", strict=True):
8
+ """Check if vector is monotonous.
9
+
10
+ Args:
11
+ x(np.ndarray)
12
+ direction(str): 'increasing' or 'decreasing'
13
+ strict(bool): defaults to True
14
+ Returns:
15
+ (bool)
16
+ """
17
+ allowed_directions = ["increasing", "decreasing"]
18
+ if direction not in allowed_directions:
19
+ raise ValueError(f'Direction "{direction}" must be in {allowed_directions}.')
20
+
21
+ if direction == "decreasing":
22
+ x = -x
23
+
24
+ if strict:
25
+ comp_op = op.gt # >
26
+ else:
27
+ comp_op = op.ge # >=
28
+
29
+ return np.all(comp_op(x[1:], x[:-1]))
30
+
31
+
32
+ def monotonize(x, direction="increasing", strict=True):
33
+ """Cut trailing non-monotonous values.
34
+
35
+ Args:
36
+ x
37
+ direction - montonously 'increasing' (default) or 'decreasing'
38
+ strict - strictly (default) or non-strictly monotonous
39
+ Returns:
40
+ truncated array
41
+ """
42
+ allowed_directions = ["increasing", "decreasing"]
43
+ if direction not in allowed_directions:
44
+ raise ValueError(f'Direction "{direction}" must be in {allowed_directions}.')
45
+
46
+ if strict:
47
+ comp_op = op.le # >=
48
+ else:
49
+ comp_op = op.lt # >
50
+
51
+ if direction == "decreasing":
52
+ comp = comp_op(x[:-1], x[1:])
53
+ else:
54
+ comp = comp_op(x[1:], x[:-1])
55
+
56
+ if np.all(~comp):
57
+ last_idx = len(comp) + 1
58
+ else:
59
+ last_idx = np.argmax(comp) + 1
60
+
61
+ return x[:last_idx]
62
+
63
+
64
+ def interpolator(x, y, fill_value="extrapolate"):
65
+ return scipy.interpolate.interp1d(x, y, fill_value=fill_value)
66
+
67
+
68
+ class SampleInterpolator:
69
+ """For samples on a regular grid"""
70
+
71
+ def __init__(self, sampling_rate: float):
72
+ self.sampling_rate = float(sampling_rate)
73
+ # dummy values for compatibililty with Interpolators from scipy.interpolate
74
+ self.x = np.array([0.0, 1.0])
75
+ self.y = self.x / self.sampling_rate
76
+
77
+ def __call__(self, sample_numbers):
78
+ return np.asarray(sample_numbers) / self.sampling_rate
79
+
80
+
81
+ def time_from_log(logfilename, line_number=1):
82
+ """Parse time stamp from a specified lines in a log file.
83
+
84
+ Args:
85
+ logfilename(str)
86
+ line_number(int): line in the log file from which parse the time stamp (defaults to 1 - will read the first line (not 0-indexed!))
87
+ Returns:
88
+ (datetime) time stamp
89
+ """
90
+ with open(logfilename, "r") as f:
91
+ for _ in range(line_number):
92
+ current_line = f.readline()
93
+
94
+ current_line_parts = current_line.partition(" ")[0]
95
+ return datetime.strptime(current_line_parts, "%Y-%m-%d,%H:%M:%S.%f")
96
+
97
+
98
+ def samplenumber_from_timestamps(target_time, timestamps, sample_at_timestamps=None):
99
+ """Gets samplenumber from timestamps given time.
100
+
101
+ Args:
102
+ target_time (numpy.ndarrary): time of desired sample
103
+ timestamps (numpy.ndarrary): list of timestamps
104
+ sample_at_timestamps: can be provided for sparsely stamped data, the sample number for each timestamp
105
+ Returns:
106
+ samplenumber at target time (as an index, starts at 0, can be <0 if target is before first timestamps) (np.intp)
107
+ """
108
+ if not ismonotonous(timestamps, strict=True):
109
+ raise ValueError(f"Timestamps must increase strictly monotonously.")
110
+
111
+ if sample_at_timestamps is None:
112
+ sample_at_timestamps = range(timestamps.shape[0])
113
+
114
+ f = interpolator(timestamps, sample_at_timestamps)
115
+ samplenumber = np.intp(np.round(f(target_time)))
116
+
117
+ return samplenumber
118
+
119
+
120
+ def samplerange_from_timestamps(target_epoch, timestamps, sample_at_timestamps=None):
121
+ """Gets range of samples from timestamps given a epoch defined by start and stop time.
122
+
123
+ Args:
124
+ target_epoch (numpy.ndarrary): start and stop time
125
+ timestamps (numpy.ndarrary): list of timestamps
126
+ sample_at_timestamps: can be provided for sparsely stamped data, the sample number for each timestamp
127
+ Returns:
128
+ range of samples spanning epoch (as an indices, starts at 0, can be <0 if targets extends to before first timestamps)
129
+ """
130
+ samplenumber_start = samplenumber_from_timestamps(target_epoch[0], timestamps, sample_at_timestamps)
131
+ samplenumber_end = samplenumber_from_timestamps(target_epoch[1], timestamps, sample_at_timestamps)
132
+ return range(samplenumber_start, samplenumber_end)
133
+
134
+
135
+ def timestamp_from_samplenumber(samplenumber, timestamps, sample_at_timestamps=None):
136
+ """Gets samplenumber from timestamps given time.
137
+
138
+ Args:
139
+ samplenumber (numpy.ndarrary): sample number for which we want the time stamp
140
+ timestamps (numpy.ndarrary): list of timestamps
141
+ sample_at_timestamps: can be provided for sparsely stamped data, the sample number for each timestamp
142
+ Returns:
143
+ time stamp for that sample (float)
144
+ """
145
+ if not ismonotonous(timestamps, strict=True):
146
+ raise ValueError(f"Timestamps must increase strictly monotonously.")
147
+
148
+ if sample_at_timestamps is None:
149
+ sample_at_timestamps = range(timestamps.shape[0])
150
+
151
+ f = interpolator(sample_at_timestamps, timestamps)
152
+ timestamp = f(samplenumber)
153
+
154
+ return timestamp
155
+
156
+
157
+ def samples_from_samples(sample_in, in_stamps, in_samplenumber=None, out_stamps=None, out_samplenumber=None):
158
+ """Convert between different sampling grids via a common clock.
159
+ Args:
160
+ sample_in: sample number in INPUT sampling grid
161
+ in_stamps: time stamps of samples numbers `in_samplenumber` in OUTPUT sampling grid
162
+ in_samplenumber: sample numbers for INPUT timestamps (defaults to None for densely stamped data)
163
+ out_stamps: time stamps of samples numbers `out_samplenumber` in OUTPUT sampling grid
164
+ out_samplenumber: sample numbers for INPUT timestamps (defaults to None for densely stamped data)
165
+ Returns:
166
+ sample number in the OUTPUT sampling grid corresponding to sample_in
167
+ """
168
+ time_in = timestamp_from_samplenumber(sample_in, in_stamps, in_samplenumber)
169
+ sample_out = samplenumber_from_timestamps(time_in, out_stamps, out_samplenumber)
170
+ return sample_out
171
+
172
+
173
+ def timestamp_from_cycles(cycleOffset, cycleSecs):
174
+ times = cycleOffset + cycleSecs / 8000
175
+ # correct counter overflows
176
+ overflows = np.cumsum((np.diff(times) <= 0).astype(np.uint))
177
+ overflows = np.insert(overflows, 0, 0)
178
+ times = times + overflows * 128
179
+ # offset such that first time is zero
180
+ times = times - np.min(times)
181
+ return times
@@ -7,7 +7,7 @@ import scipy.ndimage
7
7
  import scipy.signal
8
8
  import scipy.stats
9
9
  from scipy.ndimage import maximum_filter1d
10
- from samplestamps import SampStamp
10
+ from .io.samplestamps import SampStamp
11
11
  from . import io
12
12
 
13
13
 
@@ -1,7 +1,7 @@
1
1
  """Create self-documenting xarray dataset from behavioral recordings and annotations."""
2
2
 
3
3
  import numpy as np
4
- from samplestamps.samplestamps import SampStamp, SimpleStamp
4
+ from .io.samplestamps import SampStamp, SimpleStamp
5
5
  import scipy.interpolate
6
6
  import scipy.stats
7
7
  import xarray as xr
@@ -145,7 +145,11 @@ def assemble(
145
145
  else:
146
146
  basename = filepath_daq
147
147
 
148
- audio_loader = io.get_loader(kind="audio", basename=basename, basename_is_full_name=filepath_daq_is_custom)
148
+ audio_loader = io.get_loader(
149
+ kind="audio",
150
+ basename=basename,
151
+ basename_is_full_name=filepath_daq_is_custom,
152
+ )
149
153
  if not audio_loader and filepath_daq_is_custom:
150
154
  audio_loader = io.audio.AudioFile(basename)
151
155
 
@@ -177,6 +181,7 @@ def assemble(
177
181
  ss_ball, last_sample_number_ball, sampling_rate_ball = ld.load_times(filepath_timestamps_ball, filepath_daq)
178
182
 
179
183
  filepath_timestamps_movie = Path(root, res_path, datename, f"{datename}_movieframes.csv")
184
+ ss_movie = None
180
185
  if os.path.exists(filepath_daq) and os.path.exists(filepath_timestamps_movie):
181
186
  ss_movie, last_sample_number_movie, sampling_rate_movie = ld.load_movietimes(filepath_timestamps_movie, filepath_daq)
182
187
 
@@ -284,20 +289,31 @@ def assemble(
284
289
  with_movieparams = False
285
290
  if include_movieparams:
286
291
  logger.info("Loading movie params:")
287
- movieparams_loader = io.get_loader(kind="movieparams", basename=os.path.join(root, dat_path, datename, datename))
288
- if movieparams_loader:
289
- try:
290
- xr_movieparams = movieparams_loader.make(movieparams_loader.path)
291
- xr_movieparams = add_time(xr_movieparams, ss_movie, dim="frame_number_movie", suffix="_movie")
292
-
293
- logger.info(f" {movieparams_loader.path} loaded.")
294
- with_movieparams = True
295
- except Exception as e:
296
- logger.info(f" Loading {movieparams_loader.path} failed.")
297
- logger.exception(e)
292
+ if ss_movie is None:
293
+ logger.warning(" Failed loading movie params - no ss_movie")
298
294
  else:
299
- logger.info(" Found no movie params data.")
300
- logger.info("Done.")
295
+ movieparams_loader = io.get_loader(
296
+ kind="movieparams",
297
+ basename=os.path.join(root, dat_path, datename, datename),
298
+ )
299
+ if movieparams_loader:
300
+ try:
301
+ xr_movieparams = movieparams_loader.make(movieparams_loader.path)
302
+ xr_movieparams = add_time(
303
+ xr_movieparams,
304
+ ss_movie,
305
+ dim="frame_number_movie",
306
+ suffix="_movie",
307
+ )
308
+
309
+ logger.info(f" {movieparams_loader.path} loaded.")
310
+ with_movieparams = True
311
+ except Exception as e:
312
+ logger.info(f" Loading {movieparams_loader.path} failed.")
313
+ logger.exception(e)
314
+ else:
315
+ logger.warning(" Found no movie params data.")
316
+ logger.info("Done.")
301
317
 
302
318
  # Init empty and event data
303
319
  auto_event_seconds: Dict[str, Any] = {}
@@ -352,7 +368,9 @@ def assemble(
352
368
  # load MANUAL SONG ANNOTATIONS
353
369
  logger.info("Loading manual annotations:")
354
370
  manual_annot_loader = io.get_loader(
355
- kind="annotations_manual", basename=filepath_annotations, basename_is_full_name=custom_filepath_annotations
371
+ kind="annotations_manual",
372
+ basename=filepath_annotations,
373
+ basename_is_full_name=custom_filepath_annotations,
356
374
  )
357
375
  if manual_annot_loader:
358
376
  try:
@@ -377,7 +395,9 @@ def assemble(
377
395
  ) # or construct form audio file name - .ext + _definitions.csv
378
396
 
379
397
  definitions_loader = io.get_loader(
380
- kind="definitions_manual", basename=filepath_definitions, basename_is_full_name=custom_filepath_definitions
398
+ kind="definitions_manual",
399
+ basename=filepath_definitions,
400
+ basename_is_full_name=custom_filepath_definitions,
381
401
  )
382
402
  if definitions_loader:
383
403
  try:
@@ -404,14 +424,20 @@ def assemble(
404
424
  else:
405
425
  basename = filepath_daq
406
426
 
407
- audio_loader = io.get_loader(kind="audio", basename=basename, basename_is_full_name=filepath_daq_is_custom)
427
+ audio_loader = io.get_loader(
428
+ kind="audio",
429
+ basename=basename,
430
+ basename_is_full_name=filepath_daq_is_custom,
431
+ )
408
432
  if not audio_loader and filepath_daq_is_custom:
409
433
  audio_loader = io.audio.AudioFile(basename)
410
434
 
411
435
  if audio_loader:
412
436
  try:
413
437
  song_raw, non_song_raw, samplerate = audio_loader.load(
414
- audio_loader.path, return_nonsong_channels=True, lazy=lazy_load_song
438
+ audio_loader.path,
439
+ return_nonsong_channels=True,
440
+ lazy=lazy_load_song,
415
441
  )
416
442
  logger.info(f" {audio_loader.path} with shape {song_raw.shape} loaded using {audio_loader.NAME}.")
417
443
  except Exception as e:
@@ -441,7 +467,10 @@ def assemble(
441
467
  f"No tracks - setting first/last tracked frame numbers to those of the first/last sample in the recording ({first_tracked_frame}, {last_tracked_frame})."
442
468
  )
443
469
  else:
444
- first_tracked_frame, last_tracked_frame = int(xr_tracks.frame_number[0]), int(xr_tracks.frame_number[-1])
470
+ first_tracked_frame, last_tracked_frame = (
471
+ int(xr_tracks.frame_number[0]),
472
+ int(xr_tracks.frame_number[-1]),
473
+ )
445
474
  logger.info(f"Tracked frame {first_tracked_frame} to {last_tracked_frame}.")
446
475
 
447
476
  # construct desired sample grid for data
@@ -544,7 +573,14 @@ def assemble(
544
573
 
545
574
  if with_tracks:
546
575
  logger.info(" Tracking")
547
- xr_tracks = align_time(xr_tracks, ss, target_samples, ref_time=ref_time, target_time=time, extrapolate=True)
576
+ xr_tracks = align_time(
577
+ xr_tracks,
578
+ ss,
579
+ target_samples,
580
+ ref_time=ref_time,
581
+ target_time=time,
582
+ extrapolate=True,
583
+ )
548
584
  xr_tracks.attrs.update(
549
585
  {
550
586
  "description": 'coords are "allocentric" - rel. to the full frame',
@@ -595,7 +631,13 @@ def assemble(
595
631
  if with_balltracker:
596
632
  logger.info(" Balltracker")
597
633
  xr_balltracks = align_time(
598
- xr_balltracks, ss_ball, target_samples, target_time=time, dim="frame_number_ball", suffix="_ball", ref_time=ref_time
634
+ xr_balltracks,
635
+ ss_ball,
636
+ target_samples,
637
+ target_time=time,
638
+ dim="frame_number_ball",
639
+ suffix="_ball",
640
+ ref_time=ref_time,
599
641
  )
600
642
  xr_balltracks.attrs.update(
601
643
  {
@@ -639,10 +681,17 @@ def assemble(
639
681
  if "sampletime" not in dataset:
640
682
  dataset.coords["sampletime"] = time
641
683
  if "nearest_frame" not in dataset:
642
- dataset.coords["nearest_frame"] = (("time"), (ss.times2frames(dataset["time"] + ref_time).astype(np.intp)))
684
+ dataset.coords["nearest_frame"] = (
685
+ ("time"),
686
+ (ss.times2frames(dataset["time"] + ref_time).astype(np.intp)),
687
+ )
643
688
 
644
689
  # convert spatial units to mm using info in attrs
645
- dataset = convert_spatial_units(dataset, to_units="mm", names=["body_positions", "pose_positions", "pose_positions_allo"])
690
+ dataset = convert_spatial_units(
691
+ dataset,
692
+ to_units="mm",
693
+ names=["body_positions", "pose_positions", "pose_positions_allo"],
694
+ )
646
695
 
647
696
  # save command line args
648
697
  dataset.attrs = {
@@ -688,7 +737,12 @@ def _interp(ds, dim, target_frames_float, interp_kwargs={}):
688
737
  # but now the first dim is size 1 - so we need to fix this
689
738
  coords = ds_new.coords
690
739
  # maps for the too short dims: dim name - ref dim
691
- coords_map = {"flies": "flies", "chambers": "flies", "data_ball": "data_ball", "params_movie": "params_movie"}
740
+ coords_map = {
741
+ "flies": "flies",
742
+ "chambers": "flies",
743
+ "data_ball": "data_ball",
744
+ "params_movie": "params_movie",
745
+ }
692
746
  new_coords = {}
693
747
  for key in coords_map.keys():
694
748
  if key in coords:
@@ -710,7 +764,12 @@ def _interp(ds, dim, target_frames_float, interp_kwargs={}):
710
764
  # now interp all remaining dims
711
765
  for fly in range(1, new_shape[1]):
712
766
  interpolator = scipy.interpolate.interp1d(
713
- ds[dim].data, ds.data[:, fly, ...], axis=0, bounds_error=False, assume_sorted=True, **interp_kwargs
767
+ ds[dim].data,
768
+ ds.data[:, fly, ...],
769
+ axis=0,
770
+ bounds_error=False,
771
+ assume_sorted=True,
772
+ **interp_kwargs,
714
773
  )
715
774
  ds_x.data[:, fly, ...] = interpolator(target_frames_float)
716
775
 
@@ -736,12 +795,23 @@ def align_time(
736
795
  ds = _interp(ds, dim, target_frames_float, interp_kwargs)
737
796
  # ds = ds.interp({dim: target_frames_float}, assume_sorted=True, kwargs=interp_kwargs)
738
797
 
739
- ds = ds.drop_vars(["frame_number" + suffix, "frame_times" + suffix, "frame_samples" + suffix], errors="Ignore")
798
+ ds = ds.drop_vars(
799
+ ["frame_number" + suffix, "frame_times" + suffix, "frame_samples" + suffix],
800
+ errors="Ignore",
801
+ )
740
802
 
741
803
  # time_new = ds['frametimes' + suffix] - ref_time
742
804
  ds = ds.assign_coords({"time": ((dim), target_time)})
743
805
  ds = ds.swap_dims({dim: "time"})
744
- ds = ds.assign_coords({"nearest_frame" + suffix: (("time"), np.round(target_frames_float).astype(np.intp))})
806
+ ds = ds.assign_coords(
807
+ {
808
+ "nearest_frame"
809
+ + suffix: (
810
+ ("time"),
811
+ np.round(target_frames_float).astype(np.intp),
812
+ )
813
+ }
814
+ )
745
815
 
746
816
  fps = 1 / np.nanmean(np.diff(ds["frametimes" + suffix]))
747
817
  ds.attrs.update({"video_fps": fps})
@@ -802,7 +872,12 @@ def assemble_metrics(
802
872
  time = dataset.time.data
803
873
  nearest_frame = dataset.nearest_frame.data
804
874
 
805
- pose_names = {"thorax": "thorax", "head": "head", "left_wing": "left_wing", "right_wing": "right_wing"}
875
+ pose_names = {
876
+ "thorax": "thorax",
877
+ "head": "head",
878
+ "left_wing": "left_wing",
879
+ "right_wing": "right_wing",
880
+ }
806
881
  if custom_pose_names is not None:
807
882
  pose_names.update(custom_pose_names)
808
883
 
@@ -912,7 +987,11 @@ def assemble_metrics(
912
987
  ds_dict["abs_features"] = xr.DataArray(
913
988
  data=absolute.data,
914
989
  dims=["time", "flies", "absolute_features"],
915
- coords={"time": time, "absolute_features": abs_feature_names, "nearest_frame": (("time"), nearest_frame)},
990
+ coords={
991
+ "time": time,
992
+ "absolute_features": abs_feature_names,
993
+ "nearest_frame": (("time"), nearest_frame),
994
+ },
916
995
  attrs={
917
996
  "description": 'coords are "egocentric" - rel. to box',
918
997
  "sampling_rate_Hz": sampling_rate,
@@ -932,7 +1011,14 @@ def assemble_metrics(
932
1011
  )
933
1012
  rel_velocities_mag = np.sqrt(rel_velocities_forward**2 + rel_velocities_lateral**2)
934
1013
 
935
- list_relative = [dis, rel_angles, rel_orientation, rel_velocities_mag, rel_velocities_forward, rel_velocities_lateral]
1014
+ list_relative = [
1015
+ dis,
1016
+ rel_angles,
1017
+ rel_orientation,
1018
+ rel_velocities_mag,
1019
+ rel_velocities_forward,
1020
+ rel_velocities_lateral,
1021
+ ]
936
1022
 
937
1023
  rel_feature_names = [
938
1024
  "distance",
@@ -947,7 +1033,11 @@ def assemble_metrics(
947
1033
  ds_dict["rel_features"] = xr.DataArray(
948
1034
  data=relative.data,
949
1035
  dims=["time", "flies", "relative_flies", "relative_features"],
950
- coords={"time": time, "relative_features": rel_feature_names, "nearest_frame": (("time"), nearest_frame)},
1036
+ coords={
1037
+ "time": time,
1038
+ "relative_features": rel_feature_names,
1039
+ "nearest_frame": (("time"), nearest_frame),
1040
+ },
951
1041
  attrs={
952
1042
  "description": 'coords are "egocentric" - rel. to box',
953
1043
  "sampling_rate_Hz": sampling_rate,
@@ -1,4 +0,0 @@
1
- # python:
2
- # - 3.8
3
- # numpy:
4
- # - 1.19
@@ -1,3 +0,0 @@
1
- anaconda_upload: yes
2
- always_yes: yes
3
- changeps1: no
File without changes
File without changes