xarray-behave 0.35.7__tar.gz → 0.36.0__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.
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/.github/workflows/publish.yaml +18 -16
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/PKG-INFO +3 -5
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/README.md +1 -2
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/build_env.yml +0 -6
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/conda/xarray-behave/meta.yaml +11 -24
- xarray_behave-0.36.0/condarc.yml +9 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/pyproject.toml +1 -4
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/__init__.py +1 -1
- xarray_behave-0.36.0/src/xarray_behave/io/samplestamps/__init__.py +6 -0
- xarray_behave-0.36.0/src/xarray_behave/io/samplestamps/samplestamps.py +114 -0
- xarray_behave-0.36.0/src/xarray_behave/io/samplestamps/utils.py +181 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/loaders.py +1 -1
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/xarray_behave.py +107 -21
- xarray_behave-0.36.0/tests/test_sampstamps.py +34 -0
- xarray-behave-0.35.7/conda/xarray-behave/conda_build_config.yaml +0 -4
- xarray-behave-0.35.7/condarc.yml +0 -3
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/.gitignore +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/LICENSE +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/conda/xarray-behave/bld.bat +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/conda/xarray-behave/build.sh +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/doc/demo.ipynb +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/doc/demo_behavioral_features.ipynb +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/doc/demo_behavioral_features_large_group.ipynb +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/doc/ncb.mplstyle +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/setup.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/annot.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/event_utils.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/__init__.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/app.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/audio_player.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/das.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/formbuilder.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/forms/das_make.yaml +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/forms/das_predict.yaml +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/forms/das_train.yaml +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/forms/envelope_computation.yaml +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/forms/export_for_das.yaml +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/forms/from_dir.yaml +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/forms/from_file.yaml +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/forms/from_zarr.yaml +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/forms/spec_freq.yaml +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/icon.png +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/table.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/utils.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/view_dialog.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/views.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/widgets.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/io/__init__.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/io/annotations.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/io/annotations_manual.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/io/audio.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/io/balltracks.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/io/movieparams.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/io/poses.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/io/timestamps.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/io/tracks.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/metrics.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/tests/test_annot.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/tests/test_assemble.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/tests/test_assemble_metrics.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/tests/test_imports.py +0 -0
- {xarray-behave-0.35.7 → xarray_behave-0.36.0}/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-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: xarray-behave
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.36.0
|
|
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
|
|
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
|
|
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.
|
|
@@ -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
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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"
|
|
@@ -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', '
|
|
14
|
+
'dask', 'toolz', 'pysoundfile', 'opencv-python-headless']
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
|
|
17
17
|
[project.optional-dependencies]
|
|
@@ -19,9 +19,6 @@ gui = ['pyside6', 'pyqtgraph>0.12.2', 'pyvideoreader', 'qtpy', 'superqt',
|
|
|
19
19
|
'sounddevice', 'scikit-image', 'pyyaml', 'defopt', 'colorcet',
|
|
20
20
|
'peakutils']
|
|
21
21
|
|
|
22
|
-
[project.scripts]
|
|
23
|
-
xb = "xarray_behave.gui.app:cli"
|
|
24
|
-
|
|
25
22
|
[project.urls]
|
|
26
23
|
source = "https://github.com/janclemenslab/xarray-behave"
|
|
27
24
|
documentation = "https://janclemenslab.org/das/"
|
|
@@ -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
|
|
@@ -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
|
|
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(
|
|
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(
|
|
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(
|
|
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",
|
|
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",
|
|
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(
|
|
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,
|
|
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 =
|
|
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(
|
|
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,
|
|
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"] = (
|
|
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(
|
|
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 = {
|
|
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,
|
|
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(
|
|
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(
|
|
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 = {
|
|
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={
|
|
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 = [
|
|
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={
|
|
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,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
def test_monotonize():
|
|
2
|
+
import numpy as np
|
|
3
|
+
from xarray_behave.io.samplestamps.utils import monotonize, interpolator, ismonotonous
|
|
4
|
+
|
|
5
|
+
inc_strict = np.array([0, 1, 2, 3])
|
|
6
|
+
inc_nonstrict = np.array([0, 1, 2, 2])
|
|
7
|
+
dec_strict = inc_strict[::-1]
|
|
8
|
+
dec_nonstrict = inc_nonstrict[::-1]
|
|
9
|
+
assert ismonotonous(inc_strict, direction="increasing", strict=True) == True
|
|
10
|
+
assert ismonotonous(inc_strict, direction="increasing", strict=False) == True
|
|
11
|
+
assert ismonotonous(inc_nonstrict, direction="increasing", strict=True) == False
|
|
12
|
+
assert ismonotonous(inc_nonstrict, direction="decreasing", strict=False) == False
|
|
13
|
+
assert ismonotonous(dec_strict, direction="decreasing", strict=True) == True
|
|
14
|
+
assert ismonotonous(dec_nonstrict, direction="decreasing", strict=True) == False
|
|
15
|
+
assert ismonotonous(np.array([1]), direction="increasing", strict=True) == True
|
|
16
|
+
assert ismonotonous(np.array([1]), direction="increasing", strict=False) == True
|
|
17
|
+
|
|
18
|
+
x = np.array([0, 1, 2, 2, 1])
|
|
19
|
+
print(f"montonize {x}")
|
|
20
|
+
print(f" strict, inc: {monotonize(x)}")
|
|
21
|
+
assert np.all(monotonize(x) == [0, 1, 2])
|
|
22
|
+
print(f" strict, dec: {monotonize(x, direction='decreasing')}")
|
|
23
|
+
assert np.all(monotonize(x, direction="decreasing") == [0])
|
|
24
|
+
print(f" nonstrict, in: {monotonize(x, strict=False)}")
|
|
25
|
+
assert np.all(monotonize(x, strict=False) == [0, 1, 2, 2])
|
|
26
|
+
|
|
27
|
+
x = np.array([2, 1, 0, 0, 1])
|
|
28
|
+
print(f"montonize {x}")
|
|
29
|
+
print(f" strict, inc: {monotonize(x)}")
|
|
30
|
+
assert np.all(monotonize(x) == [2])
|
|
31
|
+
print(f" strict, dec: {monotonize(x, direction='decreasing')}")
|
|
32
|
+
assert np.all(monotonize(x, direction="decreasing") == [2, 1, 0])
|
|
33
|
+
print(f" nonstrict, dec: {monotonize(x, strict=False, direction='decreasing')}")
|
|
34
|
+
assert np.all(monotonize(x, strict=False, direction="decreasing") == [2, 1, 0, 0])
|
xarray-behave-0.35.7/condarc.yml
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xarray-behave-0.35.7 → xarray_behave-0.36.0}/doc/demo_behavioral_features_large_group.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/forms/envelope_computation.yaml
RENAMED
|
File without changes
|
{xarray-behave-0.35.7 → xarray_behave-0.36.0}/src/xarray_behave/gui/forms/export_for_das.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|