xarray-behave 0.35.7__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.7 → xarray-behave-0.35.8}/.github/workflows/publish.yaml +18 -16
  2. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/PKG-INFO +2 -4
  3. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/README.md +1 -2
  4. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/build_env.yml +0 -6
  5. {xarray-behave-0.35.7 → 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.7 → xarray-behave-0.35.8}/pyproject.toml +1 -1
  8. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/__init__.py +1 -1
  9. xarray-behave-0.35.8/src/xarray_behave/io/samplestamps/__init__.py +6 -0
  10. xarray-behave-0.35.8/src/xarray_behave/io/samplestamps/samplestamps.py +114 -0
  11. xarray-behave-0.35.8/src/xarray_behave/io/samplestamps/utils.py +181 -0
  12. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/loaders.py +1 -1
  13. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/xarray_behave.py +107 -21
  14. xarray-behave-0.35.7/conda/xarray-behave/conda_build_config.yaml +0 -4
  15. xarray-behave-0.35.7/condarc.yml +0 -3
  16. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/.gitignore +0 -0
  17. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/LICENSE +0 -0
  18. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/conda/xarray-behave/bld.bat +0 -0
  19. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/conda/xarray-behave/build.sh +0 -0
  20. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/doc/demo.ipynb +0 -0
  21. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/doc/demo_behavioral_features.ipynb +0 -0
  22. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/doc/demo_behavioral_features_large_group.ipynb +0 -0
  23. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/doc/ncb.mplstyle +0 -0
  24. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/setup.py +0 -0
  25. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/annot.py +0 -0
  26. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/event_utils.py +0 -0
  27. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/__init__.py +0 -0
  28. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/app.py +0 -0
  29. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/audio_player.py +0 -0
  30. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/das.py +0 -0
  31. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/formbuilder.py +0 -0
  32. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/das_make.yaml +0 -0
  33. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/das_predict.yaml +0 -0
  34. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/das_train.yaml +0 -0
  35. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/envelope_computation.yaml +0 -0
  36. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/export_for_das.yaml +0 -0
  37. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/from_dir.yaml +0 -0
  38. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/from_file.yaml +0 -0
  39. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/from_zarr.yaml +0 -0
  40. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/forms/spec_freq.yaml +0 -0
  41. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/icon.png +0 -0
  42. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/table.py +0 -0
  43. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/utils.py +0 -0
  44. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/view_dialog.py +0 -0
  45. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/views.py +0 -0
  46. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/gui/widgets.py +0 -0
  47. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/io/__init__.py +0 -0
  48. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/io/annotations.py +0 -0
  49. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/io/annotations_manual.py +0 -0
  50. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/io/audio.py +0 -0
  51. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/io/balltracks.py +0 -0
  52. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/io/movieparams.py +0 -0
  53. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/io/poses.py +0 -0
  54. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/io/timestamps.py +0 -0
  55. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/io/tracks.py +0 -0
  56. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/src/xarray_behave/metrics.py +0 -0
  57. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/tests/test_annot.py +0 -0
  58. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/tests/test_assemble.py +0 -0
  59. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/tests/test_assemble_metrics.py +0 -0
  60. {xarray-behave-0.35.7 → xarray-behave-0.35.8}/tests/test_imports.py +0 -0
  61. {xarray-behave-0.35.7 → 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.7
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.7"
3
+ __version__ = "0.35.8"
4
4
 
5
5
  from .xarray_behave import assemble, assemble_metrics, load, save
6
6
  import os
@@ -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
 
@@ -288,11 +292,19 @@ def assemble(
288
292
  if ss_movie is None:
289
293
  logger.warning(" Failed loading movie params - no ss_movie")
290
294
  else:
291
- movieparams_loader = io.get_loader(kind="movieparams", basename=os.path.join(root, dat_path, datename, datename))
295
+ movieparams_loader = io.get_loader(
296
+ kind="movieparams",
297
+ basename=os.path.join(root, dat_path, datename, datename),
298
+ )
292
299
  if movieparams_loader:
293
300
  try:
294
301
  xr_movieparams = movieparams_loader.make(movieparams_loader.path)
295
- xr_movieparams = add_time(xr_movieparams, ss_movie, dim="frame_number_movie", suffix="_movie")
302
+ xr_movieparams = add_time(
303
+ xr_movieparams,
304
+ ss_movie,
305
+ dim="frame_number_movie",
306
+ suffix="_movie",
307
+ )
296
308
 
297
309
  logger.info(f" {movieparams_loader.path} loaded.")
298
310
  with_movieparams = True
@@ -356,7 +368,9 @@ def assemble(
356
368
  # load MANUAL SONG ANNOTATIONS
357
369
  logger.info("Loading manual annotations:")
358
370
  manual_annot_loader = io.get_loader(
359
- 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,
360
374
  )
361
375
  if manual_annot_loader:
362
376
  try:
@@ -381,7 +395,9 @@ def assemble(
381
395
  ) # or construct form audio file name - .ext + _definitions.csv
382
396
 
383
397
  definitions_loader = io.get_loader(
384
- 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,
385
401
  )
386
402
  if definitions_loader:
387
403
  try:
@@ -408,14 +424,20 @@ def assemble(
408
424
  else:
409
425
  basename = filepath_daq
410
426
 
411
- 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
+ )
412
432
  if not audio_loader and filepath_daq_is_custom:
413
433
  audio_loader = io.audio.AudioFile(basename)
414
434
 
415
435
  if audio_loader:
416
436
  try:
417
437
  song_raw, non_song_raw, samplerate = audio_loader.load(
418
- 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,
419
441
  )
420
442
  logger.info(f" {audio_loader.path} with shape {song_raw.shape} loaded using {audio_loader.NAME}.")
421
443
  except Exception as e:
@@ -445,7 +467,10 @@ def assemble(
445
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})."
446
468
  )
447
469
  else:
448
- 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
+ )
449
474
  logger.info(f"Tracked frame {first_tracked_frame} to {last_tracked_frame}.")
450
475
 
451
476
  # construct desired sample grid for data
@@ -548,7 +573,14 @@ def assemble(
548
573
 
549
574
  if with_tracks:
550
575
  logger.info(" Tracking")
551
- 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
+ )
552
584
  xr_tracks.attrs.update(
553
585
  {
554
586
  "description": 'coords are "allocentric" - rel. to the full frame',
@@ -599,7 +631,13 @@ def assemble(
599
631
  if with_balltracker:
600
632
  logger.info(" Balltracker")
601
633
  xr_balltracks = align_time(
602
- 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,
603
641
  )
604
642
  xr_balltracks.attrs.update(
605
643
  {
@@ -643,10 +681,17 @@ def assemble(
643
681
  if "sampletime" not in dataset:
644
682
  dataset.coords["sampletime"] = time
645
683
  if "nearest_frame" not in dataset:
646
- 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
+ )
647
688
 
648
689
  # convert spatial units to mm using info in attrs
649
- 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
+ )
650
695
 
651
696
  # save command line args
652
697
  dataset.attrs = {
@@ -692,7 +737,12 @@ def _interp(ds, dim, target_frames_float, interp_kwargs={}):
692
737
  # but now the first dim is size 1 - so we need to fix this
693
738
  coords = ds_new.coords
694
739
  # maps for the too short dims: dim name - ref dim
695
- 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
+ }
696
746
  new_coords = {}
697
747
  for key in coords_map.keys():
698
748
  if key in coords:
@@ -714,7 +764,12 @@ def _interp(ds, dim, target_frames_float, interp_kwargs={}):
714
764
  # now interp all remaining dims
715
765
  for fly in range(1, new_shape[1]):
716
766
  interpolator = scipy.interpolate.interp1d(
717
- 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,
718
773
  )
719
774
  ds_x.data[:, fly, ...] = interpolator(target_frames_float)
720
775
 
@@ -740,12 +795,23 @@ def align_time(
740
795
  ds = _interp(ds, dim, target_frames_float, interp_kwargs)
741
796
  # ds = ds.interp({dim: target_frames_float}, assume_sorted=True, kwargs=interp_kwargs)
742
797
 
743
- 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
+ )
744
802
 
745
803
  # time_new = ds['frametimes' + suffix] - ref_time
746
804
  ds = ds.assign_coords({"time": ((dim), target_time)})
747
805
  ds = ds.swap_dims({dim: "time"})
748
- 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
+ )
749
815
 
750
816
  fps = 1 / np.nanmean(np.diff(ds["frametimes" + suffix]))
751
817
  ds.attrs.update({"video_fps": fps})
@@ -806,7 +872,12 @@ def assemble_metrics(
806
872
  time = dataset.time.data
807
873
  nearest_frame = dataset.nearest_frame.data
808
874
 
809
- 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
+ }
810
881
  if custom_pose_names is not None:
811
882
  pose_names.update(custom_pose_names)
812
883
 
@@ -916,7 +987,11 @@ def assemble_metrics(
916
987
  ds_dict["abs_features"] = xr.DataArray(
917
988
  data=absolute.data,
918
989
  dims=["time", "flies", "absolute_features"],
919
- 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
+ },
920
995
  attrs={
921
996
  "description": 'coords are "egocentric" - rel. to box',
922
997
  "sampling_rate_Hz": sampling_rate,
@@ -936,7 +1011,14 @@ def assemble_metrics(
936
1011
  )
937
1012
  rel_velocities_mag = np.sqrt(rel_velocities_forward**2 + rel_velocities_lateral**2)
938
1013
 
939
- 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
+ ]
940
1022
 
941
1023
  rel_feature_names = [
942
1024
  "distance",
@@ -951,7 +1033,11 @@ def assemble_metrics(
951
1033
  ds_dict["rel_features"] = xr.DataArray(
952
1034
  data=relative.data,
953
1035
  dims=["time", "flies", "relative_flies", "relative_features"],
954
- 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
+ },
955
1041
  attrs={
956
1042
  "description": 'coords are "egocentric" - rel. to box',
957
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