Compare commits

..

No commits in common. "master" and "plot_event_timeline" have entirely different histories.

93 changed files with 2430 additions and 89767 deletions

3
.gitignore vendored
View File

@ -5,8 +5,6 @@
data
env
output
trash
.vscode
# Mac Stuff
*.DS_Store
@ -16,7 +14,6 @@ trash
__pycache__/
*.py[cod]
*$py.class
poster/main.pdf
# C extensions
*.so

294
README.md
View File

@ -1,90 +1,248 @@
# Chirpdetector 2023
<!-- Improved compatibility of back to top link: See: https://github.com/othneildrew/Best-README-Template/pull/73 -->
<a name="readme-top"></a>
<!--
*** Thanks for checking out the Best-README-Template. If you have a suggestion
*** that would make this better, please fork the repo and create a pull request
*** or simply open an issue with the tag "enhancement".
*** Don't forget to give the project a star!
*** Thanks again! Now go create something AMAZING! :D
-->
<!-- PROJECT SHIELDS -->
<!--
*** I'm using markdown "reference style" links for readability.
*** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
*** See the bottom of this document for the declaration of the reference variables
*** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
*** https://www.markdownguide.org/basic-syntax/#reference-style-links
-->
<!-- [![Contributors][contributors-shield]][contributors-url] -->
<!-- [![Forks][forks-shield]][forks-url] -->
<!-- [![Stargazers][stars-shield]][stars-url] -->
<!-- [![Issues][issues-shield]][issues-url] -->
<!-- [![MIT License][license-shield]][license-url] -->
<!-- [![LinkedIn][linkedin-shield]][linkedin-url] -->
<!-- PROJECT LOGO -->
<br />
<div align="center">
<a href="https://github.com/github_username/repo_name">
<img src="assets/logo.png" alt="Logo" width="150" height="100">
</a>
<h3 align="center">chirpdetector</h3>
<p align="center">
An algorithm to detect the chirps of weakly electric fish.
<br />
<a href="https://github.com/github_username/repo_name"><strong>Explore the docs »</strong></a>
<br />
<br />
<a href="https://github.com/github_username/repo_name">View Demo</a>
·
<a href="https://github.com/github_username/repo_name/issues">Report Bug</a>
·
<a href="https://github.com/github_username/repo_name/issues">Request Feature</a>
</p>
</div>
Chirps are transient communication singals of many wave-type electric fish. Because they are so fast, detecting them when the recorded signal includes multiple individuals is hard. But to understand if, and what kind of information they transmit in a natural setting, analyzing chirps in multiple freely interacting individual is nessecary. This repository documents an approach to detect these signals on electrode grid recordings with many freely behaving individuals by extracting EOD parameters that change during a chirp and detecting anomalies on them.
The majority of the code and its tests were part of a lab rotation with the [Neuroethology](https://github.com/bendalab) at the University of Tuebingen. It also contains a [poster](poster_printed/main.pdf) and a more thorough [lab protocol](protocol/main.pdf).
## The Approach
<!-- TABLE OF CONTENTS -->
<details>
<summary>Table of Contents</summary>
<ol>
<li>
<a href="#about-the-project">About The Project</a>
<ul>
<li><a href="#built-with">Built With</a></li>
</ul>
</li>
<li>
<a href="#getting-started">Getting Started</a>
<ul>
<li><a href="#prerequisites">Prerequisites</a></li>
<li><a href="#installation">Installation</a></li>
</ul>
</li>
<li><a href="#usage">Usage</a></li>
<li><a href="#roadmap">Roadmap</a></li>
<li><a href="#contributing">Contributing</a></li>
<li><a href="#license">License</a></li>
<li><a href="#contact">Contact</a></li>
<li><a href="#acknowledgments">Acknowledgments</a></li>
</ol>
</details>
To detect chirps, we extract some features of the raw signal using frequency traces that were computed beforehand using the [wavetracker](https://github.com/tillraab/wavetracker). For a frequency band of a single fish, we filter the signal using a bandpass filter, extract the instantaneous frequency, the envelelope and the envelope of a frequency band slightly above the fundamental frequency of the fish. We then transform those features using various filters and detect the peaks on them. Peaks on all three features are chirps. In an ideal world, the result looks like this:
![chirps](assets/good_.svg)
We always use the strongest electrode relative to the fish of interest. By that, we include the spatial demensions to increase our detection performance, if fish are spaced sufficiently apart. The full algorithm is thoroughly explained in the lab protocol. All parameters can be tuned using a `yaml` config file. While this approach excels in assigning detected chirps to the currect fish, the actial chirp detection is not that reliable. If peak detection thresholds are set for each feature manually, the detector can become quite reliable but performance will be sensible to changes in the recording setup. But if features are normalized to generalize the detector, noise often introduces false detections, especially during amplitude breakdowns. Below is an example of a recording with false detections:
<!-- ABOUT THE PROJECT -->
## About The Project
![chirps](assets/bad_.svg)
[![Product Name Screen Shot][product-screenshot]](https://example.com)
## Open questions
Here's a blank template to get started: To avoid retyping too much info. Do a search and replace with your text editor for the following: `github_username`, `repo_name`, `twitter_handle`, `linkedin_username`, `email_client`, `email`, `project_title`, `project_description`
One of the three features we exract to detect peaks is the instantaneous frequency of the baseline EOD of a single fish. To do that, we apply a narrow bandpass filter (+- 5 Hz) around the fundamental frequency in a single 5 second window. If when the fish chirps, its real frequency should increase beyond the filter cutoff frequencies and we should see a slight increase in the instantaneous frequency. But sometimes, the frequency decreases or does not change at all.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
To understand this, we simulated chirps while changing some parameters of the chirp, notably the height, width, kurtosis, contrast and EOD phase in which the chirp is produced. What we find, is that all parameters that change the integral of the chirp frequency evolution (i.e. the height, width, and kurtosis) occasionally result in negative instantaneous frequencies in the narrow filtered EOD. Specifically, if the integral increases, the instantaneous frequency increases as well, until an integer + 0.57 is reached. At this point, the frequency flips around the x axis and gradually becomes positive again. This is illustrated in the figure below for the chirp width. The integral of the zero-shifted instantaneous frequency is indicated as the title of each subplot. 1.57 would be $\pi/2$ but how this relates to the observed pattern is not clear yet. What becomes clear from the waveform below is that the point at which the EOD flips is the point where the amplitude of just the filtered signal breaks down to 0.
![chirps](assets/width.svg)
To explore the other parameters, there is a jupyter notebook in the `notebooks` [here](chirp_instantaneous_freq/chirp_exploration.ipynb).
<!-- ### Built With -->
<!-- # Chirp detection - GP2023 -->
<!-- ## Git-Repository and commands -->
<!-- * [![Next][Next.js]][Next-url] -->
<!-- * [![React][React.js]][React-url] -->
<!-- * [![Vue][Vue.js]][Vue-url] -->
<!-- * [![Angular][Angular.io]][Angular-url] -->
<!-- * [![Svelte][Svelte.dev]][Svelte-url] -->
<!-- * [![Laravel][Laravel.com]][Laravel-url] -->
<!-- * [![Bootstrap][Bootstrap.com]][Bootstrap-url] -->
<!-- * [![JQuery][JQuery.com]][JQuery-url] -->
<!-- - Go to the [Bendalab Git-Server](https://whale.am28.uni-tuebingen.de/git/) (https://whale.am28.uni-tuebingen.de/git/) -->
<!-- - Create your own account (and tell me ;D) -->
<!-- * I'll invite you the repository -->
<!-- - Clone the repository -->
<!-- - -->
<!-- ```sh -->
<!-- git clone https://whale.am28.uni-tuebingen.de/git/raab/GP2023_chirp_detection.git -->
<!-- ``` -->
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- ## Basic git commands -->
<!-- - pull changes in git -->
<!-- ```shell -->
<!-- git pull origin <branch> -->
<!-- ``` -->
<!-- - commit chances -->
<!-- ```shell -->
<!-- git commit -m '<explaination>' file # commit one file -->
<!-- git commit -a -m '<explaination>' # commit all files -->
<!-- ``` -->
<!-- - push commits -->
<!-- ```shell -->
<!-- git push origin <branch> -->
<!-- ``` -->
<!-- ## Branches -->
<!-- Use branches to work on specific topics (e.g. 'algorithm', 'analysis', 'writing', ore even more specific ones) and merge -->
<!-- them into Master-Branch when it works are up to your expectations. -->
<!-- GETTING STARTED -->
## Getting Started
<!-- The "master" branch should always contain a working/correct version of your project. -->
This is an example of how you may give instructions on setting up your project locally.
To get a local copy up and running follow these simple example steps.
<!-- - Create/change into branches -->
<!-- ```shell -->
<!-- # list all branches (highlight active branch) -->
<!-- git banch -a -->
<!-- # switch into existing -->
<!-- git checkout <existing branch> -->
<!-- # switch into new branch -->
<!-- git checkout master -->
<!-- git checkout -b <new branch> -->
<!-- ``` -->
<!-- ### Prerequisites -->
<!-- This is an example of how to list things you need to use the software and how to install them. -->
<!-- * npm -->
<!-- ```sh -->
<!-- npm install npm@latest -g -->
<!-- ``` -->
<!-- - Re-merging with master branch -->
<!-- 1) get current version of master and implement it into branch -->
<!-- ```shell -->
<!-- git checkout master -->
<!-- git pull origin master -->
<!-- git checkout <branch> -->
<!-- git rebase master -->
<!-- ``` -->
<!-- This resets you branch to the fork-point, executes all commits of the current master before adding the commits of you -->
<!-- branch. You may have to resolve potential conflicts. Afterwards commit the corrected version and push it to your branch. -->
### Installation
1. Get a free API Key at [https://example.com](https://example.com)
2. Clone the repo
```sh
git clone https://github.com/github_username/repo_name.git
```
3. Install NPM packages
```sh
npm install
```
4. Enter your API in `config.js`
```js
const API_KEY = 'ENTER YOUR API';
```
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- USAGE EXAMPLES -->
## Usage
Use this space to show useful examples of how a project can be used. Additional screenshots, code examples and demos work well in this space. You may also link to more resources.
_For more examples, please refer to the [Documentation](https://example.com)_
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- ROADMAP -->
## Roadmap
- [ ] Feature 1
- [ ] Feature 2
- [ ] Feature 3
- [ ] Nested Feature
See the [open issues](https://github.com/github_username/repo_name/issues) for a full list of proposed features (and known issues).
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- <!-1- CONTRIBUTING -1-> -->
<!-- ## Contributing -->
<!-- Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. -->
<!-- If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". -->
<!-- Don't forget to give the project a star! Thanks again! -->
<!-- 1. Fork the Project -->
<!-- 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) -->
<!-- 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) -->
<!-- 4. Push to the Branch (`git push origin feature/AmazingFeature`) -->
<!-- 5. Open a Pull Request -->
<!-- <p align="right">(<a href="#readme-top">back to top</a>)</p> -->
<!-- <!-1- LICENSE -1-> -->
<!-- ## License -->
<!-- Distributed under the MIT License. See `LICENSE.txt` for more information. -->
<!-- <p align="right">(<a href="#readme-top">back to top</a>)</p> -->
<!-- CONTACT -->
## Contact
Your Name - [@twitter_handle](https://twitter.com/twitter_handle) - email@email_client.com
Project Link: [https://github.com/github_username/repo_name](https://github.com/github_username/repo_name)
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- ACKNOWLEDGMENTS -->
## Acknowledgments
* []()
* []()
* []()
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[contributors-shield]: https://img.shields.io/github/contributors/github_username/repo_name.svg?style=for-the-badge
[contributors-url]: https://github.com/github_username/repo_name/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/github_username/repo_name.svg?style=for-the-badge
[forks-url]: https://github.com/github_username/repo_name/network/members
[stars-shield]: https://img.shields.io/github/stars/github_username/repo_name.svg?style=for-the-badge
[stars-url]: https://github.com/github_username/repo_name/stargazers
[issues-shield]: https://img.shields.io/github/issues/github_username/repo_name.svg?style=for-the-badge
[issues-url]: https://github.com/github_username/repo_name/issues
[license-shield]: https://img.shields.io/github/license/github_username/repo_name.svg?style=for-the-badge
[license-url]: https://github.com/github_username/repo_name/blob/master/LICENSE.txt
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
[linkedin-url]: https://linkedin.com/in/linkedin_username
[product-screenshot]: images/screenshot.png
[Next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
[Next-url]: https://nextjs.org/
[React.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB
[React-url]: https://reactjs.org/
[Vue.js]: https://img.shields.io/badge/Vue.js-35495E?style=for-the-badge&logo=vuedotjs&logoColor=4FC08D
[Vue-url]: https://vuejs.org/
[Angular.io]: https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white
[Angular-url]: https://angular.io/
[Svelte.dev]: https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte&logoColor=FF3E00
[Svelte-url]: https://svelte.dev/
[Laravel.com]: https://img.shields.io/badge/Laravel-FF2D20?style=for-the-badge&logo=laravel&logoColor=white
[Laravel-url]: https://laravel.com
[Bootstrap.com]: https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white
[Bootstrap-url]: https://getbootstrap.com
[JQuery.com]: https://img.shields.io/badge/jQuery-0769AD?style=for-the-badge&logo=jquery&logoColor=white
[JQuery-url]: https://jquery.com
<!-- 2) Update master branch master -->
<!-- - correct way: Create -->
<!-- ```shell -->
<!-- git checkout master -->
<!-- git merge <branch> -->
<!-- git push origin master -->
<!-- ``` -->

64
README1.md Normal file
View File

@ -0,0 +1,64 @@
# Chirp detection - GP2023
## Git-Repository and commands
- Go to the [Bendalab Git-Server](https://whale.am28.uni-tuebingen.de/git/) (https://whale.am28.uni-tuebingen.de/git/)
- Create your own account (and tell me ;D)
* I'll invite you the repository
- Clone the repository
-
```sh
git clone https://whale.am28.uni-tuebingen.de/git/raab/GP2023_chirp_detection.git
```
## Basic git commands
- pull changes in git
```shell
git pull origin <branch>
```
- commit chances
```shell
git commit -m '<explaination>' file # commit one file
git commit -a -m '<explaination>' # commit all files
```
- push commits
```shell
git push origin <branch>
```
## Branches
Use branches to work on specific topics (e.g. 'algorithm', 'analysis', 'writing', ore even more specific ones) and merge
them into Master-Branch when it works are up to your expectations.
The "master" branch should always contain a working/correct version of your project.
- Create/change into branches
```shell
# list all branches (highlight active branch)
git banch -a
# switch into existing
git checkout <existing branch>
# switch into new branch
git checkout master
git checkout -b <new branch>
```
- Re-merging with master branch
1) get current version of master and implement it into branch
```shell
git checkout master
git pull origin master
git checkout <branch>
git rebase master
```
This resets you branch to the fork-point, executes all commits of the current master before adding the commits of you
branch. You may have to resolve potential conflicts. Afterwards commit the corrected version and push it to your branch.
2) Update master branch master
- correct way: Create
```shell
git checkout master
git merge <branch>
git push origin master
```

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 448 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 371 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 466 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 370 KiB

View File

@ -1,241 +0,0 @@
#!/usr/bin/env python
# coding: utf-8
# # Why can the instantaneous frequency of a band-pass filtered chirp recording go down ...
# ... if a chirp is an up-modulation of the frequency?
#
# This is the question we try to answer in this notebook
# In[14]:
import matplotlib.pyplot as plt
import numpy as np
import thunderfish.fakefish as ff
from filters import instantaneous_frequency, bandpass_filter
#get_ipython().run_line_magic('matplotlib', 'qt')
# parameters that stay the same
samplerate = 20000
duration = 0.2
chirp_freq = 5
smooth = 3
# In[7]:
def make_chirp(eodf, size, width, kurtosis, contrast, phase0):
chirp_trace, amp = ff.chirps(
eodf = eodf,
samplerate = samplerate,
duration = duration,
chirp_freq = chirp_freq,
chirp_size = size,
chirp_width = width,
chirp_kurtosis = kurtosis,
chirp_contrast = contrast,
)
chirp = ff.wavefish_eods(
fish = 'Alepto',
frequency = chirp_trace,
samplerate = samplerate,
duration = duration,
phase0 = phase0,
noise_std = 0,
)
chirp *= amp
return chirp_trace, chirp
def filtered_chirp(eodf, size, width, kurtosis, contrast, phase0):
time = np.arange(0, duration, 1/samplerate)
chirp_trace, chirp = make_chirp(
eodf = eodf,
size = size,
width = width,
kurtosis = kurtosis,
contrast = contrast,
phase0 = phase0,
)
# apply filters
narrow_filtered = bandpass_filter(chirp, samplerate, eodf-10, eodf+10)
narrow_freqtime, narrow_freq = instantaneous_frequency(narrow_filtered, samplerate, smooth)
broad_filtered = bandpass_filter(chirp, samplerate, eodf-300, eodf+300)
broad_freqtime, broad_freq = instantaneous_frequency(broad_filtered, samplerate, smooth)
original = (time, chirp_trace, chirp)
broad = (broad_freqtime, broad_freq, broad_filtered)
narrow = (narrow_freqtime, narrow_freq, narrow_filtered)
return original, broad, narrow
def plot(original, broad, narrow, axs):
axs[0].plot(original[0], original[1], label='chirp trace')
axs[0].plot(broad[0], broad[1], label='broad filtered')
axs[0].plot(narrow[0], narrow[1], label='narrow filtered')
axs[1].plot(original[0], original[2], label='unfiltered')
axs[1].plot(original[0], broad[2], label='broad filtered')
axs[1].plot(original[0], narrow[2], label='narrow filtered')
original, broad, narrow = filtered_chirp(600, 100, 0.02, 1, 0.1, 0)
fig, axs = plt.subplots(2, 1, figsize=(10, 5), sharex=True)
plot(original, broad, narrow, axs)
fig.align_labels()
plt.show()
# ## Chirp size
# now that we have established an easy way to simulate and plot the chirps, lets change the chirp size and see how the narrow-filtered instantaneous frequency changes.
# In[17]:
sizes = np.arange(10, 600, 20)[:10]
fig, axs = plt.subplots(2, len(sizes), figsize=(20, 5), sharex=True, sharey='row')
integrals = []
for i, size in enumerate(sizes):
original, broad, narrow = filtered_chirp(600, size, 0.02, 1, 0.1, 0)
integral = np.sum(original[1]-600)/(20000)
integrals.append(integral)
plot(original, broad, narrow, axs[:, i])
axs[:, i][0].set_xlim(0.06, 0.14)
axs[0, i].set_title(np.round(integral, 3))
print(f'size {size} Hz; Integral {np.round(integral,3)}')
fig.legend(handles=axs[0,0].get_lines(), loc='upper center', ncol=3)
axs[0,0].set_ylabel('frequency [Hz]')
axs[1,0].set_ylabel('amplitude [a.u.]')
fig.supxlabel('time [s]')
fig.align_labels()
plt.show()
# ## Chirp width
# In[9]:
widths = np.arange(0.02, 0.08, 0.005)
fig, axs = plt.subplots(2, len(widths), figsize=(10, 5), sharex=True, sharey='row')
integrals = []
for i, width in enumerate(widths):
if i > 9:
break
original, broad, narrow = filtered_chirp(600, 100, width, 1, 0.1, 0)
integral = np.sum(original[1]-600)/(20000)
plot(original, broad, narrow, axs[:, i])
axs[:, i][0].set_xlim(0.06, 0.14)
axs[0, i].set_title(f'width {np.round(width, 2)} s')
print(f'width {width} s; Integral {np.round(integral, 3)}')
fig.legend(handles=axs[0,0].get_lines(), loc='upper center', ncol=3)
axs[0,0].set_ylabel('frequency [Hz]')
axs[1,0].set_ylabel('amplitude [a.u.]')
fig.supxlabel('time [s]')
fig.align_labels()
plt.show()
# ## Chirp kurtosis
# In[10]:
kurtosiss = np.arange(0, 20, 1.6)
fig, axs = plt.subplots(2, len(kurtosiss), figsize=(10, 5), sharex=True, sharey='row')
integrals = []
for i, kurtosis in enumerate(kurtosiss):
original, broad, narrow = filtered_chirp(600, 100, 0.02, kurtosis, 0.1, 0)
integral = np.sum(original[1]-600)/(20000)
plot(original, broad, narrow, axs[:, i])
axs[:, i][0].set_xlim(0.06, 0.14)
axs[0, i].set_title(f'kurt {np.round(kurtosis, 2)}')
print(f'kurt {kurtosis}; Integral {np.round(integral, 3)}')
fig.legend(handles=axs[0,0].get_lines(), loc='upper center', ncol=3)
axs[0,0].set_ylabel('frequency [Hz]')
axs[1,0].set_ylabel('amplitude [a.u.]')
fig.supxlabel('time [s]')
fig.align_labels()
plt.show()
# ## Chirp contrast
# In[11]:
contrasts = np.arange(0.0, 1.1, 0.1)
fig, axs = plt.subplots(2, len(sizes), figsize=(10, 5), sharex=True, sharey='row')
integrals = []
for i, contrast in enumerate(contrasts):
if i > 9:
break
original, broad, narrow = filtered_chirp(600, 100, 0.02, 1, contrast, 0)
integral = np.trapz(original[2], original[0])
integrals.append(integral)
plot(original, broad, narrow, axs[:, i])
axs[:, i][0].set_xlim(0.06, 0.14)
axs[0, i].set_title(f'contr {np.round(contrast, 2)}')
fig.legend(handles=axs[0,0].get_lines(), loc='upper center', ncol=3)
axs[0,0].set_ylabel('frequency [Hz]')
axs[1,0].set_ylabel('amplitude [a.u.]')
fig.supxlabel('time [s]')
fig.align_labels()
plt.show()
# ## Chirp phase
# In[12]:
phases = np.arange(0.0, 2 * np.pi, 0.2)
fig, axs = plt.subplots(2, len(sizes), figsize=(10, 5), sharex=True, sharey='row')
integrals = []
for i, phase in enumerate(phases):
if i > 9:
break
original, broad, narrow = filtered_chirp(600, 100, 0.02, 1, 0.1, phase)
integral = np.trapz(original[2], original[0])
integrals.append(integral)
plot(original, broad, narrow, axs[:, i])
axs[:, i][0].set_xlim(0.06, 0.14)
axs[0, i].set_title(f'phase {np.round(phase, 2)}')
fig.legend(handles=axs[0,0].get_lines(), loc='upper center', ncol=3)
axs[0,0].set_ylabel('frequency [Hz]')
axs[1,0].set_ylabel('amplitude [a.u.]')
fig.supxlabel('time [s]')
fig.align_labels()
plt.show()
# These experiments show, that the narrow filtered instantaneous freuqency only switches its sign, when the integral of the instantaneous frequency (that was used to make the signal)
# changes. Specifically, when the instantaneous frequency is 0.57, 1.57, 2.57 etc., the sign swithes.

View File

@ -1,200 +0,0 @@
from scipy.signal import butter, sosfiltfilt
from scipy.ndimage import gaussian_filter1d
import numpy as np
def instantaneous_frequency(
signal: np.ndarray,
samplerate: int,
smoothing_window: int,
) -> tuple[np.ndarray, np.ndarray]:
"""
Compute the instantaneous frequency of a signal that is approximately
sinusoidal and symmetric around 0.
Parameters
----------
signal : np.ndarray
Signal to compute the instantaneous frequency from.
samplerate : int
Samplerate of the signal.
smoothing_window : int
Window size for the gaussian filter.
Returns
-------
tuple[np.ndarray, np.ndarray]
"""
# calculate instantaneous frequency with zero crossings
roll_signal = np.roll(signal, shift=1)
time_signal = np.arange(len(signal)) / samplerate
period_index = np.arange(len(signal))[(roll_signal < 0) & (signal >= 0)][
1:-1
]
upper_bound = np.abs(signal[period_index])
lower_bound = np.abs(signal[period_index - 1])
upper_time = np.abs(time_signal[period_index])
lower_time = np.abs(time_signal[period_index - 1])
# create ratio
lower_ratio = lower_bound / (lower_bound + upper_bound)
# appy to time delta
time_delta = upper_time - lower_time
true_zero = lower_time + lower_ratio * time_delta
# create new time array
instantaneous_frequency_time = true_zero[:-1] + 0.5 * np.diff(true_zero)
# compute frequency
instantaneous_frequency = gaussian_filter1d(
1 / np.diff(true_zero), smoothing_window
)
return instantaneous_frequency_time, instantaneous_frequency
def inst_freq(signal, fs):
"""
Computes the instantaneous frequency of a periodic signal using zero-crossings.
Parameters:
-----------
signal : array-like
The input signal.
fs : float
The sampling frequency of the input signal.
Returns:
--------
freq : array-like
The instantaneous frequency of the input signal.
"""
# Compute the sign of the signal
sign = np.sign(signal)
# Compute the crossings of the sign signal with a zero line
crossings = np.where(np.diff(sign))[0]
# Compute the time differences between zero crossings
dt = np.diff(crossings) / fs
# Compute the instantaneous frequency as the reciprocal of the time differences
freq = 1 / dt
# Gaussian filter the signal
freq = gaussian_filter1d(freq, 10)
# Pad the frequency vector with zeros to match the length of the input signal
freq = np.pad(freq, (0, len(signal) - len(freq)))
return freq
def bandpass_filter(
signal: np.ndarray,
samplerate: float,
lowf: float,
highf: float,
) -> np.ndarray:
"""Bandpass filter a signal.
Parameters
----------
signal : np.ndarray
The data to be filtered
rate : float
The sampling rate
lowf : float
The low cutoff frequency
highf : float
The high cutoff frequency
Returns
-------
np.ndarray
The filtered data
"""
sos = butter(2, (lowf, highf), "bandpass", fs=samplerate, output="sos")
filtered_signal = sosfiltfilt(sos, signal)
return filtered_signal
def highpass_filter(
signal: np.ndarray,
samplerate: float,
cutoff: float,
) -> np.ndarray:
"""Highpass filter a signal.
Parameters
----------
signal : np.ndarray
The data to be filtered
rate : float
The sampling rate
cutoff : float
The cutoff frequency
Returns
-------
np.ndarray
The filtered data
"""
sos = butter(2, cutoff, "highpass", fs=samplerate, output="sos")
filtered_signal = sosfiltfilt(sos, signal)
return filtered_signal
def lowpass_filter(
signal: np.ndarray, samplerate: float, cutoff: float
) -> np.ndarray:
"""Lowpass filter a signal.
Parameters
----------
data : np.ndarray
The data to be filtered
rate : float
The sampling rate
cutoff : float
The cutoff frequency
Returns
-------
np.ndarray
The filtered data
"""
sos = butter(2, cutoff, "lowpass", fs=samplerate, output="sos")
filtered_signal = sosfiltfilt(sos, signal)
return filtered_signal
def envelope(
signal: np.ndarray, samplerate: float, cutoff_frequency: float
) -> np.ndarray:
"""Calculate the envelope of a signal using a lowpass filter.
Parameters
----------
signal : np.ndarray
The signal to calculate the envelope of
samplingrate : float
The sampling rate of the signal
cutoff_frequency : float
The cutoff frequency of the lowpass filter
Returns
-------
np.ndarray
The envelope of the signal
"""
sos = butter(2, cutoff_frequency, "lowpass", fs=samplerate, output="sos")
envelope = np.sqrt(2) * sosfiltfilt(sos, np.abs(signal))
return envelope

View File

@ -1,557 +0,0 @@
import sys
from IPython import embed
import thunderfish.powerspectrum as ps
import numpy as np
species_name = dict(
Sine="Sinewave",
Alepto="Apteronotus leptorhynchus",
Arostratus="Apteronotus rostratus",
Eigenmannia="Eigenmannia spec.",
Sternarchella="Sternarchella terminalis",
Sternopygus="Sternopygus dariensis",
)
"""Translate species ids used by wavefish_harmonics and pulsefish_eodpeaks to full species names.
"""
def abbrv_genus(name):
"""Abbreviate genus in a species name.
Parameters
----------
name: string
Full species name of the form 'Genus species subspecies'
Returns
-------
name: string
The species name with abbreviated genus, i.e. 'G. species subspecies'
"""
ns = name.split()
return ns[0][0] + ". " + " ".join(ns[1:])
# Amplitudes and phases of various wavefish species:
Sine_harmonics = dict(amplitudes=(1.0,), phases=(0.5 * np.pi,))
Apteronotus_leptorhynchus_harmonics = dict(
amplitudes=(0.90062, 0.15311, 0.072049, 0.012609, 0.011708),
phases=(1.3623, 2.3246, 0.9869, 2.6492, -2.6885),
)
Apteronotus_rostratus_harmonics = dict(
amplitudes=(
0.64707,
0.43874,
0.063592,
0.07379,
0.040199,
0.023073,
0.0097678,
),
phases=(2.2988, 0.78876, -1.316, 2.2416, 2.0413, 1.1022, -2.0513),
)
Eigenmannia_harmonics = dict(
amplitudes=(1.0087, 0.23201, 0.060524, 0.020175, 0.010087, 0.0080699),
phases=(1.3414, 1.3228, 2.9242, 2.8157, 2.6871, -2.8415),
)
Sternarchella_terminalis_harmonics = dict(
amplitudes=(
0.11457,
0.4401,
0.41055,
0.20132,
0.061364,
0.011389,
0.0057985,
),
phases=(-2.7106, 2.4472, 1.6829, 0.79085, 0.119, -0.82355, -1.9956),
)
Sternopygus_dariensis_harmonics = dict(
amplitudes=(
0.98843,
0.41228,
0.047848,
0.11048,
0.022801,
0.030706,
0.019018,
),
phases=(1.4153, 1.3141, 3.1062, -2.3961, -1.9524, 0.54321, 1.6844),
)
wavefish_harmonics = dict(
Sine=Sine_harmonics,
Alepto=Apteronotus_leptorhynchus_harmonics,
Arostratus=Apteronotus_rostratus_harmonics,
Eigenmannia=Eigenmannia_harmonics,
Sternarchella=Sternarchella_terminalis_harmonics,
Sternopygus=Sternopygus_dariensis_harmonics,
)
"""Amplitudes and phases of EOD waveforms of various species of wave-type electric fish."""
def wavefish_spectrum(fish):
"""Amplitudes and phases of a wavefish EOD.
Parameters
----------
fish: string, dict or tuple of lists/arrays
Specify relative amplitudes and phases of the fundamental and its harmonics.
If string then take amplitudes and phases from the `wavefish_harmonics` dictionary.
If dictionary then take amplitudes and phases from the 'amlitudes' and 'phases' keys.
If tuple then the first element is the list of amplitudes and
the second one the list of relative phases in radians.
Returns
-------
amplitudes: array of floats
Amplitudes of the fundamental and its harmonics.
phases: array of floats
Phases in radians of the fundamental and its harmonics.
Raises
------
KeyError:
Unknown fish.
IndexError:
Amplitudes and phases differ in length.
"""
if isinstance(fish, (tuple, list)):
amplitudes = fish[0]
phases = fish[1]
elif isinstance(fish, dict):
amplitudes = fish["amplitudes"]
phases = fish["phases"]
else:
if fish not in wavefish_harmonics:
raise KeyError(
"unknown wavefish. Choose one of "
+ ", ".join(wavefish_harmonics.keys())
)
amplitudes = wavefish_harmonics[fish]["amplitudes"]
phases = wavefish_harmonics[fish]["phases"]
if len(amplitudes) != len(phases):
raise IndexError("need exactly as many phases as amplitudes")
# remove NaNs:
for k in reversed(range(len(amplitudes))):
if np.isfinite(amplitudes[k]) or np.isfinite(phases[k]):
amplitudes = amplitudes[: k + 1]
phases = phases[: k + 1]
break
return amplitudes, phases
def wavefish_eods(
fish="Eigenmannia",
frequency=100.0,
samplerate=44100.0,
duration=1.0,
phase0=0.0,
noise_std=0.05,
):
"""Simulate EOD waveform of a wave-type fish.
The waveform is constructed by superimposing sinewaves of integral
multiples of the fundamental frequency - the fundamental and its
harmonics. The fundamental frequency of the EOD (EODf) is given by
`frequency`. With `fish` relative amplitudes and phases of the
fundamental and its harmonics are specified.
The generated waveform is `duration` seconds long and is sampled with
`samplerate` Hertz. Gaussian white noise with a standard deviation of
`noise_std` is added to the generated waveform.
Parameters
----------
fish: string, dict or tuple of lists/arrays
Specify relative amplitudes and phases of the fundamental and its harmonics.
If string then take amplitudes and phases from the `wavefish_harmonics` dictionary.
If dictionary then take amplitudes and phases from the 'amlitudes' and 'phases' keys.
If tuple then the first element is the list of amplitudes and
the second one the list of relative phases in radians.
frequency: float or array of floats
EOD frequency of the fish in Hertz. Either fixed number or array for
time-dependent frequencies.
samplerate: float
Sampling rate in Hertz.
duration: float
Duration of the generated data in seconds. Only used if frequency is scalar.
phase0: float
Phase offset of the EOD waveform in radians.
noise_std: float
Standard deviation of additive Gaussian white noise.
Returns
-------
data: array of floats
Generated data of a wave-type fish.
Raises
------
KeyError:
Unknown fish.
IndexError:
Amplitudes and phases differ in length.
"""
# get relative amplitude and phases:
amplitudes, phases = wavefish_spectrum(fish)
# compute phase:
if np.isscalar(frequency):
phase = np.arange(0, duration, 1.0 / samplerate)
phase *= frequency
else:
phase = np.cumsum(frequency) / samplerate
# generate EOD:
data = np.zeros(len(phase))
for har, (ampl, phi) in enumerate(zip(amplitudes, phases)):
if np.isfinite(ampl) and np.isfinite(phi):
data += ampl * np.sin(
2 * np.pi * (har + 1) * phase + phi - (har + 1) * phase0
)
# add noise:
data += noise_std * np.random.randn(len(data))
return data
def normalize_wavefish(fish, mode="peak"):
"""Normalize amplitudes and phases of wave-type EOD waveform.
The amplitudes and phases of the Fourier components are adjusted
such that the resulting EOD waveform has a peak-to-peak amplitude
of two and the peak of the waveform is at time zero (mode is set
to 'peak') or that the fundamental has an amplitude of one and a
phase of 0 (mode is set to 'zero').
Parameters
----------
fish: string, dict or tuple of lists/arrays
Specify relative amplitudes and phases of the fundamental and its harmonics.
If string then take amplitudes and phases from the `wavefish_harmonics` dictionary.
If dictionary then take amplitudes and phases from the 'amlitudes' and 'phases' keys.
If tuple then the first element is the list of amplitudes and
the second one the list of relative phases in radians.
mode: 'peak' or 'zero'
How to normalize amplitude and phases:
- 'peak': normalize waveform to a peak-to-peak amplitude of two
and shift it such that its peak is at time zero.
- 'zero': scale amplitude of fundamental to one and its phase to zero.
Returns
-------
amplitudes: array of floats
Adjusted amplitudes of the fundamental and its harmonics.
phases: array of floats
Adjusted phases in radians of the fundamental and its harmonics.
"""
# get relative amplitude and phases:
amplitudes, phases = wavefish_spectrum(fish)
if mode == "zero":
newamplitudes = np.array(amplitudes) / amplitudes[0]
newphases = np.array(
[p + (k + 1) * (-phases[0]) for k, p in enumerate(phases)]
)
newphases %= 2.0 * np.pi
newphases[newphases > np.pi] -= 2.0 * np.pi
return newamplitudes, newphases
else:
# generate waveform:
eodf = 100.0
rate = 100000.0
data = wavefish_eods(fish, eodf, rate, 2.0 / eodf, noise_std=0.0)
# normalize amplitudes:
ampl = 0.5 * (np.max(data) - np.min(data))
newamplitudes = np.array(amplitudes) / ampl
# shift phases:
deltat = np.argmax(data[: int(rate / eodf)]) / rate
deltap = 2.0 * np.pi * deltat * eodf
newphases = np.array(
[p + (k + 1) * deltap for k, p in enumerate(phases)]
)
newphases %= 2.0 * np.pi
newphases[newphases > np.pi] -= 2.0 * np.pi
# return:
return newamplitudes, newphases
def export_wavefish(fish, name="Unknown_harmonics", file=None):
"""Serialize wavefish parameter to python code.
Add output to the wavefish_harmonics dictionary!
Parameters
----------
fish: string, dict or tuple of lists/arrays
Specify relative amplitudes and phases of the fundamental and its harmonics.
If string then take amplitudes and phases from the `wavefish_harmonics` dictionary.
If dictionary then take amplitudes and phases from the 'amlitudes' and 'phases' keys.
If tuple then the first element is the list of amplitudes and
the second one the list of relative phases in radians.
name: string
Name of the dictionary to be written.
file: string or file or None
File name or open file object where to write wavefish dictionary.
Returns
-------
fish: dict
Dictionary with amplitudes and phases.
"""
# get relative amplitude and phases:
amplitudes, phases = wavefish_spectrum(fish)
# write out dictionary:
if file is None:
file = sys.stdout
try:
file.write("")
closeit = False
except AttributeError:
file = open(file, "w")
closeit = True
n = 6
file.write(name + " = \\\n")
file.write(" dict(amplitudes=(")
file.write(", ".join(["%.5g" % a for a in amplitudes[:n]]))
for k in range(n, len(amplitudes), n):
file.write(",\n")
file.write(" " * (9 + 12))
file.write(", ".join(["%.5g" % a for a in amplitudes[k : k + n]]))
file.write("),\n")
file.write(" " * 9 + "phases=(")
file.write(", ".join(["%.5g" % p for p in phases[:n]]))
for k in range(n, len(phases), n):
file.write(",\n")
file.write(" " * (9 + 8))
file.write(", ".join(["%.5g" % p for p in phases[k : k + n]]))
file.write("))\n")
if closeit:
file.close()
# return dictionary:
harmonics = dict(amplitudes=amplitudes, phases=phases)
return harmonics
def chirps(
eodf=100.0,
samplerate=44100.0,
duration=1.0,
chirp_times=[0.5],
chirp_size=[100.0],
chirp_width=[0.01],
chirp_kurtosis=[1.0],
chirp_contrast=[0.05],
):
"""Simulate frequency trace with chirps.
A chirp is modeled as a Gaussian frequency modulation.
The first chirp is placed at 0.5/chirp_freq.
Parameters
----------
eodf: float
EOD frequency of the fish in Hertz.
samplerate: float
Sampling rate in Hertz.
duration: float
Duration of the generated data in seconds.
chirp_times: float
Timestamps of every single chirp in seconds.
chirp_size: list
Size of each chirp (maximum frequency increase above eodf) in Hertz.
chirp_width: list
Width of every single chirp at 10% height in seconds.
chirp_kurtosis: list:
Shape of every single chirp. =1: Gaussian, >1: more rectangular, <1: more peaked.
chirp_contrast: float
Maximum amplitude reduction of EOD during every respective chirp.
Returns
-------
frequency: array of floats
Generated frequency trace that can be passed on to wavefish_eods().
amplitude: array of floats
Generated amplitude modulation that can be used to multiply the trace generated by
wavefish_eods().
"""
# baseline eod frequency and amplitude modulation:
n = len(np.arange(0, duration, 1.0 / samplerate))
frequency = eodf * np.ones(n)
am = np.ones(n)
for time, width, size, kurtosis, contrast in zip(
chirp_times, chirp_width, chirp_size, chirp_kurtosis, chirp_contrast
):
# chirp frequency waveform:
chirp_t = np.arange(-2.0 * width, 2.0 * width, 1.0 / samplerate)
chirp_sig = 0.5 * width / (2.0 * np.log(10.0)) ** (0.5 / kurtosis)
gauss = np.exp(-0.5 * ((chirp_t / chirp_sig) ** 2.0) ** kurtosis)
# add chirps on baseline eodf:
index = int(time * samplerate)
i0 = index - len(gauss) // 2
i1 = i0 + len(gauss)
gi0 = 0
gi1 = len(gauss)
if i0 < 0:
gi0 -= i0
i0 = 0
if i1 >= len(frequency):
gi1 -= i1 - len(frequency)
i1 = len(frequency)
frequency[i0:i1] += size * gauss[gi0:gi1]
am[i0:i1] -= contrast * gauss[gi0:gi1]
return frequency, am
def rises(
eodf,
samplerate,
duration,
rise_times,
rise_size,
rise_tau,
decay_tau,
):
"""Simulate frequency trace with rises.
A rise is modeled as a double exponential frequency modulation.
Parameters
----------
eodf: float
EOD frequency of the fish in Hertz.
samplerate: float
Sampling rate in Hertz.
duration: float
Duration of the generated data in seconds.
rise_times: list
Timestamp of each of the rises in seconds.
rise_size: list
Size of the respective rise (frequency increase above eodf) in Hertz.
rise_tau: list
Time constant of the frequency increase of the respective rise in seconds.
decay_tau: list
Time constant of the frequency decay of the respective rise in seconds.
Returns
-------
data: array of floats
Generate frequency trace that can be passed on to wavefish_eods().
"""
n = len(np.arange(0, duration, 1.0 / samplerate))
# baseline eod frequency:
frequency = eodf * np.ones(n)
for time, size, riset, decayt in zip(
rise_times, rise_size, rise_tau, decay_tau
):
# rise frequency waveform:
rise_t = np.arange(0.0, 5.0 * decayt, 1.0 / samplerate)
rise = size * (1.0 - np.exp(-rise_t / riset)) * np.exp(-rise_t / decayt)
# add rises on baseline eodf:
index = int(time * samplerate)
if index + len(rise) > len(frequency):
rise_index = len(frequency) - index
frequency[index : index + rise_index] += rise[:rise_index]
break
else:
frequency[index : index + len(rise)] += rise
return frequency
class FishSignal:
def __init__(self, samplerate, duration, eodf, nchirps, nrises):
time = np.arange(0, duration, 1 / samplerate)
chirp_times = np.random.uniform(0, duration, nchirps)
rise_times = np.random.uniform(0, duration, nrises)
# pick random parameters for chirps
chirp_size = np.random.uniform(60, 200, nchirps)
chirp_width = np.random.uniform(0.01, 0.1, nchirps)
chirp_kurtosis = np.random.uniform(1, 1, nchirps)
chirp_contrast = np.random.uniform(0.1, 0.5, nchirps)
# pick random parameters for rises
rise_size = np.random.uniform(10, 100, nrises)
rise_tau = np.random.uniform(0.5, 1.5, nrises)
decay_tau = np.random.uniform(5, 15, nrises)
# generate frequency trace with chirps
chirp_trace, chirp_amp = chirps(
eodf=0.0,
samplerate=samplerate,
duration=duration,
chirp_times=chirp_times,
chirp_size=chirp_size,
chirp_width=chirp_width,
chirp_kurtosis=chirp_kurtosis,
chirp_contrast=chirp_contrast,
)
# generate frequency trace with rises
rise_trace = rises(
eodf=0.0,
samplerate=samplerate,
duration=duration,
rise_times=rise_times,
rise_size=rise_size,
rise_tau=rise_tau,
decay_tau=decay_tau,
)
# combine traces to one
full_trace = chirp_trace + rise_trace + eodf
# make the EOD from the frequency trace
fish = wavefish_eods(
fish="Alepto",
frequency=full_trace,
samplerate=samplerate,
duration=duration,
phase0=0.0,
noise_std=0.05,
)
signal = fish * chirp_amp
self.signal = signal
self.trace = full_trace
self.time = time
self.samplerate = samplerate
self.eodf = eodf
def visualize(self):
spec, freqs, spectime = ps.spectrogram(
data=self.signal,
ratetime=self.samplerate,
freq_resolution=0.5,
overlap_frac=0.5,
)
fig, (ax1, ax2) = plt.subplots(2, 1, height_ratios=[1, 4], sharex=True)
ax1.plot(self.time, self.signal)
ax1.set_ylabel("Amplitude")
ax1.set_xlabel("Time (s)")
ax1.set_title("EOD signal")
ax2.imshow(
ps.decibel(spec),
origin="lower",
aspect="auto",
extent=[spectime[0], spectime[-1], freqs[0], freqs[-1]],
)
ax2.set_ylabel("Frequency (Hz)")
ax2.set_xlabel("Time (s)")
ax2.set_title("Spectrogram")
ax2.set_ylim(0, 2000)
plt.show()

View File

@ -1,411 +0,0 @@
import cmocean as cmo
import matplotlib.pyplot as plt
import numpy as np
from cycler import cycler
from matplotlib.colors import ListedColormap
def PlotStyle() -> None:
class style:
# lightcmap = cmocean.tools.lighten(cmocean.cm.haline, 0.8)
# units
cm = 1 / 2.54
mm = 1 / 25.4
# colors
black = "#111116"
white = "#e0e4f7"
gray = "#6c6e7d"
blue = "#89b4fa"
sapphire = "#74c7ec"
sky = "#89dceb"
teal = "#94e2d5"
green = "#a6e3a1"
yellow = "#f9d67f"
orange = "#faa472"
maroon = "#eb8486"
red = "#e0e4f7"
purple = "#d89bf7"
pink = "#f59edb"
lavender = "#b4befe"
gblue1 = "#f37588"
gblue2 = "#faa472"
gblue3 = "#f9d67f"
g = "#f3626c"
@classmethod
def lims(cls, track1, track2):
"""Helper function to get frequency y axis limits from two
fundamental frequency tracks.
Args:
track1 (array): First track
track2 (array): Second track
start (int): Index for first value to be plotted
stop (int): Index for second value to be plotted
padding (int): Padding for the upper and lower limit
Returns:
lower (float): lower limit
upper (float): upper limit
"""
allfunds_tmp = (
np.concatenate(
[
track1,
track2,
]
)
.ravel()
.tolist()
)
lower = np.min(allfunds_tmp)
upper = np.max(allfunds_tmp)
return lower, upper
@classmethod
def circled_annotation(cls, text, axis, xpos, ypos, padding=0.25):
axis.text(
xpos,
ypos,
text,
ha="center",
va="center",
zorder=1000,
bbox=dict(
boxstyle=f"circle, pad={padding}",
fc="white",
ec="black",
lw=1,
),
)
@classmethod
def fade_cmap(cls, cmap):
my_cmap = cmap(np.arange(cmap.N))
my_cmap[:, -1] = np.linspace(0, 1, cmap.N)
my_cmap = ListedColormap(my_cmap)
return my_cmap
@classmethod
def hide_ax(cls, ax):
ax.xaxis.set_visible(False)
plt.setp(ax.spines.values(), visible=False)
ax.tick_params(left=False, labelleft=False)
ax.patch.set_visible(False)
@classmethod
def hide_xax(cls, ax):
ax.xaxis.set_visible(False)
ax.spines["bottom"].set_visible(False)
@classmethod
def hide_yax(cls, ax):
ax.yaxis.set_visible(False)
ax.spines["left"].set_visible(False)
@classmethod
def set_boxplot_color(cls, bp, color):
plt.setp(bp["boxes"], color=color)
plt.setp(bp["whiskers"], color=white)
plt.setp(bp["caps"], color=white)
plt.setp(bp["medians"], color=black)
@classmethod
def label_subplots(cls, labels, axes, fig):
for axis, label in zip(axes, labels):
X = axis.get_position().x0
Y = axis.get_position().y1
fig.text(X, Y, label, weight="bold")
@classmethod
def letter_subplots(
cls, axes=None, letters=None, xoffset=-0.1, yoffset=1.0, **kwargs
):
"""Add letters to the corners of subplots (panels). By default each axis is
given an uppercase bold letter label placed in the upper-left corner.
Args
axes : list of pyplot ax objects. default plt.gcf().axes.
letters : list of strings to use as labels, default ["A", "B", "C", ...]
xoffset, yoffset : positions of each label relative to plot frame
(default -0.1,1.0 = upper left margin). Can also be a list of
offsets, in which case it should be the same length as the number of
axes.
Other keyword arguments will be passed to annotate() when panel letters
are added.
Returns:
list of strings for each label added to the axes
Examples:
Defaults:
>>> fig, axes = plt.subplots(1,3)
>>> letter_subplots() # boldfaced A, B, C
Common labeling schemes inferred from the first letter:
>>> fig, axes = plt.subplots(1,4)
# panels labeled (a), (b), (c), (d)
>>> letter_subplots(letters='(a)')
Fully custom lettering:
>>> fig, axes = plt.subplots(2,1)
>>> letter_subplots(axes, letters=['(a.1)', '(b.2)'], fontweight='normal')
Per-axis offsets:
>>> fig, axes = plt.subplots(1,2)
>>> letter_subplots(axes, xoffset=[-0.1, -0.15])
Matrix of axes:
>>> fig, axes = plt.subplots(2,2, sharex=True, sharey=True)
# fig.axes is a list when axes is a 2x2 matrix
>>> letter_subplots(fig.axes)
"""
# get axes:
if axes is None:
axes = plt.gcf().axes
# handle single axes:
try:
iter(axes)
except TypeError:
axes = [axes]
# set up letter defaults (and corresponding fontweight):
fontweight = "bold"
ulets = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ"[: len(axes)])
llets = list("abcdefghijklmnopqrstuvwxyz"[: len(axes)])
if letters is None or letters == "A":
letters = ulets
elif letters == "(a)":
letters = ["({})".format(lett) for lett in llets]
fontweight = "normal"
elif letters == "(A)":
letters = ["({})".format(lett) for lett in ulets]
fontweight = "normal"
elif letters in ("lower", "lowercase", "a"):
letters = llets
# make sure there are x and y offsets for each ax in axes:
if isinstance(xoffset, (int, float)):
xoffset = [xoffset] * len(axes)
else:
assert len(xoffset) == len(axes)
if isinstance(yoffset, (int, float)):
yoffset = [yoffset] * len(axes)
else:
assert len(yoffset) == len(axes)
# defaults for annotate (kwargs is second so it can overwrite these defaults):
my_defaults = dict(
fontweight=fontweight,
fontsize="large",
ha="center",
va="center",
xycoords="axes fraction",
annotation_clip=False,
)
kwargs = dict(list(my_defaults.items()) + list(kwargs.items()))
list_txts = []
for ax, lbl, xoff, yoff in zip(axes, letters, xoffset, yoffset):
t = ax.annotate(lbl, xy=(xoff, yoff), **kwargs)
list_txts.append(t)
return list_txts
pass
# rcparams text setup
SMALL_SIZE = 12
MEDIUM_SIZE = 14
BIGGER_SIZE = 16
black = "#111116"
white = "#e0e4f7"
gray = "#6c6e7d"
dark_gray = "#2a2a32"
# rcparams
plt.rc("font", size=MEDIUM_SIZE) # controls default text sizes
plt.rc("axes", titlesize=MEDIUM_SIZE) # fontsize of the axes title
plt.rc("axes", labelsize=MEDIUM_SIZE) # fontsize of the x and y labels
plt.rc("xtick", labelsize=SMALL_SIZE) # fontsize of the tick labels
plt.rc("ytick", labelsize=SMALL_SIZE) # fontsize of the tick labels
plt.rc("legend", fontsize=SMALL_SIZE) # legend fontsize
plt.rc("figure", titlesize=BIGGER_SIZE) # fontsize of the figure title
plt.rcParams["image.cmap"] = "cmo.thermal"
plt.rcParams["axes.xmargin"] = 0.05
plt.rcParams["axes.ymargin"] = 0.1
plt.rcParams["axes.titlelocation"] = "left"
plt.rcParams["axes.titlesize"] = BIGGER_SIZE
# plt.rcParams["axes.titlepad"] = -10
plt.rcParams["legend.frameon"] = False
plt.rcParams["legend.loc"] = "best"
plt.rcParams["legend.borderpad"] = 0.4
plt.rcParams["legend.facecolor"] = black
plt.rcParams["legend.edgecolor"] = black
plt.rcParams["legend.framealpha"] = 0.7
plt.rcParams["legend.borderaxespad"] = 0.5
plt.rcParams["legend.fancybox"] = False
# # specify the custom font to use
# plt.rcParams["font.family"] = "sans-serif"
# plt.rcParams["font.sans-serif"] = "Helvetica Now Text"
# dark mode modifications
plt.rcParams["boxplot.flierprops.color"] = white
plt.rcParams["boxplot.flierprops.markeredgecolor"] = gray
plt.rcParams["boxplot.boxprops.color"] = gray
plt.rcParams["boxplot.whiskerprops.color"] = gray
plt.rcParams["boxplot.capprops.color"] = gray
plt.rcParams["boxplot.medianprops.color"] = black
plt.rcParams["text.color"] = white
plt.rcParams["axes.facecolor"] = black # axes background color
plt.rcParams["axes.edgecolor"] = white # axes edge color
# plt.rcParams["axes.grid"] = True # display grid or not
# plt.rcParams["axes.grid.axis"] = "y" # which axis the grid is applied to
plt.rcParams["axes.labelcolor"] = white
plt.rcParams["axes.axisbelow"] = True # draw axis gridlines and ticks:
plt.rcParams["axes.spines.left"] = True # display axis spines
plt.rcParams["axes.spines.bottom"] = True
plt.rcParams["axes.spines.top"] = False
plt.rcParams["axes.spines.right"] = False
plt.rcParams["axes.prop_cycle"] = cycler(
"color",
[
"#b4befe",
"#89b4fa",
"#74c7ec",
"#89dceb",
"#94e2d5",
"#a6e3a1",
"#f9e2af",
"#fab387",
"#eba0ac",
"#f38ba8",
"#cba6f7",
"#f5c2e7",
],
)
plt.rcParams["xtick.color"] = white # color of the ticks
plt.rcParams["ytick.color"] = white # color of the ticks
plt.rcParams["grid.color"] = white # grid color
plt.rcParams["figure.facecolor"] = black # figure face color
plt.rcParams["figure.edgecolor"] = black # figure edge color
plt.rcParams["savefig.facecolor"] = black # figure face color when saving
return style
if __name__ == "__main__":
s = PlotStyle()
import matplotlib.cbook as cbook
import matplotlib.cm as cm
import matplotlib.pyplot as plt
from matplotlib.patches import PathPatch
from matplotlib.path import Path
# Fixing random state for reproducibility
np.random.seed(19680801)
delta = 0.025
x = y = np.arange(-3.0, 3.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-(X**2) - Y**2)
Z2 = np.exp(-((X - 1) ** 2) - (Y - 1) ** 2)
Z = (Z1 - Z2) * 2
fig1, ax = plt.subplots()
im = ax.imshow(
Z,
interpolation="bilinear",
cmap=cm.RdYlGn,
origin="lower",
extent=[-3, 3, -3, 3],
vmax=abs(Z).max(),
vmin=-abs(Z).max(),
)
plt.show()
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(9, 4))
# Fixing random state for reproducibility
np.random.seed(19680801)
# generate some random test data
all_data = [np.random.normal(0, std, 100) for std in range(6, 10)]
# plot violin plot
axs[0].violinplot(all_data, showmeans=False, showmedians=True)
axs[0].set_title("Violin plot")
# plot box plot
axs[1].boxplot(all_data)
axs[1].set_title("Box plot")
# adding horizontal grid lines
for ax in axs:
ax.yaxis.grid(True)
ax.set_xticks(
[y + 1 for y in range(len(all_data))],
labels=["x1", "x2", "x3", "x4"],
)
ax.set_xlabel("Four separate samples")
ax.set_ylabel("Observed values")
plt.show()
# Fixing random state for reproducibility
np.random.seed(19680801)
# Compute pie slices
N = 20
theta = np.linspace(0.0, 2 * np.pi, N, endpoint=False)
radii = 10 * np.random.rand(N)
width = np.pi / 4 * np.random.rand(N)
colors = cmo.cm.haline(radii / 10.0)
ax = plt.subplot(projection="polar")
ax.bar(theta, radii, width=width, bottom=0.0, color=colors, alpha=0.5)
plt.show()
methods = [
None,
"none",
"nearest",
"bilinear",
"bicubic",
"spline16",
"spline36",
"hanning",
"hamming",
"hermite",
"kaiser",
"quadric",
"catrom",
"gaussian",
"bessel",
"mitchell",
"sinc",
"lanczos",
]
# Fixing random state for reproducibility
np.random.seed(19680801)
grid = np.random.rand(4, 4)
fig, axs = plt.subplots(
nrows=3,
ncols=6,
figsize=(9, 6),
subplot_kw={"xticks": [], "yticks": []},
)
for ax, interp_method in zip(axs.flat, methods):
ax.imshow(grid, interpolation=interp_method)
ax.set_title(str(interp_method))
plt.tight_layout()
plt.show()

View File

@ -1,150 +0,0 @@
import matplotlib.pyplot as plt
import numpy as np
from filters import bandpass_filter, inst_freq, instantaneous_frequency
from fish_signal import chirps, wavefish_eods
from IPython import embed
def switch_test(test, defaultparams, testparams):
if test == "width":
defaultparams["chirp_width"] = testparams["chirp_width"]
key = "chirp_width"
elif test == "size":
defaultparams["chirp_size"] = testparams["chirp_size"]
key = "chirp_size"
elif test == "kurtosis":
defaultparams["chirp_kurtosis"] = testparams["chirp_kurtosis"]
key = "chirp_kurtosis"
elif test == "contrast":
defaultparams["chirp_contrast"] = testparams["chirp_contrast"]
key = "chirp_contrast"
else:
raise ValueError("Test not recognized")
return key, defaultparams
def extract_dict(dict, index):
return {key: value[index] for key, value in dict.items()}
def test(test1, test2, resolution=10):
assert test1 in [
"width",
"size",
"kurtosis",
"contrast",
], "Test1 not recognized"
assert test2 in [
"width",
"size",
"kurtosis",
"contrast",
], "Test2 not recognized"
# Define the parameters for the chirp simulations
ntest = resolution
defaultparams = dict(
chirp_size=np.ones(ntest) * 100,
chirp_width=np.ones(ntest) * 0.1,
chirp_kurtosis=np.ones(ntest) * 1.0,
chirp_contrast=np.ones(ntest) * 0.5,
)
testparams = dict(
chirp_width=np.linspace(0.01, 0.2, ntest),
chirp_size=np.linspace(50, 300, ntest),
chirp_kurtosis=np.linspace(0.5, 1.5, ntest),
chirp_contrast=np.linspace(0.01, 1.0, ntest),
)
key1, chirp_params = switch_test(test1, defaultparams, testparams)
key2, chirp_params = switch_test(test2, chirp_params, testparams)
# make the chirp trace
eodf = 500
samplerate = 20000
duration = 2
chirp_times = [0.5, 1, 1.5]
wide_cutoffs = 200
tight_cutoffs = 10
distances = np.full((ntest, ntest), np.nan)
fig, axs = plt.subplots(
ntest, ntest, figsize=(10, 10), sharex=True, sharey=True
)
axs = axs.flatten()
iter0 = 0
for iter1, test1_param in enumerate(chirp_params[key1]):
for iter2, test2_param in enumerate(chirp_params[key2]):
# get the chirp parameters for the current test
inner_chirp_params = extract_dict(chirp_params, iter2)
inner_chirp_params[key1] = test1_param
inner_chirp_params[key2] = test2_param
# make the chirp trace for the current chirp parameters
sizes = np.ones(len(chirp_times)) * inner_chirp_params["chirp_size"]
widths = (
np.ones(len(chirp_times)) * inner_chirp_params["chirp_width"]
)
kurtosis = (
np.ones(len(chirp_times)) * inner_chirp_params["chirp_kurtosis"]
)
contrast = (
np.ones(len(chirp_times)) * inner_chirp_params["chirp_contrast"]
)
# make the chirp trace
chirp_trace, ampmod = chirps(
eodf,
samplerate,
duration,
chirp_times,
sizes,
widths,
kurtosis,
contrast,
)
signal = wavefish_eods(
fish="Alepto",
frequency=chirp_trace,
samplerate=samplerate,
duration=duration,
phase0=0.0,
noise_std=0.05,
)
signal = signal * ampmod
# apply broadband filter
wide_signal = bandpass_filter(
signal, samplerate, eodf - wide_cutoffs, eodf + wide_cutoffs
)
tight_signal = bandpass_filter(
signal, samplerate, eodf - tight_cutoffs, eodf + tight_cutoffs
)
# get the instantaneous frequency
wide_frequency = inst_freq(wide_signal, samplerate)
tight_frequency = inst_freq(tight_signal, samplerate)
bool_mask = wide_frequency != 0
axs[iter0].plot(wide_frequency[bool_mask])
axs[iter0].plot(tight_frequency[bool_mask])
fig.supylabel(key1)
fig.supxlabel(key2)
iter0 += 1
plt.show()
def main():
test("contrast", "kurtosis")
if __name__ == "__main__":
main()

View File

@ -10,84 +10,73 @@ from modules.filters import bandpass_filter
def main(folder):
file = os.path.join(folder, "traces-grid.raw")
file = os.path.join(folder, 'traces-grid.raw')
data = open_data(folder, 60.0, 0, channel=-1)
time = np.load(folder + "times.npy", allow_pickle=True)
freq = np.load(folder + "fund_v.npy", allow_pickle=True)
ident = np.load(folder + "ident_v.npy", allow_pickle=True)
idx = np.load(folder + "idx_v.npy", allow_pickle=True)
time = np.load(folder + 'times.npy', allow_pickle=True)
freq = np.load(folder + 'fund_v.npy', allow_pickle=True)
ident = np.load(folder + 'ident_v.npy', allow_pickle=True)
idx = np.load(folder + 'idx_v.npy', allow_pickle=True)
t0 = 3 * 60 * 60 + 6 * 60 + 43.5
t0 = 3*60*60 + 6*60 + 43.5
dt = 60
data_oi = data[t0 * data.samplerate : (t0 + dt) * data.samplerate, :]
data_oi = data[t0 * data.samplerate: (t0+dt)*data.samplerate, :]
for i in [10]:
# getting the spectogramm
spec_power, spec_freqs, spec_times = spectrogram(
data_oi[:, i],
ratetime=data.samplerate,
freq_resolution=50,
overlap_frac=0.0,
)
fig, ax = plt.subplots(figsize=(20 / 2.54, 12 / 2.54))
ax.pcolormesh(
spec_times, spec_freqs, decibel(spec_power), vmin=-100, vmax=-50
)
data_oi[:, i], ratetime=data.samplerate, freq_resolution=50, overlap_frac=0.0)
fig, ax = plt.subplots(figsize=(20/2.54, 12/2.54))
ax.pcolormesh(spec_times, spec_freqs, decibel(
spec_power), vmin=-100, vmax=-50)
for track_id in np.unique(ident):
# window_index for time array in time window
window_index = np.arange(len(idx))[
(ident == track_id)
& (time[idx] >= t0)
& (time[idx] <= (t0 + dt))
]
window_index = np.arange(len(idx))[(ident == track_id) &
(time[idx] >= t0) &
(time[idx] <= (t0+dt))]
freq_temp = freq[window_index]
time_temp = time[idx[window_index]]
# mean_freq = np.mean(freq_temp)
# fdata = bandpass_filter(data_oi[:, track_id], data.samplerate, mean_freq-5, mean_freq+200)
#mean_freq = np.mean(freq_temp)
#fdata = bandpass_filter(data_oi[:, track_id], data.samplerate, mean_freq-5, mean_freq+200)
ax.plot(time_temp - t0, freq_temp)
ax.set_ylim(500, 1000)
plt.show()
# filter plot
id = 10.0
id = 10.
i = 10
window_index = np.arange(len(idx))[
(ident == id) & (time[idx] >= t0) & (time[idx] <= (t0 + dt))
]
window_index = np.arange(len(idx))[(ident == id) &
(time[idx] >= t0) &
(time[idx] <= (t0+dt))]
freq_temp = freq[window_index]
time_temp = time[idx[window_index]]
mean_freq = np.mean(freq_temp)
fdata = bandpass_filter(
data_oi[:, i],
rate=data.samplerate,
lowf=mean_freq - 5,
highf=mean_freq + 200,
)
data_oi[:, i], rate=data.samplerate, lowf=mean_freq-5, highf=mean_freq+200)
fig, ax = plt.subplots()
ax.plot(np.arange(len(fdata)) / data.samplerate, fdata, marker="*")
ax.plot(np.arange(len(fdata))/data.samplerate, fdata, marker='*')
# plt.show()
# freqency analyis of filtered data
time_fdata = np.arange(len(fdata)) / data.samplerate
time_fdata = np.arange(len(fdata))/data.samplerate
roll_fdata = np.roll(fdata, shift=1)
period_index = np.arange(len(fdata))[(roll_fdata < 0) & (fdata >= 0)]
plt.plot(time_fdata, fdata)
plt.scatter(time_fdata[period_index], fdata[period_index], c="r")
plt.scatter(time_fdata[period_index - 1], fdata[period_index - 1], c="r")
plt.scatter(time_fdata[period_index], fdata[period_index], c='r')
plt.scatter(time_fdata[period_index-1], fdata[period_index-1], c='r')
upper_bound = np.abs(fdata[period_index])
lower_bound = np.abs(fdata[period_index - 1])
lower_bound = np.abs(fdata[period_index-1])
upper_times = np.abs(time_fdata[period_index])
lower_times = np.abs(time_fdata[period_index - 1])
lower_times = np.abs(time_fdata[period_index-1])
lower_ratio = lower_bound / (lower_bound + upper_bound)
upper_ratio = upper_bound / (lower_bound + upper_bound)
lower_ratio = lower_bound/(lower_bound+upper_bound)
upper_ratio = upper_bound/(lower_bound+upper_bound)
time_delta = upper_times - lower_times
true_zero = lower_times + time_delta * lower_ratio
time_delta = upper_times-lower_times
true_zero = lower_times + time_delta*lower_ratio
plt.scatter(true_zero, np.zeros(len(true_zero)))
@ -95,7 +84,7 @@ def main(folder):
inst_freq = 1 / np.diff(true_zero)
filtered_inst_freq = gaussian_filter1d(inst_freq, 0.005)
fig, ax = plt.subplots()
ax.plot(filtered_inst_freq, marker=".")
ax.plot(filtered_inst_freq, marker='.')
# in 5 sekunden welcher fisch auf einer elektrode am
embed()
@ -110,7 +99,5 @@ def main(folder):
pass
if __name__ == "__main__":
main(
"/Users/acfw/Documents/uni_tuebingen/chirpdetection/gp_benda/data/2022-06-02-10_00/"
)
if __name__ == '__main__':
main('/Users/acfw/Documents/uni_tuebingen/chirpdetection/gp_benda/data/2022-06-02-10_00/')

View File

@ -1,38 +0,0 @@
from thunderfish.dataloader import DataLoader as open_data
from thunderfish.powerspectrum import spectrogram, decibel
from IPython import embed
import matplotlib.pyplot as plt
import numpy as np
import os
from scipy.ndimage import gaussian_filter1d
from modules.filters import bandpass_filter
from modules.filehandling import LoadData
def main(folder):
data = LoadData(folder)
t0 = 3 * 60 * 60 + 6 * 60 + 43.5
dt = 60
data_oi = data.raw[t0 * data.raw_rate : (t0 + dt) * data.raw_rate, :]
# good electrode
electrode = 10
data_oi = data_oi[:, electrode]
fig, axs = plt.subplots(2, 1)
axs[0].plot(np.arange(data_oi.shape[0]) / data.raw_rate, data_oi)
for tr, track_id in enumerate(np.unique(data.ident[~np.isnan(data.ident)])):
rack_window_index = np.arange(len(data.idx))[
(data.ident == track_id)
& (data.time[data.idx] >= t0)
& (data.time[data.idx] <= (t0 + dt))
]
freq_fish = data.freq[rack_window_index]
axs[1].plot(np.arange(freq_fish.shape[0]) / data.raw_rate, freq_fish)
plt.show()
if __name__ == "__main__":
main(
"/Users/acfw/Documents/uni_tuebingen/chirpdetection/GP2023_chirp_detection/data/2022-06-02-10_00/"
)

View File

@ -1,75 +1,59 @@
import os
import os
import os
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.pyplot as plt
from IPython import embed
from pandas import read_csv
from modules.logger import makeLogger
from scipy.ndimage import gaussian_filter1d
logger = makeLogger(__name__)
class Behavior:
"""Load behavior data from csv file as class attributes
Attributes
----------
behavior: 0: chasing onset, 1: chasing offset, 2: physical contact
behavior_type:
behavioral_category:
comment_start:
comment_stop:
dataframe: pandas dataframe with all the data
duration_s:
media_file:
observation_date:
observation_id:
start_s: start time of the event in seconds
stop_s: stop time of the event in seconds
total_length:
behavior_type:
behavioral_category:
comment_start:
comment_stop:
dataframe: pandas dataframe with all the data
duration_s:
media_file:
observation_date:
observation_id:
start_s: start time of the event in seconds
stop_s: stop time of the event in seconds
total_length:
"""
def __init__(self, folder_path: str) -> None:
LED_on_time_BORIS = np.load(
os.path.join(folder_path, "LED_on_time.npy"), allow_pickle=True
)
self.time = np.load(
os.path.join(folder_path, "times.npy"), allow_pickle=True
)
csv_filename = [
f for f in os.listdir(folder_path) if f.endswith(".csv")
][
0
] # check if there are more than one csv file
LED_on_time_BORIS = np.load(os.path.join(folder_path, 'LED_on_time.npy'), allow_pickle=True)
self.time = np.load(os.path.join(folder_path, "times.npy"), allow_pickle=True)
csv_filename = [f for f in os.listdir(folder_path) if f.endswith('.csv')][0] # check if there are more than one csv file
self.dataframe = read_csv(os.path.join(folder_path, csv_filename))
self.chirps = np.load(
os.path.join(folder_path, "chirps.npy"), allow_pickle=True
)
self.chirps_ids = np.load(
os.path.join(folder_path, "chirps_ids.npy"), allow_pickle=True
)
self.chirps = np.load(os.path.join(folder_path, 'chirps.npy'), allow_pickle=True)
self.chirps_ids = np.load(os.path.join(folder_path, 'chirps_ids.npy'), allow_pickle=True)
for k, key in enumerate(self.dataframe.keys()):
key = key.lower()
if " " in key:
key = key.replace(" ", "_")
if "(" in key:
key = key.replace("(", "")
key = key.replace(")", "")
setattr(
self, key, np.array(self.dataframe[self.dataframe.keys()[k]])
)
key = key.lower()
if ' ' in key:
key = key.replace(' ', '_')
if '(' in key:
key = key.replace('(', '')
key = key.replace(')', '')
setattr(self, key, np.array(self.dataframe[self.dataframe.keys()[k]]))
last_LED_t_BORIS = LED_on_time_BORIS[-1]
real_time_range = self.time[-1] - self.time[0]
factor = 1.034141
shift = last_LED_t_BORIS - real_time_range * factor
self.start_s = (self.start_s - shift) / factor
self.stop_s = (self.stop_s - shift) / factor
"""
1 - chasing onset
2 - chasing offset
@ -97,77 +81,53 @@ temporal encpding needs to be corrected ... not exactly 25FPS.
behavior = data['Behavior']
"""
def correct_chasing_events(
category: np.ndarray, timestamps: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
onset_ids = np.arange(len(category))[category == 0]
offset_ids = np.arange(len(category))[category == 1]
category: np.ndarray,
timestamps: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
onset_ids = np.arange(
len(category))[category == 0]
offset_ids = np.arange(
len(category))[category == 1]
# Check whether on- or offset is longer and calculate length difference
if len(onset_ids) > len(offset_ids):
len_diff = len(onset_ids) - len(offset_ids)
longer_array = onset_ids
shorter_array = offset_ids
logger.info(f"Onsets are greater than offsets by {len_diff}")
logger.info(f'Onsets are greater than offsets by {len_diff}')
elif len(onset_ids) < len(offset_ids):
len_diff = len(offset_ids) - len(onset_ids)
longer_array = offset_ids
shorter_array = onset_ids
logger.info(f"Offsets are greater than offsets by {len_diff}")
logger.info(f'Offsets are greater than offsets by {len_diff}')
elif len(onset_ids) == len(offset_ids):
logger.info("Chasing events are equal")
logger.info('Chasing events are equal')
return category, timestamps
# Correct the wrong chasing events; delete double events
wrong_ids = []
for i in range(len(longer_array) - (len_diff + 1)):
if (shorter_array[i] > longer_array[i]) & (
shorter_array[i] < longer_array[i + 1]
):
for i in range(len(longer_array)-(len_diff+1)):
if (shorter_array[i] > longer_array[i]) & (shorter_array[i] < longer_array[i+1]):
pass
else:
wrong_ids.append(longer_array[i])
longer_array = np.delete(longer_array, i)
category = np.delete(category, wrong_ids)
timestamps = np.delete(timestamps, wrong_ids)
category = np.delete(
category, wrong_ids)
timestamps = np.delete(
timestamps, wrong_ids)
return category, timestamps
def event_triggered_chirps(
event: np.ndarray,
chirps: np.ndarray,
time_before_event: int,
time_after_event: int,
) -> tuple[np.ndarray, np.ndarray]:
event_chirps = [] # chirps that are in specified window around event
centered_chirps = (
[]
) # timestamps of chirps around event centered on the event timepoint
for event_timestamp in event:
start = event_timestamp - time_before_event # timepoint of window start
stop = event_timestamp + time_after_event # timepoint of window ending
chirps_around_event = [
c for c in chirps if (c >= start) & (c <= stop)
] # get chirps that are in a -5 to +5 sec window around event
event_chirps.append(chirps_around_event)
if len(chirps_around_event) == 0:
continue
else:
centered_chirps.append(chirps_around_event - event_timestamp)
centered_chirps = np.concatenate(
centered_chirps, axis=0
) # convert list of arrays to one array for plotting
return event_chirps, centered_chirps
def main(datapath: str):
# behavior is pandas dataframe with all the data
bh = Behavior(datapath)
# chirps are not sorted in time (presumably due to prior groupings)
# get and sort chirps and corresponding fish_ids of the chirps
chirps = bh.chirps[np.argsort(bh.chirps)]
@ -184,105 +144,39 @@ def main(datapath: str):
chasing_offset = timestamps[category == 1]
physical_contact = timestamps[category == 2]
##### TODO Physical contact-triggered chirps (PTC) mit Rasterplot #####
# Wahrscheinlichkeit von Phys auf Ch und vice versa
# Chasing-triggered chirps (CTC) mit Rasterplot
# Wahrscheinlichkeit von Chase auf Ch und vice versa
# First overview plot
fig1, ax1 = plt.subplots()
ax1.scatter(
chirps,
np.ones_like(chirps),
marker="*",
color="royalblue",
label="Chirps",
)
ax1.scatter(
chasing_onset,
np.ones_like(chasing_onset) * 2,
marker=".",
color="forestgreen",
label="Chasing onset",
)
ax1.scatter(
chasing_offset,
np.ones_like(chasing_offset) * 2.5,
marker=".",
color="firebrick",
label="Chasing offset",
)
ax1.scatter(
physical_contact,
np.ones_like(physical_contact) * 3,
marker="x",
color="black",
label="Physical contact",
)
ax1.scatter(chirps, np.ones_like(chirps), marker='*', color='royalblue', label='Chirps')
ax1.scatter(chasing_onset, np.ones_like(chasing_onset)*2, marker='.', color='forestgreen', label='Chasing onset')
ax1.scatter(chasing_offset, np.ones_like(chasing_offset)*2.5, marker='.', color='firebrick', label='Chasing offset')
ax1.scatter(physical_contact, np.ones_like(physical_contact)*3, marker='x', color='black', label='Physical contact')
plt.legend()
# plt.show()
plt.close()
# Get fish ids
fish_ids = np.unique(chirps_fish_ids)
##### Chasing triggered chirps CTC #####
# Evaluate how many chirps were emitted in specific time window around the chasing onset events
# Iterate over chasing onsets (later over fish)
time_around_event = 5 # time window around the event in which chirps are counted, 5 = -5 to +5 sec around event
#### Loop crashes at concatenate in function ####
# for i in range(len(fish_ids)):
# fish = fish_ids[i]
# chirps = chirps[chirps_fish_ids == fish]
# print(fish)
chasing_chirps, centered_chasing_chirps = event_triggered_chirps(
chasing_onset, chirps, time_around_event, time_around_event
)
physical_chirps, centered_physical_chirps = event_triggered_chirps(
physical_contact, chirps, time_around_event, time_around_event
)
# Kernel density estimation ???
# centered_chasing_chirps_convolved = gaussian_filter1d(centered_chasing_chirps, 5)
# centered_chasing = chasing_onset[0] - chasing_onset[0] ## get the 0 timepoint for plotting; set one chasing event to 0
offsets = [0.5, 1]
fig4, ax4 = plt.subplots(
figsize=(20 / 2.54, 12 / 2.54), constrained_layout=True
)
ax4.eventplot(
np.array([centered_chasing_chirps, centered_physical_chirps]),
lineoffsets=offsets,
linelengths=0.25,
colors=["g", "r"],
)
ax4.vlines(0, 0, 1.5, "tab:grey", "dashed", "Timepoint of event")
# ax4.plot(centered_chasing_chirps_convolved)
ax4.set_yticks(offsets)
ax4.set_yticklabels(["Chasings", "Physical \n contacts"])
ax4.set_xlabel("Time[s]")
ax4.set_ylabel("Type of event")
plt.show()
all_fish_ids = np.unique(chirps_fish_ids)
# Associate chirps to inidividual fish
fish1 = chirps[chirps_fish_ids == fish_ids[0]]
fish2 = chirps[chirps_fish_ids == fish_ids[1]]
fish1 = chirps[chirps_fish_ids == all_fish_ids[0]]
fish2 = chirps[chirps_fish_ids == all_fish_ids[1]]
fish = [len(fish1), len(fish2)]
### Plots:
# 1. All recordings, all fish, all chirps
# One CTC, one PTC
# 2. All recordings, only winners
# One CTC, one PTC
# 3. All recordings, all losers
# One CTC, one PTC
#### Chirp counts per fish general #####
fig2, ax2 = plt.subplots()
x = ["Fish1", "Fish2"]
x = ['Fish1', 'Fish2']
width = 0.35
ax2.bar(x, fish, width=width)
ax2.set_ylabel("Chirp count")
ax2.set_ylabel('Chirp count')
# plt.show()
plt.close()
##### Count chirps emitted during chasing events and chirps emitted out of chasing events #####
chirps_in_chasings = []
for onset, offset in zip(chasing_onset, chasing_offset):
@ -299,24 +193,25 @@ def main(datapath: str):
counts_chirps_chasings += 1
# chirps in chasing events
fig3, ax3 = plt.subplots()
ax3.bar(
["Chirps in chasing events", "Chasing events without Chirps"],
[counts_chirps_chasings, chasings_without_chirps],
width=width,
)
plt.ylabel("Count")
# plt.show()
plt.close()
fig3 , ax3 = plt.subplots()
ax3.bar(['Chirps in chasing events', 'Chasing events without Chirps'], [counts_chirps_chasings, chasings_without_chirps], width=width)
plt.ylabel('Count')
plt.show()
plt.close()
# comparison between chasing events with and without chirps
# comparison between chasing events with and without chirps
embed()
exit()
if __name__ == "__main__":
if __name__ == '__main__':
# Path to the data
datapath = "../data/mount_data/2020-05-13-10_00/"
datapath = "../data/mount_data/2020-05-13-10_00/"
datapath = '../data/mount_data/2020-05-13-10_00/'
main(datapath)

View File

@ -1,34 +1,24 @@
import numpy as np
from modules.filters import create_chirp, bandpass_filter
import matplotlib.pyplot as plt
from chirpdetection import instantaneos_frequency
from IPython import embed
import matplotlib.pyplot as plt
from thunderfish import fakefish
# create chirp
from modules.filters import bandpass_filter
from modules.datahandling import instantaneous_frequency
from modules.simulations import create_chirp
time, signal, ampl, freq = create_chirp(chirptimes=[0.05, 0.2501, 0.38734, 0.48332, 0.73434, 0.823424], )
# filter signal with bandpass_filter
# trying thunderfish fakefish chirp simulation ---------------------------------
samplerate = 44100
freq, ampl = fakefish.chirps(eodf=500, chirp_contrast=0.2)
data = fakefish.wavefish_eods(
fish="Alepto", frequency=freq, phase0=3, samplerate=samplerate
)
# filter signal with bandpass_filter
data_filterd = bandpass_filter(data * ampl + 1, samplerate, 0.01, 1.99)
signal = bandpass_filter(signal, 1/0.00001, 495, 505)
embed()
data_freq_time, data_freq = instantaneous_frequency(data, samplerate, 5)
exit()
fig, axs = plt.subplots(2, 1, figsize=(10, 10))
axs[0].plot(time, signal)
# plot instatneous frequency
fig, ax = plt.subplots(4, 1, figsize=(20 / 2.54, 12 / 2.54), sharex=True)
baseline_freq_time, baseline_freq = instantaneos_frequency(signal, 1/0.00001)
axs[1].plot(baseline_freq_time[1:], baseline_freq[1:])
ax[0].plot(np.arange(len(data)) / samplerate, data * ampl)
# ax[0].scatter(true_zero, np.zeros_like(true_zero), color='red')
ax[1].plot(np.arange(len(data_filterd)) / samplerate, data_filterd)
ax[2].plot(np.arange(len(freq)) / samplerate, freq)
ax[3].plot(data_freq_time, data_freq)
plt.show()
embed()

View File

@ -1,26 +1,24 @@
from dataclasses import dataclass
from itertools import compress
from dataclasses import dataclass
import matplotlib.gridspec as gr
import matplotlib.pyplot as plt
import numpy as np
from IPython import embed
import matplotlib.pyplot as plt
import matplotlib.gridspec as gr
from scipy.signal import find_peaks
from thunderfish.powerspectrum import spectrogram, decibel
from sklearn.preprocessing import normalize
from modules.filters import bandpass_filter, envelope, highpass_filter
from modules.filehandling import ConfLoader, LoadData, make_outputdir
from modules.plotstyle import PlotStyle
from modules.logger import makeLogger
from modules.datahandling import (
flatten,
purge_duplicates,
group_timestamps,
instantaneous_frequency,
minmaxnorm,
purge_duplicates,
)
from modules.filehandling import ConfLoader, LoadData, make_outputdir
from modules.filters import bandpass_filter, envelope, highpass_filter
from modules.logger import makeLogger
from modules.plotstyle import PlotStyle
from scipy.signal import find_peaks
from thunderfish.powerspectrum import decibel, spectrogram
# from sklearn.preprocessing import normalize
logger = makeLogger(__name__)
@ -28,7 +26,7 @@ ps = PlotStyle()
@dataclass
class ChirpPlotBuffer:
class PlotBuffer:
"""
Buffer to save data that is created in the main detection loop
@ -59,6 +57,7 @@ class ChirpPlotBuffer:
frequency_peaks: np.ndarray
def plot_buffer(self, chirps: np.ndarray, plot: str) -> None:
logger.debug("Starting plotting")
# make data for plotting
@ -84,32 +83,28 @@ class ChirpPlotBuffer:
q50 + self.search_frequency + self.config.minimal_bandwidth / 2,
q50 + self.search_frequency - self.config.minimal_bandwidth / 2,
)
print(search_upper, search_lower)
# get indices on raw data
start_idx = int((self.t0 - 5) * self.data.raw_rate)
start_idx = (self.t0 - 5) * self.data.raw_rate
window_duration = (self.dt + 10) * self.data.raw_rate
stop_idx = int(start_idx + window_duration)
if start_idx < 0:
start_idx = 0
stop_idx = start_idx + window_duration
# get raw data
try:
data_oi = self.data.raw[start_idx:stop_idx, self.electrode]
except:
embed()
data_oi = self.data.raw[start_idx:stop_idx, self.electrode]
self.time = self.time - self.t0
self.frequency_time = self.frequency_time - self.t0
if len(chirps) > 0:
chirps = np.asarray(chirps) - self.t0
chirps = np.asarray(chirps) - self.t0
self.t0_old = self.t0
self.t0 = 0
fig = plt.figure(figsize=(14 * ps.cm, 18 * ps.cm))
fig = plt.figure(
figsize=(14 / 2.54, 20 / 2.54)
)
gs0 = gr.GridSpec(3, 1, figure=fig, height_ratios=[1, 1, 1])
gs0 = gr.GridSpec(
3, 1, figure=fig, height_ratios=[1, 1, 1]
)
gs1 = gs0[0].subgridspec(1, 1)
gs2 = gs0[1].subgridspec(3, 1, hspace=0.4)
gs3 = gs0[2].subgridspec(3, 1, hspace=0.4)
@ -135,11 +130,11 @@ class ChirpPlotBuffer:
data_oi,
self.data.raw_rate,
self.t0 - 5,
[np.min(self.frequency) - 300, np.max(self.frequency) + 300],
[np.max(self.frequency) - 200, np.max(self.frequency) + 200]
)
ax0.set_ylim(np.min(self.frequency) - 100, np.max(self.frequency) + 200)
for track_id in self.data.ids:
t0_track = self.t0_old - 5
dt_track = self.dt + 10
window_idx = np.arange(len(self.data.idx))[
@ -150,163 +145,102 @@ class ChirpPlotBuffer:
# get tracked frequencies and their times
f = self.data.freq[window_idx]
# t = self.data.time[
# self.data.idx[self.data.ident == self.track_id]]
# tmask = (t >= t0_track) & (t <= (t0_track + dt_track))
t = self.data.time[self.data.idx[window_idx]]
t = self.data.time[
self.data.idx[self.data.ident == self.track_id]]
tmask = (t >= t0_track) & (t <= (t0_track + dt_track))
if track_id == self.track_id:
ax0.plot(t - self.t0_old, f, lw=lw, zorder=10, color=ps.gblue1)
ax0.plot(t[tmask]-self.t0_old, f, lw=lw,
zorder=10, color=ps.gblue1)
else:
ax0.plot(t - self.t0_old, f, lw=lw, zorder=10, color=ps.black)
# ax0.fill_between(
# np.arange(self.t0, self.t0 + self.dt, 1 / self.data.raw_rate),
# q50 - self.config.minimal_bandwidth / 2,
# q50 + self.config.minimal_bandwidth / 2,
# color=ps.gblue1,
# lw=1,
# ls="dashed",
# alpha=0.5,
# )
# ax0.fill_between(
# np.arange(self.t0, self.t0 + self.dt, 1 / self.data.raw_rate),
# search_lower,
# search_upper,
# color=ps.gblue2,
# lw=1,
# ls="dashed",
# alpha=0.5,
# )
ax0.axhline(
ax0.plot(t[tmask]-self.t0_old, f, lw=lw,
zorder=10, color=ps.gray, alpha=0.5)
ax0.fill_between(
np.arange(self.t0, self.t0 + self.dt, 1 / self.data.raw_rate),
q50 - self.config.minimal_bandwidth / 2,
q50 + self.config.minimal_bandwidth / 2,
color=ps.gblue1,
lw=1,
ls="dashed",
alpha=0.5,
)
ax0.axhline(
q50 + self.config.minimal_bandwidth / 2,
color=ps.gblue1,
ax0.fill_between(
np.arange(self.t0, self.t0 + self.dt, 1 / self.data.raw_rate),
search_lower,
search_upper,
color=ps.gblue2,
lw=1,
ls="dashed",
alpha=0.5,
)
ax0.axhline(search_lower, color=ps.gblue2, lw=1, ls="dashed")
ax0.axhline(search_upper, color=ps.gblue2, lw=1, ls="dashed")
# ax0.axhline(q50, spec_times[0], spec_times[-1],
# color=ps.gblue1, lw=2, ls="dashed")
# ax0.axhline(q50 + self.search_frequency,
# spec_times[0], spec_times[-1],
# color=ps.gblue2, lw=2, ls="dashed")
if len(chirps) > 0:
for chirp in chirps:
ax0.scatter(
chirp,
np.median(self.frequency),
c=ps.red,
marker=".",
edgecolors=ps.black,
facecolors=ps.red,
zorder=10,
s=70,
)
for chirp in chirps:
ax0.scatter(
chirp, np.median(self.frequency) + 150, c=ps.black, marker="v"
)
# plot waveform of filtered signal
ax1.plot(
self.time,
self.baseline * waveform_scaler,
c=ps.gray,
lw=lw,
alpha=0.5,
)
ax1.plot(
self.time,
self.baseline_envelope_unfiltered * waveform_scaler,
c=ps.gblue1,
lw=lw,
label="baseline envelope",
)
ax1.plot(self.time, self.baseline * waveform_scaler,
c=ps.gray, lw=lw, alpha=0.5)
ax1.plot(self.time, self.baseline_envelope_unfiltered *
waveform_scaler, c=ps.gblue1, lw=lw, label="baseline envelope")
# plot waveform of filtered search signal
ax2.plot(
self.time,
self.search * waveform_scaler,
c=ps.gray,
lw=lw,
alpha=0.5,
)
ax2.plot(
self.time,
self.search_envelope_unfiltered * waveform_scaler,
c=ps.gblue2,
lw=lw,
label="search envelope",
)
ax2.plot(self.time, self.search * waveform_scaler,
c=ps.gray, lw=lw, alpha=0.5)
ax2.plot(self.time, self.search_envelope_unfiltered *
waveform_scaler, c=ps.gblue2, lw=lw, label="search envelope")
# plot baseline instantaneous frequency
ax3.plot(
self.frequency_time,
self.frequency,
c=ps.gblue3,
lw=lw,
label="baseline inst. freq.",
)
ax3.plot(self.frequency_time, self.frequency,
c=ps.gblue3, lw=lw, label="baseline inst. freq.")
# plot filtered and rectified envelope
# ax4.plot(
# self.time, self.baseline_envelope * waveform_scaler, c=ps.gblue1, lw=lw
# )
ax4.plot(self.time, self.baseline_envelope, c=ps.gblue1, lw=lw)
ax4.scatter(
(self.time)[self.baseline_peaks],
# (self.baseline_envelope * waveform_scaler)[self.baseline_peaks],
(self.baseline_envelope)[self.baseline_peaks],
edgecolors=ps.black,
facecolors=ps.red,
self.baseline_envelope[self.baseline_peaks],
edgecolors=ps.red,
zorder=10,
marker=".",
s=70,
# facecolors="none",
marker="o",
facecolors="none",
)
# plot envelope of search signal
# ax5.plot(self.time, self.search_envelope * waveform_scaler, c=ps.gblue2, lw=lw)
ax5.plot(self.time, self.search_envelope, c=ps.gblue2, lw=lw)
ax5.scatter(
(self.time)[self.search_peaks],
# (self.search_envelope * waveform_scaler)[self.search_peaks],
(self.search_envelope)[self.search_peaks],
edgecolors=ps.black,
facecolors=ps.red,
self.search_envelope[self.search_peaks],
edgecolors=ps.red,
zorder=10,
marker=".",
s=70,
# facecolors="none",
marker="o",
facecolors="none",
)
# plot filtered instantaneous frequency
ax6.plot(self.frequency_time, self.frequency_filtered, c=ps.gblue3, lw=lw)
ax6.plot(self.frequency_time,
self.frequency_filtered, c=ps.gblue3, lw=lw)
ax6.scatter(
self.frequency_time[self.frequency_peaks],
self.frequency_filtered[self.frequency_peaks],
edgecolors=ps.black,
facecolors=ps.red,
edgecolors=ps.red,
zorder=10,
marker=".",
s=70,
# facecolors="none",
marker="o",
facecolors="none",
)
ax0.set_ylabel("Frequency [Hz]")
ax1.set_ylabel(r"$\mu$V")
ax2.set_ylabel(r"$\mu$V")
ax0.set_ylabel("frequency [Hz]")
ax1.set_ylabel("a.u.")
ax2.set_ylabel("a.u.")
ax3.set_ylabel("Hz")
ax4.set_ylabel(r"$\mu$V")
ax5.set_ylabel(r"$\mu$V")
ax6.set_ylabel("Hz")
ax6.set_xlabel("Time [s]")
ax5.set_ylabel("a.u.")
ax6.set_xlabel("time [s]")
plt.setp(ax0.get_xticklabels(), visible=False)
plt.setp(ax1.get_xticklabels(), visible=False)
@ -321,7 +255,8 @@ class ChirpPlotBuffer:
# ax7.spines.bottom.set_bounds((0, 5))
ax0.set_xlim(0, self.config.window)
plt.subplots_adjust(left=0.165, right=0.975, top=0.98, bottom=0.074, hspace=0.2)
plt.subplots_adjust(left=0.165, right=0.975,
top=0.98, bottom=0.074, hspace=0.2)
fig.align_labels()
if plot == "show":
@ -332,9 +267,8 @@ class ChirpPlotBuffer:
self.config.outputdir + self.data.datapath.split("/")[-2] + "/"
)
# plt.savefig(f"{out}{self.track_id}_{self.t0_old}.pdf")
# plt.savefig(f"{out}{self.track_id}_{self.t0_old}.svg")
plt.savefig(f"{out}{self.track_id}_{self.t0_old}.png")
plt.savefig(f"{out}{self.track_id}_{self.t0_old}.pdf")
plt.savefig(f"{out}{self.track_id}_{self.t0_old}.svg")
plt.close()
@ -343,7 +277,7 @@ def plot_spectrogram(
signal: np.ndarray,
samplerate: float,
window_start_seconds: float,
ylims: list[float],
ylims: list[float]
) -> np.ndarray:
"""
Plot a spectrogram of a signal.
@ -384,7 +318,7 @@ def plot_spectrogram(
aspect="auto",
origin="lower",
interpolation="gaussian",
# alpha=0.6,
alpha=1,
)
# axis.use_sticky_edges = False
return spec_times
@ -427,7 +361,9 @@ def extract_frequency_bands(
q25, q75 = q50 - minimal_bandwidth / 2, q50 + minimal_bandwidth / 2
# filter baseline
filtered_baseline = bandpass_filter(raw_data, samplerate, lowf=q25, highf=q75)
filtered_baseline = bandpass_filter(
raw_data, samplerate, lowf=q25, highf=q75
)
# filter search area
filtered_search_freq = bandpass_filter(
@ -472,17 +408,20 @@ def window_median_all_track_ids(
track_ids = []
for _, track_id in enumerate(np.unique(data.ident[~np.isnan(data.ident)])):
# the window index combines the track id and the time window
window_idx = np.arange(len(data.idx))[
(data.ident == track_id)
& (data.time[data.idx] >= window_start_seconds)
& (data.time[data.idx] <= (window_start_seconds + window_duration_seconds))
& (
data.time[data.idx]
<= (window_start_seconds + window_duration_seconds)
)
]
if len(data.freq[window_idx]) > 0:
frequency_percentiles.append(
np.percentile(data.freq[window_idx], [25, 50, 75])
)
np.percentile(data.freq[window_idx], [25, 50, 75]))
track_ids.append(track_id)
# convert to numpy array
@ -492,71 +431,6 @@ def window_median_all_track_ids(
return frequency_percentiles, track_ids
def array_center(array: np.ndarray) -> float:
"""
Return the center value of an array.
If the array length is even, returns
the mean of the two center values.
Parameters
----------
array : np.ndarray
Array to calculate the center from.
Returns
-------
float
"""
if len(array) % 2 == 0:
return np.mean(array[int(len(array) / 2) - 1 : int(len(array) / 2) + 1])
else:
return array[int(len(array) / 2)]
def has_chirp(baseline_frequency: np.ndarray, peak_height: float) -> bool:
"""
Check if a fish has a chirp.
Parameters
----------
baseline_frequency : np.ndarray
Baseline frequency of the fish.
peak_height : float
Minimal peak height of a chirp on the instant. freq.
Returns
-------
bool: True if the fish has a chirp, False otherwise.
"""
peaks, _ = find_peaks(baseline_frequency, height=peak_height)
if len(peaks) > 0:
return True
else:
return False
def mask_low_amplitudes(envelope, threshold):
"""
Mask low amplitudes in the envelope.
Parameters
----------
envelope : np.ndarray
Envelope of the signal.
threshold : float
Threshold to mask low amplitudes.
Returns
-------
np.ndarray
"""
mask = np.ones_like(envelope, dtype=bool)
mask[envelope < threshold] = False
return mask
def find_searchband(
current_frequency: np.ndarray,
percentiles_ids: np.ndarray,
@ -590,34 +464,39 @@ def find_searchband(
# frequency window where second filter filters is potentially allowed
# to filter. This is the search window, in which we want to find
# a gap in the other fish's EODs.
current_median = np.median(current_frequency)
search_window = np.arange(
current_median + config.search_df_lower,
current_median + config.search_df_upper,
np.median(current_frequency) + config.search_df_lower,
np.median(current_frequency) + config.search_df_upper,
config.search_res,
)
# search window in boolean
bool_lower = np.ones_like(search_window, dtype=bool)
bool_upper = np.ones_like(search_window, dtype=bool)
search_window_bool = np.ones_like(search_window, dtype=bool)
search_window_bool = np.ones_like(len(search_window), dtype=bool)
# make seperate arrays from the qartiles
q25 = np.asarray([i[0] for i in frequency_percentiles])
q75 = np.asarray([i[2] for i in frequency_percentiles])
# get tracks that fall into search window
check_track_ids = percentiles_ids[(q25 > current_median) & (q75 < search_window[-1])]
check_track_ids = percentiles_ids[
(q25 > search_window[0]) & (
q75 < search_window[-1])
]
# iterate through theses tracks
if check_track_ids.size != 0:
for j, check_track_id in enumerate(check_track_ids):
q25_temp = q25[percentiles_ids == check_track_id]
q75_temp = q75[percentiles_ids == check_track_id]
bool_lower[search_window > q25_temp - config.search_res] = False
bool_upper[search_window < q75_temp + config.search_res] = False
search_window_bool[(bool_lower == False) & (bool_upper == False)] = False
print(q25_temp, q75_temp)
search_window_bool[
(search_window > q25_temp) & (search_window < q75_temp)
] = False
# find gaps in search window
search_window_indices = np.arange(len(search_window))
@ -630,13 +509,12 @@ def find_searchband(
nonzeros = search_window_gaps[np.nonzero(search_window_gaps)[0]]
nonzeros = nonzeros[~np.isnan(nonzeros)]
if len(nonzeros) == 0:
return config.default_search_freq
# if the first value is -1, the array starst with true, so a gap
if nonzeros[0] == -1:
stops = search_window_indices[search_window_gaps == -1]
starts = np.append(0, search_window_indices[search_window_gaps == 1])
starts = np.append(
0, search_window_indices[search_window_gaps == 1]
)
# if the last value is -1, the array ends with true, so a gap
if nonzeros[-1] == 1:
@ -665,31 +543,24 @@ def find_searchband(
# the center of the search frequency band is then the center of
# the longest gap
search_freq = array_center(longest_search_window) - current_median
search_freq = (
longest_search_window[-1] - longest_search_window[0]
) / 2
return search_freq
return config.default_search_freq
def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
def main(datapath: str, plot: str) -> None:
assert plot in [
"save",
"show",
"false",
], "plot must be 'save', 'show' or 'false'"
assert debug in [
"false",
"electrode",
"fish",
], "debug must be 'false', 'electrode' or 'fish'"
if debug != "false":
assert plot == "show", "debug mode only runs when plot is 'show'"
# load raw file
print("datapath", datapath)
data = LoadData(datapath)
# load config file
@ -718,7 +589,7 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
raw_time = np.arange(data.raw.shape[0]) / data.raw_rate
# good chirp times for data: 2022-06-02-10_00
# window_start_index = (3 * 60 * 60 + 6 * 60 + 43.5) * data.raw_rate
# window_start_index = (3 * 60 * 60 + 6 * 60 + 43.5 + 5) * data.raw_rate
# window_duration_index = 60 * data.raw_rate
# t0 = 0
@ -742,7 +613,8 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
multiwindow_ids = []
for st, window_start_index in enumerate(window_start_indices):
logger.info(f"Processing window {st} of {len(window_start_indices)}")
logger.info(f"Processing window {st+1} of {len(window_start_indices)}")
window_start_seconds = window_start_index / data.raw_rate
window_duration_seconds = window_duration / data.raw_rate
@ -756,7 +628,10 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
)
# iterate through all fish
for tr, track_id in enumerate(np.unique(data.ident[~np.isnan(data.ident)])):
for tr, track_id in enumerate(
np.unique(data.ident[~np.isnan(data.ident)])
):
logger.debug(f"Processing track {tr} of {len(data.ids)}")
# get index of track data in this time window
@ -773,25 +648,37 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
current_frequencies = data.freq[track_window_index]
current_powers = data.powers[track_window_index, :]
# approximate sampling rate to compute expected durations if there
# is data available for this time window for this fish id
track_samplerate = np.mean(1 / np.diff(data.time))
expected_duration = (
(window_start_seconds + window_duration_seconds)
- window_start_seconds
) * track_samplerate
# check if tracked data available in this window
if len(current_frequencies) < 3:
logger.warning(f"Track {track_id} has no data in window {st}, skipping.")
if len(current_frequencies) < expected_duration / 2:
logger.warning(
f"Track {track_id} has no data in window {st}, skipping."
)
continue
# check if there are powers available in this window
nanchecker = np.unique(np.isnan(current_powers))
if (len(nanchecker) == 1) and nanchecker[0] is True:
logger.warning(
f"No powers available for track {track_id} window {st}," "skipping."
f"No powers available for track {track_id} window {st},"
"skipping."
)
continue
# find the strongest electrodes for the current fish in the current
# window
best_electrode_index = np.argsort(np.nanmean(current_powers, axis=0))[
-config.number_electrodes :
]
best_electrode_index = np.argsort(
np.nanmean(current_powers, axis=0)
)[-config.number_electrodes:]
# find a frequency above the baseline of the current fish in which
# no other fish is active to search for chirps there
@ -811,8 +698,10 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
# iterate through electrodes
for el, electrode_index in enumerate(best_electrode_index):
logger.debug(
f"Processing electrode {el+1} of " f"{len(best_electrode_index)}"
f"Processing electrode {el+1} of "
f"{len(best_electrode_index)}"
)
# LOAD DATA FOR CURRENT ELECTRODE AND CURRENT FISH ------------
@ -821,7 +710,9 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
current_raw_data = data.raw[
window_start_index:window_stop_index, electrode_index
]
current_raw_time = raw_time[window_start_index:window_stop_index]
current_raw_time = raw_time[
window_start_index:window_stop_index
]
# EXTRACT FEATURES --------------------------------------------
@ -843,13 +734,6 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
cutoff_frequency=config.baseline_envelope_cutoff,
)
# create a mask that removes areas where amplitudes are very
# because the instantaneous frequency is not reliable there
amplitude_mask = mask_low_amplitudes(
baseline_envelope_unfiltered, config.baseline_min_amplitude
)
# highpass filter baseline envelope to remove slower
# fluctuations e.g. due to motion envelope
@ -860,10 +744,18 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
highf=config.baseline_envelope_bandpass_highf,
)
# invert baseline envelope to find troughs in the baseline
# highbass filter introduced filter effects, i.e. oscillations
# around peaks. Compute the envelope of the highpass filtered
# and inverted baseline envelope to remove these oscillations
baseline_envelope = -baseline_envelope
baseline_envelope = envelope(
signal=baseline_envelope,
samplerate=data.raw_rate,
cutoff_frequency=config.baseline_envelope_envelope_cutoff,
)
# compute the envelope of the search band. Peaks in the search
# band envelope correspond to troughs in the baseline envelope
# during chirps
@ -873,7 +765,6 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
samplerate=data.raw_rate,
cutoff_frequency=config.search_envelope_cutoff,
)
search_envelope = search_envelope_unfiltered
# compute instantaneous frequency of the baseline band to find
@ -883,34 +774,39 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
# chirp. This phenomenon is only observed on chirps on a narrow
# filtered baseline such as the one we are working with.
baseline_frequency = instantaneous_frequency(
baselineband,
data.raw_rate,
config.baseline_frequency_smoothing,
(
baseline_frequency_time,
baseline_frequency,
) = instantaneous_frequency(
signal=baselineband,
samplerate=data.raw_rate,
smoothing_window=config.baseline_frequency_smoothing,
)
# Take the absolute of the instantaneous frequency to invert
# troughs into peaks. This is nessecary since the narrow
# pass band introduces these anomalies. Also substract by the
# median to set it to 0.
# bandpass filter the instantaneous frequency to remove slow
# fluctuations. Just as with the baseline envelope, we then
# compute the envelope of the signal to remove the oscillations
# around the peaks
baseline_frequency_samplerate = np.mean(
np.diff(baseline_frequency_time)
)
baseline_frequency_filtered = np.abs(
baseline_frequency - np.median(baseline_frequency)
)
# # check if there is at least one superthreshold peak on the
# # instantaneous and exit the loop if not. This is used to
# # prevent windows that do definetely not include a chirp
# # to enter normalization, where small changes due to noise
# # would be amplified
baseline_frequency_filtered = highpass_filter(
signal=baseline_frequency_filtered,
samplerate=baseline_frequency_samplerate,
cutoff=config.baseline_frequency_highpass_cutoff,
)
# if not has_chirp(
# baseline_frequency_filtered[amplitude_mask],
# config.baseline_frequency_peakheight,
# ):
# logger.warning(
# f"Amplitude to small for the chirp detection of track {track_id} window {st},")
# continue
baseline_frequency_filtered = envelope(
signal=-baseline_frequency_filtered,
samplerate=baseline_frequency_samplerate,
cutoff_frequency=config.baseline_frequency_envelope_cutoff,
)
# CUT OFF OVERLAP ---------------------------------------------
@ -921,7 +817,6 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
no_edges = np.arange(
int(window_edge), len(baseline_envelope) - int(window_edge)
)
current_raw_time = current_raw_time[no_edges]
baselineband = baselineband[no_edges]
baseline_envelope_unfiltered = baseline_envelope_unfiltered[no_edges]
@ -930,57 +825,57 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
search_envelope_unfiltered = search_envelope_unfiltered[no_edges]
search_envelope = search_envelope[no_edges]
# get instantaneous frequency withoup edges
no_edges_t0 = int(window_edge) / data.raw_rate
no_edges_t1 = baseline_frequency_time[-1] - (
int(window_edge) / data.raw_rate
)
no_edges = (baseline_frequency_time >= no_edges_t0) & (
baseline_frequency_time <= no_edges_t1
)
baseline_frequency_filtered = baseline_frequency_filtered[
no_edges
]
baseline_frequency = baseline_frequency[no_edges]
baseline_frequency_filtered = baseline_frequency_filtered[no_edges]
baseline_frequency_time = current_raw_time
# # get instantaneous frequency withoup edges
# no_edges_t0 = int(window_edge) / data.raw_rate
# no_edges_t1 = baseline_frequency_time[-1] - (
# int(window_edge) / data.raw_rate
# )
# no_edges = (baseline_frequency_time >= no_edges_t0) & (
# baseline_frequency_time <= no_edges_t1
# )
# baseline_frequency_filtered = baseline_frequency_filtered[no_edges]
# baseline_frequency = baseline_frequency[no_edges]
# baseline_frequency_time = (
# baseline_frequency_time[no_edges] + window_start_seconds
# )
baseline_frequency_time = (
baseline_frequency_time[no_edges] + window_start_seconds
)
# NORMALIZE ---------------------------------------------------
# normalize all three feature arrays to the same range to make
# peak detection simpler
# baseline_envelope = minmaxnorm([baseline_envelope])[0]
# search_envelope = minmaxnorm([search_envelope])[0]
# baseline_frequency_filtered = minmaxnorm(
# [baseline_frequency_filtered]
# )[0]
baseline_envelope = normalize([baseline_envelope])[0]
search_envelope = normalize([search_envelope])[0]
baseline_frequency_filtered = normalize(
[baseline_frequency_filtered]
)[0]
# PEAK DETECTION ----------------------------------------------
# detect peaks baseline_enelope
baseline_peak_indices, _ = find_peaks(
baseline_envelope, prominence=config.baseline_prominence
baseline_envelope, prominence=config.prominence
)
# detect peaks search_envelope
search_peak_indices, _ = find_peaks(
search_envelope, prominence=config.search_prominence
search_envelope, prominence=config.prominence
)
# detect peaks inst_freq_filtered
frequency_peak_indices, _ = find_peaks(
baseline_frequency_filtered,
prominence=config.frequency_prominence,
baseline_frequency_filtered, prominence=config.prominence
)
# DETECT CHIRPS IN SEARCH WINDOW ------------------------------
# get the peak timestamps from the peak indices
baseline_peak_timestamps = current_raw_time[baseline_peak_indices]
search_peak_timestamps = current_raw_time[search_peak_indices]
baseline_peak_timestamps = current_raw_time[
baseline_peak_indices
]
search_peak_timestamps = current_raw_time[
search_peak_indices]
frequency_peak_timestamps = baseline_frequency_time[
frequency_peak_indices
@ -988,13 +883,14 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
# check if one list is empty and if so, skip to the next
# electrode because a chirp cannot be detected if one is empty
one_feature_empty = (
len(baseline_peak_timestamps) == 0
or len(search_peak_timestamps) == 0
# or len(frequency_peak_timestamps) == 0
or len(frequency_peak_timestamps) == 0
)
if one_feature_empty and (debug == "false"):
if one_feature_empty:
continue
# group peak across feature arrays but only if they
@ -1003,34 +899,37 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
sublists = [
list(baseline_peak_timestamps),
list(search_peak_timestamps),
# list(frequency_peak_timestamps),
list(frequency_peak_timestamps),
]
singleelectrode_chirps = group_timestamps(
sublists=sublists,
at_least_in=2,
at_least_in=3,
difference_threshold=config.chirp_window_threshold,
)
# check it there are chirps detected after grouping, continue
# with the loop if not
if (len(singleelectrode_chirps) == 0) and (debug == "false"):
if len(singleelectrode_chirps) == 0:
continue
# append chirps from this electrode to the multilectrode list
multielectrode_chirps.append(singleelectrode_chirps)
# only initialize the plotting buffer if chirps are detected
chirp_detected = el == (config.number_electrodes - 1) & (
plot in ["show", "save"]
chirp_detected = (
(el == config.number_electrodes - 1)
& (len(singleelectrode_chirps) > 0)
& (plot in ["show", "save"])
)
if chirp_detected or (debug != "elecrode"):
if chirp_detected:
logger.debug("Detected chirp, ititialize buffer ...")
# save data to Buffer
buffer = ChirpPlotBuffer(
buffer = PlotBuffer(
config=config,
t0=window_start_seconds,
dt=window_duration_seconds,
@ -1055,10 +954,6 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
logger.debug("Buffer initialized!")
if debug == "electrode":
logger.info(f"Plotting electrode {el} ...")
buffer.plot_buffer(chirps=singleelectrode_chirps, plot=plot)
logger.debug(
f"Processed all electrodes for fish {track_id} for this"
"window, sorting chirps ..."
@ -1067,7 +962,7 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
# check if there are chirps detected in multiple electrodes and
# continue the loop if not
if (len(multielectrode_chirps) == 0) and (debug == "false"):
if len(multielectrode_chirps) == 0:
continue
# validate multielectrode chirps, i.e. check if they are
@ -1092,18 +987,9 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
# if chirps are detected and the plot flag is set, plot the
# chirps, otheswise try to delete the buffer if it exists
if debug == "fish":
logger.info(f"Plotting fish {track_id} ...")
buffer.plot_buffer(multielectrode_chirps_validated, plot)
if (
(len(multielectrode_chirps_validated) > 0)
& (plot in ["show", "save"])
& (debug == "false")
):
if len(multielectrode_chirps_validated) > 0:
try:
buffer.plot_buffer(multielectrode_chirps_validated, plot)
del buffer
except NameError:
pass
else:
@ -1118,6 +1004,7 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
multiwindow_chirps_flat = []
multiwindow_ids_flat = []
for track_id in np.unique(multiwindow_ids):
# get chirps for this fish and flatten the list
current_track_bool = np.asarray(multiwindow_ids) == track_id
current_track_chirps = flatten(
@ -1126,7 +1013,9 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
# add flattened chirps to the list
multiwindow_chirps_flat.extend(current_track_chirps)
multiwindow_ids_flat.extend(list(np.ones_like(current_track_chirps) * track_id))
multiwindow_ids_flat.extend(
list(np.ones_like(current_track_chirps) * track_id)
)
# purge duplicates, i.e. chirps that are very close to each other
# duplites arise due to overlapping windows
@ -1138,7 +1027,9 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
np.asarray(multiwindow_ids_flat) == track_id
]
if len(tr_chirps) > 0:
tr_chirps_purged = purge_duplicates(tr_chirps, config.chirp_window_threshold)
tr_chirps_purged = purge_duplicates(
tr_chirps, config.chirp_window_threshold
)
purged_chirps.extend(list(tr_chirps_purged))
purged_ids.extend(list(np.ones_like(tr_chirps_purged) * track_id))
@ -1149,14 +1040,13 @@ def chirpdetection(datapath: str, plot: str, debug: str = "false") -> None:
purged_chirps = purged_chirps[np.argsort(purged_chirps)]
# save them into the data directory
np.save(datapath + "chirp_times_gp.npy", purged_chirps)
np.save(datapath + "chirp_ids_gp.npy", purged_ids)
np.save(datapath + "chirps.npy", purged_chirps)
np.save(datapath + "chirp_ids.npy", purged_ids)
if __name__ == "__main__":
# datapath = "/home/weygoldt/Data/uni/chirpdetection/GP2023_chirp_detection/data/mount_data/2020-05-13-10_00/"
datapath = "../data/2022-06-02-10_00/"
# datapath = "/home/weygoldt/Data/uni/efishdata/2016-colombia/fishgrid/2016-04-09-22_25/"
# datapath = "/home/weygoldt/Data/uni/chirpdetection/GP2023_chirp_detection/data/mount_data/2020-03-13-10_00/"
datapath = "../data/2022-06-02-10_00/"
datapath = "../../../local_data/randgrid/"
chirpdetection(datapath, plot="save", debug="false")
main(datapath, plot="save")

View File

@ -1,46 +1,47 @@
# Path setup ------------------------------------------------------------------
# directory setup
dataroot: "../data/"
outputdir: "../output/"
dataroot: "../data/" # path to data
outputdir: "../output/" # path to save plots to
# Duration and overlap of the analysis window in seconds
window: 10
overlap: 1
edge: 0.25
# Rolling window parameters ---------------------------------------------------
# Number of electrodes to go over
number_electrodes: 3
minimum_electrodes: 2
window: 5 # rolling window length in seconds
overlap: 1 # window overlap in seconds
edge: 0.25 # window edge cufoffs to mitigate filter edge effects
# Search window bandwidth and minimal baseline bandwidth
minimal_bandwidth: 20
# Electrode iteration parameters ----------------------------------------------
# Instantaneous frequency smoothing usint a gaussian kernel of this width
baseline_frequency_smoothing: 5
number_electrodes: 2 # number of electrodes to go over
minimum_electrodes: 1 # mimumun number of electrodes a chirp must be on
# Baseline processing parameters
baseline_envelope_cutoff: 25
baseline_envelope_bandpass_lowf: 4
baseline_envelope_bandpass_highf: 100
baseline_envelope_envelope_cutoff: 4
# Feature extraction parameters -----------------------------------------------
# search envelope processing parameters
search_envelope_cutoff: 5
search_df_lower: 20 # start searching this far above the baseline
search_df_upper: 100 # stop searching this far above the baseline
search_res: 1 # search window resolution
default_search_freq: 60 # search here if no need for a search frequency
minimal_bandwidth: 10 # minimal bandpass filter width for baseline
search_bandwidth: 10 # minimal bandpass filter width for search frequency
baseline_frequency_smoothing: 3 # instantaneous frequency smoothing
# Instantaneous frequency bandpass filter cutoff frequencies
baseline_frequency_highpass_cutoff: 0.000005
baseline_frequency_envelope_cutoff: 0.000005
# Feature processing parameters -----------------------------------------------
# peak detecion parameters
prominence: 0.005
baseline_frequency_peakheight: 5 # the min peak height of the baseline instfreq
baseline_min_amplitude: 0.0001 # the minimal value of the baseline envelope
baseline_envelope_cutoff: 25 # envelope estimation cutoff
baseline_envelope_bandpass_lowf: 2 # envelope badpass lower cutoff
baseline_envelope_bandpass_highf: 100 # envelope bandbass higher cutoff
search_envelope_cutoff: 10 # search envelope estimation cufoff
# search freq parameter
search_df_lower: 20
search_df_upper: 100
search_res: 1
search_bandwidth: 10
default_search_freq: 50
# Classify events as chirps if they are less than this time apart
chirp_window_threshold: 0.05
# Peak detecion parameters ----------------------------------------------------
# baseline_prominence: 0.00005 # peak prominence threshold for baseline envelope
# search_prominence: 0.000004 # peak prominence threshold for search envelope
# frequency_prominence: 2 # peak prominence threshold for baseline freq
baseline_prominence: 0.00005 # peak prominence threshold for baseline envelope
search_prominence: 0.000005 # peak prominence threshold for search envelope
frequency_prominence: 1 # peak prominence threshold for baseline freq
# Classify events as chirps if they are less than this time apart
chirp_window_threshold: 0.02

View File

@ -1,669 +0,0 @@
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from tqdm import tqdm
from IPython import embed
from pandas import read_csv
from modules.logger import makeLogger
from modules.plotstyle import PlotStyle
from modules.datahandling import causal_kde1d, acausal_kde1d, flatten
logger = makeLogger(__name__)
ps = PlotStyle()
class Behavior:
"""Load behavior data from csv file as class attributes
Attributes
----------
behavior: 0: chasing onset, 1: chasing offset, 2: physical contact
behavior_type:
behavioral_category:
comment_start:
comment_stop:
dataframe: pandas dataframe with all the data
duration_s:
media_file:
observation_date:
observation_id:
start_s: start time of the event in seconds
stop_s: stop time of the event in seconds
total_length:
"""
def __init__(self, folder_path: str) -> None:
print(f"{folder_path}")
LED_on_time_BORIS = np.load(
os.path.join(folder_path, "LED_on_time.npy"), allow_pickle=True
)
self.time = np.load(
os.path.join(folder_path, "times.npy"), allow_pickle=True
)
csv_filename = [
f for f in os.listdir(folder_path) if f.endswith(".csv")
][
0
] # check if there are more than one csv file
self.dataframe = read_csv(os.path.join(folder_path, csv_filename))
self.chirps = np.load(
os.path.join(folder_path, "chirps.npy"), allow_pickle=True
)
self.chirps_ids = np.load(
os.path.join(folder_path, "chirp_ids.npy"), allow_pickle=True
)
for k, key in enumerate(self.dataframe.keys()):
key = key.lower()
if " " in key:
key = key.replace(" ", "_")
if "(" in key:
key = key.replace("(", "")
key = key.replace(")", "")
setattr(
self, key, np.array(self.dataframe[self.dataframe.keys()[k]])
)
last_LED_t_BORIS = LED_on_time_BORIS[-1]
real_time_range = self.time[-1] - self.time[0]
factor = 1.034141
shift = last_LED_t_BORIS - real_time_range * factor
self.start_s = (self.start_s - shift) / factor
self.stop_s = (self.stop_s - shift) / factor
"""
1 - chasing onset
2 - chasing offset
3 - physical contact event
temporal encpding needs to be corrected ... not exactly 25FPS.
### correspinding python code ###
factor = 1.034141
LED_on_time_BORIS = np.load(os.path.join(folder_path, 'LED_on_time.npy'), allow_pickle=True)
last_LED_t_BORIS = LED_on_time_BORIS[-1]
real_time_range = times[-1] - times[0]
shift = last_LED_t_BORIS - real_time_range * factor
data = pd.read_csv(os.path.join(folder_path, file[1:-7] + '.csv'))
boris_times = data['Start (s)']
data_times = []
for Cevent_t in boris_times:
Cevent_boris_times = (Cevent_t - shift) / factor
data_times.append(Cevent_boris_times)
data_times = np.array(data_times)
behavior = data['Behavior']
"""
def correct_chasing_events(
category: np.ndarray, timestamps: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
onset_ids = np.arange(len(category))[category == 0]
offset_ids = np.arange(len(category))[category == 1]
wrong_bh = np.arange(len(category))[category != 2][:-1][
np.diff(category[category != 2]) == 0
]
if onset_ids[0] > offset_ids[0]:
offset_ids = np.delete(offset_ids, 0)
help_index = offset_ids[0]
wrong_bh = np.append(wrong_bh[help_index])
category = np.delete(category, wrong_bh)
timestamps = np.delete(timestamps, wrong_bh)
# Check whether on- or offset is longer and calculate length difference
if len(onset_ids) > len(offset_ids):
len_diff = len(onset_ids) - len(offset_ids)
logger.info(f"Onsets are greater than offsets by {len_diff}")
elif len(onset_ids) < len(offset_ids):
len_diff = len(offset_ids) - len(onset_ids)
logger.info(f"Offsets are greater than onsets by {len_diff}")
elif len(onset_ids) == len(offset_ids):
logger.info("Chasing events are equal")
return category, timestamps
def event_triggered_chirps(
event: np.ndarray,
chirps: np.ndarray,
time_before_event: int,
time_after_event: int,
dt: float,
width: float,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
event_chirps = [] # chirps that are in specified window around event
# timestamps of chirps around event centered on the event timepoint
centered_chirps = []
for event_timestamp in event:
start = event_timestamp - time_before_event
stop = event_timestamp + time_after_event
chirps_around_event = [c for c in chirps if (c >= start) & (c <= stop)]
event_chirps.append(chirps_around_event)
if len(chirps_around_event) == 0:
continue
else:
centered_chirps.append(chirps_around_event - event_timestamp)
time = np.arange(-time_before_event, time_after_event, dt)
# Kernel density estimation with some if's
if len(centered_chirps) == 0:
centered_chirps = np.array([])
centered_chirps_convolved = np.zeros(len(time))
else:
# convert list of arrays to one array for plotting
centered_chirps = np.concatenate(centered_chirps, axis=0)
centered_chirps_convolved = (
acausal_kde1d(centered_chirps, time, width)
) / len(event)
return event_chirps, centered_chirps, centered_chirps_convolved
def main(datapath: str):
foldernames = [
datapath + x + "/"
for x in os.listdir(datapath)
if os.path.isdir(datapath + x)
]
nrecording_chirps = []
nrecording_chirps_fish_ids = []
nrecording_chasing_onsets = []
nrecording_chasing_offsets = []
nrecording_physicals = []
# Iterate over all recordings and save chirp- and event-timestamps
for folder in foldernames:
# exclude folder with empty LED_on_time.npy
if folder == "../data/mount_data/2020-05-12-10_00/":
continue
bh = Behavior(folder)
# Chirps are already sorted
category = bh.behavior
timestamps = bh.start_s
chirps = bh.chirps
nrecording_chirps.append(chirps)
chirps_fish_ids = bh.chirps_ids
nrecording_chirps_fish_ids.append(chirps_fish_ids)
fish_ids = np.unique(chirps_fish_ids)
# Correct for doubles in chasing on- and offsets to get the right on-/offset pairs
# Get rid of tracking faults (two onsets or two offsets after another)
category, timestamps = correct_chasing_events(category, timestamps)
# Split categories
chasing_onsets = timestamps[category == 0]
nrecording_chasing_onsets.append(chasing_onsets)
chasing_offsets = timestamps[category == 1]
nrecording_chasing_offsets.append(chasing_offsets)
physical_contacts = timestamps[category == 2]
nrecording_physicals.append(physical_contacts)
# Define time window for chirps around event analysis
time_before_event = 30
time_after_event = 60
dt = 0.01
width = 1.5 # width of kernel for all recordings, currently gaussian kernel
recording_width = 2 # width of kernel for each recording
time = np.arange(-time_before_event, time_after_event, dt)
##### Chirps around events, all fish, all recordings #####
# Centered chirps per event type
nrecording_centered_onset_chirps = []
nrecording_centered_offset_chirps = []
nrecording_centered_physical_chirps = []
# Bootstrapped chirps per recording and per event: 27[1000[n]] 27 recs, 1000 shuffles, n chirps
nrecording_shuffled_convolved_onset_chirps = []
nrecording_shuffled_convolved_offset_chirps = []
nrecording_shuffled_convolved_physical_chirps = []
nbootstrapping = 100
for i in range(len(nrecording_chirps)):
chirps = nrecording_chirps[i]
chasing_onsets = nrecording_chasing_onsets[i]
chasing_offsets = nrecording_chasing_offsets[i]
physical_contacts = nrecording_physicals[i]
# Chirps around chasing onsets
(
_,
centered_chasing_onset_chirps,
cc_chasing_onset_chirps,
) = event_triggered_chirps(
chasing_onsets,
chirps,
time_before_event,
time_after_event,
dt,
recording_width,
)
# Chirps around chasing offsets
(
_,
centered_chasing_offset_chirps,
cc_chasing_offset_chirps,
) = event_triggered_chirps(
chasing_offsets,
chirps,
time_before_event,
time_after_event,
dt,
recording_width,
)
# Chirps around physical contacts
(
_,
centered_physical_chirps,
cc_physical_chirps,
) = event_triggered_chirps(
physical_contacts,
chirps,
time_before_event,
time_after_event,
dt,
recording_width,
)
nrecording_centered_onset_chirps.append(centered_chasing_onset_chirps)
nrecording_centered_offset_chirps.append(centered_chasing_offset_chirps)
nrecording_centered_physical_chirps.append(centered_physical_chirps)
## Shuffled chirps ##
nshuffled_onset_chirps = []
nshuffled_offset_chirps = []
nshuffled_physical_chirps = []
# for j in tqdm(range(nbootstrapping)):
# # Calculate interchirp intervals; add first chirp timestamp in beginning to get equal lengths
# interchirp_intervals = np.append(np.array([chirps[0]]), np.diff(chirps))
# np.random.shuffle(interchirp_intervals)
# shuffled_chirps = np.cumsum(interchirp_intervals)
# # Shuffled chasing onset chirps
# _, _, cc_shuffled_onset_chirps = event_triggered_chirps(chasing_onsets, shuffled_chirps, time_before_event, time_after_event, dt, recording_width)
# nshuffled_onset_chirps.append(cc_shuffled_onset_chirps)
# # Shuffled chasing offset chirps
# _, _, cc_shuffled_offset_chirps = event_triggered_chirps(chasing_offsets, shuffled_chirps, time_before_event, time_after_event, dt, recording_width)
# nshuffled_offset_chirps.append(cc_shuffled_offset_chirps)
# # Shuffled physical contact chirps
# _, _, cc_shuffled_physical_chirps = event_triggered_chirps(physical_contacts, shuffled_chirps, time_before_event, time_after_event, dt, recording_width)
# nshuffled_physical_chirps.append(cc_shuffled_physical_chirps)
# rec_shuffled_q5_onset, rec_shuffled_median_onset, rec_shuffled_q95_onset = np.percentile(
# nshuffled_onset_chirps, (5, 50, 95), axis=0)
# rec_shuffled_q5_offset, rec_shuffled_median_offset, rec_shuffled_q95_offset = np.percentile(
# nshuffled_offset_chirps, (5, 50, 95), axis=0)
# rec_shuffled_q5_physical, rec_shuffled_median_physical, rec_shuffled_q95_physical = np.percentile(
# nshuffled_physical_chirps, (5, 50, 95), axis=0)
# #### Recording plots ####
# fig, ax = plt.subplots(1, 3, figsize=(28*ps.cm, 16*ps.cm, ), constrained_layout=True, sharey='all')
# ax[0].set_xlabel('Time[s]')
# # Plot chasing onsets
# ax[0].set_ylabel('Chirp rate [Hz]')
# ax[0].plot(time, cc_chasing_onset_chirps, color=ps.yellow, zorder=2)
# ax0 = ax[0].twinx()
# ax0.eventplot(centered_chasing_onset_chirps, linelengths=0.2, colors=ps.gray, alpha=0.25, zorder=1)
# ax0.vlines(0, 0, 1.5, ps.white, 'dashed')
# ax[0].set_zorder(ax0.get_zorder()+1)
# ax[0].patch.set_visible(False)
# ax0.set_yticklabels([])
# ax0.set_yticks([])
# ######## median - q5, median + q95
# ax[0].fill_between(time, rec_shuffled_q5_onset, rec_shuffled_q95_onset, color=ps.gray, alpha=0.5)
# ax[0].plot(time, rec_shuffled_median_onset, color=ps.black)
# # Plot chasing offets
# ax[1].set_xlabel('Time[s]')
# ax[1].plot(time, cc_chasing_offset_chirps, color=ps.orange, zorder=2)
# ax1 = ax[1].twinx()
# ax1.eventplot(centered_chasing_offset_chirps, linelengths=0.2, colors=ps.gray, alpha=0.25, zorder=1)
# ax1.vlines(0, 0, 1.5, ps.white, 'dashed')
# ax[1].set_zorder(ax1.get_zorder()+1)
# ax[1].patch.set_visible(False)
# ax1.set_yticklabels([])
# ax1.set_yticks([])
# ax[1].fill_between(time, rec_shuffled_q5_offset, rec_shuffled_q95_offset, color=ps.gray, alpha=0.5)
# ax[1].plot(time, rec_shuffled_median_offset, color=ps.black)
# # Plot physical contacts
# ax[2].set_xlabel('Time[s]')
# ax[2].plot(time, cc_physical_chirps, color=ps.maroon, zorder=2)
# ax2 = ax[2].twinx()
# ax2.eventplot(centered_physical_chirps, linelengths=0.2, colors=ps.gray, alpha=0.25, zorder=1)
# ax2.vlines(0, 0, 1.5, ps.white, 'dashed')
# ax[2].set_zorder(ax2.get_zorder()+1)
# ax[2].patch.set_visible(False)
# ax2.set_yticklabels([])
# ax2.set_yticks([])
# ax[2].fill_between(time, rec_shuffled_q5_physical, rec_shuffled_q95_physical, color=ps.gray, alpha=0.5)
# ax[2].plot(time, rec_shuffled_median_physical, ps.black)
# fig.suptitle(f'Recording: {i}')
# # plt.show()
# plt.close()
# nrecording_shuffled_convolved_onset_chirps.append(nshuffled_onset_chirps)
# nrecording_shuffled_convolved_offset_chirps.append(nshuffled_offset_chirps)
# nrecording_shuffled_convolved_physical_chirps.append(nshuffled_physical_chirps)
#### New shuffle approach ####
bootstrap_onset = []
bootstrap_offset = []
bootstrap_physical = []
# New bootstrapping approach
for n in range(nbootstrapping):
diff_onset = np.diff(np.sort(flatten(nrecording_centered_onset_chirps)))
diff_offset = np.diff(
np.sort(flatten(nrecording_centered_offset_chirps))
)
diff_physical = np.diff(
np.sort(flatten(nrecording_centered_physical_chirps))
)
np.random.shuffle(diff_onset)
shuffled_onset = np.cumsum(diff_onset)
np.random.shuffle(diff_offset)
shuffled_offset = np.cumsum(diff_offset)
np.random.shuffle(diff_physical)
shuffled_physical = np.cumsum(diff_physical)
kde_onset(acausal_kde1d(shuffled_onset, time, width)) / (27 * 100)
kde_offset = (acausal_kde1d(shuffled_offset, time, width)) / (27 * 100)
kde_physical = (acausal_kde1d(shuffled_physical, time, width)) / (
27 * 100
)
bootstrap_onset.append(kde_onset)
bootstrap_offset.append(kde_offset)
bootstrap_physical.append(kde_physical)
# New shuffle approach q5, q50, q95
onset_q5, onset_median, onset_q95 = np.percentile(
bootstrap_onset, [5, 50, 95], axis=0
)
offset_q5, offset_median, offset_q95 = np.percentile(
bootstrap_offset, [5, 50, 95], axis=0
)
physical_q5, physical_median, physical_q95 = np.percentile(
bootstrap_physical, [5, 50, 95], axis=0
)
# vstack um 1. Dim zu cutten
# nrecording_shuffled_convolved_onset_chirps = np.vstack(nrecording_shuffled_convolved_onset_chirps)
# nrecording_shuffled_convolved_offset_chirps = np.vstack(nrecording_shuffled_convolved_offset_chirps)
# nrecording_shuffled_convolved_physical_chirps = np.vstack(nrecording_shuffled_convolved_physical_chirps)
# shuffled_q5_onset, shuffled_median_onset, shuffled_q95_onset = np.percentile(
# nrecording_shuffled_convolved_onset_chirps, (5, 50, 95), axis=0)
# shuffled_q5_offset, shuffled_median_offset, shuffled_q95_offset = np.percentile(
# nrecording_shuffled_convolved_offset_chirps, (5, 50, 95), axis=0)
# shuffled_q5_physical, shuffled_median_physical, shuffled_q95_physical = np.percentile(
# nrecording_shuffled_convolved_physical_chirps, (5, 50, 95), axis=0)
# Flatten all chirps
all_chirps = np.concatenate(nrecording_chirps).ravel() # not centered
# Flatten event timestamps
all_onsets = np.concatenate(
nrecording_chasing_onsets
).ravel() # not centered
all_offsets = np.concatenate(
nrecording_chasing_offsets
).ravel() # not centered
all_physicals = np.concatenate(nrecording_physicals).ravel() # not centered
# Flatten all chirps around events
all_onset_chirps = np.concatenate(
nrecording_centered_onset_chirps
).ravel() # centered
all_offset_chirps = np.concatenate(
nrecording_centered_offset_chirps
).ravel() # centered
all_physical_chirps = np.concatenate(
nrecording_centered_physical_chirps
).ravel() # centered
# Convolute all chirps
# Divide by total number of each event over all recordings
all_onset_chirps_convolved = (
acausal_kde1d(all_onset_chirps, time, width)
) / len(all_onsets)
all_offset_chirps_convolved = (
acausal_kde1d(all_offset_chirps, time, width)
) / len(all_offsets)
all_physical_chirps_convolved = (
acausal_kde1d(all_physical_chirps, time, width)
) / len(all_physicals)
# Plot all events with all shuffled
fig, ax = plt.subplots(
1,
3,
figsize=(
28 * ps.cm,
16 * ps.cm,
),
constrained_layout=True,
sharey="all",
)
# offsets = np.arange(1,28,1)
ax[0].set_xlabel("Time[s]")
# Plot chasing onsets
ax[0].set_ylabel("Chirp rate [Hz]")
ax[0].plot(time, all_onset_chirps_convolved, color=ps.yellow, zorder=2)
ax0 = ax[0].twinx()
nrecording_centered_onset_chirps = np.asarray(
nrecording_centered_onset_chirps, dtype=object
)
ax0.eventplot(
np.array(nrecording_centered_onset_chirps),
linelengths=0.5,
colors=ps.gray,
alpha=0.25,
zorder=1,
)
ax0.vlines(0, 0, 1.5, ps.white, "dashed")
ax[0].set_zorder(ax0.get_zorder() + 1)
ax[0].patch.set_visible(False)
ax0.set_yticklabels([])
ax0.set_yticks([])
# ax[0].fill_between(time, shuffled_q5_onset, shuffled_q95_onset, color=ps.gray, alpha=0.5)
# ax[0].plot(time, shuffled_median_onset, color=ps.black)
ax[0].fill_between(time, onset_q5, onset_q95, color=ps.gray, alpha=0.5)
ax[0].plot(time, onset_median, color=ps.black)
# Plot chasing offets
ax[1].set_xlabel("Time[s]")
ax[1].plot(time, all_offset_chirps_convolved, color=ps.orange, zorder=2)
ax1 = ax[1].twinx()
nrecording_centered_offset_chirps = np.asarray(
nrecording_centered_offset_chirps, dtype=object
)
ax1.eventplot(
np.array(nrecording_centered_offset_chirps),
linelengths=0.5,
colors=ps.gray,
alpha=0.25,
zorder=1,
)
ax1.vlines(0, 0, 1.5, ps.white, "dashed")
ax[1].set_zorder(ax1.get_zorder() + 1)
ax[1].patch.set_visible(False)
ax1.set_yticklabels([])
ax1.set_yticks([])
# ax[1].fill_between(time, shuffled_q5_offset, shuffled_q95_offset, color=ps.gray, alpha=0.5)
# ax[1].plot(time, shuffled_median_offset, color=ps.black)
ax[1].fill_between(time, offset_q5, offset_q95, color=ps.gray, alpha=0.5)
ax[1].plot(time, offset_median, color=ps.black)
# Plot physical contacts
ax[2].set_xlabel("Time[s]")
ax[2].plot(time, all_physical_chirps_convolved, color=ps.maroon, zorder=2)
ax2 = ax[2].twinx()
nrecording_centered_physical_chirps = np.asarray(
nrecording_centered_physical_chirps, dtype=object
)
ax2.eventplot(
np.array(nrecording_centered_physical_chirps),
linelengths=0.5,
colors=ps.gray,
alpha=0.25,
zorder=1,
)
ax2.vlines(0, 0, 1.5, ps.white, "dashed")
ax[2].set_zorder(ax2.get_zorder() + 1)
ax[2].patch.set_visible(False)
ax2.set_yticklabels([])
ax2.set_yticks([])
# ax[2].fill_between(time, shuffled_q5_physical, shuffled_q95_physical, color=ps.gray, alpha=0.5)
# ax[2].plot(time, shuffled_median_physical, ps.black)
ax[2].fill_between(
time, physical_q5, physical_q95, color=ps.gray, alpha=0.5
)
ax[2].plot(time, physical_median, ps.black)
fig.suptitle("All recordings")
plt.show()
plt.close()
embed()
# chasing_durations = []
# # Calculate chasing duration to evaluate a nice time window for kernel density estimation
# for onset, offset in zip(chasing_onsets, chasing_offsets):
# duration = offset - onset
# chasing_durations.append(duration)
# fig, ax = plt.subplots()
# ax.boxplot(chasing_durations)
# plt.show()
# plt.close()
# # Associate chirps to individual fish
# fish1 = chirps[chirps_fish_ids == fish_ids[0]]
# fish2 = chirps[chirps_fish_ids == fish_ids[1]]
# fish = [len(fish1), len(fish2)]
# Convolution over all recordings
# Rasterplot for each recording
# #### Chirps around events, winner VS loser, one recording ####
# # Load file with fish ids and winner/loser info
# meta = pd.read_csv('../data/mount_data/order_meta.csv')
# current_recording = meta[meta.index == 43]
# fish1 = current_recording['rec_id1'].values
# fish2 = current_recording['rec_id2'].values
# # Implement check if fish_ids from meta and chirp detection are the same???
# winner = current_recording['winner'].values
# if winner == fish1:
# loser = fish2
# elif winner == fish2:
# loser = fish1
# winner_chirps = chirps[chirps_fish_ids == winner]
# loser_chirps = chirps[chirps_fish_ids == loser]
# # Event triggered winner chirps
# _, winner_centered_onset, winner_cc_onset = event_triggered_chirps(chasing_onsets, winner_chirps, time_before_event, time_after_event, dt, width)
# _, winner_centered_offset, winner_cc_offset = event_triggered_chirps(chasing_offsets, winner_chirps, time_before_event, time_after_event, dt, width)
# _, winner_centered_physical, winner_cc_physical = event_triggered_chirps(physical_contacts, winner_chirps, time_before_event, time_after_event, dt, width)
# # Event triggered loser chirps
# _, loser_centered_onset, loser_cc_onset = event_triggered_chirps(chasing_onsets, loser_chirps, time_before_event, time_after_event, dt, width)
# _, loser_centered_offset, loser_cc_offset = event_triggered_chirps(chasing_offsets, loser_chirps, time_before_event, time_after_event, dt, width)
# _, loser_centered_physical, loser_cc_physical = event_triggered_chirps(physical_contacts, loser_chirps, time_before_event, time_after_event, dt, width)
# ########## Winner VS Loser plot ##########
# fig, ax = plt.subplots(2, 3, figsize=(50 / 2.54, 15 / 2.54), constrained_layout=True, sharey='row')
# offset = [1.35]
# ax[1][0].set_xlabel('Time[s]')
# ax[1][1].set_xlabel('Time[s]')
# ax[1][2].set_xlabel('Time[s]')
# # Plot winner chasing onsets
# ax[0][0].set_ylabel('Chirp rate [Hz]')
# ax[0][0].plot(time, winner_cc_onset, color='tab:blue', zorder=100)
# ax0 = ax[0][0].twinx()
# ax0.eventplot(np.array([winner_centered_onset]), lineoffsets=offset, linelengths=0.1, colors=['tab:green'], alpha=0.25, zorder=-100)
# ax0.set_ylabel('Event')
# ax0.vlines(0, 0, 1.5, 'tab:grey', 'dashed')
# ax[0][0].set_zorder(ax0.get_zorder()+1)
# ax[0][0].patch.set_visible(False)
# ax0.set_yticklabels([])
# ax0.set_yticks([])
# # Plot winner chasing offets
# ax[0][1].plot(time, winner_cc_offset, color='tab:blue', zorder=100)
# ax1 = ax[0][1].twinx()
# ax1.eventplot(np.array([winner_centered_offset]), lineoffsets=offset, linelengths=0.1, colors=['tab:purple'], alpha=0.25, zorder=-100)
# ax1.vlines(0, 0, 1.5, 'tab:grey', 'dashed')
# ax[0][1].set_zorder(ax1.get_zorder()+1)
# ax[0][1].patch.set_visible(False)
# ax1.set_yticklabels([])
# ax1.set_yticks([])
# # Plot winner physical contacts
# ax[0][2].plot(time, winner_cc_physical, color='tab:blue', zorder=100)
# ax2 = ax[0][2].twinx()
# ax2.eventplot(np.array([winner_centered_physical]), lineoffsets=offset, linelengths=0.1, colors=['tab:red'], alpha=0.25, zorder=-100)
# ax2.vlines(0, 0, 1.5, 'tab:grey', 'dashed')
# ax[0][2].set_zorder(ax2.get_zorder()+1)
# ax[0][2].patch.set_visible(False)
# ax2.set_yticklabels([])
# ax2.set_yticks([])
# # Plot loser chasing onsets
# ax[1][0].set_ylabel('Chirp rate [Hz]')
# ax[1][0].plot(time, loser_cc_onset, color='tab:blue', zorder=100)
# ax3 = ax[1][0].twinx()
# ax3.eventplot(np.array([loser_centered_onset]), lineoffsets=offset, linelengths=0.1, colors=['tab:green'], alpha=0.25, zorder=-100)
# ax3.vlines(0, 0, 1.5, 'tab:grey', 'dashed')
# ax[1][0].set_zorder(ax3.get_zorder()+1)
# ax[1][0].patch.set_visible(False)
# ax3.set_yticklabels([])
# ax3.set_yticks([])
# # Plot loser chasing offsets
# ax[1][1].plot(time, loser_cc_offset, color='tab:blue', zorder=100)
# ax4 = ax[1][1].twinx()
# ax4.eventplot(np.array([loser_centered_offset]), lineoffsets=offset, linelengths=0.1, colors=['tab:purple'], alpha=0.25, zorder=-100)
# ax4.vlines(0, 0, 1.5, 'tab:grey', 'dashed')
# ax[1][1].set_zorder(ax4.get_zorder()+1)
# ax[1][1].patch.set_visible(False)
# ax4.set_yticklabels([])
# ax4.set_yticks([])
# # Plot loser physical contacts
# ax[1][2].plot(time, loser_cc_physical, color='tab:blue', zorder=100)
# ax5 = ax[1][2].twinx()
# ax5.eventplot(np.array([loser_centered_physical]), lineoffsets=offset, linelengths=0.1, colors=['tab:red'], alpha=0.25, zorder=-100)
# ax5.vlines(0, 0, 1.5, 'tab:grey', 'dashed')
# ax[1][2].set_zorder(ax5.get_zorder()+1)
# ax[1][2].patch.set_visible(False)
# ax5.set_yticklabels([])
# ax5.set_yticks([])
# plt.show()
# plt.close()
# for i in range(len(fish_ids)):
# fish = fish_ids[i]
# chirps_temp = chirps[chirps_fish_ids == fish]
# print(fish)
#### Chirps around events, only losers, one recording ####
if __name__ == "__main__":
# Path to the data
datapath = "../data/mount_data/"
main(datapath)

View File

@ -1,59 +0,0 @@
import os
import pandas as pd
import numpy as np
from chirpdetection import chirpdetection
from IPython import embed
# check rec ../data/mount_data/2020-03-25-10_00/ starting at 3175
def get_valid_datasets(dataroot):
datasets = sorted(
[
name
for name in os.listdir(dataroot)
if os.path.isdir(os.path.join(dataroot, name))
]
)
valid_datasets = []
for dataset in datasets:
path = os.path.join(dataroot, dataset)
csv_name = "-".join(dataset.split("-")[:3]) + ".csv"
if os.path.exists(os.path.join(path, csv_name)) is False:
continue
if os.path.exists(os.path.join(path, "ident_v.npy")) is False:
continue
ident = np.load(os.path.join(path, "ident_v.npy"))
number_of_fish = len(np.unique(ident[~np.isnan(ident)]))
if number_of_fish != 2:
continue
valid_datasets.append(dataset)
datapaths = [
os.path.join(dataroot, dataset) + "/" for dataset in valid_datasets
]
return datapaths, valid_datasets
def main(datapaths):
for path in datapaths:
chirpdetection(path, plot="show")
if __name__ == "__main__":
dataroot = "../data/mount_data/"
datapaths, valid_datasets = get_valid_datasets(dataroot)
recs = pd.DataFrame(columns=["recording"], data=valid_datasets)
recs.to_csv("../recs.csv", index=False)
# datapaths = ['../data/mount_data/2020-03-25-10_00/']
main(datapaths)
# window 1524 + 244 in dataset index 4 is nice example

View File

@ -1,47 +0,0 @@
import os
from paramiko import SSHClient
from scp import SCPClient
from IPython import embed
from pandas import read_csv
ssh = SSHClient()
ssh.load_system_host_keys()
ssh.connect(
hostname="kraken",
username="efish",
password="fwNix4U",
)
# SCPCLient takes a paramiko transport as its only argument
scp = SCPClient(ssh.get_transport())
data = read_csv("../recs.csv")
foldernames = data["recording"].values
directory = f"/Users/acfw/Documents/uni_tuebingen/chirpdetection/GP2023_chirp_detection/data/mount_data/"
for foldername in foldernames:
if not os.path.exists(directory + foldername):
os.makedirs(directory + foldername)
files = [
("-").join(foldername.split("-")[:3]) + ".csv",
"chirp_ids.npy",
"chirps.npy",
"fund_v.npy",
"ident_v.npy",
"idx_v.npy",
"times.npy",
"spec.npy",
"LED_on_time.npy",
"sign_v.npy",
]
for f in files:
scp.get(
f"/home/efish/behavior/2019_tube_competition/{foldername}/{f}",
directory + foldername,
)
scp.close()

View File

@ -1,171 +0,0 @@
import numpy as np
import os
from IPython import embed
from pandas import read_csv
from modules.logger import makeLogger
from modules.datahandling import causal_kde1d, acausal_kde1d, flatten
logger = makeLogger(__name__)
class Behavior:
"""Load behavior data from csv file as class attributes
Attributes
----------
behavior: 0: chasing onset, 1: chasing offset, 2: physical contact
behavior_type:
behavioral_category:
comment_start:
comment_stop:
dataframe: pandas dataframe with all the data
duration_s:
media_file:
observation_date:
observation_id:
start_s: start time of the event in seconds
stop_s: stop time of the event in seconds
total_length:
"""
def __init__(self, folder_path: str) -> None:
LED_on_time_BORIS = np.load(
os.path.join(folder_path, "LED_on_time.npy"), allow_pickle=True
)
csv_filename = os.path.split(folder_path[:-1])[-1]
csv_filename = "-".join(csv_filename.split("-")[:-1]) + ".csv"
# embed()
# csv_filename = [f for f in os.listdir(
# folder_path) if f.endswith('.csv')][0]
# logger.info(f'CSV file: {csv_filename}')
self.dataframe = read_csv(os.path.join(folder_path, csv_filename))
self.chirps = np.load(
os.path.join(folder_path, "chirps.npy"), allow_pickle=True
)
self.chirps_ids = np.load(
os.path.join(folder_path, "chirp_ids.npy"), allow_pickle=True
)
self.ident = np.load(
os.path.join(folder_path, "ident_v.npy"), allow_pickle=True
)
self.idx = np.load(
os.path.join(folder_path, "idx_v.npy"), allow_pickle=True
)
self.freq = np.load(
os.path.join(folder_path, "fund_v.npy"), allow_pickle=True
)
self.time = np.load(
os.path.join(folder_path, "times.npy"), allow_pickle=True
)
self.spec = np.load(
os.path.join(folder_path, "spec.npy"), allow_pickle=True
)
for k, key in enumerate(self.dataframe.keys()):
key = key.lower()
if " " in key:
key = key.replace(" ", "_")
if "(" in key:
key = key.replace("(", "")
key = key.replace(")", "")
setattr(
self, key, np.array(self.dataframe[self.dataframe.keys()[k]])
)
last_LED_t_BORIS = LED_on_time_BORIS[-1]
real_time_range = self.time[-1] - self.time[0]
factor = 1.034141
shift = last_LED_t_BORIS - real_time_range * factor
self.start_s = (self.start_s - shift) / factor
self.stop_s = (self.stop_s - shift) / factor
def correct_chasing_events(
category: np.ndarray, timestamps: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
onset_ids = np.arange(len(category))[category == 0]
offset_ids = np.arange(len(category))[category == 1]
wrong_bh = np.arange(len(category))[category != 2][:-1][
np.diff(category[category != 2]) == 0
]
if category[category != 2][-1] == 0:
wrong_bh = np.append(
wrong_bh, np.arange(len(category))[category != 2][-1]
)
if onset_ids[0] > offset_ids[0]:
offset_ids = np.delete(offset_ids, 0)
help_index = offset_ids[0]
wrong_bh = np.append(wrong_bh[help_index])
category = np.delete(category, wrong_bh)
timestamps = np.delete(timestamps, wrong_bh)
new_onset_ids = np.arange(len(category))[category == 0]
new_offset_ids = np.arange(len(category))[category == 1]
# Check whether on- or offset is longer and calculate length difference
if len(new_onset_ids) > len(new_offset_ids):
embed()
logger.warning("Onsets are greater than offsets")
elif len(new_onset_ids) < len(new_offset_ids):
logger.warning("Offsets are greater than onsets")
elif len(new_onset_ids) == len(new_offset_ids):
# logger.info('Chasing events are equal')
pass
return category, timestamps
def center_chirps(
events: np.ndarray,
chirps: np.ndarray,
time_before_event: int,
time_after_event: int,
# dt: float,
# width: float,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
event_chirps = [] # chirps that are in specified window around event
# timestamps of chirps around event centered on the event timepoint
centered_chirps = []
for event_timestamp in events:
start = event_timestamp - time_before_event
stop = event_timestamp + time_after_event
chirps_around_event = [c for c in chirps if (c >= start) & (c <= stop)]
if len(chirps_around_event) == 0:
continue
centered_chirps.append(chirps_around_event - event_timestamp)
event_chirps.append(chirps_around_event)
centered_chirps = np.sort(flatten(centered_chirps))
event_chirps = np.sort(flatten(event_chirps))
if len(centered_chirps) != len(event_chirps):
raise ValueError(
"Non centered chirps and centered chirps are not equal"
)
# time = np.arange(-time_before_event, time_after_event, dt)
# # Kernel density estimation with some if's
# if len(centered_chirps) == 0:
# centered_chirps = np.array([])
# centered_chirps_convolved = np.zeros(len(time))
# else:
# # convert list of arrays to one array for plotting
# centered_chirps = np.concatenate(centered_chirps, axis=0)
# centered_chirps_convolved = (acausal_kde1d(
# centered_chirps, time, width)) / len(event)
return centered_chirps

View File

@ -2,10 +2,9 @@ import numpy as np
from typing import List, Any
from scipy.ndimage import gaussian_filter1d
from scipy.stats import gamma, norm
from scipy.signal import resample
def minmaxnorm(data):
def scale01(data):
"""
Normalize data to [0, 1]
@ -20,57 +19,14 @@ def minmaxnorm(data):
Normalized data.
"""
return (data - np.min(data)) / (np.max(data) - np.min(data))
def instantaneous_frequency2(
signal: np.ndarray, fs: float, interpolation: str = "linear"
) -> np.ndarray:
"""
Compute the instantaneous frequency of a periodic signal using zero crossings and resample the frequency using linear
or cubic interpolation to match the dimensions of the input array.
Parameters
----------
signal : np.ndarray
Input signal.
fs : float
Sampling frequency of the input signal.
interpolation : str, optional
Interpolation method to use during resampling. Should be either 'linear' or 'cubic'. Default is 'linear'.
Returns
-------
freq : np.ndarray
Instantaneous frequency of the input signal, resampled to match the dimensions of the input array.
"""
# Find zero crossings
zero_crossings = np.where(np.diff(np.sign(signal)))[0]
# Compute time differences between zero crossings
time_diff = np.diff(zero_crossings) / fs
# Compute instantaneous frequency as inverse of time differences
freq = 1 / time_diff
# Resample the frequency using specified interpolation method to match the dimensions of the input array
orig_len = len(signal)
freq = resample(freq, orig_len)
if interpolation == "linear":
freq = np.interp(np.arange(0, orig_len), np.arange(0, orig_len), freq)
elif interpolation == "cubic":
freq = resample(freq, orig_len, window="cubic")
return freq
return (2*((data - np.min(data)) / (np.max(data) - np.min(data)))) - 1
def instantaneous_frequency(
signal: np.ndarray,
samplerate: int,
smoothing_window: int,
interpolation: str = "linear",
) -> np.ndarray:
) -> tuple[np.ndarray, np.ndarray]:
"""
Compute the instantaneous frequency of a signal that is approximately
sinusoidal and symmetric around 0.
@ -83,8 +39,6 @@ def instantaneous_frequency(
Samplerate of the signal.
smoothing_window : int
Window size for the gaussian filter.
interpolation : str, optional
Interpolation method to use during resampling. Should be either 'linear' or 'cubic'. Default is 'linear'.
Returns
-------
@ -118,16 +72,7 @@ def instantaneous_frequency(
1 / np.diff(true_zero), smoothing_window
)
# Resample the frequency using specified interpolation method to match the dimensions of the input array
orig_len = len(signal)
freq = resample(instantaneous_frequency, orig_len)
if interpolation == "linear":
freq = np.interp(np.arange(0, orig_len), np.arange(0, orig_len), freq)
elif interpolation == "cubic":
freq = resample(freq, orig_len, window="cubic")
return freq
return instantaneous_frequency_time, instantaneous_frequency
def purge_duplicates(
@ -161,6 +106,7 @@ def purge_duplicates(
group = [timestamps[0]]
for i in range(1, len(timestamps)):
# check the difference between current timestamp and previous
# timestamp is less than the threshold
if timestamps[i] - timestamps[i - 1] < threshold:
@ -222,9 +168,6 @@ def group_timestamps(
]
timestamps.sort()
if len(timestamps) == 0:
return []
groups = []
current_group = [timestamps[0]]
@ -379,6 +322,7 @@ def acausal_kde1d(spikes, time, width):
if __name__ == "__main__":
timestamps = [
[1.2, 1.5, 1.3],
[],

View File

@ -1,8 +1,7 @@
import os
import matplotlib.pyplot as plt
import numpy as np
import yaml
import numpy as np
from thunderfish.dataloader import DataLoader
@ -35,15 +34,12 @@ class LoadData:
"""
def __init__(self, datapath: str) -> None:
# load raw data
self.datapath = datapath
self.file = os.path.join(datapath, "traces-grid1.raw")
if os.path.isfile(self.file) == False:
self.raw = np.load(os.path.join(datapath, "raw.npy"))
self.raw_rate = 20000.0
else:
self.raw = DataLoader(self.file, 60.0, 0, channel=-1)
self.raw_rate = self.raw.samplerate
self.raw = DataLoader(self.file, 60.0, 0, channel=-1)
self.raw_rate = self.raw.samplerate
# load wavetracker files
self.time = np.load(datapath + "times.npy", allow_pickle=True)

View File

@ -3,10 +3,10 @@ import numpy as np
def bandpass_filter(
signal: np.ndarray,
samplerate: float,
lowf: float,
highf: float,
signal: np.ndarray,
samplerate: float,
lowf: float,
highf: float,
) -> np.ndarray:
"""Bandpass filter a signal.
@ -60,7 +60,9 @@ def highpass_filter(
def lowpass_filter(
signal: np.ndarray, samplerate: float, cutoff: float
signal: np.ndarray,
samplerate: float,
cutoff: float
) -> np.ndarray:
"""Lowpass filter a signal.
@ -84,9 +86,10 @@ def lowpass_filter(
return filtered_signal
def envelope(
signal: np.ndarray, samplerate: float, cutoff_frequency: float
) -> np.ndarray:
def envelope(signal: np.ndarray,
samplerate: float,
cutoff_frequency: float
) -> np.ndarray:
"""Calculate the envelope of a signal using a lowpass filter.
Parameters

View File

@ -2,13 +2,12 @@ import logging
def makeLogger(name: str):
# create logger formats for file and terminal
file_formatter = logging.Formatter(
"[ %(levelname)s ] ~ %(asctime)s ~ %(module)s.%(funcName)s: %(message)s"
)
"[ %(levelname)s ] ~ %(asctime)s ~ %(module)s.%(funcName)s: %(message)s")
console_formatter = logging.Formatter(
"[ %(levelname)s ] in %(module)s.%(funcName)s: %(message)s"
)
"[ %(levelname)s ] in %(module)s.%(funcName)s: %(message)s")
# create logging file if loglevel is debug
file_handler = logging.FileHandler(f"gridtools_log.log", mode="w")
@ -30,6 +29,7 @@ def makeLogger(name: str):
if __name__ == "__main__":
# initiate logger
mylogger = makeLogger(__name__)

View File

@ -7,6 +7,7 @@ from matplotlib.colors import ListedColormap
def PlotStyle() -> None:
class style:
# lightcmap = cmocean.tools.lighten(cmocean.cm.haline, 0.8)
# units
@ -22,17 +23,16 @@ def PlotStyle() -> None:
sky = "#89dceb"
teal = "#94e2d5"
green = "#a6e3a1"
yellow = "#f9d67f"
orange = "#faa472"
maroon = "#eb8486"
red = "#e0e4f7"
purple = "#d89bf7"
pink = "#f59edb"
yellow = "#f9e2af"
orange = "#fab387"
maroon = "#eba0ac"
red = "#f38ba8"
purple = "#cba6f7"
pink = "#f5c2e7"
lavender = "#b4befe"
gblue1 = "#f37588"
gblue2 = "#faa472"
gblue3 = "#f9d67f"
g = "#f3626c"
gblue1 = "#8cb8ff"
gblue2 = "#7cdcdc"
gblue3 = "#82e896"
@classmethod
def lims(cls, track1, track2):
@ -75,15 +75,13 @@ def PlotStyle() -> None:
va="center",
zorder=1000,
bbox=dict(
boxstyle=f"circle, pad={padding}",
fc="white",
ec="black",
lw=1,
boxstyle=f"circle, pad={padding}", fc="white", ec="black", lw=1
),
)
@classmethod
def fade_cmap(cls, cmap):
my_cmap = cmap(np.arange(cmap.N))
my_cmap[:, -1] = np.linspace(0, 1, cmap.N)
my_cmap = ListedColormap(my_cmap)
@ -110,9 +108,9 @@ def PlotStyle() -> None:
@classmethod
def set_boxplot_color(cls, bp, color):
plt.setp(bp["boxes"], color=color)
plt.setp(bp["whiskers"], color=white)
plt.setp(bp["caps"], color=white)
plt.setp(bp["medians"], color=black)
plt.setp(bp["whiskers"], color=color)
plt.setp(bp["caps"], color=color)
plt.setp(bp["medians"], color=color)
@classmethod
def label_subplots(cls, labels, axes, fig):
@ -231,7 +229,7 @@ def PlotStyle() -> None:
plt.rc("legend", fontsize=SMALL_SIZE) # legend fontsize
plt.rc("figure", titlesize=BIGGER_SIZE) # fontsize of the figure title
plt.rcParams["image.cmap"] = "cmo.thermal"
plt.rcParams["image.cmap"] = 'cmo.haline'
plt.rcParams["axes.xmargin"] = 0.05
plt.rcParams["axes.ymargin"] = 0.1
plt.rcParams["axes.titlelocation"] = "left"
@ -256,53 +254,53 @@ def PlotStyle() -> None:
plt.rcParams["boxplot.boxprops.color"] = gray
plt.rcParams["boxplot.whiskerprops.color"] = gray
plt.rcParams["boxplot.capprops.color"] = gray
plt.rcParams["boxplot.medianprops.color"] = black
plt.rcParams["boxplot.medianprops.color"] = gray
plt.rcParams["text.color"] = white
plt.rcParams["axes.facecolor"] = black # axes background color
plt.rcParams["axes.edgecolor"] = white # axes edge color
plt.rcParams["axes.edgecolor"] = gray # axes edge color
# plt.rcParams["axes.grid"] = True # display grid or not
# plt.rcParams["axes.grid.axis"] = "y" # which axis the grid is applied to
plt.rcParams["axes.labelcolor"] = white
plt.rcParams["axes.axisbelow"] = True # draw axis gridlines and ticks:
plt.rcParams["axes.axisbelow"] = True # draw axis gridlines and ticks:
plt.rcParams["axes.spines.left"] = True # display axis spines
plt.rcParams["axes.spines.bottom"] = True
plt.rcParams["axes.spines.top"] = False
plt.rcParams["axes.spines.right"] = False
plt.rcParams["axes.prop_cycle"] = cycler(
"color",
[
"#b4befe",
"#89b4fa",
"#74c7ec",
"#89dceb",
"#94e2d5",
"#a6e3a1",
"#f9e2af",
"#fab387",
"#eba0ac",
"#f38ba8",
"#cba6f7",
"#f5c2e7",
],
)
plt.rcParams["xtick.color"] = white # color of the ticks
plt.rcParams["ytick.color"] = white # color of the ticks
plt.rcParams["grid.color"] = white # grid color
plt.rcParams["figure.facecolor"] = black # figure face color
plt.rcParams["figure.edgecolor"] = black # figure edge color
'color', [
'#b4befe',
'#89b4fa',
'#74c7ec',
'#89dceb',
'#94e2d5',
'#a6e3a1',
'#f9e2af',
'#fab387',
'#eba0ac',
'#f38ba8',
'#cba6f7',
'#f5c2e7',
])
plt.rcParams["xtick.color"] = gray # color of the ticks
plt.rcParams["ytick.color"] = gray # color of the ticks
plt.rcParams["grid.color"] = dark_gray # grid color
plt.rcParams["figure.facecolor"] = black # figure face color
plt.rcParams["figure.edgecolor"] = black # figure edge color
plt.rcParams["savefig.facecolor"] = black # figure face color when saving
return style
if __name__ == "__main__":
s = PlotStyle()
import matplotlib.cbook as cbook
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import matplotlib.pyplot as plt
from matplotlib.patches import PathPatch
import matplotlib.cbook as cbook
from matplotlib.path import Path
from matplotlib.patches import PathPatch
# Fixing random state for reproducibility
np.random.seed(19680801)
@ -310,20 +308,14 @@ if __name__ == "__main__":
delta = 0.025
x = y = np.arange(-3.0, 3.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-(X**2) - Y**2)
Z2 = np.exp(-((X - 1) ** 2) - (Y - 1) ** 2)
Z1 = np.exp(-X**2 - Y**2)
Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)
Z = (Z1 - Z2) * 2
fig1, ax = plt.subplots()
im = ax.imshow(
Z,
interpolation="bilinear",
cmap=cm.RdYlGn,
origin="lower",
extent=[-3, 3, -3, 3],
vmax=abs(Z).max(),
vmin=-abs(Z).max(),
)
im = ax.imshow(Z, interpolation='bilinear', cmap=cm.RdYlGn,
origin='lower', extent=[-3, 3, -3, 3],
vmax=abs(Z).max(), vmin=-abs(Z).max())
plt.show()
@ -336,22 +328,22 @@ if __name__ == "__main__":
all_data = [np.random.normal(0, std, 100) for std in range(6, 10)]
# plot violin plot
axs[0].violinplot(all_data, showmeans=False, showmedians=True)
axs[0].set_title("Violin plot")
axs[0].violinplot(all_data,
showmeans=False,
showmedians=True)
axs[0].set_title('Violin plot')
# plot box plot
axs[1].boxplot(all_data)
axs[1].set_title("Box plot")
axs[1].set_title('Box plot')
# adding horizontal grid lines
for ax in axs:
ax.yaxis.grid(True)
ax.set_xticks(
[y + 1 for y in range(len(all_data))],
labels=["x1", "x2", "x3", "x4"],
)
ax.set_xlabel("Four separate samples")
ax.set_ylabel("Observed values")
ax.set_xticks([y + 1 for y in range(len(all_data))],
labels=['x1', 'x2', 'x3', 'x4'])
ax.set_xlabel('Four separate samples')
ax.set_ylabel('Observed values')
plt.show()
@ -363,45 +355,24 @@ if __name__ == "__main__":
theta = np.linspace(0.0, 2 * np.pi, N, endpoint=False)
radii = 10 * np.random.rand(N)
width = np.pi / 4 * np.random.rand(N)
colors = cmo.cm.haline(radii / 10.0)
colors = cmo.cm.haline(radii / 10.)
ax = plt.subplot(projection="polar")
ax = plt.subplot(projection='polar')
ax.bar(theta, radii, width=width, bottom=0.0, color=colors, alpha=0.5)
plt.show()
methods = [
None,
"none",
"nearest",
"bilinear",
"bicubic",
"spline16",
"spline36",
"hanning",
"hamming",
"hermite",
"kaiser",
"quadric",
"catrom",
"gaussian",
"bessel",
"mitchell",
"sinc",
"lanczos",
]
methods = [None, 'none', 'nearest', 'bilinear', 'bicubic', 'spline16',
'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric',
'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos']
# Fixing random state for reproducibility
np.random.seed(19680801)
grid = np.random.rand(4, 4)
fig, axs = plt.subplots(
nrows=3,
ncols=6,
figsize=(9, 6),
subplot_kw={"xticks": [], "yticks": []},
)
fig, axs = plt.subplots(nrows=3, ncols=6, figsize=(9, 6),
subplot_kw={'xticks': [], 'yticks': []})
for ax, interp_method in zip(axs.flat, methods):
ax.imshow(grid, interpolation=interp_method)

View File

@ -1,411 +0,0 @@
import cmocean as cmo
import matplotlib.pyplot as plt
import numpy as np
from cycler import cycler
from matplotlib.colors import ListedColormap
def PlotStyle() -> None:
class style:
# lightcmap = cmocean.tools.lighten(cmocean.cm.haline, 0.8)
# units
cm = 1 / 2.54
mm = 1 / 25.4
# colors
black = "#111116"
white = "#e0e4f7"
gray = "#6c6e7d"
blue = "#89b4fa"
sapphire = "#74c7ec"
sky = "#89dceb"
teal = "#94e2d5"
green = "#a6e3a1"
yellow = "#f9d67f"
orange = "#faa472"
maroon = "#eb8486"
red = "#f37588"
purple = "#d89bf7"
pink = "#f59edb"
lavender = "#b4befe"
gblue1 = "#89b4fa"
gblue2 = "#89dceb"
gblue3 = "#a6e3a1"
g = "#76a0fa"
@classmethod
def lims(cls, track1, track2):
"""Helper function to get frequency y axis limits from two
fundamental frequency tracks.
Args:
track1 (array): First track
track2 (array): Second track
start (int): Index for first value to be plotted
stop (int): Index for second value to be plotted
padding (int): Padding for the upper and lower limit
Returns:
lower (float): lower limit
upper (float): upper limit
"""
allfunds_tmp = (
np.concatenate(
[
track1,
track2,
]
)
.ravel()
.tolist()
)
lower = np.min(allfunds_tmp)
upper = np.max(allfunds_tmp)
return lower, upper
@classmethod
def circled_annotation(cls, text, axis, xpos, ypos, padding=0.25):
axis.text(
xpos,
ypos,
text,
ha="center",
va="center",
zorder=1000,
bbox=dict(
boxstyle=f"circle, pad={padding}",
fc="white",
ec="black",
lw=1,
),
)
@classmethod
def fade_cmap(cls, cmap):
my_cmap = cmap(np.arange(cmap.N))
my_cmap[:, -1] = np.linspace(0, 1, cmap.N)
my_cmap = ListedColormap(my_cmap)
return my_cmap
@classmethod
def hide_ax(cls, ax):
ax.xaxis.set_visible(False)
plt.setp(ax.spines.values(), visible=False)
ax.tick_params(left=False, labelleft=False)
ax.patch.set_visible(False)
@classmethod
def hide_xax(cls, ax):
ax.xaxis.set_visible(False)
ax.spines["bottom"].set_visible(False)
@classmethod
def hide_yax(cls, ax):
ax.yaxis.set_visible(False)
ax.spines["left"].set_visible(False)
@classmethod
def set_boxplot_color(cls, bp, color):
plt.setp(bp["boxes"], color=color)
plt.setp(bp["whiskers"], color=white)
plt.setp(bp["caps"], color=white)
plt.setp(bp["medians"], color=black)
@classmethod
def label_subplots(cls, labels, axes, fig):
for axis, label in zip(axes, labels):
X = axis.get_position().x0
Y = axis.get_position().y1
fig.text(X, Y, label, weight="bold")
@classmethod
def letter_subplots(
cls, axes=None, letters=None, xoffset=-0.1, yoffset=1.0, **kwargs
):
"""Add letters to the corners of subplots (panels). By default each axis is
given an uppercase bold letter label placed in the upper-left corner.
Args
axes : list of pyplot ax objects. default plt.gcf().axes.
letters : list of strings to use as labels, default ["A", "B", "C", ...]
xoffset, yoffset : positions of each label relative to plot frame
(default -0.1,1.0 = upper left margin). Can also be a list of
offsets, in which case it should be the same length as the number of
axes.
Other keyword arguments will be passed to annotate() when panel letters
are added.
Returns:
list of strings for each label added to the axes
Examples:
Defaults:
>>> fig, axes = plt.subplots(1,3)
>>> letter_subplots() # boldfaced A, B, C
Common labeling schemes inferred from the first letter:
>>> fig, axes = plt.subplots(1,4)
# panels labeled (a), (b), (c), (d)
>>> letter_subplots(letters='(a)')
Fully custom lettering:
>>> fig, axes = plt.subplots(2,1)
>>> letter_subplots(axes, letters=['(a.1)', '(b.2)'], fontweight='normal')
Per-axis offsets:
>>> fig, axes = plt.subplots(1,2)
>>> letter_subplots(axes, xoffset=[-0.1, -0.15])
Matrix of axes:
>>> fig, axes = plt.subplots(2,2, sharex=True, sharey=True)
# fig.axes is a list when axes is a 2x2 matrix
>>> letter_subplots(fig.axes)
"""
# get axes:
if axes is None:
axes = plt.gcf().axes
# handle single axes:
try:
iter(axes)
except TypeError:
axes = [axes]
# set up letter defaults (and corresponding fontweight):
fontweight = "bold"
ulets = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ"[: len(axes)])
llets = list("abcdefghijklmnopqrstuvwxyz"[: len(axes)])
if letters is None or letters == "A":
letters = ulets
elif letters == "(a)":
letters = ["({})".format(lett) for lett in llets]
fontweight = "normal"
elif letters == "(A)":
letters = ["({})".format(lett) for lett in ulets]
fontweight = "normal"
elif letters in ("lower", "lowercase", "a"):
letters = llets
# make sure there are x and y offsets for each ax in axes:
if isinstance(xoffset, (int, float)):
xoffset = [xoffset] * len(axes)
else:
assert len(xoffset) == len(axes)
if isinstance(yoffset, (int, float)):
yoffset = [yoffset] * len(axes)
else:
assert len(yoffset) == len(axes)
# defaults for annotate (kwargs is second so it can overwrite these defaults):
my_defaults = dict(
fontweight=fontweight,
fontsize="large",
ha="center",
va="center",
xycoords="axes fraction",
annotation_clip=False,
)
kwargs = dict(list(my_defaults.items()) + list(kwargs.items()))
list_txts = []
for ax, lbl, xoff, yoff in zip(axes, letters, xoffset, yoffset):
t = ax.annotate(lbl, xy=(xoff, yoff), **kwargs)
list_txts.append(t)
return list_txts
pass
# rcparams text setup
SMALL_SIZE = 12
MEDIUM_SIZE = 14
BIGGER_SIZE = 16
black = "#111116"
white = "#e0e4f7"
gray = "#6c6e7d"
dark_gray = "#2a2a32"
# rcparams
plt.rc("font", size=MEDIUM_SIZE) # controls default text sizes
plt.rc("axes", titlesize=MEDIUM_SIZE) # fontsize of the axes title
plt.rc("axes", labelsize=MEDIUM_SIZE) # fontsize of the x and y labels
plt.rc("xtick", labelsize=SMALL_SIZE) # fontsize of the tick labels
plt.rc("ytick", labelsize=SMALL_SIZE) # fontsize of the tick labels
plt.rc("legend", fontsize=SMALL_SIZE) # legend fontsize
plt.rc("figure", titlesize=BIGGER_SIZE) # fontsize of the figure title
plt.rcParams["image.cmap"] = "cmo.haline"
plt.rcParams["axes.xmargin"] = 0.05
plt.rcParams["axes.ymargin"] = 0.1
plt.rcParams["axes.titlelocation"] = "left"
plt.rcParams["axes.titlesize"] = BIGGER_SIZE
# plt.rcParams["axes.titlepad"] = -10
plt.rcParams["legend.frameon"] = False
plt.rcParams["legend.loc"] = "best"
plt.rcParams["legend.borderpad"] = 0.4
plt.rcParams["legend.facecolor"] = black
plt.rcParams["legend.edgecolor"] = black
plt.rcParams["legend.framealpha"] = 0.7
plt.rcParams["legend.borderaxespad"] = 0.5
plt.rcParams["legend.fancybox"] = False
# # specify the custom font to use
# plt.rcParams["font.family"] = "sans-serif"
# plt.rcParams["font.sans-serif"] = "Helvetica Now Text"
# dark mode modifications
plt.rcParams["boxplot.flierprops.color"] = white
plt.rcParams["boxplot.flierprops.markeredgecolor"] = gray
plt.rcParams["boxplot.boxprops.color"] = gray
plt.rcParams["boxplot.whiskerprops.color"] = gray
plt.rcParams["boxplot.capprops.color"] = gray
plt.rcParams["boxplot.medianprops.color"] = black
plt.rcParams["text.color"] = white
plt.rcParams["axes.facecolor"] = black # axes background color
plt.rcParams["axes.edgecolor"] = white # axes edge color
# plt.rcParams["axes.grid"] = True # display grid or not
# plt.rcParams["axes.grid.axis"] = "y" # which axis the grid is applied to
plt.rcParams["axes.labelcolor"] = white
plt.rcParams["axes.axisbelow"] = True # draw axis gridlines and ticks:
plt.rcParams["axes.spines.left"] = True # display axis spines
plt.rcParams["axes.spines.bottom"] = True
plt.rcParams["axes.spines.top"] = False
plt.rcParams["axes.spines.right"] = False
plt.rcParams["axes.prop_cycle"] = cycler(
"color",
[
"#b4befe",
"#89b4fa",
"#74c7ec",
"#89dceb",
"#94e2d5",
"#a6e3a1",
"#f9e2af",
"#fab387",
"#eba0ac",
"#f38ba8",
"#cba6f7",
"#f5c2e7",
],
)
plt.rcParams["xtick.color"] = white # color of the ticks
plt.rcParams["ytick.color"] = white # color of the ticks
plt.rcParams["grid.color"] = white # grid color
plt.rcParams["figure.facecolor"] = black # figure face color
plt.rcParams["figure.edgecolor"] = black # figure edge color
plt.rcParams["savefig.facecolor"] = black # figure face color when saving
return style
if __name__ == "__main__":
s = PlotStyle()
import matplotlib.cbook as cbook
import matplotlib.cm as cm
import matplotlib.pyplot as plt
from matplotlib.patches import PathPatch
from matplotlib.path import Path
# Fixing random state for reproducibility
np.random.seed(19680801)
delta = 0.025
x = y = np.arange(-3.0, 3.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-(X**2) - Y**2)
Z2 = np.exp(-((X - 1) ** 2) - (Y - 1) ** 2)
Z = (Z1 - Z2) * 2
fig1, ax = plt.subplots()
im = ax.imshow(
Z,
interpolation="bilinear",
cmap=cm.RdYlGn,
origin="lower",
extent=[-3, 3, -3, 3],
vmax=abs(Z).max(),
vmin=-abs(Z).max(),
)
plt.show()
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(9, 4))
# Fixing random state for reproducibility
np.random.seed(19680801)
# generate some random test data
all_data = [np.random.normal(0, std, 100) for std in range(6, 10)]
# plot violin plot
axs[0].violinplot(all_data, showmeans=False, showmedians=True)
axs[0].set_title("Violin plot")
# plot box plot
axs[1].boxplot(all_data)
axs[1].set_title("Box plot")
# adding horizontal grid lines
for ax in axs:
ax.yaxis.grid(True)
ax.set_xticks(
[y + 1 for y in range(len(all_data))],
labels=["x1", "x2", "x3", "x4"],
)
ax.set_xlabel("Four separate samples")
ax.set_ylabel("Observed values")
plt.show()
# Fixing random state for reproducibility
np.random.seed(19680801)
# Compute pie slices
N = 20
theta = np.linspace(0.0, 2 * np.pi, N, endpoint=False)
radii = 10 * np.random.rand(N)
width = np.pi / 4 * np.random.rand(N)
colors = cmo.cm.haline(radii / 10.0)
ax = plt.subplot(projection="polar")
ax.bar(theta, radii, width=width, bottom=0.0, color=colors, alpha=0.5)
plt.show()
methods = [
None,
"none",
"nearest",
"bilinear",
"bicubic",
"spline16",
"spline36",
"hanning",
"hamming",
"hermite",
"kaiser",
"quadric",
"catrom",
"gaussian",
"bessel",
"mitchell",
"sinc",
"lanczos",
]
# Fixing random state for reproducibility
np.random.seed(19680801)
grid = np.random.rand(4, 4)
fig, axs = plt.subplots(
nrows=3,
ncols=6,
figsize=(9, 6),
subplot_kw={"xticks": [], "yticks": []},
)
for ax, interp_method in zip(axs.flat, methods):
ax.imshow(grid, interpolation=interp_method)
ax.set_title(str(interp_method))
plt.tight_layout()
plt.show()

View File

@ -1,411 +0,0 @@
import cmocean as cmo
import matplotlib.pyplot as plt
import numpy as np
from cycler import cycler
from matplotlib.colors import ListedColormap
def PlotStyle() -> None:
class style:
# lightcmap = cmocean.tools.lighten(cmocean.cm.haline, 0.8)
# units
cm = 1 / 2.54
mm = 1 / 25.4
# colors
black = "#111116"
white = "#111116"
gray = "#111116"
blue = "#89b4fa"
sapphire = "#74c7ec"
sky = "#89dceb"
teal = "#94e2d5"
green = "#a6e3a1"
yellow = "#f9d67f"
orange = "#faa472"
maroon = "#eb8486"
red = "#e0e4f7"
purple = "#d89bf7"
pink = "#f59edb"
lavender = "#b4befe"
gblue1 = "#f37588"
gblue2 = "#faa472"
gblue3 = "#f9d67f"
g = "#f3626c"
@classmethod
def lims(cls, track1, track2):
"""Helper function to get frequency y axis limits from two
fundamental frequency tracks.
Args:
track1 (array): First track
track2 (array): Second track
start (int): Index for first value to be plotted
stop (int): Index for second value to be plotted
padding (int): Padding for the upper and lower limit
Returns:
lower (float): lower limit
upper (float): upper limit
"""
allfunds_tmp = (
np.concatenate(
[
track1,
track2,
]
)
.ravel()
.tolist()
)
lower = np.min(allfunds_tmp)
upper = np.max(allfunds_tmp)
return lower, upper
@classmethod
def circled_annotation(cls, text, axis, xpos, ypos, padding=0.25):
axis.text(
xpos,
ypos,
text,
ha="center",
va="center",
zorder=1000,
bbox=dict(
boxstyle=f"circle, pad={padding}",
fc="white",
ec="black",
lw=1,
),
)
@classmethod
def fade_cmap(cls, cmap):
my_cmap = cmap(np.arange(cmap.N))
my_cmap[:, -1] = np.linspace(0, 1, cmap.N)
my_cmap = ListedColormap(my_cmap)
return my_cmap
@classmethod
def hide_ax(cls, ax):
ax.xaxis.set_visible(False)
plt.setp(ax.spines.values(), visible=False)
ax.tick_params(left=False, labelleft=False)
ax.patch.set_visible(False)
@classmethod
def hide_xax(cls, ax):
ax.xaxis.set_visible(False)
ax.spines["bottom"].set_visible(False)
@classmethod
def hide_yax(cls, ax):
ax.yaxis.set_visible(False)
ax.spines["left"].set_visible(False)
@classmethod
def set_boxplot_color(cls, bp, color):
plt.setp(bp["boxes"], color=color)
plt.setp(bp["whiskers"], color=white)
plt.setp(bp["caps"], color=white)
plt.setp(bp["medians"], color=black)
@classmethod
def label_subplots(cls, labels, axes, fig):
for axis, label in zip(axes, labels):
X = axis.get_position().x0
Y = axis.get_position().y1
fig.text(X, Y, label, weight="bold")
@classmethod
def letter_subplots(
cls, axes=None, letters=None, xoffset=-0.1, yoffset=1.0, **kwargs
):
"""Add letters to the corners of subplots (panels). By default each axis is
given an uppercase bold letter label placed in the upper-left corner.
Args
axes : list of pyplot ax objects. default plt.gcf().axes.
letters : list of strings to use as labels, default ["A", "B", "C", ...]
xoffset, yoffset : positions of each label relative to plot frame
(default -0.1,1.0 = upper left margin). Can also be a list of
offsets, in which case it should be the same length as the number of
axes.
Other keyword arguments will be passed to annotate() when panel letters
are added.
Returns:
list of strings for each label added to the axes
Examples:
Defaults:
>>> fig, axes = plt.subplots(1,3)
>>> letter_subplots() # boldfaced A, B, C
Common labeling schemes inferred from the first letter:
>>> fig, axes = plt.subplots(1,4)
# panels labeled (a), (b), (c), (d)
>>> letter_subplots(letters='(a)')
Fully custom lettering:
>>> fig, axes = plt.subplots(2,1)
>>> letter_subplots(axes, letters=['(a.1)', '(b.2)'], fontweight='normal')
Per-axis offsets:
>>> fig, axes = plt.subplots(1,2)
>>> letter_subplots(axes, xoffset=[-0.1, -0.15])
Matrix of axes:
>>> fig, axes = plt.subplots(2,2, sharex=True, sharey=True)
# fig.axes is a list when axes is a 2x2 matrix
>>> letter_subplots(fig.axes)
"""
# get axes:
if axes is None:
axes = plt.gcf().axes
# handle single axes:
try:
iter(axes)
except TypeError:
axes = [axes]
# set up letter defaults (and corresponding fontweight):
fontweight = "bold"
ulets = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ"[: len(axes)])
llets = list("abcdefghijklmnopqrstuvwxyz"[: len(axes)])
if letters is None or letters == "A":
letters = ulets
elif letters == "(a)":
letters = ["({})".format(lett) for lett in llets]
fontweight = "normal"
elif letters == "(A)":
letters = ["({})".format(lett) for lett in ulets]
fontweight = "normal"
elif letters in ("lower", "lowercase", "a"):
letters = llets
# make sure there are x and y offsets for each ax in axes:
if isinstance(xoffset, (int, float)):
xoffset = [xoffset] * len(axes)
else:
assert len(xoffset) == len(axes)
if isinstance(yoffset, (int, float)):
yoffset = [yoffset] * len(axes)
else:
assert len(yoffset) == len(axes)
# defaults for annotate (kwargs is second so it can overwrite these defaults):
my_defaults = dict(
fontweight=fontweight,
fontsize="large",
ha="center",
va="center",
xycoords="axes fraction",
annotation_clip=False,
)
kwargs = dict(list(my_defaults.items()) + list(kwargs.items()))
list_txts = []
for ax, lbl, xoff, yoff in zip(axes, letters, xoffset, yoffset):
t = ax.annotate(lbl, xy=(xoff, yoff), **kwargs)
list_txts.append(t)
return list_txts
pass
# rcparams text setup
SMALL_SIZE = 12
MEDIUM_SIZE = 14
BIGGER_SIZE = 16
black = "#e0e4f7"
white = "#111116"
# gray = "#6c6e7d"
# dark_gray = "#2a2a32"
# rcparams
plt.rc("font", size=MEDIUM_SIZE) # controls default text sizes
plt.rc("axes", titlesize=MEDIUM_SIZE) # fontsize of the axes title
plt.rc("axes", labelsize=MEDIUM_SIZE) # fontsize of the x and y labels
plt.rc("xtick", labelsize=SMALL_SIZE) # fontsize of the tick labels
plt.rc("ytick", labelsize=SMALL_SIZE) # fontsize of the tick labels
plt.rc("legend", fontsize=SMALL_SIZE) # legend fontsize
plt.rc("figure", titlesize=BIGGER_SIZE) # fontsize of the figure title
plt.rcParams["image.cmap"] = "cmo.haline"
plt.rcParams["axes.xmargin"] = 0.05
plt.rcParams["axes.ymargin"] = 0.1
plt.rcParams["axes.titlelocation"] = "left"
plt.rcParams["axes.titlesize"] = BIGGER_SIZE
# plt.rcParams["axes.titlepad"] = -10
plt.rcParams["legend.frameon"] = False
plt.rcParams["legend.loc"] = "best"
plt.rcParams["legend.borderpad"] = 0.4
plt.rcParams["legend.facecolor"] = black
plt.rcParams["legend.edgecolor"] = black
plt.rcParams["legend.framealpha"] = 0.7
plt.rcParams["legend.borderaxespad"] = 0.5
plt.rcParams["legend.fancybox"] = False
# # specify the custom font to use
# plt.rcParams["font.family"] = "sans-serif"
# plt.rcParams["font.sans-serif"] = "Helvetica Now Text"
# dark mode modifications
# plt.rcParams["boxplot.flierprops.color"] = white
# plt.rcParams["boxplot.flierprops.markeredgecolor"] = gray
# plt.rcParams["boxplot.boxprops.color"] = gray
# plt.rcParams["boxplot.whiskerprops.color"] = gray
# plt.rcParams["boxplot.capprops.color"] = gray
# plt.rcParams["boxplot.medianprops.color"] = black
# plt.rcParams["text.color"] = white
# plt.rcParams["axes.facecolor"] = black # axes background color
# plt.rcParams["axes.edgecolor"] = white # axes edge color
# # plt.rcParams["axes.grid"] = True # display grid or not
# # plt.rcParams["axes.grid.axis"] = "y" # which axis the grid is applied to
# plt.rcParams["axes.labelcolor"] = white
# plt.rcParams["axes.axisbelow"] = True # draw axis gridlines and ticks:
# plt.rcParams["axes.spines.left"] = True # display axis spines
# plt.rcParams["axes.spines.bottom"] = True
# plt.rcParams["axes.spines.top"] = False
# plt.rcParams["axes.spines.right"] = False
# plt.rcParams["axes.prop_cycle"] = cycler(
# "color",
# [
# "#b4befe",
# "#89b4fa",
# "#74c7ec",
# "#89dceb",
# "#94e2d5",
# "#a6e3a1",
# "#f9e2af",
# "#fab387",
# "#eba0ac",
# "#f38ba8",
# "#cba6f7",
# "#f5c2e7",
# ],
# )
# plt.rcParams["xtick.color"] = white # color of the ticks
# plt.rcParams["ytick.color"] = white # color of the ticks
# plt.rcParams["grid.color"] = white # grid color
# plt.rcParams["figure.facecolor"] = black # figure face color
# plt.rcParams["figure.edgecolor"] = black # figure edge color
# plt.rcParams["savefig.facecolor"] = black # figure face color when saving
return style
if __name__ == "__main__":
s = PlotStyle()
import matplotlib.cbook as cbook
import matplotlib.cm as cm
import matplotlib.pyplot as plt
from matplotlib.patches import PathPatch
from matplotlib.path import Path
# Fixing random state for reproducibility
np.random.seed(19680801)
delta = 0.025
x = y = np.arange(-3.0, 3.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-(X**2) - Y**2)
Z2 = np.exp(-((X - 1) ** 2) - (Y - 1) ** 2)
Z = (Z1 - Z2) * 2
fig1, ax = plt.subplots()
im = ax.imshow(
Z,
interpolation="bilinear",
cmap=cm.RdYlGn,
origin="lower",
extent=[-3, 3, -3, 3],
vmax=abs(Z).max(),
vmin=-abs(Z).max(),
)
plt.show()
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(9, 4))
# Fixing random state for reproducibility
np.random.seed(19680801)
# generate some random test data
all_data = [np.random.normal(0, std, 100) for std in range(6, 10)]
# plot violin plot
axs[0].violinplot(all_data, showmeans=False, showmedians=True)
axs[0].set_title("Violin plot")
# plot box plot
axs[1].boxplot(all_data)
axs[1].set_title("Box plot")
# adding horizontal grid lines
for ax in axs:
ax.yaxis.grid(True)
ax.set_xticks(
[y + 1 for y in range(len(all_data))],
labels=["x1", "x2", "x3", "x4"],
)
ax.set_xlabel("Four separate samples")
ax.set_ylabel("Observed values")
plt.show()
# Fixing random state for reproducibility
np.random.seed(19680801)
# Compute pie slices
N = 20
theta = np.linspace(0.0, 2 * np.pi, N, endpoint=False)
radii = 10 * np.random.rand(N)
width = np.pi / 4 * np.random.rand(N)
colors = cmo.cm.haline(radii / 10.0)
ax = plt.subplot(projection="polar")
ax.bar(theta, radii, width=width, bottom=0.0, color=colors, alpha=0.5)
plt.show()
methods = [
None,
"none",
"nearest",
"bilinear",
"bicubic",
"spline16",
"spline36",
"hanning",
"hamming",
"hermite",
"kaiser",
"quadric",
"catrom",
"gaussian",
"bessel",
"mitchell",
"sinc",
"lanczos",
]
# Fixing random state for reproducibility
np.random.seed(19680801)
grid = np.random.rand(4, 4)
fig, axs = plt.subplots(
nrows=3,
ncols=6,
figsize=(9, 6),
subplot_kw={"xticks": [], "yticks": []},
)
for ax, interp_method in zip(axs.flat, methods):
ax.imshow(grid, interpolation=interp_method)
ax.set_title(str(interp_method))
plt.tight_layout()
plt.show()

View File

@ -37,7 +37,7 @@ def create_chirp(
ck = 0
csig = 0.5 * chirpduration / np.power(2.0 * np.log(10.0), 0.5 / kurtosis)
# csig = csig*-1
#csig = csig*-1
for k, t in enumerate(time):
a = 1.0
f = eodf

View File

@ -1,372 +0,0 @@
import os
import matplotlib.pyplot as plt
import numpy as np
from extract_chirps import get_valid_datasets
from IPython import embed
from modules.behaviour_handling import Behavior, correct_chasing_events
from modules.logger import makeLogger
from modules.plotstyle import PlotStyle
from pandas import read_csv
from scipy.stats import pearsonr, wilcoxon
ps = PlotStyle()
logger = makeLogger(__name__)
def get_chirp_winner_loser(folder_name, Behavior, order_meta_df):
foldername = folder_name.split("/")[-2]
winner_row = order_meta_df[order_meta_df["recording"] == foldername]
winner = winner_row["winner"].values[0].astype(int)
winner_fish1 = winner_row["fish1"].values[0].astype(int)
winner_fish2 = winner_row["fish2"].values[0].astype(int)
if winner > 0:
if winner == winner_fish1:
winner_fish_id = winner_row["rec_id1"].values[0]
loser_fish_id = winner_row["rec_id2"].values[0]
elif winner == winner_fish2:
winner_fish_id = winner_row["rec_id2"].values[0]
loser_fish_id = winner_row["rec_id1"].values[0]
chirp_winner = len(
Behavior.chirps[Behavior.chirps_ids == winner_fish_id]
)
chirp_loser = len(Behavior.chirps[Behavior.chirps_ids == loser_fish_id])
return chirp_winner, chirp_loser
else:
return np.nan, np.nan
def get_chirp_size(folder_name, Behavior, order_meta_df, id_meta_df):
foldername = folder_name.split("/")[-2]
folder_row = order_meta_df[order_meta_df["recording"] == foldername]
fish1 = folder_row["fish1"].values[0].astype(int)
fish2 = folder_row["fish2"].values[0].astype(int)
winner = folder_row["winner"].values[0].astype(int)
groub = folder_row["group"].values[0].astype(int)
size_fish1_row = id_meta_df[
(id_meta_df["group"] == groub) & (id_meta_df["fish"] == fish1)
]
size_fish2_row = id_meta_df[
(id_meta_df["group"] == groub) & (id_meta_df["fish"] == fish2)
]
size_winners = [size_fish1_row[col].values[0] for col in ["l1", "l2", "l3"]]
size_fish1 = np.nanmean(size_winners)
size_losers = [size_fish2_row[col].values[0] for col in ["l1", "l2", "l3"]]
size_fish2 = np.nanmean(size_losers)
if winner == fish1:
if size_fish1 > size_fish2:
size_diff_bigger = size_fish1 - size_fish2
size_diff_smaller = size_fish2 - size_fish1
elif size_fish1 < size_fish2:
size_diff_bigger = size_fish1 - size_fish2
size_diff_smaller = size_fish2 - size_fish1
else:
size_diff_bigger = 0
size_diff_smaller = 0
winner_fish_id = folder_row["rec_id1"].values[0]
loser_fish_id = folder_row["rec_id2"].values[0]
elif winner == fish2:
if size_fish2 > size_fish1:
size_diff_bigger = size_fish2 - size_fish1
size_diff_smaller = size_fish1 - size_fish2
elif size_fish2 < size_fish1:
size_diff_bigger = size_fish2 - size_fish1
size_diff_smaller = size_fish1 - size_fish2
else:
size_diff_bigger = 0
size_diff_smaller = 0
winner_fish_id = folder_row["rec_id2"].values[0]
loser_fish_id = folder_row["rec_id1"].values[0]
else:
size_diff_bigger = np.nan
size_diff_smaller = np.nan
winner_fish_id = np.nan
loser_fish_id = np.nan
return (
size_diff_bigger,
size_diff_smaller,
winner_fish_id,
loser_fish_id,
)
chirp_winner = len(Behavior.chirps[Behavior.chirps_ids == winner_fish_id])
chirp_loser = len(Behavior.chirps[Behavior.chirps_ids == loser_fish_id])
return size_diff_bigger, chirp_winner, size_diff_smaller, chirp_loser
def get_chirp_freq(folder_name, Behavior, order_meta_df):
foldername = folder_name.split("/")[-2]
folder_row = order_meta_df[order_meta_df["recording"] == foldername]
fish1 = folder_row["fish1"].values[0].astype(int)
fish2 = folder_row["fish2"].values[0].astype(int)
fish1_freq = folder_row["rec_id1"].values[0].astype(int)
fish2_freq = folder_row["rec_id2"].values[0].astype(int)
chirp_freq_fish1 = np.nanmedian(Behavior.freq[Behavior.ident == fish1_freq])
chirp_freq_fish2 = np.nanmedian(Behavior.freq[Behavior.ident == fish2_freq])
winner = folder_row["winner"].values[0].astype(int)
if winner == fish1:
# if chirp_freq_fish1 > chirp_freq_fish2:
# freq_diff_higher = chirp_freq_fish1 - chirp_freq_fish2
# freq_diff_lower = chirp_freq_fish2 - chirp_freq_fish1
# elif chirp_freq_fish1 < chirp_freq_fish2:
# freq_diff_higher = chirp_freq_fish1 - chirp_freq_fish2
# freq_diff_lower = chirp_freq_fish2 - chirp_freq_fish1
# else:
# freq_diff_higher = np.nan
# freq_diff_lower = np.nan
# winner_fish_id = np.nan
# loser_fish_id = np.nan
winner_fish_id = folder_row["rec_id1"].values[0]
winner_fish_freq = chirp_freq_fish1
loser_fish_id = folder_row["rec_id2"].values[0]
loser_fish_freq = chirp_freq_fish2
elif winner == fish2:
# if chirp_freq_fish2 > chirp_freq_fish1:
# freq_diff_higher = chirp_freq_fish2 - chirp_freq_fish1
# freq_diff_lower = chirp_freq_fish1 - chirp_freq_fish2
# elif chirp_freq_fish2 < chirp_freq_fish1:
# freq_diff_higher = chirp_freq_fish2 - chirp_freq_fish1
# freq_diff_lower = chirp_freq_fish1 - chirp_freq_fish2
# else:
# freq_diff_higher = np.nan
# freq_diff_lower = np.nan
# winner_fish_id = np.nan
# loser_fish_id = np.nan
winner_fish_id = folder_row["rec_id2"].values[0]
winner_fish_freq = chirp_freq_fish2
loser_fish_id = folder_row["rec_id1"].values[0]
loser_fish_freq = chirp_freq_fish1
else:
winner_fish_freq = np.nan
loser_fish_freq = np.nan
winner_fish_id = np.nan
loser_fish_id = np.nan
return winner_fish_freq, winner_fish_id, loser_fish_freq, loser_fish_id
chirp_winner = len(Behavior.chirps[Behavior.chirps_ids == winner_fish_id])
chirp_loser = len(Behavior.chirps[Behavior.chirps_ids == loser_fish_id])
return winner_fish_freq, chirp_winner, loser_fish_freq, chirp_loser
def main(datapath: str):
foldernames = [
datapath + x + "/"
for x in os.listdir(datapath)
if os.path.isdir(datapath + x)
]
foldernames, _ = get_valid_datasets(datapath)
path_order_meta = ("/").join(
foldernames[0].split("/")[:-2]
) + "/order_meta.csv"
order_meta_df = read_csv(path_order_meta)
order_meta_df["recording"] = order_meta_df["recording"].str[1:-1]
path_id_meta = ("/").join(foldernames[0].split("/")[:-2]) + "/id_meta.csv"
id_meta_df = read_csv(path_id_meta)
chirps_winner = []
chirps_loser = []
size_diffs_winner = []
size_diffs_loser = []
size_chirps_winner = []
size_chirps_loser = []
freq_diffs_higher = []
freq_diffs_lower = []
freq_chirps_winner = []
freq_chirps_loser = []
for foldername in foldernames:
# behabvior is pandas dataframe with all the data
if foldername == "../data/mount_data/2020-05-12-10_00/":
continue
bh = Behavior(foldername)
# chirps are not sorted in time (presumably due to prior groupings)
# get and sort chirps and corresponding fish_ids of the chirps
category = bh.behavior
timestamps = bh.start_s
# Correct for doubles in chasing on- and offsets to get the right on-/offset pairs
# Get rid of tracking faults (two onsets or two offsets after another)
category, timestamps = correct_chasing_events(category, timestamps)
winner_chirp, loser_chirp = get_chirp_winner_loser(
foldername, bh, order_meta_df
)
chirps_winner.append(winner_chirp)
chirps_loser.append(loser_chirp)
(
size_diff_bigger,
chirp_winner,
size_diff_smaller,
chirp_loser,
) = get_chirp_size(foldername, bh, order_meta_df, id_meta_df)
(
freq_winner,
chirp_freq_winner,
freq_loser,
chirp_freq_loser,
) = get_chirp_freq(foldername, bh, order_meta_df)
freq_diffs_higher.append(freq_winner)
freq_diffs_lower.append(freq_loser)
freq_chirps_winner.append(chirp_freq_winner)
freq_chirps_loser.append(chirp_freq_loser)
if np.isnan(size_diff_bigger):
continue
size_diffs_winner.append(size_diff_bigger)
size_diffs_loser.append(size_diff_smaller)
size_chirps_winner.append(chirp_winner)
size_chirps_loser.append(chirp_loser)
pearsonr(size_diffs_winner, size_chirps_winner)
pearsonr(size_diffs_loser, size_chirps_loser)
fig, (ax1, ax2, ax3) = plt.subplots(
1,
3,
figsize=(21 * ps.cm, 7 * ps.cm),
width_ratios=[1, 0.8, 0.8],
sharey=True,
)
plt.subplots_adjust(
left=0.11, right=0.948, top=0.86, wspace=0.343, bottom=0.198
)
scatterwinner = 1.15
scatterloser = 1.85
chirps_winner = np.asarray(chirps_winner)[~np.isnan(chirps_winner)]
chirps_loser = np.asarray(chirps_loser)[~np.isnan(chirps_loser)]
embed()
exit()
freq_diffs_higher = np.asarray(freq_diffs_higher)[
~np.isnan(freq_diffs_higher)
]
freq_diffs_lower = np.asarray(freq_diffs_lower)[~np.isnan(freq_diffs_lower)]
freq_chirps_winner = np.asarray(freq_chirps_winner)[
~np.isnan(freq_chirps_winner)
]
freq_chirps_loser = np.asarray(freq_chirps_loser)[
~np.isnan(freq_chirps_loser)
]
stat = wilcoxon(chirps_winner, chirps_loser)
print(stat)
winner_color = ps.gblue2
loser_color = ps.gblue1
bplot1 = ax1.boxplot(
chirps_winner, positions=[0.9], showfliers=False, patch_artist=True
)
bplot2 = ax1.boxplot(
chirps_loser, positions=[2.1], showfliers=False, patch_artist=True
)
ax1.scatter(
np.ones(len(chirps_winner)) * scatterwinner,
chirps_winner,
color=winner_color,
)
ax1.scatter(
np.ones(len(chirps_loser)) * scatterloser,
chirps_loser,
color=loser_color,
)
ax1.set_xticklabels(["Winner", "Loser"])
ax1.text(
0.1,
0.85,
f"n={len(chirps_loser)}",
transform=ax1.transAxes,
color=ps.white,
)
for w, l in zip(chirps_winner, chirps_loser):
ax1.plot(
[scatterwinner, scatterloser],
[w, l],
color=ps.white,
alpha=0.6,
linewidth=1,
zorder=-1,
)
ax1.set_ylabel("Chirp counts", color=ps.white)
ax1.set_xlabel("Competition outcome", color=ps.white)
ps.set_boxplot_color(bplot1, winner_color)
ps.set_boxplot_color(bplot2, loser_color)
ax2.scatter(
size_diffs_winner,
size_chirps_winner,
color=winner_color,
label="Winner",
)
ax2.scatter(
size_diffs_loser, size_chirps_loser, color=loser_color, label="Loser"
)
ax2.text(
0.05,
0.85,
f"n={len(size_chirps_loser)}",
transform=ax2.transAxes,
color=ps.white,
)
ax2.set_xlabel("Size difference [cm]")
# ax2.set_xticks(np.arange(-10, 10.1, 2))
ax3.scatter(freq_diffs_higher, freq_chirps_winner, color=winner_color)
ax3.scatter(freq_diffs_lower, freq_chirps_loser, color=loser_color)
ax3.text(
0.1,
0.85,
f"n={len(np.asarray(freq_chirps_winner)[~np.isnan(freq_chirps_loser)])}",
transform=ax3.transAxes,
color=ps.white,
)
ax3.set_xlabel("EODf [Hz]")
handles, labels = ax2.get_legend_handles_labels()
fig.legend(
handles, labels, loc="upper center", ncol=2, bbox_to_anchor=(0.5, 1.04)
)
# pearson r
plt.savefig("../poster/figs/chirps_winner_loser.pdf")
plt.show()
if __name__ == "__main__":
# Path to the data
datapath = "../data/mount_data/"
main(datapath)

View File

@ -1,117 +0,0 @@
import numpy as np
import os
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import pearsonr, spearmanr
from thunderfish.powerspectrum import decibel
from IPython import embed
from pandas import read_csv
from modules.logger import makeLogger
from modules.plotstyle import PlotStyle
from modules.behaviour_handling import Behavior, correct_chasing_events
from modules.datahandling import flatten
ps = PlotStyle()
logger = makeLogger(__name__)
def main(datapath: str):
foldernames = [
datapath + x + "/"
for x in os.listdir(datapath)
if os.path.isdir(datapath + x)
]
time_precents = []
chirps_percents = []
for foldername in foldernames:
# behabvior is pandas dataframe with all the data
if foldername == "../data/mount_data/2020-05-12-10_00/":
continue
bh = Behavior(foldername)
category = bh.behavior
timestamps = bh.start_s
# Correct for doubles in chasing on- and offsets to get the right on-/offset pairs
# Get rid of tracking faults (two onsets or two offsets after another)
category, timestamps = correct_chasing_events(category, timestamps)
chasing_onset = timestamps[category == 0]
chasing_offset = timestamps[category == 1]
if len(chasing_onset) != len(chasing_offset):
embed()
chirps_in_chasings = []
for onset, offset in zip(chasing_onset, chasing_offset):
chirps_in_chasing = [
c for c in bh.chirps if (c > onset) & (c < offset)
]
chirps_in_chasings.append(chirps_in_chasing)
try:
time_chasing = np.sum(
chasing_offset[chasing_offset < 3 * 60 * 60]
- chasing_onset[chasing_onset < 3 * 60 * 60]
)
except:
time_chasing = np.sum(
chasing_offset[chasing_offset < 3 * 60 * 60]
- chasing_onset[chasing_onset < 3 * 60 * 60][:-1]
)
time_chasing_percent = (time_chasing / (3 * 60 * 60)) * 100
chirps_chasing = np.asarray(flatten(chirps_in_chasings))
chirps_chasing_new = chirps_chasing[chirps_chasing < 3 * 60 * 60]
chirps_percent = (
len(chirps_chasing_new) / len(bh.chirps[bh.chirps < 3 * 60 * 60])
) * 100
time_precents.append(time_chasing_percent)
chirps_percents.append(chirps_percent)
fig, ax = plt.subplots(1, 1, figsize=(7 * ps.cm, 7 * ps.cm))
scatter_time = 1.20
scatter_chirps = 1.80
size = 10
bplot1 = ax.boxplot(
[time_precents, chirps_percents], showfliers=False, patch_artist=True
)
ps.set_boxplot_color(bplot1, ps.gray)
ax.set_xticklabels(["Time \nchasing", "Chirps \nin chasing"])
ax.set_ylabel("Percent")
ax.scatter(
np.ones(len(time_precents)) * scatter_time,
time_precents,
facecolor=ps.white,
s=size,
)
ax.scatter(
np.ones(len(chirps_percents)) * scatter_chirps,
chirps_percents,
facecolor=ps.white,
s=size,
)
for i in range(len(time_precents)):
ax.plot(
[scatter_time, scatter_chirps],
[time_precents[i], chirps_percents[i]],
alpha=0.6,
linewidth=1,
color=ps.white,
)
ax.text(0.1, 0.9, f"n={len(time_precents)}", transform=ax.transAxes)
plt.subplots_adjust(left=0.221, bottom=0.186, right=0.97, top=0.967)
plt.savefig("../poster/figs/chirps_in_chasing.pdf")
plt.show()
if __name__ == "__main__":
# Path to the data
datapath = "../data/mount_data/"
main(datapath)

View File

@ -1,160 +1,203 @@
import numpy as np
import os
import os
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.pyplot as plt
from thunderfish.powerspectrum import decibel
from IPython import embed
from pandas import read_csv
from modules.logger import makeLogger
from modules.plotstyle import PlotStyle
from modules.behaviour_handling import Behavior, correct_chasing_events
from extract_chirps import get_valid_datasets
ps = PlotStyle()
logger = makeLogger(__name__)
class Behavior:
"""Load behavior data from csv file as class attributes
Attributes
----------
behavior: 0: chasing onset, 1: chasing offset, 2: physical contact
behavior_type:
behavioral_category:
comment_start:
comment_stop:
dataframe: pandas dataframe with all the data
duration_s:
media_file:
observation_date:
observation_id:
start_s: start time of the event in seconds
stop_s: stop time of the event in seconds
total_length:
"""
def __init__(self, folder_path: str) -> None:
LED_on_time_BORIS = np.load(os.path.join(folder_path, 'LED_on_time.npy'), allow_pickle=True)
csv_filename = [f for f in os.listdir(folder_path) if f.endswith('.csv')][0]
logger.info(f'CSV file: {csv_filename}')
self.dataframe = read_csv(os.path.join(folder_path, csv_filename))
self.chirps = np.load(os.path.join(folder_path, 'chirps.npy'), allow_pickle=True)
self.chirps_ids = np.load(os.path.join(folder_path, 'chirps_ids.npy'), allow_pickle=True)
self.ident = np.load(os.path.join(folder_path, 'ident_v.npy'), allow_pickle=True)
self.idx = np.load(os.path.join(folder_path, 'idx_v.npy'), allow_pickle=True)
self.freq = np.load(os.path.join(folder_path, 'fund_v.npy'), allow_pickle=True)
self.time = np.load(os.path.join(folder_path, "times.npy"), allow_pickle=True)
self.spec = np.load(os.path.join(folder_path, "spec.npy"), allow_pickle=True)
for k, key in enumerate(self.dataframe.keys()):
key = key.lower()
if ' ' in key:
key = key.replace(' ', '_')
if '(' in key:
key = key.replace('(', '')
key = key.replace(')', '')
setattr(self, key, np.array(self.dataframe[self.dataframe.keys()[k]]))
last_LED_t_BORIS = LED_on_time_BORIS[-1]
real_time_range = self.time[-1] - self.time[0]
factor = 1.034141
shift = last_LED_t_BORIS - real_time_range * factor
self.start_s = (self.start_s - shift) / factor
self.stop_s = (self.stop_s - shift) / factor
def correct_chasing_events(
category: np.ndarray,
timestamps: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
onset_ids = np.arange(
len(category))[category == 0]
offset_ids = np.arange(
len(category))[category == 1]
# Check whether on- or offset is longer and calculate length difference
if len(onset_ids) > len(offset_ids):
len_diff = len(onset_ids) - len(offset_ids)
longer_array = onset_ids
shorter_array = offset_ids
logger.info(f'Onsets are greater than offsets by {len_diff}')
elif len(onset_ids) < len(offset_ids):
len_diff = len(offset_ids) - len(onset_ids)
longer_array = offset_ids
shorter_array = onset_ids
logger.info(f'Offsets are greater than offsets by {len_diff}')
elif len(onset_ids) == len(offset_ids):
logger.info('Chasing events are equal')
return category, timestamps
# Correct the wrong chasing events; delete double events
wrong_ids = []
for i in range(len(longer_array)-(len_diff+1)):
if (shorter_array[i] > longer_array[i]) & (shorter_array[i] < longer_array[i+1]):
pass
else:
wrong_ids.append(longer_array[i])
longer_array = np.delete(longer_array, i)
category = np.delete(
category, wrong_ids)
timestamps = np.delete(
timestamps, wrong_ids)
return category, timestamps
def main(datapath: str):
foldernames = [
datapath + x + "/"
for x in os.listdir(datapath)
if os.path.isdir(datapath + x)
]
foldernames, _ = get_valid_datasets(datapath)
for foldername in foldernames[3:4]:
print(foldername)
# foldername = foldernames[0]
if foldername == "../data/mount_data/2020-05-12-10_00/":
continue
# behabvior is pandas dataframe with all the data
bh = Behavior(foldername)
# 2020-06-11-10
category = bh.behavior
timestamps = bh.start_s
# Correct for doubles in chasing on- and offsets to get the right on-/offset pairs
# Get rid of tracking faults (two onsets or two offsets after another)
category, timestamps = correct_chasing_events(category, timestamps)
# split categories
chasing_onset = (timestamps[category == 0] / 60) / 60
chasing_offset = (timestamps[category == 1] / 60) / 60
physical_contact = (timestamps[category == 2] / 60) / 60
all_fish_ids = np.unique(bh.chirps_ids)
fish1_id = all_fish_ids[0]
fish2_id = all_fish_ids[1]
# Associate chirps to inidividual fish
fish1 = (bh.chirps[bh.chirps_ids == fish1_id] / 60) / 60
fish2 = (bh.chirps[bh.chirps_ids == fish2_id] / 60) / 60
embed()
exit()
fish1_color = ps.gblue2
fish2_color = ps.gblue1
fig, ax = plt.subplots(
5,
1,
figsize=(21 * ps.cm, 10 * ps.cm),
height_ratios=[0.5, 0.5, 0.5, 0.2, 6],
sharex=True,
)
# marker size
s = 80
ax[0].scatter(
physical_contact,
np.ones(len(physical_contact)),
color=ps.gray,
marker="|",
s=s,
)
ax[1].scatter(
chasing_onset,
np.ones(len(chasing_onset)),
color=ps.gray,
marker="|",
s=s,
)
ax[2].scatter(
fish1,
np.ones(len(fish1)) - 0.25,
color=fish1_color,
marker="|",
s=s,
)
ax[2].scatter(
fish2,
np.zeros(len(fish2)) + 0.25,
color=fish2_color,
marker="|",
s=s,
)
freq_temp = bh.freq[bh.ident == fish1_id]
time_temp = bh.time[bh.idx[bh.ident == fish1_id]]
ax[4].plot((time_temp / 60) / 60, freq_temp, color=fish1_color)
freq_temp = bh.freq[bh.ident == fish2_id]
time_temp = bh.time[bh.idx[bh.ident == fish2_id]]
ax[4].plot((time_temp / 60) / 60, freq_temp, color=fish2_color)
# ax[3].imshow(decibel(bh.spec), extent=[bh.time[0]/60/60, bh.time[-1]/60/60, 0, 2000], aspect='auto', origin='lower')
# behabvior is pandas dataframe with all the data
bh = Behavior(datapath)
# chirps are not sorted in time (presumably due to prior groupings)
# get and sort chirps and corresponding fish_ids of the chirps
chirps = bh.chirps[np.argsort(bh.chirps)]
chirps_fish_ids = bh.chirps_ids[np.argsort(bh.chirps)]
category = bh.behavior
timestamps = bh.start_s
# Correct for doubles in chasing on- and offsets to get the right on-/offset pairs
# Get rid of tracking faults (two onsets or two offsets after another)
category, timestamps = correct_chasing_events(category, timestamps)
# split categories
chasing_onset = (timestamps[category == 0]/ 60) /60
chasing_offset = (timestamps[category == 1]/ 60) /60
physical_contact = (timestamps[category == 2] / 60) /60
all_fish_ids = np.unique(chirps_fish_ids)
fish1_id = all_fish_ids[0]
fish2_id = all_fish_ids[1]
# Associate chirps to inidividual fish
fish1 = (chirps[chirps_fish_ids == fish1_id] / 60) /60
fish2 = (chirps[chirps_fish_ids == fish2_id] / 60) /60
fish1_color = ps.red
fish2_color = ps.orange
fig, ax = plt.subplots(4, 1, figsize=(10, 5), height_ratios=[0.5, 0.5, 0.5, 6], sharex=True)
# marker size
s = 200
ax[0].scatter(physical_contact, np.ones(len(physical_contact)), color='firebrick', marker='|', s=s)
ax[1].scatter(chasing_onset, np.ones(len(chasing_onset)), color='green', marker='|', s=s )
ax[2].scatter(fish1, np.ones(len(fish1))-0.25, color=fish1_color, marker='|', s=s)
ax[2].scatter(fish2, np.zeros(len(fish2))+0.25, color=fish2_color, marker='|', s=s)
freq_temp = bh.freq[bh.ident==fish1_id]
time_temp = bh.time[bh.idx[bh.ident==fish1_id]]
ax[3].plot((time_temp/ 60) /60, freq_temp, color=fish1_color)
freq_temp = bh.freq[bh.ident==fish2_id]
time_temp = bh.time[bh.idx[bh.ident==fish2_id]]
ax[3].plot((time_temp/ 60) /60, freq_temp, color=fish2_color)
#ax[3].imshow(decibel(bh.spec), extent=[bh.time[0]/60/60, bh.time[-1]/60/60, 0, 2000], aspect='auto', origin='lower')
# Hide grid lines
ax[0].grid(False)
ax[0].set_frame_on(False)
ax[0].set_xticks([])
ax[0].set_yticks([])
ps.hide_ax(ax[0])
ax[1].grid(False)
ax[1].set_frame_on(False)
ax[1].set_xticks([])
ax[1].set_yticks([])
ps.hide_ax(ax[1])
ax[2].grid(False)
ax[2].set_frame_on(False)
ax[2].set_yticks([])
ax[2].set_xticks([])
ps.hide_ax(ax[2])
ax[4].axvspan(0, 3, 0, 5, facecolor="grey", alpha=0.5)
ax[4].set_xticks(np.arange(0, 6.1, 0.5))
ps.hide_ax(ax[3])
labelpad = 30
fsize = 12
ax[0].set_ylabel(
"Contact", rotation=0, labelpad=labelpad, fontsize=fsize
)
ax[0].yaxis.set_label_coords(-0.062, -0.08)
ax[1].set_ylabel(
"Chasing", rotation=0, labelpad=labelpad, fontsize=fsize
)
ax[1].yaxis.set_label_coords(-0.06, -0.08)
ax[2].set_ylabel(
"Chirps", rotation=0, labelpad=labelpad, fontsize=fsize
)
ax[2].yaxis.set_label_coords(-0.07, -0.08)
ax[4].set_ylabel("EODf")
ax[4].set_xlabel("Time [h]")
# ax[0].set_title(foldername.split('/')[-2])
# 2020-03-31-9_59
plt.subplots_adjust(left=0.158, right=0.987, top=0.918, bottom=0.136)
plt.savefig("../poster/figs/timeline.svg")
plt.show()
ax[0].grid(False)
ax[0].set_frame_on(False)
ax[0].set_xticks([])
ax[0].set_yticks([])
ps.hide_ax(ax[0])
ax[1].grid(False)
ax[1].set_frame_on(False)
ax[1].set_xticks([])
ax[1].set_yticks([])
ps.hide_ax(ax[1])
ax[2].grid(False)
ax[2].set_frame_on(False)
ax[2].set_yticks([])
ax[2].set_xticks([])
ps.hide_ax(ax[2])
ax[3].axvspan(0, 3, 0, 5, facecolor='grey', alpha=0.5)
ax[3].set_xticks(np.arange(0, 6.1, 0.5))
labelpad = 40
ax[0].set_ylabel('Physical contact', rotation=0, labelpad=labelpad)
ax[1].set_ylabel('Chasing events', rotation=0, labelpad=labelpad)
ax[2].set_ylabel('Chirps', rotation=0, labelpad=labelpad)
ax[3].set_ylabel('EODf')
ax[3].set_xlabel('Time [h]')
plt.show()
embed()
# plot chirps
if __name__ == "__main__":
if __name__ == '__main__':
# Path to the data
datapath = "../data/mount_data/"
datapath = '../data/mount_data/2020-05-13-10_00/'
main(datapath)

View File

@ -11,45 +11,43 @@ ps = PlotStyle()
def main():
# Load data
datapath = "../data/2022-06-02-10_00/"
data = LoadData(datapath)
# good chirp times for data: 2022-06-02-10_00
window_start_seconds = 3 * 60 * 60 + 6 * 60 + 43.5 + 9 + 6.20
window_start_seconds = 3 * 60 * 60 + 6 * 60 + 43.5 + 9 + 6.25
window_start_index = window_start_seconds * data.raw_rate
window_duration_seconds = 0.4
window_duration_seconds = 0.2
window_duration_index = window_duration_seconds * data.raw_rate
timescaler = 1000
raw = data.raw[
window_start_index : window_start_index + window_duration_index, 10
]
raw = data.raw[window_start_index:window_start_index +
window_duration_index, 10]
fig, (ax1, ax2) = plt.subplots(
1, 2, figsize=(21 * ps.cm, 8 * ps.cm), sharex=True, sharey=True
)
fig, (ax1, ax2, ax3) = plt.subplots(
3, 1, figsize=(12 * ps.cm, 10*ps.cm), sharex=True, sharey=True)
# plot instantaneous frequency
filtered1 = bandpass_filter(
signal=raw, lowf=750, highf=1200, samplerate=data.raw_rate
)
signal=raw, lowf=750, highf=1200, samplerate=data.raw_rate)
filtered2 = bandpass_filter(
signal=raw, lowf=550, highf=700, samplerate=data.raw_rate
)
signal=raw, lowf=550, highf=700, samplerate=data.raw_rate)
freqtime1, freq1 = instantaneous_frequency(
filtered1, data.raw_rate, smoothing_window=3
)
filtered1, data.raw_rate, smoothing_window=3)
freqtime2, freq2 = instantaneous_frequency(
filtered2, data.raw_rate, smoothing_window=3
)
filtered2, data.raw_rate, smoothing_window=3)
ax1.plot(freqtime1 * timescaler, freq1, color=ps.g, lw=2, label="Fish 1")
ax1.plot(freqtime2 * timescaler, freq2, color=ps.gray, lw=2, label="Fish 2")
# ax.legend(bbox_to_anchor=(1.04, 1), borderaxespad=0)
# # ps.hide_xax(ax1)
ax1.plot(freqtime1*timescaler, freq1, color=ps.gblue1,
lw=2, label=f"fish 1, {np.median(freq1):.0f} Hz")
ax1.plot(freqtime2*timescaler, freq2, color=ps.gblue3,
lw=2, label=f"fish 2, {np.median(freq2):.0f} Hz")
ax1.legend(bbox_to_anchor=(0, 1.02, 1, 0.2), loc="lower center",
mode="normal", borderaxespad=0, ncol=2)
ps.hide_xax(ax1)
# plot fine spectrogram
spec_power, spec_freqs, spec_times = spectrogram(
@ -59,15 +57,15 @@ def main():
overlap_frac=0.2,
)
ylims = [300, 1300]
ylims = [300, 1200]
fmask = np.zeros(spec_freqs.shape, dtype=bool)
fmask[(spec_freqs > ylims[0]) & (spec_freqs < ylims[1])] = True
ax1.imshow(
ax2.imshow(
decibel(spec_power[fmask, :]),
extent=[
spec_times[0] * timescaler,
spec_times[-1] * timescaler,
spec_times[0]*timescaler,
spec_times[-1]*timescaler,
spec_freqs[fmask][0],
spec_freqs[fmask][-1],
],
@ -75,24 +73,23 @@ def main():
origin="lower",
interpolation="gaussian",
alpha=1,
# vmin=-100,
# vmax=-80,
)
ps.hide_xax(ax2)
# # plot coarse spectrogram
# plot coarse spectrogram
spec_power, spec_freqs, spec_times = spectrogram(
raw,
ratetime=data.raw_rate,
freq_resolution=15,
freq_resolution=10,
overlap_frac=0.3,
)
fmask = np.zeros(spec_freqs.shape, dtype=bool)
fmask[(spec_freqs > ylims[0]) & (spec_freqs < ylims[1])] = True
ax2.imshow(
ax3.imshow(
decibel(spec_power[fmask, :]),
extent=[
spec_times[0] * timescaler,
spec_times[-1] * timescaler,
spec_times[0]*timescaler,
spec_times[-1]*timescaler,
spec_freqs[fmask][0],
spec_freqs[fmask][-1],
],
@ -102,32 +99,23 @@ def main():
alpha=1,
)
# ps.hide_xax(ax3)
ax2.plot(freqtime1 * timescaler, freq1, color=ps.g, lw=2, label="_")
ax2.plot(freqtime2 * timescaler, freq2, color=ps.gray, lw=2, label="_")
ax2.set_xlim(75, 200)
ax1.set_ylim(400, 1200)
fig.supxlabel("Time [ms]", fontsize=14)
fig.supylabel("Frequency [Hz]", fontsize=14)
handles, labels = ax1.get_legend_handles_labels()
ax2.legend(
handles,
labels,
bbox_to_anchor=(1.04, 1),
loc="upper left",
ncol=1,
)
ps.letter_subplots(xoffset=[-0.27, -0.1], yoffset=1.05)
ax3.set_xlabel("time [ms]")
ax2.set_ylabel("frequency [Hz]")
plt.subplots_adjust(
left=0.12, right=0.85, top=0.89, bottom=0.18, hspace=0.35
)
ax1.set_yticks(np.arange(400, 1201, 400))
ax1.spines.left.set_bounds((400, 1200))
ax2.set_yticks(np.arange(400, 1201, 400))
ax2.spines.left.set_bounds((400, 1200))
ax3.set_yticks(np.arange(400, 1201, 400))
ax3.spines.left.set_bounds((400, 1200))
plt.subplots_adjust(left=0.17, right=0.98, top=0.9,
bottom=0.14, hspace=0.35)
plt.savefig("../poster/figs/introplot.pdf")
plt.savefig('../poster/figs/introplot.pdf')
plt.show()
if __name__ == "__main__":
if __name__ == '__main__':
main()

View File

@ -1,561 +0,0 @@
from modules.plotstyle import PlotStyle
from modules.behaviour_handling import (
Behavior,
correct_chasing_events,
center_chirps,
)
from modules.datahandling import flatten, causal_kde1d, acausal_kde1d
from modules.logger import makeLogger
from pandas import read_csv
from IPython import embed
from tqdm import tqdm
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import os
from extract_chirps import get_valid_datasets
logger = makeLogger(__name__)
ps = PlotStyle()
def bootstrap(
data,
nresamples,
kde_time,
kernel_width,
event_times,
time_before,
time_after,
):
bootstrapped_kdes = []
data = data[data <= 3 * 60 * 60] # only night time
diff_data = np.diff(np.sort(data), prepend=0)
# if len(data) != 0:
# mean_chirprate = (len(data) - 1) / (data[-1] - data[0])
for i in tqdm(range(nresamples)):
np.random.shuffle(diff_data)
bootstrapped_data = np.cumsum(diff_data)
# bootstrapped_data = data + np.random.randn(len(data)) * 10
bootstrap_data_centered = center_chirps(
bootstrapped_data, event_times, time_before, time_after
)
bootstrapped_kde = acausal_kde1d(
bootstrap_data_centered, time=kde_time, width=kernel_width
)
bootstrapped_kde = list(np.asarray(bootstrapped_kde) / len(event_times))
bootstrapped_kdes.append(bootstrapped_kde)
return bootstrapped_kdes
def jackknife(
data,
nresamples,
subsetsize,
kde_time,
kernel_width,
event_times,
time_before,
time_after,
):
jackknife_kdes = []
data = data[data <= 3 * 60 * 60] # only night time
subsetsize = int(len(data) * subsetsize)
diff_data = np.diff(np.sort(data), prepend=0)
for i in tqdm(range(nresamples)):
jackknifed_data = np.random.choice(diff_data, subsetsize, replace=False)
jackknifed_data = np.cumsum(jackknifed_data)
jackknifed_data_centered = center_chirps(
jackknifed_data, event_times, time_before, time_after
)
jackknifed_kde = acausal_kde1d(
jackknifed_data_centered, time=kde_time, width=kernel_width
)
jackknifed_kde = list(np.asarray(jackknifed_kde) / len(event_times))
jackknife_kdes.append(jackknifed_kde)
return jackknife_kdes
def get_chirp_winner_loser(folder_name, Behavior, order_meta_df):
foldername = folder_name.split("/")[-2]
winner_row = order_meta_df[order_meta_df["recording"] == foldername]
winner = winner_row["winner"].values[0].astype(int)
winner_fish1 = winner_row["fish1"].values[0].astype(int)
winner_fish2 = winner_row["fish2"].values[0].astype(int)
if winner > 0:
if winner == winner_fish1:
winner_fish_id = winner_row["rec_id1"].values[0]
loser_fish_id = winner_row["rec_id2"].values[0]
elif winner == winner_fish2:
winner_fish_id = winner_row["rec_id2"].values[0]
loser_fish_id = winner_row["rec_id1"].values[0]
chirp_winner = Behavior.chirps[Behavior.chirps_ids == winner_fish_id]
chirp_loser = Behavior.chirps[Behavior.chirps_ids == loser_fish_id]
return chirp_winner, chirp_loser
return None, None
def main(dataroot):
foldernames, _ = np.asarray(get_valid_datasets(dataroot))
plot_all = True
time_before = 90
time_after = 90
dt = 0.001
kernel_width = 2
kde_time = np.arange(-time_before, time_after, dt)
nbootstraps = 50
meta_path = ("/").join(foldernames[0].split("/")[:-2]) + "/order_meta.csv"
meta = pd.read_csv(meta_path)
meta["recording"] = meta["recording"].str[1:-1]
winner_onsets = []
winner_offsets = []
winner_physicals = []
loser_onsets = []
loser_offsets = []
loser_physicals = []
winner_onsets_boot = []
winner_offsets_boot = []
winner_physicals_boot = []
loser_onsets_boot = []
loser_offsets_boot = []
loser_physicals_boot = []
onset_count = 0
offset_count = 0
physical_count = 0
# winner_count = 0
# loser_count = 0
# winner_onset_chirpcount = 0
# winner_offset_chirpcount = 0
# winner_physical_chirpcount = 0
# loser_onset_chirpcount = 0
# loser_offset_chirpcount = 0
# loser_physical_chirpcount = 0
fig, ax = plt.subplots(
1, 2, figsize=(14 * ps.cm, 7 * ps.cm), sharey=True, sharex=True
)
# Iterate over all recordings and save chirp- and event-timestamps
good_recs = np.asarray([0, 15])
for i, folder in tqdm(enumerate(foldernames[good_recs])):
foldername = folder.split("/")[-2]
# logger.info('Loading data from folder: {}'.format(foldername))
broken_folders = ["../data/mount_data/2020-05-12-10_00/"]
if folder in broken_folders:
continue
bh = Behavior(folder)
category, timestamps = correct_chasing_events(bh.behavior, bh.start_s)
category = category[timestamps < 3 * 60 * 60] # only night time
timestamps = timestamps[timestamps < 3 * 60 * 60] # only night time
winner, loser = get_chirp_winner_loser(folder, bh, meta)
if winner is None:
continue
# winner_count += len(winner)
# loser_count += len(loser)
onsets = timestamps[category == 0]
offsets = timestamps[category == 1]
physicals = timestamps[category == 2]
onset_count += len(onsets)
offset_count += len(offsets)
physical_count += len(physicals)
winner_onsets.append(
center_chirps(winner, onsets, time_before, time_after)
)
winner_offsets.append(
center_chirps(winner, offsets, time_before, time_after)
)
winner_physicals.append(
center_chirps(winner, physicals, time_before, time_after)
)
loser_onsets.append(
center_chirps(loser, onsets, time_before, time_after)
)
loser_offsets.append(
center_chirps(loser, offsets, time_before, time_after)
)
loser_physicals.append(
center_chirps(loser, physicals, time_before, time_after)
)
# winner_onset_chirpcount += len(winner_onsets[-1])
# winner_offset_chirpcount += len(winner_offsets[-1])
# winner_physical_chirpcount += len(winner_physicals[-1])
# loser_onset_chirpcount += len(loser_onsets[-1])
# loser_offset_chirpcount += len(loser_offsets[-1])
# loser_physical_chirpcount += len(loser_physicals[-1])
# bootstrap
# chirps = [winner, winner, winner, loser, loser, loser]
# winner_onsets_boot.append(bootstrap(
# winner,
# nresamples=nbootstraps,
# kde_time=kde_time,
# kernel_width=kernel_width,
# event_times=onsets,
# time_before=time_before,
# time_after=time_after))
# winner_offsets_boot.append(bootstrap(
# winner,
# nresamples=nbootstraps,
# kde_time=kde_time,
# kernel_width=kernel_width,
# event_times=offsets,
# time_before=time_before,
# time_after=time_after))
# winner_physicals_boot.append(bootstrap(
# winner,
# nresamples=nbootstraps,
# kde_time=kde_time,
# kernel_width=kernel_width,
# event_times=physicals,
# time_before=time_before,
# time_after=time_after))
# loser_onsets_boot.append(bootstrap(
# loser,
# nresamples=nbootstraps,
# kde_time=kde_time,
# kernel_width=kernel_width,
# event_times=onsets,
# time_before=time_before,
# time_after=time_after))
loser_offsets_boot.append(
bootstrap(
loser,
nresamples=nbootstraps,
kde_time=kde_time,
kernel_width=kernel_width,
event_times=offsets,
time_before=time_before,
time_after=time_after,
)
)
# loser_physicals_boot.append(bootstrap(
# loser,
# nresamples=nbootstraps,
# kde_time=kde_time,
# kernel_width=kernel_width,
# event_times=physicals,
# time_before=time_before,
# time_after=time_after))
# loser_offsets_jackknife = jackknife(
# loser,
# nresamples=nbootstraps,
# subsetsize=0.9,
# kde_time=kde_time,
# kernel_width=kernel_width,
# event_times=offsets,
# time_before=time_before,
# time_after=time_after)
if plot_all:
# winner_onsets_conv = acausal_kde1d(
# winner_onsets[-1], kde_time, kernel_width)
# winner_offsets_conv = acausal_kde1d(
# winner_offsets[-1], kde_time, kernel_width)
# winner_physicals_conv = acausal_kde1d(
# winner_physicals[-1], kde_time, kernel_width)
# loser_onsets_conv = acausal_kde1d(
# loser_onsets[-1], kde_time, kernel_width)
loser_offsets_conv = acausal_kde1d(
loser_offsets[-1], kde_time, kernel_width
)
# loser_physicals_conv = acausal_kde1d(
# loser_physicals[-1], kde_time, kernel_width)
ax[i].plot(
kde_time,
loser_offsets_conv / len(offsets),
lw=2,
zorder=100,
c=ps.gblue1,
)
ax[i].fill_between(
kde_time,
np.percentile(loser_offsets_boot[-1], 1, axis=0),
np.percentile(loser_offsets_boot[-1], 99, axis=0),
color="gray",
alpha=0.8,
)
ax[i].plot(
kde_time,
np.median(loser_offsets_boot[-1], axis=0),
color=ps.black,
linewidth=2,
)
ax[i].axvline(0, color=ps.gray, linestyle="--")
# ax[i].fill_between(
# kde_time,
# np.percentile(loser_offsets_jackknife, 5, axis=0),
# np.percentile(loser_offsets_jackknife, 95, axis=0),
# color=ps.blue,
# alpha=0.5)
# ax[i].plot(kde_time, np.median(loser_offsets_jackknife, axis=0),
# color=ps.white, linewidth=2)
ax[i].set_xlim(-60, 60)
fig.supylabel("Chirp rate (a.u.)", fontsize=14)
fig.supxlabel("Time (s)", fontsize=14)
# fig, ax = plt.subplots(2, 3, figsize=(
# 21*ps.cm, 10*ps.cm), sharey=True, sharex=True)
# ax[0, 0].set_title(
# f"{foldername}, onsets {len(onsets)}, offsets {len(offsets)}, physicals {len(physicals)},winner {len(winner)}, looser {len(loser)} , onsets")
# ax[0, 0].plot(kde_time, winner_onsets_conv/len(onsets))
# ax[0, 1].plot(kde_time, winner_offsets_conv /
# len(offsets))
# ax[0, 2].plot(kde_time, winner_physicals_conv /
# len(physicals))
# ax[1, 0].plot(kde_time, loser_onsets_conv/len(onsets))
# ax[1, 1].plot(kde_time, loser_offsets_conv/len(offsets))
# ax[1, 2].plot(kde_time, loser_physicals_conv /
# len(physicals))
# # plot bootstrap lines
# for kde in winner_onsets_boot[-1]:
# ax[0, 0].plot(kde_time, kde,
# color='gray')
# for kde in winner_offsets_boot[-1]:
# ax[0, 1].plot(kde_time, kde,
# color='gray')
# for kde in winner_physicals_boot[-1]:
# ax[0, 2].plot(kde_time, kde,
# color='gray')
# for kde in loser_onsets_boot[-1]:
# ax[1, 0].plot(kde_time, kde,
# color='gray')
# for kde in loser_offsets_boot[-1]:
# ax[1, 1].plot(kde_time, kde,
# color='gray')
# for kde in loser_physicals_boot[-1]:
# ax[1, 2].plot(kde_time, kde,
# color='gray')
# plot bootstrap percentiles
# ax[0, 0].fill_between(
# kde_time,
# np.percentile(winner_onsets_boot[-1], 5, axis=0),
# np.percentile(winner_onsets_boot[-1], 95, axis=0),
# color='gray',
# alpha=0.5)
# ax[0, 1].fill_between(
# kde_time,
# np.percentile(winner_offsets_boot[-1], 5, axis=0),
# np.percentile(
# winner_offsets_boot[-1], 95, axis=0),
# color='gray',
# alpha=0.5)
# ax[0, 2].fill_between(
# kde_time,
# np.percentile(
# winner_physicals_boot[-1], 5, axis=0),
# np.percentile(
# winner_physicals_boot[-1], 95, axis=0),
# color='gray',
# alpha=0.5)
# ax[1, 0].fill_between(
# kde_time,
# np.percentile(loser_onsets_boot[-1], 5, axis=0),
# np.percentile(loser_onsets_boot[-1], 95, axis=0),
# color='gray',
# alpha=0.5)
# ax[1, 1].fill_between(
# kde_time,
# np.percentile(loser_offsets_boot[-1], 5, axis=0),
# np.percentile(loser_offsets_boot[-1], 95, axis=0),
# color='gray',
# alpha=0.5)
# ax[1, 2].fill_between(
# kde_time,
# np.percentile(
# loser_physicals_boot[-1], 5, axis=0),
# np.percentile(
# loser_physicals_boot[-1], 95, axis=0),
# color='gray',
# alpha=0.5)
# ax[0, 0].plot(kde_time, np.median(winner_onsets_boot[-1], axis=0),
# color='black', linewidth=2)
# ax[0, 1].plot(kde_time, np.median(winner_offsets_boot[-1], axis=0),
# color='black', linewidth=2)
# ax[0, 2].plot(kde_time, np.median(winner_physicals_boot[-1], axis=0),
# color='black', linewidth=2)
# ax[1, 0].plot(kde_time, np.median(loser_onsets_boot[-1], axis=0),
# color='black', linewidth=2)
# ax[1, 1].plot(kde_time, np.median(loser_offsets_boot[-1], axis=0),
# color='black', linewidth=2)
# ax[1, 2].plot(kde_time, np.median(loser_physicals_boot[-1], axis=0),
# color='black', linewidth=2)
# ax[0, 0].set_xlim(-30, 30)
# winner_onsets = np.sort(flatten(winner_onsets))
# winner_offsets = np.sort(flatten(winner_offsets))
# winner_physicals = np.sort(flatten(winner_physicals))
# loser_onsets = np.sort(flatten(loser_onsets))
# loser_offsets = np.sort(flatten(loser_offsets))
# loser_physicals = np.sort(flatten(loser_physicals))
# winner_onsets_conv = acausal_kde1d(
# winner_onsets, kde_time, kernel_width)
# winner_offsets_conv = acausal_kde1d(
# winner_offsets, kde_time, kernel_width)
# winner_physicals_conv = acausal_kde1d(
# winner_physicals, kde_time, kernel_width)
# loser_onsets_conv = acausal_kde1d(
# loser_onsets, kde_time, kernel_width)
# loser_offsets_conv = acausal_kde1d(
# loser_offsets, kde_time, kernel_width)
# loser_physicals_conv = acausal_kde1d(
# loser_physicals, kde_time, kernel_width)
# winner_onsets_conv = winner_onsets_conv / onset_count
# winner_offsets_conv = winner_offsets_conv / offset_count
# winner_physicals_conv = winner_physicals_conv / physical_count
# loser_onsets_conv = loser_onsets_conv / onset_count
# loser_offsets_conv = loser_offsets_conv / offset_count
# loser_physicals_conv = loser_physicals_conv / physical_count
# winner_onsets_boot = np.concatenate(
# winner_onsets_boot)
# winner_offsets_boot = np.concatenate(
# winner_offsets_boot)
# winner_physicals_boot = np.concatenate(
# winner_physicals_boot)
# loser_onsets_boot = np.concatenate(
# loser_onsets_boot)
# loser_offsets_boot = np.concatenate(
# loser_offsets_boot)
# loser_physicals_boot = np.concatenate(
# loser_physicals_boot)
# percs = [5, 50, 95]
# winner_onsets_boot_quarts = np.percentile(
# winner_onsets_boot, percs, axis=0)
# winner_offsets_boot_quarts = np.percentile(
# winner_offsets_boot, percs, axis=0)
# winner_physicals_boot_quarts = np.percentile(
# winner_physicals_boot, percs, axis=0)
# loser_onsets_boot_quarts = np.percentile(
# loser_onsets_boot, percs, axis=0)
# loser_offsets_boot_quarts = np.percentile(
# loser_offsets_boot, percs, axis=0)
# loser_physicals_boot_quarts = np.percentile(
# loser_physicals_boot, percs, axis=0)
# fig, ax = plt.subplots(2, 3, figsize=(
# 21*ps.cm, 10*ps.cm), sharey=True, sharex=True)
# ax[0, 0].plot(kde_time, winner_onsets_conv)
# ax[0, 1].plot(kde_time, winner_offsets_conv)
# ax[0, 2].plot(kde_time, winner_physicals_conv)
# ax[1, 0].plot(kde_time, loser_onsets_conv)
# ax[1, 1].plot(kde_time, loser_offsets_conv)
# ax[1, 2].plot(kde_time, loser_physicals_conv)
# ax[0, 0].plot(kde_time, winner_onsets_boot_quarts[1], c=ps.black)
# ax[0, 1].plot(kde_time, winner_offsets_boot_quarts[1], c=ps.black)
# ax[0, 2].plot(kde_time, winner_physicals_boot_quarts[1], c=ps.black)
# ax[1, 0].plot(kde_time, loser_onsets_boot_quarts[1], c=ps.black)
# ax[1, 1].plot(kde_time, loser_offsets_boot_quarts[1], c=ps.black)
# ax[1, 2].plot(kde_time, loser_physicals_boot_quarts[1], c=ps.black)
# for kde in winner_onsets_boot:
# ax[0, 0].plot(kde_time, kde,
# color='gray')
# for kde in winner_offsets_boot:
# ax[0, 1].plot(kde_time, kde,
# color='gray')
# for kde in winner_physicals_boot:
# ax[0, 2].plot(kde_time, kde,
# color='gray')
# for kde in loser_onsets_boot:
# ax[1, 0].plot(kde_time, kde,
# color='gray')
# for kde in loser_offsets_boot:
# ax[1, 1].plot(kde_time, kde,
# color='gray')
# for kde in loser_physicals_boot:
# ax[1, 2].plot(kde_time, kde,
# color='gray')
# ax[0, 0].fill_between(kde_time,
# winner_onsets_boot_quarts[0],
# winner_onsets_boot_quarts[2],
# color=ps.gray,
# alpha=0.5)
# ax[0, 1].fill_between(kde_time,
# winner_offsets_boot_quarts[0],
# winner_offsets_boot_quarts[2],
# color=ps.gray,
# alpha=0.5)
# ax[0, 2].fill_between(kde_time,
# loser_physicals_boot_quarts[0],
# loser_physicals_boot_quarts[2],
# color=ps.gray,
# alpha=0.5)
# ax[1, 0].fill_between(kde_time,
# loser_onsets_boot_quarts[0],
# loser_onsets_boot_quarts[2],
# color=ps.gray,
# alpha=0.5)
# ax[1, 1].fill_between(kde_time,
# loser_offsets_boot_quarts[0],
# loser_offsets_boot_quarts[2],
# color=ps.gray,
# alpha=0.5)
# ax[1, 2].fill_between(kde_time,
# loser_physicals_boot_quarts[0],
# loser_physicals_boot_quarts[2],
# color=ps.gray,
# alpha=0.5)
plt.subplots_adjust(bottom=0.21, top=0.93)
plt.savefig("../poster/figs/kde.pdf")
plt.show()
if __name__ == "__main__":
main("../data/mount_data/")

View File

@ -1,14 +0,0 @@
audioio==0.9.5
cmocean==2.0
cycler==0.11.0
ipython==8.10.0
matplotlib==3.7.0
numpy==1.23.5
pandas==1.5.3
paramiko==2.11.1
PyYAML==6.0
scikit_learn==1.2.1
scipy==1.10.1
scp==0.14.5
thunderfish==1.9.9
tqdm==4.64.1

View File

@ -1,10 +0,0 @@
#!/usr/bin/env bash
color='\033[1;91m'
nocolor='\033[0m'
message='Running scripts in directory: '
for py_file in $(ls plot_*); do
echo -e $message$color$py_file$nocolor
python3 $py_file
done

View File

@ -1,277 +0,0 @@
import numpy as np
from extract_chirps import get_valid_datasets
import os
import numpy as np
import matplotlib.pyplot as plt
from thunderfish.powerspectrum import decibel
from IPython import embed
from pandas import read_csv
from modules.logger import makeLogger
from modules.plotstyle import PlotStyle
from modules.behaviour_handling import Behavior, correct_chasing_events
ps = PlotStyle()
logger = makeLogger(__name__)
def get_chirp_winner_loser(folder_name, Behavior, order_meta_df):
foldername = folder_name.split('/')[-2]
winner_row = order_meta_df[order_meta_df['recording'] == foldername]
winner = winner_row['winner'].values[0].astype(int)
winner_fish1 = winner_row['fish1'].values[0].astype(int)
winner_fish2 = winner_row['fish2'].values[0].astype(int)
if winner > 0:
if winner == winner_fish1:
winner_fish_id = winner_row['rec_id1'].values[0]
loser_fish_id = winner_row['rec_id2'].values[0]
elif winner == winner_fish2:
winner_fish_id = winner_row['rec_id2'].values[0]
loser_fish_id = winner_row['rec_id1'].values[0]
chirp_winner = len(
Behavior.chirps[Behavior.chirps_ids == winner_fish_id])
chirp_loser = len(
Behavior.chirps[Behavior.chirps_ids == loser_fish_id])
return chirp_winner, chirp_loser
else:
return np.nan, np.nan
def get_chirp_size(folder_name, Behavior, order_meta_df, id_meta_df):
foldername = folder_name.split('/')[-2]
folder_row = order_meta_df[order_meta_df['recording'] == foldername]
fish1 = folder_row['fish1'].values[0].astype(int)
fish2 = folder_row['fish2'].values[0].astype(int)
groub = folder_row['group'].values[0].astype(int)
size_fish1_row = id_meta_df[(id_meta_df['group'] == groub) & (
id_meta_df['fish'] == fish1)]
size_fish2_row = id_meta_df[(id_meta_df['group'] == groub) & (
id_meta_df['fish'] == fish2)]
size_winners = [size_fish1_row[col].values[0]
for col in ['l1', 'l2', 'l3']]
mean_size_winner = np.nanmean(size_winners)
size_losers = [size_fish2_row[col].values[0] for col in ['l1', 'l2', 'l3']]
mean_size_loser = np.nanmean(size_losers)
if mean_size_winner > mean_size_loser:
size_diff = mean_size_winner - mean_size_loser
winner_fish_id = folder_row['rec_id1'].values[0]
loser_fish_id = folder_row['rec_id2'].values[0]
elif mean_size_winner < mean_size_loser:
size_diff = mean_size_loser - mean_size_winner
winner_fish_id = folder_row['rec_id2'].values[0]
loser_fish_id = folder_row['rec_id1'].values[0]
else:
size_diff = np.nan
winner_fish_id = np.nan
loser_fish_id = np.nan
chirp_diff = len(Behavior.chirps[Behavior.chirps_ids == winner_fish_id]) - len(
Behavior.chirps[Behavior.chirps_ids == loser_fish_id])
return size_diff, chirp_diff
def get_chirp_freq(folder_name, Behavior, order_meta_df):
foldername = folder_name.split('/')[-2]
folder_row = order_meta_df[order_meta_df['recording'] == foldername]
fish1 = folder_row['rec_id1'].values[0].astype(int)
fish2 = folder_row['rec_id2'].values[0].astype(int)
chirp_freq_fish1 = np.nanmedian(
Behavior.freq[Behavior.ident == fish1])
chirp_freq_fish2 = np.nanmedian(
Behavior.freq[Behavior.ident == fish2])
if chirp_freq_fish1 > chirp_freq_fish2:
freq_diff = chirp_freq_fish1 - chirp_freq_fish2
winner_fish_id = folder_row['rec_id1'].values[0]
loser_fish_id = folder_row['rec_id2'].values[0]
elif chirp_freq_fish1 < chirp_freq_fish2:
freq_diff = chirp_freq_fish2 - chirp_freq_fish1
winner_fish_id = folder_row['rec_id2'].values[0]
loser_fish_id = folder_row['rec_id1'].values[0]
chirp_diff = len(Behavior.chirps[Behavior.chirps_ids == winner_fish_id]) - len(
Behavior.chirps[Behavior.chirps_ids == loser_fish_id])
return freq_diff, chirp_diff
def main(datapath: str):
foldernames = [
datapath + x + '/' for x in os.listdir(datapath) if os.path.isdir(datapath+x)]
foldernames, _ = get_valid_datasets(datapath)
path_order_meta = (
'/').join(foldernames[0].split('/')[:-2]) + '/order_meta.csv'
order_meta_df = read_csv(path_order_meta)
order_meta_df['recording'] = order_meta_df['recording'].str[1:-1]
path_id_meta = (
'/').join(foldernames[0].split('/')[:-2]) + '/id_meta.csv'
id_meta_df = read_csv(path_id_meta)
chirps_winner = []
size_diffs = []
size_chirps_diffs = []
chirps_loser = []
freq_diffs = []
freq_chirps_diffs = []
for foldername in foldernames:
# behabvior is pandas dataframe with all the data
if foldername == '../data/mount_data/2020-05-12-10_00/':
continue
bh = Behavior(foldername)
# chirps are not sorted in time (presumably due to prior groupings)
# get and sort chirps and corresponding fish_ids of the chirps
category = bh.behavior
timestamps = bh.start_s
# Correct for doubles in chasing on- and offsets to get the right on-/offset pairs
# Get rid of tracking faults (two onsets or two offsets after another)
category, timestamps = correct_chasing_events(category, timestamps)
# winner_chirp, loser_chirp = get_chirp_winner_loser(
# foldername, bh, order_meta_df)
# chirps_winner.append(winner_chirp)
# chirps_loser.append(loser_chirp)
# size_diff, chirp_diff = get_chirp_size(
# foldername, bh, order_meta_df, id_meta_df)
# size_diffs.append(size_diff)
# size_chirps_diffs.append(chirp_diff)
# freq_diff, freq_chirps_diff = get_chirp_freq(
# foldername, bh, order_meta_df)
# freq_diffs.append(freq_diff)
# freq_chirps_diffs.append(freq_chirps_diff)
folder_name = foldername.split('/')[-2]
winner_row = order_meta_df[order_meta_df['recording'] == folder_name]
winner = winner_row['winner'].values[0].astype(int)
winner_fish1 = winner_row['fish1'].values[0].astype(int)
winner_fish2 = winner_row['fish2'].values[0].astype(int)
groub = winner_row['group'].values[0].astype(int)
size_rows = id_meta_df[id_meta_df['group'] == groub]
if winner == winner_fish1:
winner_fish_id = winner_row['rec_id1'].values[0]
loser_fish_id = winner_row['rec_id2'].values[0]
size_winners = []
for l in ['l1', 'l2', 'l3']:
size_winner = size_rows[size_rows['fish']
== winner_fish1][l].values[0]
size_winners.append(size_winner)
mean_size_winner = np.nanmean(size_winners)
size_losers = []
for l in ['l1', 'l2', 'l3']:
size_loser = size_rows[size_rows['fish']
== winner_fish2][l].values[0]
size_losers.append(size_loser)
mean_size_loser = np.nanmean(size_losers)
size_diffs.append(mean_size_winner - mean_size_loser)
elif winner == winner_fish2:
winner_fish_id = winner_row['rec_id2'].values[0]
loser_fish_id = winner_row['rec_id1'].values[0]
size_winners = []
for l in ['l1', 'l2', 'l3']:
size_winner = size_rows[size_rows['fish']
== winner_fish2][l].values[0]
size_winners.append(size_winner)
mean_size_winner = np.nanmean(size_winners)
size_losers = []
for l in ['l1', 'l2', 'l3']:
size_loser = size_rows[size_rows['fish']
== winner_fish1][l].values[0]
size_losers.append(size_loser)
mean_size_loser = np.nanmean(size_losers)
size_diffs.append(mean_size_winner - mean_size_loser)
else:
continue
print(foldername)
all_fish_ids = np.unique(bh.chirps_ids)
chirp_winner = len(bh.chirps[bh.chirps_ids == winner_fish_id])
chirp_loser = len(bh.chirps[bh.chirps_ids == loser_fish_id])
freq_winner = np.nanmedian(bh.freq[bh.ident == winner_fish_id])
freq_loser = np.nanmedian(bh.freq[bh.ident == loser_fish_id])
chirps_winner.append(chirp_winner)
chirps_loser.append(chirp_loser)
size_chirps_diffs.append(chirp_winner - chirp_loser)
freq_diffs.append(freq_winner - freq_loser)
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(
22*ps.cm, 12*ps.cm), width_ratios=[1.5, 1, 1])
plt.subplots_adjust(left=0.098, right=0.945, top=0.94, wspace=0.343)
scatterwinner = 1.15
scatterloser = 1.85
chirps_winner = np.asarray(chirps_winner)[~np.isnan(chirps_winner)]
chirps_loser = np.asarray(chirps_loser)[~np.isnan(chirps_loser)]
bplot1 = ax1.boxplot(chirps_winner, positions=[
1], showfliers=False, patch_artist=True)
bplot2 = ax1.boxplot(chirps_loser, positions=[
2], showfliers=False, patch_artist=True)
ax1.scatter(np.ones(len(chirps_winner)) *
scatterwinner, chirps_winner, color='r')
ax1.scatter(np.ones(len(chirps_loser)) *
scatterloser, chirps_loser, color='r')
ax1.set_xticklabels(['winner', 'loser'])
ax1.text(0.1, 0.9, f'n = {len(chirps_winner)}',
transform=ax1.transAxes, color=ps.white)
for w, l in zip(chirps_winner, chirps_loser):
ax1.plot([scatterwinner, scatterloser], [w, l],
color='r', alpha=0.5, linewidth=0.5)
ax1.set_ylabel('Chirps [n]', color=ps.white)
colors1 = ps.red
ps.set_boxplot_color(bplot1, colors1)
colors1 = ps.orange
ps.set_boxplot_color(bplot2, colors1)
ax2.scatter(size_diffs, size_chirps_diffs, color='r')
ax2.set_xlabel('Size difference [mm]')
ax2.set_ylabel('Chirps [n]')
ax3.scatter(freq_diffs, size_chirps_diffs, color='r')
# ax3.scatter(freq_diffs, freq_chirps_diffs, color='r')
ax3.set_xlabel('Frequency difference [Hz]')
ax3.set_yticklabels([])
ax3.set
plt.savefig('../poster/figs/chirps_winner_loser.pdf')
plt.show()
if __name__ == '__main__':
# Path to the data
datapath = '../data/mount_data/'
main(datapath)

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 393 KiB

BIN
poster/figs/Untitled.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
poster/figs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

1184
poster/figs/logo.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 84 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 462 KiB

BIN
poster/main.pdf Normal file

Binary file not shown.

119
poster/main.tex Normal file
View File

@ -0,0 +1,119 @@
\documentclass[25pt, a0paper, landscape, margin=0mm, innermargin=20mm,
blockverticalspace=2mm, colspace=20mm, subcolspace=0mm]{tikzposter} %Default values for poster format options.
\input{packages}
\input{style}
\begin{document}
\renewcommand{\baselinestretch}{1}
\title{\parbox{1900pt}{Pushing the limits of time-frequency uncertainty in the
detection of transient communication signals in weakly electric fish}}
\author{Sina Prause, Alexander Wendt, Patrick Weygoldt}
\institute{Supervised by Till Raab \& Jan Benda, Neurothology Group,
University of Tübingen}
\usetitlestyle[]{sampletitle}
\maketitle
\renewcommand{\baselinestretch}{1.4}
\begin{columns}
\column{0.5}
\myblock[TranspBlock]{Introduction}{
\begin{minipage}[t]{0.55\linewidth}
The time-frequency tradeoff makes reliable signal detecion and simultaneous
sender identification of freely interacting individuals impossible.
This profoundly limits our current understanding of chirps to experiments
with single - or physically separated - individuals.
\end{minipage} \hfill
\begin{minipage}[t]{0.40\linewidth}
\vspace{-1.5cm}
\begin{tikzfigure}[]
\label{tradeoff}
\includegraphics[width=\linewidth]{figs/introplot}
\end{tikzfigure}
\end{minipage}
}
\myblock[TranspBlock]{A chirp detection algorithm}{
\begin{tikzfigure}[]
\label{modulations}
\includegraphics[width=\linewidth]{figs/algorithm}
\end{tikzfigure}
}
\column{0.5}
\myblock[TranspBlock]{Chirps and diadic competitions}{
\begin{minipage}[t]{0.7\linewidth}
\begin{tikzfigure}[]
\label{modulations}
\includegraphics[width=\linewidth]{figs/placeholder1}
\end{tikzfigure}
\end{minipage} \hfill
\begin{minipage}[t]{0.25\linewidth}
\lipsum[3][1-3]
\end{minipage}
\begin{minipage}[t]{0.7\linewidth}
\begin{tikzfigure}[]
\label{modulations}
\includegraphics[width=\linewidth]{figs/placeholder1}
\end{tikzfigure}
\end{minipage} \hfill
\begin{minipage}[t]{0.25\linewidth}
\lipsum[3][1-3]
\end{minipage}
\begin{minipage}[t]{0.7\linewidth}
\begin{tikzfigure}[]
\label{modulations}
\includegraphics[width=\linewidth]{figs/placeholder1}
\end{tikzfigure}
\end{minipage} \hfill
\begin{minipage}[t]{0.25\linewidth}
\lipsum[3][1-3]
\end{minipage}
}
\myblock[TranspBlock]{Conclusion}{
\lipsum[3][1-9]
}
% \column{0.3}
% \myblock[TranspBlock]{More Results}{
% \begin{tikzfigure}[]
% \label{results}
% \includegraphics[width=\linewidth]{example-image-a}
% \end{tikzfigure}
% \begin{multicols}{2}
% \lipsum[5][1-8]
% \end{multicols}
% \vspace{-1cm}
% }
% \myblock[TranspBlock]{Conclusion}{
% \begin{itemize}
% \setlength\itemsep{0.5em}
% \item \lipsum[1][1]
% \item \lipsum[1][1]
% \item \lipsum[1][1]
% \end{itemize}
% \vspace{0.2cm}
% }
\end{columns}
\node[
above right,
text=white,
outer sep=45pt,
minimum width=\paperwidth,
align=center,
draw,
fill=boxes,
color=boxes,
] at (-0.51\paperwidth,-43.5) {
\textcolor{text}{\normalsize Contact: \{name\}.\{surname\}@student.uni-tuebingen.de}};
\end{document}

View File

@ -1,10 +1,11 @@
\usepackage[utf8]{inputenc}
\usepackage[scaled]{helvet}
\renewcommand\familydefault{\sfdefault}
\renewcommand\familydefault{\sfdefault}
\usepackage[T1]{fontenc}
\usepackage{wrapfig}
\usepackage{setspace}
\usepackage{multicol}
\setlength{\columnsep}{1.5cm}
\usepackage{xspace}
\usepackage{tikz}
\usepackage{tikz}
\usepackage{lipsum}

View File

@ -16,11 +16,10 @@
\colorlet{notefgcolor}{background}
\colorlet{notebgcolor}{background}
% Title setup
\settitle{
% Rearrange the order of the minipages to e.g. center the title between the logos
\begin{minipage}[c]{0.8\paperwidth}
\begin{minipage}[c]{0.6\paperwidth}
% \centering
\vspace{2.5cm}\hspace{1.5cm}
\color{text}{\Huge{\textbf{\@title}} \par}
@ -31,28 +30,26 @@
\vspace{2.5cm}
\end{minipage}
\begin{minipage}[c]{0.2\paperwidth}
% \centering
\vspace{1cm}\hspace{1cm}
\includegraphics[scale=1]{example-image-a}
\end{minipage}
\begin{minipage}[c]{0.2\paperwidth}
% \vspace{1cm}\hspace{1cm}
\centering
% \vspace{1cm}
\hspace{-7cm}
\includegraphics[width=0.7\linewidth]{figs/efishlogo.pdf}
\includegraphics[scale=1]{example-image-a}
\end{minipage}}
% \begin{minipage}[c]{0.2\paperwidth}
% \vspace{1cm}\hspace{1cm}
% \centering
% \includegraphics[width=\linewidth]{example-image-a}
% \end{minipage}}
% define title style with background box (currently white)
% definie title style with background box
\definetitlestyle{sampletitle}{
width=841mm,
width=1189mm,
roundedcorners=0,
linewidth=0pt,
innersep=15pt,
titletotopverticalspace=0mm,
titletoblockverticalspace=5pt
}{
\begin{scope}[line width=\titlelinewidth,
rounded corners=\titleroundedcorners]
\begin{scope}[line width=\titlelinewidth, rounded corners=\titleroundedcorners]
\draw[fill=text, color=boxes]
(\titleposleft,\titleposbottom)
rectangle

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@ -1,116 +0,0 @@
\documentclass[25pt, a0paper, portrait, margin=0mm, innermargin=20mm,
blockverticalspace=2mm, colspace=20mm, subcolspace=0mm]{tikzposter} %Default values for poster format options.
\input{packages}
\input{style}
\begin{document}
\renewcommand{\baselinestretch}{1}
\title{\parbox{1600pt}{Detecting chirps based on dynamic filtering for the \\ analysis of social interactions in weakly electric fish}}
\author{Sina Prause, Alexander Wendt, and Patrick Weygoldt}
\institute{Supervised by Till Raab \& Jan Benda, Neuroethology Lab, University of Tuebingen}
\usetitlestyle[]{sampletitle}
\maketitle
\renewcommand{\baselinestretch}{1.4}
\begin{columns}
\column{0.4}
\myblock[TranspBlock]{Introduction}{
\textbf{Chirps} are one type of communication signals in weakly electric fish. They are characterized by \textbf{short frequency excursions} and are emitted during various social contexts. The time-frequency uncertainty of the Fourier transform makes it nearly impossible to reliably \textbf{detect and assign} chirps in freely interacting fish based on spectral methods. To overcome these limits, we developed a method based on \textbf{dynamic filtering} and subsequent \textbf{feature detection}.
\vspace{1cm}
\begin{tikzfigure}[]
\label{griddrawing}
\includegraphics[width=\linewidth]{figs/introplot}
\end{tikzfigure}
}
\myblock[TranspBlock]{Chirp detection algorithm}{
\begin{tikzfigure}[]
\label{fig:alg1}
\includegraphics[width=0.9\linewidth]{figs/algorithm1}
\end{tikzfigure}
\vspace{1cm}
\begin{tikzfigure}[]
\label{fig:alg2}
\includegraphics[width=1\linewidth]{figs/algorithm}
\end{tikzfigure}
\vspace{0cm}
}
\column{0.6}
\myblock[TranspBlock]{Chirps in dyadic competitions (Data by Till Raab, 2020)}{
\vspace{-2.75cm}
\begin{tikzfigure}[]
\label{fig:example_b}
\includegraphics[width=\linewidth]{figs/timeline.pdf}
\end{tikzfigure}
\noindent
\begin{multicols}{2}
\begin{itemize}
% \setlength\itemsep{0.5em}
\item The electric behavior of two fish competing for one shelter were recorded in a light and dark condition.
\item Using video recordings, behavior was classified as chasings or physical contacts.
\end{itemize}
\end{multicols}
\vspace{-2cm}
\noindent
\begin{tikzfigure}[]
\label{fig:example_b}
\includegraphics[width=\linewidth]{figs/chirps_winner_loser.pdf}
\end{tikzfigure}
\noindent
\begin{multicols}{2}
\begin{itemize}
\item Losers tend to chirp more.
\item Larger fish usually win. The smaller the size difference the more chirps are emitted.
\columnbreak
\item EOD frequency has no effect on the competition outcome and the chirp rate.
\end{itemize}
\end{multicols}
}
\myblock[TranspBlock]{Chirps emitted by loser fish might stop chasing events}{
\vspace{-1.2cm}
\begin{minipage}{0.6666\linewidth}
\begin{tikzfigure}[]
\includegraphics[width=1.1\linewidth]{figs/kde.pdf}
\end{tikzfigure}
\end{minipage}
\begin{minipage}{0.3333\linewidth}
\begin{tikzfigure}[]
\includegraphics[width=1\linewidth]{figs/chirps_in_chasing.pdf}
\end{tikzfigure}
\end{minipage}
\noindent
\begin{multicols}{2}
\begin{itemize}
\item In most cases there is no correlation between chirping and chasing- or physical contact events.
\item The chirp rate during chasings only increases for some dyads.
\end{itemize}
\end{multicols}
}
\myblock[GrayBlock]{Conclusion}{
\begin{itemize}
\setlength\itemsep{0.5em}
\item First tests indicate that our algorithm is able to detect chirps in recordings of multiple fish.
\item In some cases the chirp rate drastically increases before chasing stops.
\item Behavioral analysis needs to consider more variables, such as sex, size, and interindividual differences in chirping behavior.
\end{itemize}
\vspace{0.2cm}
}
\end{columns}
\node [above right,
text=white,
outer sep=45pt,
minimum width=\paperwidth,
align=center,
draw,
fill=boxes,
color=boxes] at (-43.6,-61) {
\textcolor{white}{
\normalsize Contact: \{name\}.\{surname\}@student.uni-tuebingen.de}};
\end{document}

Binary file not shown.

View File

@ -1,32 +0,0 @@
We detected 25766 chirps in this dataset which included 27 valid trials of recordings that lasted 6 hours each. The number of detected chirps is close to the currently largest reported dataset by \textcite{obotiWhyBrownGhost2022} which contains multiple different experiments. This was achieved by extending the chirp detection algorithm by \textcite{henningerTrackingActivityPatterns2020}. We added a dynamic search frequency and combined peaks of the envelope in the EOD amplitude, the envelope of the dynamic search frequency, and the instantaneous frequency for chirp detection. The chirps we detected on a dataset published by \textcite{raabElectrocommunicationSignalsIndicate2021} indicate that individuals that win the competition for a superior shelter chirp less than the losers. Moreover, in some of these pairings, chirps emitted by the loser are temporally correlated with the offset of an agonistic interaction. This indicates, that chirps might be used by losers to terminate chasing events.
\subsection{Assessing detection performance}
While our chirp detector detected many chirps, the performance of the algorithm was not quantified yet. We only used a small trial dataset to visually asses whether chirps are detected or not. Additionally, the EOD$f$ of the fish in the trial data set were rather far apart, making it easy to correctly assign chirps. To assess the performance of our analysis data set, we visually inspected each iteration of the algorithm (in 5 seconds snippets) for one whole recording. However, for future uses of this detector, it is imperative to quantify the detection performance and tune the parameters accordingly. This is especially important considering that the main innovation of this algorithm is not to detect but to correctly assign chirps. We predict that the performance in the correct assignment will most likely drop with decreasing difference frequencies. If this is the case we have to quantify this parameter as well. In conclusion, future work on this algorithm should include a synthetic data set that reflects the natural variability to quantify the detection performances of multiple chirp detection parameters.
\subsection{The problem of normalization}
Currently, a major flaw of the chirp detector is that the peak detection parameters are fixed for each feature, and decided upon by trial and error. We reached the most error-free performance with high thresholds. A side effect is that the great majority of the chirps are only detected on a single electrode. If chirps would be detectable on multiple electrodes, the smallest number of electrodes on which a chirp must be detected could be used as an additional detection parameter. But to do this, ideally, we would need to be able to use the same peak detection parameters across all electrodes and for all features. And to achieve this we should normalize our feature arrays not only across features but also across electrodes. The difficulty with normalization arises with the rolling windows and the changing electrodes. If there is no chirp in a certain window, normalizing across electrodes over just that window would scale up all noise so that the peak detector would find many peaks. Most of the time
these peaks do not occur at the same time and thus are not falsely detected as chirps, but sometimes, they are. The current method to fix this is to not normalize at all and hard-code peak detection thresholds. This solution is not flexible and might fail completely for new data sets without manually adjusting the parameters. The seemingly easy approach would be to save the data of all feature arrays for each iteration to disk and normalize across all data before running the chirp detector. But the fact that for each iteration we jump between electrodes makes this step complicated and impractical. Another approach could be, to determine 'chirp-less' windows by a single parameter before normalization. If e.g. there in no peak in the search frequency, we could just skip to the next window instead of running the whole detection routine. Normalization could then be performed only in windows that have a peak in search frequency.
\subsection{Why can the instantaneous frequency go down during a chirp?} \label{ref:insta}
While computing the instantaneous frequency we often encountered troughs instead of peaks in frequency during chirps. We simply circumvented this issue by taking the absolute of this instantaneous frequency. This phenomenon is critical because chirps are defined by a positive frequency excursion and not by a decrease in frequency. This decrease was only found after we computed the band pass filter which is filtering \SI{5}{\hertz} around the baseline of the EOD$f$ of the individual fish. If we band pass-filtered in a way so that the chirp was still included in the signal, all peaks in the instantaneous frequency were positive. This indicates that the narrow band pass filter around the signal is causing these frequency drops. One explanation for this issue could be the reduced amplitude during a chirp. The band pass filter removes the high frequency components that a chirp introduces. If the amplitude reduction is high enough, the frequency information shifts to noise. If this noise has only low frequency components the instantaneous frequency drops. If the chirp contrast is low and the amplitude of the baseline does not decrease strongly, the increase in frequency that is common during a chirp might be reflected in a peak. But if the contrast is high and the amplitude in the baseline breaks down this would reflect in a trough in the instantaneous frequency. Against this theory stands the fact that we observed the troughs in frequency in cases where the amplitude of the baseline did not break down strongly.
\vspace{\baselineskip}
Another explanation are the properties of the band pass filter which can introduce a phase shift while the fish emits chirps. If the phase is shifted backward the frequency temporarily increases, and vice versa for forward phase shifts. We still do not fully understand this change in frequency yet. Further analysis should include plotting the transfer function of the filter since narrow pass bands could introduce anomalies. Additionally, we would like to simulate chirps with different parameters, such as chirp phase, height, width, and contrast to understand which parameters might result in a trough of the instantaneous frequency.
\subsection{Losers chirped more than winners}
The detected chirps in this experiment indicate that winners chirped less than losers in this competition experiment. We can hypothesize that chirps are used as a submissive signal conveying information about the physical condition \parencite{davies1978deep}, and therefore can settle the competition without fights that escalate.
This result shows similarities to rises, another signal variation used by \textit{Aperonotus leptorhynchus}. \textcite{raabElectrocommunicationSignalsIndicate2021} showed that the loser of a competition experiment emitted more rises than the winner of the competition. Furthermore, the outcome of the competition is influenced by the body size: A larger body size is a predictor for winning the competition for the shelter \parencite{raabElectrocommunicationSignalsIndicate2021}. The frequencies of the individuals, on the other hand, do not seem to play a role in the outcome of the competition experiment. This needs to be further analyzed because the EOD$f$ is sexually dimorphic \parencite{meyer1987hormone}, and we did not take the sex of the pairings into account for our analysis.
\subsection{Why chirps might only terminate chasing in some dyads}
Our results indicated that chirps can terminate chasing, hence we computed the chirp rate to the chasing on- or offset and physical events. Only for the chasing offset we could find a connection for some pairings in the competition experiment in a way that the chirp increases right before the offset of the chasing event. \textcite{raabElectrocommunicationSignalsIndicate2021} showed that rises are used by the subordinate to motivate mutual assessment. In the few cases where we observed increasing chirp frequencies at the offset of a chasing event, chirps may have been used by the loser of the competition to signal submission. Still, the mean for all pairings did not show a correlation of chirps and the chasing offsets. This can be explained by not including factors like sex and size difference in our analysis. The sex of the fish has an important role for the communication behavior in the mating season, where, the female emits a big chirp to signalize the spawning \parencite{hagedornCourtSparkElectric1985a, henningerStatisticsNaturalCommunication2018}. The size difference is also important for the outcome of the competition experiment, so there may be a link between the increase of chirping behavior and the offset of the chasing events with the size difference. This should be dissected further in the analysis of the behavioral data set.
\subsection{Summary}
In conclusion, we were able to build the first chirp detector that might be usable on large data sets including multiple and freely moving individuals. Chrips can be detected by combining features from the changes in amplitude, instantaneous frequency, and a dynamic search window that is above the fish's own EOD$f$. We tested this detector on a data set of a competition experiment. We found that more chirps are produced when the size difference between the individuals was small and that the subordinates sometimes drastically increase their chirp frequency before a chasing event ends. This is the first step towards analyzing chirp-correlated behaviors in complex laboratory or field recordings to gain a clearer picture of the potentially multiple meanings of chirps. These first observations could inspire hypotheses that can then be verified in controlled experiments.

View File

@ -1,38 +0,0 @@
Successful communication depends on the transfer of reliable information \parencite{endler1993some, searcy2010evolution, hebets2016systems}. In the animal kingdom, information is commonly conveyed by signals which are produced or displayed by one individual to be received by another \parencite{hebets2016systems, searcy2010evolution, seyfarth2010central}. The main purpose of these signals is the prediction of upcoming events and the behavior of other animals in order to make suitable decisions \parencite{endler1993some, seyfarth2010central}. Thereby, signal reliability is a necessity for communication to occur in the first place, as neither the sender would produce signals nor the receiver would respond to them if it were not beneficial for both \parencite{seyfarth2017origin}. Thus, signals conveying reliable information are evolutionary stable even if a sender's and receiver's relationship may be cooperative or competitive \parencite{seyfarth2017origin}. However, the success of communication does not only depend on the signal and its properties itself. Any signal possesses an inherent vagueness that limits its specificity in any situation \parencite{seyfarth2003signalers, seyfarth2017origin}. Therefore, a second necessity for successful communication is contextual information. The context limits the possible interpretations of the information conveyed by the signal, thus increasing its specificity \parencite{seyfarth2017origin}. Hence, both the signal properties and the contextual information frame the communicative event whose success is further determined by the underlying degree of reliability. As such, successful communication is one of the main factors for an animal's survival and reproduction which is why it is the active focus of a wide field of research today.
\subsection{Communication across modalities}
As diverse as the contexts of animal signaling, as diverse are the modalities of the signals themselves. They range from chemical, tactile, and acoustic to visual and even electric, whereby the majority is effectively multimodal \parencite{bradbury1998principles, seyfarth2010central}. One of the most famous examples is the waggle dance of the honeybee \textit{Apis mellifera}, performed to communicate the location of a potent food source to workers in the hive \parencite{von1965tanze, riley2005flight}. As the performance is mainly a visual display it likewise involves scent \parencite{thom2007scent} as well as sound and air flow \parencite{tsujiuchi2007dynamic}. The former is also used by many ant species to indicate a profitable foraging spot. Thereby, the colony is guided by pheromone trails whose compounds are released via specified glands of individual ants \parencite{sudd1959interaction, david2009trail}. Another type of dance besides the honeybee's can be found in the peacock spider \textit{Maratus volans}. However, instead of dancing their way to a promising food source, male peacock spiders perform to impress females to find a partner to mate with \parencite{girard2011multi}. Not only because of the complex motion patterns and ornament displays but likewise owing to the vibratory signals emitted by the males, the peacock spider dance probably marks one of the most impressing communication signals in the animal kingdom \parencite{girard2011multi}. Considering the above it appears that invertebrates in general exhibit an interesting repertoire of possibilities to communicate, as there are also cases of facial patterns signifying status and rival assessment in paper wasps \parencite{tibbetts2008visual} as well as vibrational signals transmitted through plants by various insects to convey sex and species information together with directional cues for the potential mate \parencite{virant2004vibrational}.
\vspace{\baselineskip}
Striking examples of communication are also found among vertebrates. In this group, one of the most prevalent ways of conveying information is active vocalization. For instance, various monkey species exhibit an impressing repertoire of different calls, whereby its type depends on the context of the situation \parencite{SCHLENKER2016894}. One prominent example are the predator-specific calls in vervet monkeys indicating an attack by either a leopard, snake, or eagle \parencite{seyfarth1980}. Moreover, many species are likewise capable of communicating via facial and bodily gestures, which has for example been found in squirrel monkeys \parencite{anderson2010flexibility}, and chimpanzees \parencite{hobaiter2011gestural}. However, not only our closest ancestors but songbirds also use their voices through inter-individual contact. Especially during courtship, males like to show off their song repertoire to convey their qualities to a potential female mate and to repel competitors \parencite{kroodsma1991function, byers2009female}. Whereas a large fraction of animal communication can hardly be overheard, there is an equally large part escaping from human perception. Rodents, for example, emit calls with frequencies way above \SI{20}{\kilo\hertz}: so-called ultrasonic vocalizations (USV) which exceed the human hearing threshold \parencite{wohr2013affective}. These USVs are situation-dependent and differ in frequency. In rats, for instance, three major call types are known: Aversive and appetitive calls emitted by juveniles and adults, and pup calls indicating social isolation \parencite{wohr2013affective, seffer2014pro}. But this is not the only strategy for transferring information in the non-perceivable realm of animal communication. Some groups developed sensory systems enabling them to act both as the sender and receiver of sensory information by using the very same mechanisms \parencite{jonesCommunicationSelfFriends2021}. Such systems are found in pretty distinct species, namely bats \parencite{simmons1979echolocation} and toothed whales \parencite{kamminga1988echolocation}, which rely on echolocation, and weakly-electric fish using electrolocation \parencite{heiligenberg1973electrolocation}. The peculiar thing about these systems is that the animals possessing them actively generate signals to navigate, communicate, and detect prey \parencite{jonesCommunicationSelfFriends2021}. These signals are reflected by objects or other animals and allow for the detection of their properties, distance, and direction. In echolocation, bats and toothed whales emit high-frequency (\SI{20}{\kilo\hertz}) tonal sounds which are produced in the larynx or nose and whose reflections are then gathered by parts of the acoustic system \parencite{schnitzler2003spatial, park2016ultrasonic}. Electrolocation in weakly-electric fish on the other hand is based on the discharge of their electric organ \parencite{heiligenberg1973electrolocation, meyer1987hormone}. They hunt and navigate by perceiving alterations of their electric field caused by other animals or objects \parencite{von1999active, heiligenberg1973electrolocation}. The ability to sense electric fields is not uncommon in aquatic species. Sharks and rays have likewise developed the ability to perceive astoundingly weak electric fields \parencite{kalmijn1971electric}. They use these inputs for prey detection and electrolocation as well, however, they do not produce an electric signal themselves \parencite{kalmijn1971electric, kalmijn1973electro}. The electric eel, for example, is one the few species that is capable of actively producing electricity \parencite{catania2014shocking}. In contrast to weakly-electric fish, however, they do not constantly surround themselves with an electric field, but rather emit a mixture of low- and high-voltage pulses \parencite{catania2014shocking, catania2015electric}. The low-voltage pulses are used for sensing the environment whereas high-voltage pulses serve as a predatory strike to incapacitate prey \parencite{brown1950electric, catania2014shocking, catania2015electric}. The latter can reach strikingly high voltages of up to 600 V \parencite{brown1950electric, catania2014shocking}.
\subsection{Beyond human perception: Weakly electric fish}
As stated above, weakly-electric fish sample their environment by surrounding themselves with an electric field produced by discharging their electric organ. Based on the waveform of the electric organ discharge (EOD), weakly-electric fish are classified into two distinct groups: Wave-type and pulse-type electric fish \parencite{bendaPhysicsElectrosensoryWorlds2020}. Whereas wave-type EODs are characterized by a continuous discharge and a periodic waveform, pulse-type EODs show brief pulses interrupted by intervals of varying length \parencite{bendaPhysicsElectrosensoryWorlds2020}. For the wave-type category, the gymnotiform brown ghost knifefish \textit{Apteronotus leptorhynchus} is one of the best and most extensively studied species \parencite{zupanc1993evoked, meyer1987hormone, malerAtlasBrainElectric1991, hupeEffectDifferenceFrequency2008, englerSpontaneousModulationsElectric2000a, hagedornCourtSparkElectric1985a, dunlapHormonalBodySize2002}. Its EOD has a quasi-sinusoidal waveform and typically lies in a frequency range of \SIrange{600}{1000}{\hertz} (Fig. \ref{fig:Electric fields}B) \parencite{zupanc1993evoked, zakonEODModulationsBrown2002a}. Moreover, \textit{A. leptorhynchus} shows a sexual dimorphism in EOD frequency (EOD$f$) with females having a lower EOD$f$ ($<$ \SI{750}{\hertz}) than male individuals ($>$ \SI{750}{\hertz}) \parencite{meyer1987hormone, zakonEODModulationsBrown2002a}. Communication between two individuals occurs first and foremost because of the physical properties of the electric field. When interacting, the electric fields of two fish superimpose and result in amplitude modulation (AM), also called beat (Fig. \ref{fig:Electric fields}A \& D) \parencite{zupanc1993evoked, walzNeuroethologyElectrocommunicationHow2013, benda2005spike, bendaPhysicsElectrosensoryWorlds2020}. That is, by interacting with another individual, one fish perceives its own EOD being modulated by another electric field. The resulting beat has a specific frequency which is given by the frequency difference of the two interacting fish \parencite{benda2005spike, walzNeuroethologyElectrocommunicationHow2013, bendaPhysicsElectrosensoryWorlds2020}. Because of the frequency dimorphism in \textit{A. leptorhynchus}, this allows for a passive exchange of information since the beat frequency concomitantly transfers the gender relation of the fish. Simply speaking, the greater the beat frequency, the more likely it is that the other fish is of the opposite gender. However, \textit{A. leptorhynchus} is likewise capable of producing active communication signals. One of the most extensively studied are chirps \parencite{zupanc1993evoked, zakonEODModulationsBrown2002a, englerSpontaneousModulationsElectric2000a, hupeElectrocommunicationSignalsFree2009, obotiWhyBrownGhost2022, dunlapDiversityStructureElectrocommunication2003}. Chirps are transient frequency modulations that, depending on the type, are characterized by a duration of ten to a few hundred milliseconds and an increase of EOD$f$ up to several hundred Hertz (Fig. \ref{fig:introplot}, red track) \parencite{bendaPhysicsElectrosensoryWorlds2020, zupanc1993evoked, engler2001differential, zakonEODModulationsBrown2002a}. They have been shown to play a role in courtship and the synchronization of spawning \parencite{hagedornCourtSparkElectric1985a, henningerStatisticsNaturalCommunication2018}, but are also associated with aggressive encounters \parencite{triefenbachChangesSignallingAgonistic2008b}, where they are suggested to serve as a submissive signal \parencite{walzNeuroethologyElectrocommunicationHow2013, henningerStatisticsNaturalCommunication2018}. However, because of the various contexts in which chirps are emitted, their function is still an ongoing debate \parencite{obotiWhyBrownGhost2022}.
\begin{figure}[H]
\centering
\includegraphics[width=0.7\linewidth]{figures/Fishies_cropped.pdf}
\mycaption{Properties of the electric field in weakly-electric fish}{\textbf{A:} The fish surround themselves with a bipolar electric field. The superposition of electric fields alters their spatial properties. Thereby, the phase of the EOD influences the extend and direction of the electric field of each fish (red marker in EOD waveform). \textbf{B:} Quasi-sinusoidal waveform of the EOD of a single wave-type electric fish. \textbf{C:} Amplitude modulations (AM) of an individual fish recorded by external electrodes. The AM is caused by the movements of the fish relative to the recording electrodes. \textbf{D:} Amplitude modulation or beat caused by the superposition of two electric fields. The frequency of the beat is given by the difference in frequency between the individual fish (Figure from \cite{raab2022social}).}
\label{fig:Electric fields}
\end{figure}
\subsection{The problem of detection}
For the exploration of the chirp function, two requirements have to be met. First, chirps need to be recorded to be analyzed in the first place. Secondly, the analysis depends on the ability to detect chirps in the recordings. Optimally, the method fulfilling these two requirements is suited for deployment in laboratory settings as well as in the field. Chirp recording is the least problematic part. As chirps alter the EOD$f$, they can readily be recorded by a pair of electrodes placed in the vicinity of the fish \parencite{bendaPhysicsElectrosensoryWorlds2020}. A common approach in laboratory settings is to simulate a conspecific with a pair of electrodes which should cause the recorded individual to chirp. These can then be recorded with a second pair of electrodes \parencite{zupanc1993evoked, hagedornCourtSparkElectric1985a, dunlapDiversityStructureElectrocommunication2003, englerSpontaneousModulationsElectric2000a}. However, the laboratory does not reflect the natural habitat of the animals since the behavioral context of chirps emitted in the wild is highly variable \parencite{henningerStatisticsNaturalCommunication2018}. Thus, there is a need of recording fish in their natural habitat while freely behaving and interacting with each other. This is why recently, research is more and more shifted to the field \parencite{henningerStatisticsNaturalCommunication2018, fugere2011electrical, zubizarreta2020seasonal}. With this increasing change in research approach, new methods that allow for the recording of multiple fish in the wild were required. One approach was developed by \textcite{henningerStatisticsNaturalCommunication2018} and successfully tested in Panama, where the authors simultaneously recorded multiple individuals of four species of weakly-electric fish. They used a grid-like array of at least 54 electrodes which was submerged at the cut bank side of a stream. Later, a refined version of the grid was applied for further recordings in Colombia \parencite{raab2022AdvancesNoninvasiveTracking}. As such, the recording problem has been solved for the laboratory as well as the natural setting. The detection of chirps, however, turned out to be the bigger problem. As part of successful detection, it is required to correctly identify the chirp in the EOD track as well as assign the individual chirp to the correct animal. This is particularly difficult in recordings of multiple fish because the detection of a chirp implies that the underlying EOD$f$ is tracked for every animal. One approach for the analysis of electric signals in weakly-electric fish are spectrograms (Fig. \ref{fig:introplot}). Since chirps are very fast changes in the EOD$f$, a sufficiently high resolution in the time domain is necessary to resolve them. But, an increase of the resolution in time is accompanied by a resolution decrease in the frequency domain, which renders the distinction of the EOD$f$s of the chirping fish impossible. Given the situation of two fish with a similar EOD$f$, and the individual with the lower frequency emitting a chirp, it is unfeasible to assign the chirp to the correct individual by only using the spectrogram. Thus, chirp detection is surely not a trivial problem. \textcite{raab2022AdvancesNoninvasiveTracking}, based on previous work by \textcite{henningerTrackingActivityPatterns2020}, developed an algorithm that solves one part of the issue. It is capable of extracting EOD tracks by using both the EOD$f$ in the spectrogram as well as the spatial distribution of EOD power across electrodes \parencite{raab2022AdvancesNoninvasiveTracking}. This way, it is possible to obtain the EOD track of individual fish in recordings with multiple fish in space and time (Fig. \ref{fig:introplot}). Only with this groundwork, it is achievable to detect chirps in the first place.
\vspace{\baselineskip}
To overcome the remaining problem of chirp assignment, we refined previous work on this issue \parencite{henningerStatisticsNaturalCommunication2018}. In the following, we describe and test the first draft of a chirp detection algorithm capable of assigning chirps to individual fish in recordings with multiple animals. Our approach was to include a dynamic filter that searches for a frequency range between the individual EOD$f$'s free of interference. In a second step, we tested the algorithm with a data set obtained by \textcite{raabElectrocommunicationSignalsIndicate2021}. Thereby, individuals of the species \textit{Apteronotus leptorhynchus} were recorded while competing for a superior shelter in dyads. We were able to detect 25766 chirps in this data set alone and analyzed the context in which they were emitted. Thereby, we could not fully replicate suggestions for chirp function from the literature. However, some of our results indicate the association of chirps with a specific type of event during aggressive encounters.
\begin{figure}[H]
\centering
\includegraphics[width=1\linewidth]{figures/introplot.pdf}
\mycaption{Spectrogram of signal containing the EODs of two fish.}{The line plots indicate the instantaneous frequency of the respective individual, which we obtained by filtering the signal around its frequency component. \textbf{A:} Fish 1 produces a chirp that is visible by the frequency excursion in the instantaneous frequency and spectrogram if the frequency resolution is sufficient (NFFT=133.3, 20\% overlap). \textbf{B:} If the frequency resolution is lower, fish can be distinguished in the spectrogram, but chirp detection and assignment are not reliable (NFFT=1333.3, 20\% overlap). }
\label{fig:introplot}
\end{figure}
\todo[inline, color=orange]{Bilder von Lepto und seiner EOD waveform?}
\todo[inline, color=orange]{Messy in-text Quellen}
\todo[inline, color=orange]{Messy references Quellen}

View File

@ -1,59 +0,0 @@
\subsection{Chirp detection algorithm}
We developed and tested the improved algorithm on the raw data from a competition experiment (more information in section \ref{Behaviour}, or in the Paper by \cite{raabElectrocommunicationSignalsIndicate2021}) with 15 electrodes arranged in a grid. To be able to analyze the communication signals of the fish, which are most prominently represented as changes in their EOD$f$, we used the frequency tracks that were already computed by \textcite{raab2022AdvancesNoninvasiveTracking}. A frequency track is an approximation of how the fundamental frequency of the EOD of a single fish evolves. But because they are estimated using a spectrogram, they lack the temporal information needed to resolve transient frequency changes of a chirp. For this reason, we use the raw data, which has the appropriate temporal resolution to resolve chirps. To extract the EOD of single individuals, the frequency tracks are used to build specific band pass filters for every fish. Because the baseline EOD$f$ of a single fish changes with temperature and communication signals, filtering had to be performed in time windows. Additionally, to obtain the best signal for a freely moving fish, we choose the electrode with the highest power for every single rolling window. For each time window, we extract the amplitude of the baseline, the instantaneous frequency, and the amplitude of the search frequency, which all change during the production of a chirp. Simultaneously detected peaks on all three features are classified as a chirp. All signal processing steps that the raw data in a single rolling window snippet goes through are summarized in figure \ref{fig:algorithm}. The following paragraphs describe the processing steps in the same order as they are organized in the code of the algorithm.
\subsubsection{Loading and preparing the data set}
The parameters used by the algorithm are all adjustable using a \codeword{yaml} configuration file, which includes sections for the path to the data set as well as to the output directory. The program loads the raw data as well as the frequency tracks for every fish in the recording and generates time windows, in which it iterates across the raw data set. For each window in time, it iterates through the respective windows on the frequency tracks of all the fish in the recording. This approach is arguably less intuitive compared to iterating through the full time of the recording on the frequency track of a single fish and then skipping to the next individual. However, it reduces the number of times the more memory- and hence computationally demanding raw data set needs to be loaded.
\subsubsection{Rolling windows and their overlap}
To reduce the edge effects caused by filtering, we overlapped the rolling windows (window duration of \SI{5}{\second}) by one second and discarded the first and last \SI{250}{\milli\second} of a single window. This resulted in a true overlap of \SI{0,5}{\milli\second} in which chirps might be detected twice. To resolve this issue, we grouped all chirps that occurred less than \SI{20}{\milli\second} apart from each other as a single chirp.
\subsubsection{Following a fish through space}
The raw data set was recorded using an electrode grid. Hence, the EOD amplitudes of single fish varies between electrodes, because the amplitude decreases with the distance between an electrode and a moving fish. These changes in amplitude convey information on where the individual of interest is located in space. For optimal detection of chirps, we should use the strongest electrode for one fish, since it should have the highest signal-to-noise ratio. Additionally, if multiple fish are further apart, it is advantageous to use the electrode that is closest to one fish, but not the other, to increase the odds of correct sender assignment. For each iteration of the algorithm, we start off with a short (\SI{5}{\second}) snippets of the approximated frequency track and respective powers of the frequencies of an individual fish on the same temporal extent. To decide upon which raw data snippet for this fish across the pool of 15 electrodes the algorithm should load for optimal performance, we first had to determine, to which electrode the fish was the closest in the current window in time. To determine the best electrodes, we simply use the ones that had the highest power in the tracked frequencies, since power is proportional to the amplitude squared. In the current implementation, we repeat the following pipeline for the two electrodes with the highest power for the current fish of interest.
\subsubsection{Feature extraction}
After determining the best electrodes, we load the raw data snipped for the respective time window. This results in a data set of the frequency track of a fish (Figure \ref{fig:algorithm}, A, red) and the raw data (Figure \ref{fig:algorithm}, A, spectrogram), which the algorithm uses to extract more information. For each frequency track of an individual fish in each window, the raw signal \SI{5}{\hertz} is then band pass filtered with cut off frequencies above and below the baseline of the frequency track. This effectively provides an approximation of the recorded signal as if the current fish was the only one in the area. However fast frequency excursions, e.g. during chirps, are lost because they exceed the frequency limits of the cutoff frequencies. In other words, the peak of the chirp is filtered out by the band pass filter and that is why we see a trough in the amplitude. We use this to our advantage because the amplitude of the signal drops at theses points in time. To use the amplitudes of this signal, we extract the envelope using a low pass (cut off at \SI{25}{\hertz}) filter multiplied by the square-root of two (Figure \ref{fig:algorithm}, B, red). In addition to the amplitude trough, a chirp should also change the frequency of the signal, even if the actual peak gets lost due to filtering. To detect such transient frequency changes, we also compute the instantaneous frequency of the filtered baseline, which can be achieved by extracting the inverse of every single period using the zero crossings of the signal (Figure \ref{fig:algorithm}, B, orange). But using just the envelope and the instantaneous frequency of the EOD baseline of the fish was not sufficient to determine whether the anomaly we detected is a chirp or is caused e.g. by the movement of the fish. To deal with this issue, we used the notion that chirps are always up-modulations of the frequency that reach values of +\SI{50}{\hertz} to +\SI{300}{\hertz} above the baseline EOD$f$ of the emitting fish \parencite{zakonEODModulationsBrown2002a}. The first approach by \textcite{henningerStatisticsNaturalCommunication2018} was to filter a band at approximately +\SI{10}{\hertz} above the baseline and look for peaks in the power of this filtered band. We call this area the 'search frequency'. However, this method comes with the limitation, that if there are multiple fish and one of them has a baseline EOD$f$ that is coincidentally higher than the frequency of the current analyzed fish, the search frequency will become unusable. If the search frequency is close to- or at the baseline frequency of a fish with higher EOD$f$, chirps from the fish with a lower EOD$f$ would not be correctly assigned to the sender (Figure \ref{fig:henninger}). We adopted the method first documented in \textcite{henningerStatisticsNaturalCommunication2018} but introduce a dynamically adjusted search frequency. In our version of the chirp detection algorithm, the search frequency is still confined to a region of +\SI{20}{\hertz} to +\SI{100}{\hertz} above the baseline. In contrast to \textcite{henningerStatisticsNaturalCommunication2018} in this window, the frequency tracks of all other individuals are evaluated to find a region with the largest frequency difference from all other individuals that is still within the usual frequency peaks of chirps. The search frequency is then extracted in this window. This should make chirp detection and, most importantly, correct assignment to the sender, possible with recordings that include multiple individuals (Figure \ref{fig:dynamic}). The search window chosen in the example in Figure \ref{fig:algorithm}, A is indicated by orange dashed lines on the spectrogram. The envelope of the search frequency is visualized by the orange line in panel B.
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth]{figures/henninger.pdf}
\mycaption{Fixed search frequency}{Schematic sketch of a fixed search frequency used by \textcite{henningerStatisticsNaturalCommunication2018}. The search frequency is placed at a static \SI{10}{\hertz} above the baseline of the lower fish. \textbf{A:} If there is no upper fish with a higher EOD$f$ in the search frequency, a chirp detection can be implemented. The decrease in amplitude and the increase of amplitude in the search frequency can be used for the chirp detection algorithm. \textbf{B:}
In the case that there is another fish with an EOD$f$ in the search frequency, chirp detection is impaired. The EOD at the search frequency contains the chirp of the lower fish and the EOD of the upper fish, which is making the detection of the chirp unreliable.}
\label{fig:henninger}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth]{figures/dynamic.pdf}
\mycaption{Dynamic search frequency}{Schematic sketch of a dynamic search frequency implemented in the new algorithm. The search frequency is dynamic in a range of \SIrange{20}{100}{\hertz} above the baseline of the lower fish. \textbf{A:} If there is no upper fish with a higher EOD$f$ in the search frequency, chirp detection can be implemented. The decrease in amplitude and the increase of amplitude in the search frequency can be used for the chirp detection algorithm. \textbf{B:}
In the case that there is another fish with an EOD$f$ in the search frequency, the search frequency changes dynamically to a space with the largest difference to the upper fish. The chirp assignment is now possible with a correct sender assignment.}
\label{fig:dynamic}
\end{figure}
\subsubsection{Feature processing}
The features we extracted, particularly the filtered baseline EOD, are also subject to change e.g. when the fish moves. But changes due to movements happen on a larger time scale. To reduce the impact of this noise, we additionally band-pass filtered the baseline envelope with cutoff frequencies \SI{2}{\hertz} (low-cutoff) and \SI{100}{\hertz} (high-cutoff). Additionally, we invert the baseline envelope to turn troughs into peaks for detection purposes. For the instantaneous frequency, we took the absolute and shifted it by the median of the EOD$f$ ($|EODf_{inst} - med({EODf_{inst}}|$). The instantaneous frequency during a chirp was negative in some cases. This irregularity is discussed in section \ref{ref:insta}.
The search frequency required no additional processing. The processed features are visualized in the panel C of Figure \ref{fig:algorithm}. We then detected the peaks of all three features using a prominence threshold of 0.00005 for the baseline envelope, 0.000004 for the search frequency, and 2 for the instantaneous frequency.
\subsubsection{Peak classification}
Since all three features should coincide temporally with the peak in frequency during a chirp, the first criterion for a chirp was that peaks were detected on all three features simultaneously. More specifically, we chose \SI{20}{\milli\second} as a tolerance window where the peaks must co-occur. Additionally, since we repeated the algorithm on the two electrodes of the highest power for the current time window, we set a threshold of the number of electrodes on which the chirp must appear to be accepted as a chirp. In the current implementation, the chirp must be detected on just a single electrode. If a chirp is detected we compute the mean of the three time points on which the peaks were found and appended the resulting time stamp to the chirp times of the current fish.
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth]{figures/10.0_11245.5.pdf}
\mycaption{Core processing pipeline of the chirp detector.}{The left side shows how the data is modified while it passes the algorithm. The right side is color-coded with respect to the plots and indicates what kind of data is visualized and how it is processed. \textbf{A:} The spectrogram on the top is used to visualize the raw data. The red line on the spectrogram indicates the tracked frequency. \textbf{B:} The subplots show the three features that the algorithm extracts from the raw data using the frequency tracks of the individual fish. Red indicates the envelope of the baseline EOD$f$ for a single fish, which we obtain by filtering the raw signal around the tracked frequency. The envelope of the 'search frequency' we obtain by filtering a narrow band inside a dynamically adjusted window above the baseline EOD$f$ of the respective fish is indicated in orange. The search frequency band is also indicated by dashed lines in the same color on the spectrogram. The yellow line is the instantaneous frequency of the filtered baseline (red), which changes if the signal disappears out of the filtering window during a chirp. \textbf{C} illustrates how the three features appear after they are processed. The dots indicate the detected peaks. The dots on the spectrogram indicate the detected chirps after the peaks are sorted.}
\label{fig:algorithm}
\end{figure}
\subsection{Behavioral data used to test the algorithm } \label{Behaviour}
We tested the algorithm on a data set published by \textcite{raabElectrocommunicationSignalsIndicate2021}. The dataset was recorded using 21 mature \textit{Apteronotus leptorhynchus} from a tropical fish supplier. 9 males and 12 females, all in non-breeding conditions, were used. The experiment was conceptualized to understand the role of rises, another communication signal, during the competition of two fish for a superior shelter. The experiments took place in a \SI{100}{\liter} tank with a superior shelter in the center surrounded by other, less optimal shelters. The bottom of the tank was equipped with 15 mono-polar electrodes with low-noise buffer headstages. A reference electrode was positioned in a corner of the tank. The electric signals were first amplified and then digitized at \SI{20}{\kilo\hertz} per channel. The movement of the fish was recorded by a video camera mounted above the aquarium. Behavior (chasing events and contacts) were annotated manually using the software package BORIS \parencite{https://doi.org/10.1111/2041-210X.12584}.
\subsection{Data analysis}
The chirp detection algorithm as well as our subsequent analysis were written in Python 3.10.9 using the packages numpy, scipy, matplotlib, \href{https://github.com/janscience/thunderfish}{thunderfish} and \href{https://github.com/janscience/audioio}{audioio} \parencite{2020SciPy-NMeth, Hunter:2007, harris2020array}. All scripts as well as the required package versions are publically available in a \href{https://whale.am28.uni-tuebingen.de/git/raab/GP2023_chirp_detection}{git repository} (\url{https://whale.am28.uni-tuebingen.de/git/raab/GP2023_chirp_detection}). The temporal relation between chirps and chasing events was analyzed using a cross-correlation analysis. We computed the temporal differences between agonistic events (chasing onset, -offset and contact) and the chirps up to $\pm$ \SI{90}{\second} before and after the event. We then convolved the temporal differences with a Gaussian kernel with a standard deviation of \SI{2}{\milli\second}. To generate a baseline to compare the convolution with we randomly shuffled the chirp intervals in 50 permutations and recomputed the convolution for each. The baseline distribution was then obtained by the median across the permutations with the 5th and 95th percentile respectively.

View File

@ -1,43 +0,0 @@
\subsection{Chirps during dyadic competitions}
We used our algorithm to detect chirps in a dyadic competition experiment where two individuals competed for one superior shelter. In this exemplary recording almost all physical contacts, chasing events, and chirps happened during the dark phase of the experiment (Figure \ref{fig:timeline}, top rows in raster plot). We can estimate that the winner (yellow) did not chirp as much as the loser (red) of the competition. In this example, the winner emitted only 10 chirps whereas the loser produced close to 400 chirps(Figure \ref{fig:timeline}).
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth]{figures/timeline.pdf}
\mycaption{Overview of a single trial of the competition experiment.}{The raster plots indicate the occurrence of the type of event. Rasterplots of chirps are color-coded with respect to the emitter of the chirp. The line plots indicate the EOD$f$ of the two fish over time as it was tracked on a spectrogram. Red was the winner of the competition, and yellow was the loser. The shaded area indicates the phase where lights were turned off.}
\label{fig:timeline}
\end{figure}
Losers tended to chirp more than winners (Figure \ref{fig:winnerloser} A). This could be observed in 16 of the 22 valid trials (total n = 28) however this difference was not significant (Wilcoxon-test: z-statistic=67.0, pvalue=0.054). The outcome of the competition experiment was influenced by the size of the individuals. The winner of the dyadic interaction tended to have a greater size than the losers (Figure \ref{fig:winnerloser} B). For small size differences, there was an increase in chirp count, indicating that chirps increased in relevance for competition as the size difference decreased. The frequencies of the emitted EOD for winner and loser did not show any relevance for the outcome of the competition because the distribution for winner and loser is evenly spread over the range of EOD$f$ (Figure \ref{fig:winnerloser} C).
\begin{figure}[H]
\centering
\includegraphics[width=\linewidth]{figures/chirps_winner_loser.pdf}
\mycaption{Competition outcome }{compared to the emitted chirps in 22 valid parings (total n=28). \textbf{A:} Winner and loser of the competition experiment displayed with their quantity of chirps. Connecting lines indicate the pairing in the experiment. \textbf{B:} Winner and Loser compared to their size difference [cm] and the number of chirps they emitted. Losers (orange) are usually smaller than winners ($\Delta$ size $<$ 0) \textbf{C}: EOD$f$s [Hz] of the winner and loser compared to their quantity of chirps.}
\label{fig:winnerloser}
\end{figure}
\subsection{Chirps emitted by loser fish might disrupt chasing events}
\begin{wrapfigure}{R}{0.6\textwidth}
\vspace{-1cm}
\begin{center}
\includegraphics[width=\linewidth]{figures/kde.pdf}
\end{center}
\mycaption{Chasing-triggered chirps}{for two selected dyads. The time axis is centered around the offset of a chasing event. The black line indicates the bootstrapped baseline. The gray area is the bootstrapped confidence interval. The red line indicates the chirp rate estimated by a convolution with a Gaussian kernel. \textbf{A:} In most cases, there was no change in the chirp rate around an offset of a chasing event. \textbf{B:} But in a subset of the dataset chirping increased just before the chasing stopped.}
\label{fig:kde}
\end{wrapfigure}
Losers tending to chirp more often than winners already hints towards a behavioral significance to chirps during competitions. We evaluated the temporal correlation of chirps and agnostic behaviors to further investigate the behavioral significance of chirps in dyadic encounters. For this, we computed the chasing-triggered chirp rate of the on- and offset of chasing events and contacts. The chasing-triggered chirp rate consists of chirps centered around agonistic events. After this sorting, we estimated the distribution of chirps for the events using a convolution with a Gaussian kernel. We focused on the offset of chasing events because the onset and physical contact did not show consistent increases in chirp rate. For approximately a third of all dyadic competitions, there was an increase in chirp rate before the offset occurred (Figure \ref{fig:kde} right). In the other cases, there was no interaction of the offset and chirps (Figure \ref{fig:kde} left). This was compared to a permutated baseline to show a significant increase in chirp rate. The summarized chirp rates across all competitions did not show an increase in chirp rate around the offsets of the chasing events.
\newpage
\FloatBarrier
\begin{wrapfigure}[14]{hr}{0.4\textwidth}
\centering
\includegraphics[width=\linewidth]{figures/chirps_in_chasing.pdf}
\mycaption{Proportion of time spent and chirps emitted in chasing events.}{Relative time spent of the dark recording phase (3 h) in chasing events for single trials (\textbf{left}) and the relative quantity of chirps that were emitted during a chasing event for each recording (\textbf{right}). The lines show that the proportion of chirps during chasing events was only elevated for a subset of the competitions.}
\label{fig:chasing}
\end{wrapfigure}
\FloatBarrier
To summarize this, we compared the percentage of time spent in chasing events with the percentage of chirps emitted in chasing events. This comparison should indicate that when the two fish are in a chasing event, and the chirps are used to carry information specifically related to competition, there should be an increase in the percentage of chirps in these chasing events. In 15 out of 27 pairings the percentage of time spent in chasing events was higher than the percentage of chirps emitted during chasing, showing that chirps may not be used for transferring information about their physical status in those cases. This indicates that chirps are not specifically used for the offset of chasing events because their percentage of occurrences is often lower than the time spent in these chasing events.

View File

@ -1,42 +0,0 @@
\begin{titlepage}
\begin{center}
% Head block
\vspace*{0.5cm}
\Large
Protokoll\\
Neurobiologisches Großpraktikum\\
\vspace{1cm}
% Title and author
\Huge
\textbf{Detecting chirps in freely interacting weakly electric fish}\\
\Large
\vspace{1cm}
\textbf{Sina Prause, Alexander Wendt and Patrick Weygoldt}
\vspace{1cm}
% Add a nice figure below the authors
% \begin{figure}[ht]
% \centering
% %\includegraphics[scale=0.6]{images/drawing.pdf}
% \mycaption{A short bold}{ and a long regular caption.}
% \end{figure}
\vfill
% Bottom block
\large
\vspace{0.5cm}
Supervised by:\\
\vspace{0.5cm}
Till Raab \& Jan Benda\\
Neuroethology\\
Institute for Neurobiology\\
Tuebingen University\\
\vspace{1cm}
\today \\
\vspace{0.5cm}
\end{center}
\end{titlepage}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,484 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="157.31206mm"
height="54.225082mm"
viewBox="0 0 157.31206 54.225082"
version="1.1"
id="svg5"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
showguides="false" />
<defs
id="defs2">
<marker
style="overflow:visible"
id="marker4127"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="TriangleStart"
markerWidth="5.3244081"
markerHeight="6.155385"
viewBox="0 0 5.3244081 6.1553851"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path4125" />
</marker>
<marker
style="overflow:visible"
id="TriangleStart"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="TriangleStart"
markerWidth="5.3244081"
markerHeight="6.155385"
viewBox="0 0 5.3244081 6.1553851"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path135" />
</marker>
<mask
maskUnits="userSpaceOnUse"
id="mask20699">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.176389;stroke-linecap:round;stroke-dasharray:0.176389, 0.352777;stroke-dashoffset:0;stroke-opacity:1"
id="rect20701"
width="23.746241"
height="29.012896"
x="14.455999"
y="25.037264" />
</mask>
<mask
maskUnits="userSpaceOnUse"
id="mask26463">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.176389;stroke-linecap:round;stroke-dasharray:0.176389, 0.352777;stroke-dashoffset:0;stroke-opacity:1"
id="rect26465"
width="24.016085"
height="29.408197"
x="48.58794"
y="24.810217" />
</mask>
<marker
style="overflow:visible"
id="marker4127-3"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="TriangleStart"
markerWidth="5.3244081"
markerHeight="6.155385"
viewBox="0 0 5.3244081 6.1553851"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path4125-6" />
</marker>
<marker
style="overflow:visible"
id="TriangleStart-7"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="TriangleStart"
markerWidth="5.3244081"
markerHeight="6.155385"
viewBox="0 0 5.3244081 6.1553851"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path135-5" />
</marker>
<mask
maskUnits="userSpaceOnUse"
id="mask26463-3">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.176389;stroke-linecap:round;stroke-dasharray:0.176389, 0.352777;stroke-dashoffset:0;stroke-opacity:1"
id="rect26465-5"
width="24.016085"
height="29.408197"
x="48.58794"
y="24.810217" />
</mask>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-2.6879387,3.5411841)">
<g
id="g29069">
<g
id="g27306"
transform="translate(1.6122597,-126.9852)">
<g
id="g20783"
transform="matrix(1.1174167,0,0,1.1174167,-7.4533966,102.29928)">
<path
id="rect111"
style="fill:none;stroke:#000000;stroke-width:0.352778;stroke-dasharray:none;marker-start:url(#marker4127);marker-end:url(#TriangleStart)"
d="M 39.393832,54.875322 H 13.679574 V 23.480161"
sodipodi:nodetypes="ccc" />
<g
id="g20697"
mask="url(#mask20699)">
<g
id="g17691"
transform="translate(-9.0524821,1.5670767)">
<path
style="fill:none;stroke:#f39ca9;stroke-width:0.386556;stroke-dasharray:none;stroke-opacity:1"
d="m 13.66089,47.485891 c 0,0 3.579334,0.431089 7.210941,0.304518 3.964707,-0.138179 8.838234,-0.560278 13.262189,-0.740152 1.601595,-0.06512 3.205275,-0.179435 4.80719,-0.122713 3.002115,0.106298 6.685233,0.512079 8.970224,0.866597 3.093977,0.480035 4.509419,0.0238 8.155396,0.0238 0.712971,0 1.383337,1.09e-4 1.383337,1.09e-4"
id="path6589"
sodipodi:nodetypes="csaassc" />
<path
style="fill:#000000;fill-opacity:0;stroke:#f39ca9;stroke-width:0.352778;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 33.565113,46.99482 V 25.014953"
id="path15375" />
</g>
<g
id="g17695"
transform="translate(-9.0524821,1.5670767)">
<path
style="fill:#000000;fill-opacity:0;stroke:#f39ca9;stroke-width:0.183859;stroke-linecap:round;stroke-dasharray:0.183859, 0.367719;stroke-dashoffset:0;stroke-opacity:1"
d="m 13.662696,45.284594 h 43.35043"
id="path17685" />
<path
style="fill:#000000;fill-opacity:0;stroke:#f39ca9;stroke-width:0.182405;stroke-linecap:round;stroke-dasharray:0.182405, 0.36481;stroke-dashoffset:0;stroke-opacity:1"
d="m 13.717208,49.619449 h 42.7493"
id="path17687" />
</g>
<g
id="g17701"
transform="translate(-9.0524821,-17.067258)">
<path
style="fill:#000000;fill-opacity:0;stroke:#f9e2ab;stroke-width:0.183859;stroke-linecap:round;stroke-dasharray:0.183859, 0.367719;stroke-dashoffset:0;stroke-opacity:1"
d="m 13.662696,45.284594 h 43.35043"
id="path17697" />
<path
style="fill:#000000;fill-opacity:0;stroke:#f9e2ab;stroke-width:0.182405;stroke-linecap:round;stroke-dasharray:0.182405, 0.36481;stroke-dashoffset:0;stroke-opacity:1"
d="m 13.717208,49.619449 h 42.7493"
id="path17699" />
</g>
</g>
<g
id="g26741">
<path
id="path20843"
style="fill:none;stroke:#000000;stroke-width:0.352778;stroke-dasharray:none;marker-start:url(#marker4127);marker-end:url(#TriangleStart)"
d="M 73.564746,54.875322 H 47.850488 V 41.044315"
sodipodi:nodetypes="ccc" />
<path
id="path20847"
style="fill:none;stroke:#000000;stroke-width:0.352778;stroke-dasharray:none;marker-end:url(#TriangleStart)"
d="M 47.850488,37.52073 V 23.689723"
sodipodi:nodetypes="cc" />
<g
id="g26461"
mask="url(#mask26463)">
<path
id="rect20911"
style="fill:#f39ca9;stroke-width:0.176389;stroke-linecap:round;stroke-dasharray:0.176389, 0.352777"
d="m 46.803364,43.109017 h 8.530365 c 1.254981,0 1.596628,3.13284 2.088089,3.13284 0.536811,0 0.906944,-3.13284 2.178978,-3.13284 H 75.279593 V 53.15153 c 0,0 -10.291757,0 -15.491839,0 -1.228712,0 -1.81324,-2.829799 -2.362169,-2.829799 -0.504598,0 -0.90127,2.829799 -2.069958,2.829799 -2.533291,0 -8.552263,0 -8.552263,0 z"
sodipodi:nodetypes="csssccssscc" />
<path
style="fill:#f39ca9;fill-opacity:1;stroke:none;stroke-width:0.176389;stroke-linecap:round;stroke-dasharray:0.176389, 0.352777;stroke-dashoffset:0;stroke-opacity:1"
d="m 57.086752,43.268426 c 0,0 -0.37156,-3.765766 0,0 z"
id="path21858" />
<path
id="rect24170"
style="fill:#f9e2ab;fill-opacity:1;stroke-width:0.176;stroke-linecap:round;stroke-dasharray:0.176, 0.351999;stroke-dashoffset:0"
d="m 46.274513,30.194793 h 9.549215 c 1.717363,0 0.878619,-5.049747 1.652599,-5.049747 0.569072,0 0.112537,5.049747 1.842854,5.049747 h 16.286061 v 0.904651 H 59.272796 c -1.758036,0 -1.156495,5.094603 -1.735013,5.094603 -0.56555,0 -0.0031,-5.094603 -1.749022,-5.094603 h -9.514248 z"
sodipodi:nodetypes="csssccssscc" />
<path
style="fill:#f39ca9;fill-opacity:1;stroke:none;stroke-width:0.176389;stroke-linecap:round;stroke-dasharray:0.176389, 0.352777;stroke-dashoffset:0;stroke-opacity:1"
d="M 57.184587,30.525973 V 30.009484"
id="path24173" />
</g>
</g>
</g>
<g
id="g27206"
transform="matrix(1.1174167,0,0,1.1174167,88.616705,85.937397)">
<g
id="g20825"
transform="translate(-47.283516,14.100222)">
<path
id="path20723"
style="fill:none;stroke:#000000;stroke-width:0.352778;stroke-dasharray:none;marker-start:url(#marker4127);marker-end:url(#TriangleStart)"
d="M 74.083516,55.417696 H 48.369258 V 24.022535"
sodipodi:nodetypes="ccc" />
<g
id="g20745"
mask="url(#mask20699)"
transform="translate(34.689684,0.54237409)">
<g
id="g20729"
transform="translate(-9.0524821,1.5670767)">
<path
style="fill:none;stroke:#f39ca9;stroke-width:0.386556;stroke-dasharray:none;stroke-opacity:1"
d="m 13.66089,47.485891 c 0,0 3.579334,0.431089 7.210941,0.304518 3.964707,-0.138179 8.838234,-0.560278 13.262189,-0.740152 1.601595,-0.06512 3.205275,-0.179435 4.80719,-0.122713 3.002115,0.106298 6.685233,0.512079 8.970224,0.866597 3.093977,0.480035 4.509419,0.0238 8.155396,0.0238 0.712971,0 1.383337,1.09e-4 1.383337,1.09e-4"
id="path20725"
sodipodi:nodetypes="csaassc" />
<path
style="fill:#000000;fill-opacity:0;stroke:#f39ca9;stroke-width:0.352778;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
d="M 33.565113,46.99482 V 25.014953"
id="path20727" />
</g>
<g
id="g20735"
transform="translate(-9.0524821,1.5670767)">
<path
style="fill:#000000;fill-opacity:0;stroke:#f39ca9;stroke-width:0.183859;stroke-linecap:round;stroke-dasharray:0.183859, 0.367719;stroke-dashoffset:0;stroke-opacity:1"
d="m 13.662696,45.284594 h 43.35043"
id="path20731" />
<path
style="fill:#000000;fill-opacity:0;stroke:#f39ca9;stroke-width:0.182405;stroke-linecap:round;stroke-dasharray:0.182405, 0.36481;stroke-dashoffset:0;stroke-opacity:1"
d="m 13.717208,49.619449 h 42.7493"
id="path20733" />
</g>
<g
id="g20741"
transform="translate(-9.0524821,-17.067258)">
<path
style="fill:#000000;fill-opacity:0;stroke:#f9e2ab;stroke-width:0.183859;stroke-linecap:round;stroke-dasharray:0.183859, 0.367719;stroke-dashoffset:0;stroke-opacity:1"
d="m 13.662696,45.284594 h 43.35043"
id="path20737" />
<path
style="fill:#000000;fill-opacity:0;stroke:#f9e2ab;stroke-width:0.182405;stroke-linecap:round;stroke-dasharray:0.182405, 0.36481;stroke-dashoffset:0;stroke-opacity:1"
d="m 13.717208,49.619449 h 42.7493"
id="path20739" />
</g>
<path
style="fill:none;stroke:#9cc2f3;stroke-width:0.386556;stroke-dasharray:none;stroke-opacity:1"
d="m 48.404425,30.32741 c 0,0 -3.345283,0.189357 -6.97689,0.315928 -3.964707,0.138179 -8.289879,-0.476491 -12.43901,-0.459534 -2.056246,0.0084 -4.106262,0.284197 -6.162517,0.278119 -2.894461,-0.0086 -6.387136,-0.08825 -8.672127,-0.442763 -3.093977,-0.480035 -4.5094191,-0.0238 -8.1553961,-0.0238 -0.712971,0 -1.383337,-1.09e-4 -1.383337,-1.09e-4"
id="path20743"
sodipodi:nodetypes="csaassc" />
</g>
</g>
<g
id="g26684"
transform="translate(0,-0.00474886)">
<path
id="path20843-6"
style="fill:none;stroke:#000000;stroke-width:0.352778;stroke-dasharray:none;marker-start:url(#marker4127-3);marker-end:url(#TriangleStart-7)"
d="M 61.186844,69.522667 H 35.472586 V 55.69166"
sodipodi:nodetypes="ccc" />
<path
id="path20847-2"
style="fill:none;stroke:#000000;stroke-width:0.352778;stroke-dasharray:none;marker-end:url(#TriangleStart-7)"
d="M 35.472586,52.168075 V 38.337068"
sodipodi:nodetypes="cc" />
<g
id="g26461-9"
mask="url(#mask26463-3)"
transform="translate(-12.377903,14.647345)">
<path
id="rect20911-1"
style="fill:#f39ca9;stroke-width:0.176389;stroke-linecap:round;stroke-dasharray:0.176389, 0.352777"
d="m 46.803364,43.109017 h 8.530365 c 1.254981,0 1.596628,3.13284 2.088089,3.13284 0.536811,0 0.906944,-3.13284 2.178978,-3.13284 H 75.279593 V 53.15153 c 0,0 -10.291757,0 -15.491839,0 -1.228712,0 -1.81324,-2.829799 -2.362169,-2.829799 -0.504598,0 -0.90127,2.829799 -2.069958,2.829799 -2.533291,0 -8.552263,0 -8.552263,0 z"
sodipodi:nodetypes="csssccssscc" />
<path
style="fill:#f39ca9;fill-opacity:1;stroke:none;stroke-width:0.176389;stroke-linecap:round;stroke-dasharray:0.176389, 0.352777;stroke-dashoffset:0;stroke-opacity:1"
d="m 57.086752,43.268426 c 0,0 -0.37156,-3.765766 0,0 z"
id="path21858-2" />
<path
style="fill:#f39ca9;fill-opacity:1;stroke:none;stroke-width:0.176389;stroke-linecap:round;stroke-dasharray:0.176389, 0.352777;stroke-dashoffset:0;stroke-opacity:1"
d="M 57.184587,30.525973 V 30.009484"
id="path24173-0" />
<path
id="path26795"
style="fill:#f9e2ab;fill-opacity:1;stroke-width:0.176389;stroke-linecap:round;stroke-dasharray:0.176389, 0.352777"
d="m 46.675328,25.652062 h 8.530365 c 1.254981,0 1.241821,-0.3105 2.182659,-0.253058 0.889795,0.05432 0.812374,0.253058 2.084408,0.253058 H 75.151557 V 35.694575 H 59.659718 c -1.228712,0 -1.76712,0.363128 -2.316049,0.363128 -0.504598,0 -0.94739,-0.363128 -2.116078,-0.363128 h -8.552263 z"
sodipodi:nodetypes="csssccssscc" />
</g>
</g>
</g>
</g>
<text
xml:space="preserve"
style="font-weight:bold;font-size:3.88056px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.176;stroke-linecap:round;stroke-dasharray:0.176, 0.351999;stroke-dashoffset:0;stroke-opacity:1"
x="79.707397"
y="42.263329"
id="text27390"><tspan
sodipodi:role="line"
id="tspan27388"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88056px;font-family:sans-serif;-inkscape-font-specification:sans-serif;fill:#000000;fill-opacity:1;stroke-width:0.176"
x="79.707397"
y="42.263329">Time</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:3.88056px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.176;stroke-linecap:round;stroke-dasharray:0.176, 0.351999;stroke-dashoffset:0;stroke-opacity:1"
x="-28.696152"
y="6.4117355"
id="text27652"
transform="rotate(-90)"><tspan
sodipodi:role="line"
id="tspan27650"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88056px;font-family:sans-serif;-inkscape-font-specification:sans-serif;fill:#000000;fill-opacity:1;stroke-width:0.176"
x="-28.696152"
y="6.4117355">Frequency</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:3.88056px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.176;stroke-linecap:round;stroke-dasharray:0.176, 0.351999;stroke-dashoffset:0;stroke-opacity:1"
x="-28.696152"
y="88.128494"
id="text27668"
transform="rotate(-90)"><tspan
sodipodi:role="line"
id="tspan27666"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88056px;font-family:sans-serif;-inkscape-font-specification:sans-serif;fill:#000000;fill-opacity:1;stroke-width:0.176"
x="-28.696152"
y="88.128494">Frequency</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:3.88056px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.176;stroke-linecap:round;stroke-dasharray:0.176, 0.351999;stroke-dashoffset:0;stroke-opacity:1"
x="-28.33526"
y="44.299961"
id="text27672"
transform="rotate(-90)"><tspan
sodipodi:role="line"
id="tspan27670"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88056px;font-family:sans-serif;-inkscape-font-specification:sans-serif;fill:#000000;fill-opacity:1;stroke-width:0.176"
x="-28.33526"
y="44.299961">Amplitude</tspan></text>
<text
xml:space="preserve"
style="font-weight:bold;font-size:3.88056px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.176;stroke-linecap:round;stroke-dasharray:0.176, 0.351999;stroke-dashoffset:0;stroke-opacity:1"
x="-28.33526"
y="126.89417"
id="text27676"
transform="rotate(-90)"><tspan
sodipodi:role="line"
id="tspan27674"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.88056px;font-family:sans-serif;-inkscape-font-specification:sans-serif;fill:#000000;fill-opacity:1;stroke-width:0.176"
x="-28.33526"
y="126.89417">Amplitude</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93889px;font-family:sans-serif;-inkscape-font-specification:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.176;stroke-linecap:round;stroke-dasharray:0.176, 0.351999;stroke-dashoffset:0;stroke-opacity:1"
x="2.6879387"
y="0"
id="text27680"><tspan
sodipodi:role="line"
id="tspan27678"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93889px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';stroke-width:0.176"
x="2.6879387"
y="0">A</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93889px;font-family:sans-serif;-inkscape-font-specification:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.176;stroke-linecap:round;stroke-dasharray:0.176, 0.351999;stroke-dashoffset:0;stroke-opacity:1"
x="84.461967"
y="-5.6843419e-14"
id="text27684"><tspan
id="tspan27682"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93889px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';stroke-width:0.176"
x="84.461967"
y="-5.6843419e-14"
sodipodi:role="line">B</tspan></text>
<g
id="g28997"
transform="translate(-3.629747,0.99082983)">
<g
id="g27968"
transform="translate(-10.561285,-5.4205261)">
<rect
style="fill:#f39ca9;fill-opacity:1;stroke:none;stroke-width:0.176;stroke-linecap:round;stroke-dasharray:0.176, 0.351999;stroke-dashoffset:0;stroke-opacity:1"
id="rect27686"
width="3.3779061"
height="3.3779061"
x="28.749498"
y="51.490963" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:3.52778px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.176;stroke-linecap:round;stroke-dasharray:0.176, 0.351999;stroke-dashoffset:0;stroke-opacity:1"
x="32.992668"
y="54.511654"
id="text27811"><tspan
sodipodi:role="line"
id="tspan27809"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;font-family:sans-serif;-inkscape-font-specification:sans-serif;fill:#000000;fill-opacity:1;stroke-width:0.176"
x="32.992668"
y="54.511654">Lower fish baseline</tspan></text>
</g>
<g
id="g27976"
transform="translate(29.373014,-5.6652521)">
<rect
style="fill:#f9e2ab;fill-opacity:1;stroke:none;stroke-width:0.176;stroke-linecap:round;stroke-dasharray:0.176, 0.351999;stroke-dashoffset:0;stroke-opacity:1"
id="rect27970"
width="3.3779061"
height="3.3779061"
x="28.749498"
y="51.490963" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:3.52778px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.176;stroke-linecap:round;stroke-dasharray:0.176, 0.351999;stroke-dashoffset:0;stroke-opacity:1"
x="32.992668"
y="54.511654"
id="text27974"><tspan
sodipodi:role="line"
id="tspan27972"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;font-family:sans-serif;-inkscape-font-specification:sans-serif;fill:#000000;fill-opacity:1;stroke-width:0.176"
x="32.992668"
y="54.511654">Lower fish search frequency</tspan></text>
</g>
<g
id="g28812"
transform="translate(84.247459,-5.6652521)">
<rect
style="fill:#9cc2f3;fill-opacity:0.933333;stroke:none;stroke-width:0.176;stroke-linecap:round;stroke-dasharray:0.176, 0.351999;stroke-dashoffset:0;stroke-opacity:1"
id="rect28806"
width="3.3779061"
height="3.3779061"
x="28.749498"
y="51.490963" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:3.52778px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.176;stroke-linecap:round;stroke-dasharray:0.176, 0.351999;stroke-dashoffset:0;stroke-opacity:1"
x="32.992668"
y="54.511654"
id="text28810"><tspan
sodipodi:role="line"
id="tspan28808"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;font-family:sans-serif;-inkscape-font-specification:sans-serif;fill:#000000;fill-opacity:1;stroke-width:0.176"
x="32.992668"
y="54.511654">Upper fish baseline</tspan></text>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@ -1,132 +0,0 @@
\documentclass[a4paper]{article} % Default text size and document class
% Format -----------------------------------------------------------------------------
\usepackage[ % To set up the page
a4paper, % A4 paper
width=160mm, % Text body width
top=25mm,bottom=25mm, % Top and bottom margins
bindingoffset=6mm % Offset of left and right pages for printing
]
{geometry}
\usepackage{setspace} % So set spacings (e.g. line height)
\onehalfspacing % Sets line width to 1.5
%\usepackage{sectsty} % To control sectional headers
% \chapternumberfont{\huge} % Set size of chapter number
% \chaptertitlefont{\huge} % Set size of chapter title
\usepackage{multicol} % Writes in multiple columns
\usepackage{wrapfig}
\usepackage{lipsum} % Inserts blind text for previewing
\usepackage{placeins} % adding float barriers
\setlength{\parindent}{0pt} % remove indentation for new paragraphs
% Language ---------------------------------------------------------------------------
\usepackage[english]{babel} % To enable language support other than us english
\usepackage[babel]{csquotes} % Omits spell checking in quotations
% Images, long tables & append pdf pages ---------------------------------------------
\usepackage{float} % Enables some options for image placement
\usepackage{graphicx} % To include graphics
\usepackage{ % Long table stuff
csvsimple, % So a csv can be converted to a table
longtable, % To make long tables
booktabs % To page break long tables properly
}
\usepackage{pdfpages} % Appends pdf pages
\usepackage{xcolor}
% Custom fancy image caption ---------------------------------------------------------
\usepackage{caption}
\captionsetup[figure]{labelsep=space}
% \captionsetup[table]{labelsep=space}
\newcommand{\mycaption}[2]{\caption[#1]{\textbar\,\textbf{#1} #2}}
% Math and code support -----------------------------------------------------------------------
\usepackage{amsmath, amsfonts} % For fancy math
\usepackage[separate-uncertainty = true]{siunitx} % for SI units
\sisetup{detect-all}
\usepackage{xcolor}
\NewDocumentCommand{\codeword}{v}{%
\texttt{\textcolor{black}{#1}}%
}
% Citation setup ---------------------------------------------------------------------
\usepackage[]{hyperref} % To make clickable links
\hypersetup{hidelinks,} % To draw no boxes around the links
\usepackage[ % Citation setup
backend=biber, % Citation backend
style=apa, % Citation format
% minbibnames=1,
% maxbibnames=99,
%maxnames=2, % Max number of names displayed in text
%sortlocale=de_DE, % Sorts by german conventions (ß = ss)
%natbib=true, % Enables \citep and \citet. Use \parencite and \textcite in future
url=false, % Disaples url
doi=true, % To print the doi
eprint=false % Disables link to eprint (?)
]{biblatex}
\DeclareDelimFormat[parencite]{finalnamedelim}{\addspace and\space} % disabels & and uses 'and'
\DeclareDelimFormat[bib,biblist]{finalnamedelim}{\addspace and\space} % disabels & and uses 'and' in the bibfile
\addbibresource{chirpdetection.bib} % Where the bibliography file is
\renewcommand{\familydefault}{\sfdefault}
\usepackage{tgheros}
\usepackage{wrapfig}
%\setlength{\marginparwidth}{1.8cm}
\usepackage[disable]{todonotes}
%\DeclareUnicodeCharacter{0301}{\textcolor{red}{*************************************}} % for finding unicode erros in the pdf
\setlength\bibitemsep{2\itemsep}
% Abstract title same size as sectional headers --------------------------------------
% see here: https://tex.stackexchange.com/questions/366169/how-to-change-font-size-for-abstract-title
\makeatletter
\renewenvironment{abstract}{%
\if@twocolumn
\section*{\abstractname}%
\else %% <- here I've removed \small
\begin{center}%
{\bfseries \Large\abstractname\vspace{\z@}}% %% <- here I've added \Large
\end{center}%
\quotation
\fi}
{\if@twocolumn\else\endquotation\fi}
\makeatother
% Document ---------------------------------------------------------------------------
\begin{document}
\input{chapters/titlepage.tex} % Add the path to the titlepage here
\listoftodos
\section{Introduction}
\label{chap:introduction}
\input{chapters/introduction.tex}
\pagebreak
\section{Methods}
\label{chap:methods}
\input{chapters/methods.tex}
\pagebreak
\section{Results}
\label{chap:results}
\input{chapters/results.tex}
\pagebreak
\section{Discussion}
\label{chap:discussion}
\input{chapters/discussion.tex}
\pagebreak
\printbibliography
%\pagebreak
%\section{Appendix}
% \label{chap:appendix}
% \input{chapters/appendix.tex}
% Append pdf outputs from other programs to the document
% \includepdf[pages=-]{appendix/ab.pdf}
% \includepdf[pages=-]{appendix/cd.pdf}
% \includepdf[pages=-]{appendix/ef.pdf}
\end{document}

View File

@ -1,86 +1,140 @@
cmocean==3.0.3
asttokens==2.2.1
backcall==0.2.0
contourpy==1.0.6
cycler==0.11.0
ipython==8.12.0
matplotlib==3.7.1
numpy==1.24.2
pandas==2.0.0
paramiko==3.1.0
PyYAML==6.0
scipy==1.10.1
scp==0.14.5
tqdm==4.65.0
decorator==5.1.1
executing==1.2.0
fonttools==4.38.0
ipython==8.8.0
jedi==0.18.2
kiwisolver==1.4.4
matplotlib==3.6.2
matplotlib-inline==0.1.6
numpy==1.24.1
packaging==23.0
pandas==1.5.2
parso==0.8.3
pexpect==4.8.0
pickleshare==0.7.5
Pillow==9.4.0
prompt-toolkit==3.0.36
ptyprocess==0.7.0
pure-eval==0.2.2
Pygments==2.14.0
pyparsing==3.0.9
python-dateutil==2.8.2
pytz==2022.7
scipy==1.10.0
six==1.16.0
sklearn==0.0.post1
stack-data==0.6.2
traitlets==5.8.0
wcwidth==0.2.5
asttokens==2.2.1
audioio==0.10.0
backcall==0.2.0
contourpy==1.0.6
cycler==0.11.0
decorator==5.1.1
executing==1.2.0
fonttools==4.38.0
ipython==8.8.0
jedi==0.18.2
kiwisolver==1.4.4
matplotlib==3.6.2
matplotlib-inline==0.1.6
numpy==1.24.1
packaging==23.0
pandas==1.5.2
parso==0.8.3
pexpect==4.8.0
pickleshare==0.7.5
Pillow==9.4.0
prompt-toolkit==3.0.36
ptyprocess==0.7.0
pure-eval==0.2.2
Pygments==2.14.0
pyparsing==3.0.9
python-dateutil==2.8.2
pytz==2022.7
scipy==1.10.0
six==1.16.0
sklearn==0.0.post1
stack-data==0.6.2
thunderfish==1.9.10
traitlets==5.8.0
wcwidth==0.2.5
appnope==0.1.3
asttokens==2.2.1
audioio==0.10.0
backcall==0.2.0
contourpy==1.0.6
cycler==0.11.0
decorator==5.1.1
executing==1.2.0
fonttools==4.38.0
ipython==8.8.0
jedi==0.18.2
kiwisolver==1.4.4
matplotlib==3.6.2
matplotlib-inline==0.1.6
numpy==1.24.1
packaging==23.0
pandas==1.5.2
parso==0.8.3
pexpect==4.8.0
pickleshare==0.7.5
Pillow==9.4.0
prompt-toolkit==3.0.36
ptyprocess==0.7.0
pure-eval==0.2.2
Pygments==2.14.0
pyparsing==3.0.9
python-dateutil==2.8.2
pytz==2022.7
scipy==1.10.0
six==1.16.0
sklearn==0.0.post1
stack-data==0.6.2
thunderfish==1.9.10
traitlets==5.8.0
wcwidth==0.2.5
asttokens==2.2.1
astunparse==1.6.3
audioio==0.10.0
backcall==0.2.0
bcrypt==4.0.1
cffi==1.15.1
cmocean==3.0.3
comm==0.1.3
contourpy==1.0.7
cryptography==40.0.2
cmocean==2.0
contourpy==1.0.6
cycler==0.11.0
debugpy==1.6.7
decorator==5.1.1
execnb==0.1.5
executing==1.2.0
fastcore==1.5.29
fonttools==4.39.3
ghapi==1.0.3
ipykernel==6.22.0
ipython==8.12.0
fonttools==4.38.0
ipython==8.8.0
jedi==0.18.2
joblib==1.2.0
jupyter_client==8.2.0
jupyter_core==5.3.0
kiwisolver==1.4.4
matplotlib==3.7.1
matplotlib==3.6.2
matplotlib-inline==0.1.6
nbdev==2.3.12
nest-asyncio==1.5.6
numpy==1.24.2
packaging==23.1
pandas==2.0.0
paramiko==3.1.0
numpy==1.24.1
packaging==23.0
pandas==1.5.2
parso==0.8.3
pexpect==4.8.0
pickleshare==0.7.5
Pillow==9.5.0
platformdirs==3.3.0
prompt-toolkit==3.0.38
psutil==5.9.5
Pillow==9.4.0
prompt-toolkit==3.0.36
ptyprocess==0.7.0
pure-eval==0.2.2
pycparser==2.21
Pygments==2.15.1
PyNaCl==1.5.0
Pygments==2.14.0
pyparsing==3.0.9
PyQt5==5.15.9
PyQt5-Qt5==5.15.2
PyQt5-sip==12.12.1
PyQt6==6.5.0
PyQt6-Qt6==6.5.0
PyQt6-sip==13.5.1
PySide2==5.13.2
PySide6==6.5.0
PySide6-Addons==6.5.0
PySide6-Essentials==6.5.0
python-dateutil==2.8.2
pytz==2023.3
pytz==2022.7
PyYAML==6.0
pyzmq==25.0.2
scikit-learn==1.2.2
scipy==1.10.1
scp==0.14.5
shiboken2==5.13.2
shiboken6==6.5.0
scikit-learn==1.2.0
scipy==1.10.0
six==1.16.0
sklearn==0.0.post1
stack-data==0.6.2
threadpoolctl==3.1.0
thunderfish==1.9.10
tornado==6.3.1
tqdm==4.65.0
traitlets==5.9.0
tzdata==2023.3
watchdog==3.0.0
wcwidth==0.2.6
traitlets==5.8.0
wcwidth==0.2.5