finished figure 3

This commit is contained in:
Jan Benda 2026-01-27 18:43:02 +01:00
parent 2328e025c7
commit 5e254c2aaf
9 changed files with 1087996 additions and 129 deletions

View File

@ -0,0 +1,58 @@
# Recording:
# Recording quality: good
# Comment : at end maldetection
# Experimenter : Alexandra Rudnaya
# WaterTemperature : 26.5°C
# WaterConductivity: 300uS/cm
# Cell:
# CellType : unkown
# Structure : Nerve
# BrainRegion : P-units
# BrainSubRegion : ~
# Depth : 1330um
# Lateral position : 0mm
# Transverse section: 8
# Cell properties:
# Firing Rate1: 142Hz
# P-Value1 : 0.21
# X Position : 0mm
# Y Position : 0mm
# Subject:
# Species : Apteronotus leptorhynchus
# Gender : Female
# Size : 15cm
# Weight : 0.8g
# Identifier : "2020lepto09"
# EOD Frequency : 667Hz
# Snout Position: 0mm
# Tail Position : 0mm
# Preparation:
# Type : in vivo
# Anaesthesia : true
# Anaesthetic : MS 222
# AnaestheticDose : 125mg/l
# LocalAnaesthesia : true
# LocalAnaesthetic : Lidocaine
# Immobilization : true
# ImmobilizationDrug: Tubocurarin 5mg/L
# ImmobilizationDose: 75ul
# Name : "2021-08-03-ac-invivo-1"
# Folder : /home/efish/data/ephys/labrotation/2021-08-03-ac-invivo-1
# File : [ trace-1.raw, trace-2.raw, trace-3.raw, trace-4.raw, trace-5.raw, stimulus-events.dat, restart-events.dat, recording-events.dat, spikes-1-events.dat, chirps-events.dat, localbeat-1-1-events.dat, localbeat-1-2-events.dat, stimuli.dat, stimulus-descriptions.dat ]
# Date : "2021-08-03"
# Time : "14:22:15.000"
# Recording duratio: 25.27min
# Mode : Acquisition
# Software : RELACS
# Software version : "0.9.8"
# Setup:
# Identifier: invivo1
# Maintainer: Jan Grewe
# Creator : Jan Grewe and Jan Benda
# Location : "5A17"
# Lab : Neuroethology Lab
# Institute : Institute for Neurobiology
# University: University Tuebingen
# Address : Auf der Morgenstelle 28
# Electrode:
# Resistance: 0MOhm

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -333,7 +333,7 @@ The P-unit model parameters and spectral analysis algorithms are available at \u
The baseline firing rate $r$ was calculated as the number of spikes divided by the duration of the baseline recording (median 32\,s). The coefficient of variation (CV) of the interspike intervals (ISI) is their standard deviation relative to their mean: $\rm{CV}_{\rm base} = \sqrt{\langle (ISI- \langle ISI \rangle) ^2 \rangle} / \langle ISI \rangle$. If the baseline was recorded several times in a recording, the measures from the longest recording were taken.
\paragraph{White noise analysis} \label{response_modulation}
When stimulated with band-limited white noise stimuli, neuronal activity is modulated around the average firing rate that is similar to the baseline firing rate and in that way encodes the time-course of the stimulus. For an estimate of the time-dependent firing rate $r(t)$ we convolved each spike train with normalized Gaussian kernels with standard deviation of 2\,ms, if not mentioned otherwise, and averaged the resulting single-trail firing rates over trials. The response modulation quantifies the variation of $r(t)$ computed as the standard deviation in time $\sigma_{s} = \sqrt{\langle (r(t)-\langle r(t) \rangle_t )^2\rangle_t}$, where $\langle \cdot \rangle_t$ denotes averaging over time.
When stimulated with band-limited white noise stimuli, neuronal activity is modulated around the average firing rate that is similar to the baseline firing rate and in that way encodes the time-course of the stimulus. For an estimate of the time-dependent firing rate $r(t)$ we convolved each spike train with normalized Gaussian kernels with standard deviation of 2\,ms and averaged the resulting single-trail firing rates over trials. The response modulation quantifies the variation of $r(t)$ computed as the standard deviation in time $\sigma_{s} = \sqrt{\langle (r(t)-\langle r(t) \rangle_t )^2\rangle_t}$, where $\langle \cdot \rangle_t$ denotes averaging over time.
\paragraph{Spectral analysis}\label{susceptibility_methods}
To characterize the relation between the spiking response evoked by white-noise stimuli, we estimated the first- and second-order susceptibilities in the frequency domain. For this we converted spike times into binary vectors $x_k$ with $\Delta t = 0.5$\,ms wide bins that are set to 2\,kHz where a spike occurred and zero otherwise. Fast Fourier transforms (FFT) $S(\omega)$ and $X(\omega)$ of the stimulus $s_k$ (also down-sampled to a sampling rate of 2\,kHz) and $x_k$, respectively, were computed numerically according to
@ -401,7 +401,7 @@ Values larger than one indicate a sharp ridge in the susceptibility matrix close
\begin{figure*}[t]
\includegraphics[width=\columnwidth]{flowchart.pdf}
\includegraphics[width=\columnwidth]{flowchart}
\caption{\label{flowchart}
Architecture of the P-unit model. Each row illustrates subsequent processing steps for three different stimulation regimes: (i) baseline activity without external stimulus, only the fish's self-generated EOD (the carrier, \eqnref{eq:eod}) is present. (ii) RAM stimulation, \eqnref{eq:ram_equation}. The amplitude of the EOD carrier is modulated with a weak (2\,\% contrast) band-limited white-noise stimulus. (iii) Noise split, \eqnsref{eq:ram_split}--\eqref{eq:Noise_split_intrinsic}, where 90\,\% of the intrinsic noise is replaced by a RAM stimulus, whose amplitude is scaled to maintain the mean firing rate and the CV of the ISIs of the model's baseline activity. As an example, simulations of the model for cell ``2012-07-03-ak'' are shown. \figitem{A} The stimuli are thresholded, \eqnref{eq:threshold2}, by setting all negative values to zero. \figitem{B} Subsequent low-pass filtering, \eqnref{eq:dendrite}, attenuates the carrier and carves out the AM signal. \figitem{C} Intrinsic Gaussian white-noise is added to the signals shown in \panel{B}. Note the reduced internal noise amplitude in the noise split (iii) condition. \figitem{D} Spiking output of the LIF model, \eqnsref{eq:LIF}--\eqref{spikethresh}, in response to the sum of \panel{B} and \panel{C}. \figitem{E} Power spectra of the LIF neuron's spiking activity. Both, baseline activity (\panel[i]{E}) and noise split (\panel[iii]{E}), have the same peaks in the response spectrum at $r$, $f_{EOD} - r$, $f_{EOD}$, and $f_{EOD} + r$. With RAM stimulation (\panel[ii]{E}), the peak at the baseline firing rate, $r$, is washed out.}
\end{figure*}
@ -485,9 +485,8 @@ Both, the reduced intrinsic noise and the RAM stimulus, need to replace the orig
\section{Results}
\begin{figure*}[t]
\includegraphics[width=\columnwidth]{motivation.pdf}
\caption{\label{fig:motivation} Nonlinearity in an electrophysiologically recorded P-unit of \lepto{} in a three-fish setting (cell identifier ``2021-08-03-ac"). Receiver with EOD frequency $f_{\rm EOD} =664$\,Hz encounters fish with EOD frequencies $f_{1}=631$\,Hz and $f_{2}=797$\,Hz. Both foreign signals have the same strength relative to the own field amplitude (10\,\% contrast). Top row: Sketch of signal processing in the nonlinear system (black box). Second row: Interference of the receiver EOD with the EODs of other fish, bold line highlights the amplitude modulation. Third row: Respective spike trains of the recorded P-unit. Fourth row: Firing rate, estimated by convolution of the spike trains with a Gaussian kernel ($\sigma = 1$\,ms). Bottom row: Power spectrum of the firing rate. \figitem{A} Baseline condition: The cell is driven by the self-generated field alone. The baseline firing rate $r$ dominates the power spectrum of the firing rate ($f_{base} = 139$\,Hz). \figitem{B} The receiver's EOD and a foreign fish with an EOD frequency $f_{1}=631$\,Hz are present. EOD interference induces an amplitude modulation, referred to as beat. \figitem{C} The receiver and a fish with an EOD frequency $f_{2}=797$\,Hz are present. The resulting beat is faster as the difference between the individual frequencies is larger. \figitem{D} All three fish with the EOD frequencies $f_{\rm EOD}$, $f_1$ and $f_2$ are present. A second-order amplitude modulation occurs, commonly referred to as envelope. Nonlinear peaks occur at the sum and difference of the two beat frequencies in the power spectrum of the firing rate.
}
\includegraphics[width=\columnwidth]{twobeats}
\caption{\label{fig:motivation} Nonlinearity in an electrophysiologically recorded P-unit of \lepto{} in a three-fish setting (cell identifier ``2021-08-03-ac"). Receiver with EOD frequency $f_{\rm EOD} =664$\,Hz encounters fish with EOD frequencies $f_{1}=631$\,Hz and $f_{2}=797$\,Hz. Both foreign signals have the same strength relative to the own field amplitude (10\,\% contrast). Top row: Sketch of signal processing in the nonlinear system (black box). Second row: Superposition of the receiver EOD with the EODs of other fish, colored line highlights the amplitude modulation. Third row: Three trials of spike trains of the recorded P-unit. Fourth row: Firing rate, estimated by convolution of the spike trains with a Gaussian kernel. Bottom row: Power spectrum of the spike trains. \figitem{A} Baseline condition: The cell is driven by the self-generated field alone. The baseline firing rate $r = 139$\,Hz dominates the power spectrum. \figitem{B} The receiver's EOD and a single conspecific with an EOD frequency $f_{1}=631$\,Hz are present. Superposition of the two EODs induces an amplitude modulation, referred to as beat, with beat frequeny $\Delta f_1=33$\,Hz. \figitem{C} The receiver and a fish with an EOD frequency $f_{2}=797$\,Hz are present. The resulting beat $\Delta f_2=133$\,Hz is faster as the difference between the EOD frequencies is larger. \figitem{D} All three fish with EOD frequencies $f_{\rm EOD}$, $f_1$, and $f_2$ are present. A second-order amplitude modulation occurs, commonly referred to as envelope. Additional peaks occur in the power spectrum of the spike response at the sum and difference of the two beat frequencies, indicating non-linear interactions between the tow frequencies in the P-unit.}
\end{figure*}
We explored a large set of electrophysiological data from primary afferents of the active and passive electrosensory system, P-units and ampullary cells \citep{Grewe2017, Hladnik2023}, that were recorded in the brown ghost knifefish \textit{Apteronotus leptorhynchus}. We re-analyzed this dataset to search for weakly nonlinear responses that have been predicted in previous theoretical work \citep{Voronenko2017}. Additional simulations of LIF-based models of P-unit spiking help to interpret the experimental findings in this theoretical framework. We start with demonstrating the basic concepts using example P-units and respective models and then compare the population of recordings in both cell types.
@ -502,7 +501,7 @@ When stimulating with both foreign signals simultaneously, additional peaks appe
\begin{figure*}[p]
\includegraphics[width=\columnwidth]{regimes}
\caption{\label{fig:regimes} Linear and nonlinear responses of a model P-unit in a three-fish setting in dependence on stimulus amplitudes. The model P-unit (identifier ``2018-05-08-ad'') was stimulated with two sine waves of equal amplitude (contrast) at difference frequencies $\Delta f_1=40$\,Hz and $\Delta f_2=228$\,Hz relative the receiver's EOD frequency. $\Delta f_2$ was set to match the baseline firing rate $r$ of the P-unit. \figitem{A--D} Top: the stimulus, an amplitude modulation of the receiver's EOD resulting from the stimulation with the two sine waves. The contrasts of both beats increase from \panel{A} to \panel{D} as indicated. Middle: Spike raster of the model P-unit response. Bottom: power spectrum of the firing rate estimated from the spike raster. \figitem{A} At low stimulus contrasts the response is linear. The only peaks in the response spectrum are at the two stimulating beat frequencies (green and purple marker). \figitem{B} At moderately higher stimulus contrast, the peaks in the response spectrum at the two beat frequencies are larger. \figitem{C} At intermediate stimulus contrasts, nonlinear responses start to appear at the sum and the difference of the stimulus frequencies (orange and red marker). \figitem{D} At higher stimulus contrasts additional peaks appear in the power spectrum. \figitem{E} Amplitude of the linear (at $\Delta f_1$ and $\Delta f_2$) and nonlinear (at $\Delta f_2 - \Delta f_1$ and $\Delta f_1 + \Delta f_2$) responses of the model P-unit as a function of beat contrast (thick lines). Thin lines indicate the initial linear and quadratic dependence on stimulus amplitude for the linear and nonlinear responses, respectively. In the linear regime, below a stimulus contrast of about 1.2\,\% (left vertical line), the only peaks in the response spectrum are at the stimulus frequencies. In the weakly nonlinear regime up to a contrast of about 3.5\,\% peaks arise at the sum and the difference of the two stimulus frequencies. At stronger stimulation the amplitudes of these nonlinear responses deviate from the quadratic dependency on stimulus contrast.}
\caption{\label{fig:regimes} Linear and nonlinear responses of a model P-unit in a three-fish setting in dependence on stimulus amplitudes. The model P-unit (identifier ``2018-05-08-ad'') was stimulated with two sine waves of equal amplitude (contrast) at difference frequencies $\Delta f_1=40$\,Hz and $\Delta f_2=228$\,Hz relative the receiver's EOD frequency. $\Delta f_2$ was set to match the baseline firing rate $r$ of the P-unit. \figitem{A--D} Top: the stimulus, an amplitude modulation of the receiver's EOD resulting from the stimulation with the two sine waves. The contrasts of both beats increase from \panel{A} to \panel{D} as indicated. Middle: Spike raster of the model P-unit response. Bottom: power spectrum of the firing rate estimated from the spike raster. \figitem{A} At low stimulus contrasts the response is linear. The only peaks in the response spectrum are at the two stimulating beat frequencies (green and purple marker), the latter enhancing the peak at baseline firig rate (blue). The largest peak, however, is always the one atthe EOD frequency of the receiver (black). Here also flanked by harmonics of $\Delta f_1$ (small black markers). \figitem{B} At moderately higher stimulus contrast, the peaks in the response spectrum at the two beat frequencies become larger. In addition, a peak at $f_{EOD} - \Delta f_2 $ appears. \figitem{C} At intermediate stimulus contrasts, nonlinear responses start to appear at the sum and the difference of the stimulus frequencies (orange and red). Additional peaks appear at harmonics of $\Delta f_1$ (small green markers) and at $f_{EOD} \Delta f_2 \pm \Delta f_1$ (small purple marker). \figitem{D} At higher stimulus contrasts further peaks appear in the power spectrum. \figitem{E} Amplitude of the linear (at $\Delta f_1$ and $\Delta f_2$) and nonlinear (at $\Delta f_2 - \Delta f_1$ and $\Delta f_1 + \Delta f_2$) responses of the model P-unit as a function of beat contrast (thick lines). Thin lines indicate the initial linear and quadratic dependence on stimulus amplitude for the linear and nonlinear responses, respectively. In the linear regime, below a stimulus contrast of about 1.2\,\% (left vertical line), the only peaks in the response spectrum are at the stimulus frequencies. In the weakly nonlinear regime up to a contrast of about 3.5\,\% peaks arise at the sum and the difference of the two stimulus frequencies. At stronger stimulation the amplitudes of these nonlinear responses deviate from the quadratic dependency on stimulus contrast.}
\end{figure*}
The stimuli used in \figref{fig:motivation} had the same not-small amplitude. Whether this stimulus condition falls into the weakly nonlinear regime as in \citet{Voronenko2017} is not clear. In order to illustrate how the responses to two beat frequencies develop over a range of amplitudes we use a stochastic leaky-integrate-and-fire (LIF) based P-unit model fitted to a specific electrophysiologically measured cell \citep{Barayeu2023}.

View File

@ -137,23 +137,29 @@ def plot_style():
ns.lsRaster = dict(color=palette['black'], lw=ns.lwthin)
ns.lsRate = dict(color=palette['blue'], lw=ns.lwmid)
ns.lsPower = dict(color=palette['gray'], lw=ns.lwmid)
ns.lsF0 = dict(color='blue', lw=ns.lwthick)
ns.lsF01 = dict(color='green', lw=ns.lwthick)
ns.lsF02 = dict(color='purple', lw=ns.lwthick)
ns.lsF012 = dict(color='orange', lw=ns.lwthick)
ns.lsF01_2 = dict(color='red', lw=ns.lwthick)
ns.lsF0m = dict(color=lighter('blue', 0.5), lw=ns.lwthin)
ns.lsF01m = dict(color=lighter('green', 0.6), lw=ns.lwthin)
ns.lsF02m = dict(color=lighter('purple', 0.5), lw=ns.lwthin)
ns.lsF012m = dict(color=darker('orange', 0.9), lw=ns.lwthin)
ns.lsF01_2m = dict(color=darker('red', 0.9), lw=ns.lwthin)
ns.lsF0 = dict(color=palette['blue'], lw=ns.lwthick)
ns.lsF01 = dict(color=palette['green'], lw=ns.lwthick)
ns.lsF02 = dict(color=palette['purple'], lw=ns.lwthick)
ns.lsF012 = dict(color=palette['orange'], lw=ns.lwthick)
ns.lsF01_2 = dict(color=palette['red'], lw=ns.lwthick)
ns.lsF0m = dict(color=lighter(palette['blue'], 0.5), lw=ns.lwthin)
ns.lsF01m = dict(color=lighter(palette['green'], 0.6), lw=ns.lwthin)
ns.lsF02m = dict(color=lighter(palette['purple'], 0.5), lw=ns.lwthin)
ns.lsF012m = dict(color=darker(palette['orange'], 0.9), lw=ns.lwthin)
ns.lsF01_2m = dict(color=darker(palette['red'], 0.9), lw=ns.lwthin)
ns.psFEOD = dict(color='black', marker='o', linestyle='none', markersize=5, mec='none', mew=0)
ns.psF0 = dict(color='blue', marker='o', linestyle='none', markersize=5, mec='none', mew=0)
ns.psF01 = dict(color='green', marker='o', linestyle='none', markersize=5, mec='none', mew=0)
ns.psF02 = dict(color='purple', marker='o', linestyle='none', markersize=5, mec='none', mew=0)
ns.psF012 = dict(color='orange', marker='o', linestyle='none', markersize=5, mec='none', mew=0)
ns.psF01_2 = dict(color='red', marker='o', linestyle='none', markersize=5, mec='none', mew=0)
ns.psFEOD = dict(color=palette['black'], marker='o', linestyle='none', markersize=4, mec='none', mew=0)
ns.psF0 = dict(color=palette['blue'], marker='o', linestyle='none', markersize=4, mec='none', mew=0)
ns.psF01 = dict(color=palette['green'], marker='o', linestyle='none', markersize=4, mec='none', mew=0)
ns.psF02 = dict(color=palette['purple'], marker='o', linestyle='none', markersize=4, mec='none', mew=0)
ns.psF012 = dict(color=palette['orange'], marker='o', linestyle='none', markersize=4, mec='none', mew=0)
ns.psF01_2 = dict(color=palette['red'], marker='o', linestyle='none', markersize=4, mec='none', mew=0)
ns.psFEODm = dict(color=palette['black'], marker='o', linestyle='none', markersize=3, mec='none', mew=0)
ns.psF0m = dict(color=palette['blue'], marker='o', linestyle='none', markersize=3, mec='none', mew=0)
ns.psF01m = dict(color=palette['green'], marker='o', linestyle='none', markersize=3, mec='none', mew=0)
ns.psF02m = dict(color=palette['purple'], marker='o', linestyle='none', markersize=3, mec='none', mew=0)
ns.psF012m = dict(color=palette['orange'], marker='o', linestyle='none', markersize=3, mec='none', mew=0)
ns.psF01_2m = dict(color=palette['red'], marker='o', linestyle='none', markersize=3, mec='none', mew=0)
ns.model_color1 = palette['purple']
ns.model_color2 = lighter(ns.model_color1, 0.6)

View File

@ -250,7 +250,16 @@
frequencies to illustrate these and it is not clear why these are
clipped in these two figures.}
\response{HM. LETS CHECK HOW IT LOOKS LIKE. BUT THIS LOW FREQUENCY RANGE IS THE RELEVANT ONE FOR CODING.}
\response{You are right. In figure 4 we show now the spectrum up to
750Hz, such that fEOD and its interactions with df2 and harmonics
are included. We labeled the additonal peaks accordingly. In figure
3 we stay with the small range, because we have so little data (only
three trials of 500ms duration) for this special setting where one
of the beat frequencies approximately matches the P-units baseline
firing rate. This is why the power spectra are very noisy. Also, for
an introductory figure we prefer to only show the few peaks that are
relevant for the rest of the manuscript, such that the reader does
not get overwhelmed. }
\issue{(6) Figure 3. Why are these example firing rates based on
convolution with a 1 ms Gaussian kernel if the analyses were based
@ -259,7 +268,9 @@
actually analyzed. More fundamentally, why would a 2-fold difference
in kernel width be appropriate for presentation vs. analysis?}
\response{DAMN. LETS REDO THE FIGURE.}
\response{This was for historical reasons. We updated figure 3 to also
use the 2ms kernel. Now all firing rates in the manuscript are based
on the 2ms kernel.}
\issue{(7) Figure 3D legend. The relationship between 2nd order AM
(envelope) and the two nonlinear peaks should be made clear. I

View File

@ -178,8 +178,14 @@ def decibel(x):
return 10*np.log10(x/1e8)
def plot_psd(ax, s, path, contrast, spikes, nfft, dt, beatf1, beatf2):
offs = 4
def peak_ampl(freqs, psd, f, df=2):
psd_snippet = psd[(freqs > f - df) & (freqs < f + df)]
return np.max(psd_snippet)
def plot_psd(ax, s, path, contrast, spikes, nfft, dt, beatf1, beatf2, eodf):
offs = 5
offsm = 3
freqs, psd = compute_power(path, contrast, spikes, nfft, dt)
psd /= freqs[1]
ax.plot(freqs, decibel(psd), **s.lsPower)
@ -187,19 +193,36 @@ def plot_psd(ax, s, path, contrast, spikes, nfft, dt, beatf1, beatf2):
label=r'$r$', clip_on=False, **s.psF0)
ax.plot(beatf1, decibel(peak_ampl(freqs, psd, beatf1)) + offs,
label=r'$\Delta f_1$', clip_on=False, **s.psF01)
ax.plot(beatf2, decibel(peak_ampl(freqs, psd, beatf2)) + 2*offs + 3,
ax.plot(beatf2, decibel(peak_ampl(freqs, psd, beatf2)) + 2*offs + 2,
label=r'$\Delta f_2$', clip_on=False, **s.psF02)
ax.plot(beatf2 - beatf1, decibel(peak_ampl(freqs, psd, beatf2 - beatf1)) + offs,
label=r'$\Delta f_2 - \Delta f_1$', clip_on=False, **s.psF01_2)
ax.plot(beatf1 + beatf2, decibel(peak_ampl(freqs, psd, beatf1 + beatf2)) + offs,
label=r'$\Delta f_1 + \Delta f_2$', clip_on=False, **s.psF012)
ax.set_xlim(0, 300)
ax.plot(eodf, decibel(peak_ampl(freqs, psd, eodf)) + offs,
label=r'$f_{EOD}$', clip_on=False, **s.psFEOD)
ax.plot(eodf + beatf1, decibel(peak_ampl(freqs, psd, eodf + beatf1)) + offsm, label=r'$f_{EOD} \pm k \Delta f_1$', **s.psFEODm)
ax.plot(eodf - beatf1, decibel(peak_ampl(freqs, psd, eodf - beatf1)) + offsm, **s.psFEODm)
if contrast > 0.02:
ax.plot(2*beatf1, decibel(peak_ampl(freqs, psd, 2*beatf1)) + offsm, label=r'$k\Delta f_1$', **s.psF01m)
ax.plot(eodf + 2*beatf1, decibel(peak_ampl(freqs, psd, eodf + 2*beatf1)) + offsm, **s.psFEODm)
if contrast > 0.008:
ax.plot(eodf - beatf2, decibel(peak_ampl(freqs, psd, eodf - beatf2)) + offsm, label=r'$f_{EOD} - \Delta f_2$', **s.psF0m)
if contrast > 0.02:
ax.plot(eodf - beatf2 + beatf1, decibel(peak_ampl(freqs, psd, eodf - beatf2 + beatf1)) + offsm, label=r'$f_{EOD} - \Delta f_2 \pm \Delta f_1$', **s.psF02m)
ax.plot(eodf - beatf2 - beatf1, decibel(peak_ampl(freqs, psd, eodf - beatf2 - beatf1)) + offsm, **s.psF02m)
if contrast > 0.05:
ax.plot(3*beatf1, decibel(peak_ampl(freqs, psd, 3*beatf1)) + offsm, **s.psF01m)
ax.plot(4*beatf1, decibel(peak_ampl(freqs, psd, 4*beatf1)) + offsm, **s.psF01m)
ax.plot(eodf - 2*beatf1, decibel(peak_ampl(freqs, psd, eodf - 2*beatf1)) + offsm, **s.psFEODm)
ax.set_xlim(0, 750)
ax.set_ylim(-60, 0)
ax.set_xticks_delta(200)
ax.set_xlabel('Frequency', 'Hz')
ax.set_ylabel('Power [dB]')
def plot_example(axs, axr, axp, s, path, cell, alpha, beatf1, beatf2,
def plot_example(axs, axr, axp, s, path, cell, alpha, beatf1, beatf2, eodf,
nfft, trials):
dt = 0.0001
tmax = nfft*dt
@ -207,13 +230,7 @@ def plot_example(axs, axr, axp, s, path, cell, alpha, beatf1, beatf2,
spikes = punit_spikes(cell, alpha, beatf1, beatf2, tmax, trials)
plot_am(axs, s, alpha, beatf1, beatf2, t1)
plot_raster(axr, s, spikes, t1)
plot_psd(axp, s, path, alpha, spikes, nfft, dt, beatf1, beatf2)
def peak_ampl(freqs, psd, f):
df = 2
psd_snippet = psd[(freqs > f - df) & (freqs < f + df)]
return np.max(psd_snippet)
plot_psd(axp, s, path, alpha, spikes, nfft, dt, beatf1, beatf2, eodf)
def amplitude(power):
@ -296,6 +313,7 @@ if __name__ == '__main__':
parameters = load_models(data_path / 'punitmodels.csv')
cell = cell_parameters(parameters, model_cell)
eodf = cell['EODf']
nfft = 2**18
print(f'Loaded data for cell {model_cell}: ')
@ -305,20 +323,20 @@ if __name__ == '__main__':
s = plot_style()
fig, (axes, axa) = plt.subplots(2, 1, height_ratios=[4, 3],
cmsize=(s.plot_width, 0.6*s.plot_width))
cmsize=(s.plot_width, 0.65*s.plot_width))
fig.subplots_adjust(leftm=8, rightm=2, topm=2, bottomm=3.5, hspace=0.6)
axe = axes.subplots(3, 4, wspace=0.4, hspace=0.2,
height_ratios=[1, 2, 3])
height_ratios=[1, 2, 0.6, 3])
fig.show_spines('lb')
# example power spectra:
for c, alpha in enumerate(alphas):
path = sims_path / f'{model_cell}-contrastspectrum-{1000*alpha:03.0f}.npz'
plot_example(axe[0, c], axe[1, c], axe[2, c], s, path,
cell, alpha, beatf1, beatf2, nfft, 100)
cell, alpha, beatf1, beatf2, eodf, nfft, 100)
axe[1, 0].xscalebar(1, -0.1, 20, 'ms', ha='right')
axe[2, 0].legend(loc='center left', bbox_to_anchor=(0, -0.8),
ncol=5, columnspacing=2)
axe[2, 3].legend(loc='center right', bbox_to_anchor=(1, -0.8),
ncol=10, columnspacing=1, handletextpad=0.1)
fig.common_yspines(axe[0, :])
fig.common_yticks(axe[2, :])
fig.tag(axe[0, :], xoffs=-3, yoffs=1.6)

View File

@ -3,6 +3,7 @@ import matplotlib.pyplot as plt
from pathlib import Path
from scipy.stats import norm
from scipy.optimize import curve_fit
from spectral import rate
from plotstyle import plot_style
@ -12,21 +13,13 @@ cell = '2021-08-03-ac-invivo-1'
data_path = Path('data')
def load_data(cell_path, f1=797, f2=631):
def load_spikes(cell_path, f1=797, f2=631):
load = False
spikes = []
index = 0
with open(cell_path / 'threefish-spikes.dat') as sf:
for line in sf:
if line.startswith('# EOD rate '):
eodf = float(line.split(':')[1].strip().replace('Hz', ''))
elif line.startswith('# Deltaf1 '):
df1 = float(line.split(':')[1].strip().replace('Hz', ''))
elif line.startswith('# Deltaf2 '):
df2 = float(line.split(':')[1].strip().replace('Hz', ''))
if abs(eodf + df1 - f1) < 1 and abs(eodf + df2 - f2) < 1:
#print(f'EODf={eodf:6.1f}Hz, Df1={df1:6.1f}Hz, Df2={df2:6.1f}Hz, EODf1={eodf + df1:6.1f}Hz, EODf2={eodf + df2:6.1f}Hz')
load = True
elif load:
if load:
if ' before:' in line:
t0 = 0.001*float(line.split(':')[1].strip().replace('ms', ''))
elif ' duration1 ' in line:
@ -38,7 +31,7 @@ def load_data(cell_path, f1=797, f2=631):
elif line.startswith('# index '):
if len(spikes) > 0:
spikes[-1] = np.array(spikes[-1])
return spikes, eodf, df1, df2, t0, t1, t2, t12
return spikes, eodf, df1, df2, t0, t1, t2, t12, index
elif line.startswith('# trial:'):
if len(spikes) > 0:
spikes[-1] = np.array(spikes[-1])
@ -46,71 +39,197 @@ def load_data(cell_path, f1=797, f2=631):
elif len(line.strip()) > 0 and line[0] != '#':
t = 0.001*float(line.strip())
spikes[-1].append(t)
elif line.startswith('# index '):
index += 1
elif line.startswith('# EOD rate '):
eodf = float(line.split(':')[1].strip().replace('Hz', ''))
elif line.startswith('# Deltaf1 '):
df1 = float(line.split(':')[1].strip().replace('Hz', ''))
elif line.startswith('# Deltaf2 '):
df2 = float(line.split(':')[1].strip().replace('Hz', ''))
if abs(eodf + df1 - f1) < 1 and abs(eodf + df2 - f2) < 1:
#print(f'EODf={eodf:6.1f}Hz, Df1={df1:6.1f}Hz, Df2={df2:6.1f}Hz, EODf1={eodf + df1:6.1f}Hz, EODf2={eodf + df2:6.1f}Hz')
load = True
print(f'no spikes found for EODf1={f1:.1f}Hz and EODf2={f2:.1f}Hz')
def align_spikes(spikes, period):
# compute rates for each trial:
tmax = np.max([s[-1] for s in spikes])
time = np.arange(0, tmax, 0.0002)
sigma = 0.001
kernel = norm.pdf(time[time < 8*sigma], loc=4*sigma, scale=sigma)
rates = []
xtime = np.append(time, time[-1] + time[1] - time[0])
for i, spiket in enumerate(spikes):
b, _ = np.histogram(spiket, xtime)
r = np.convolve(b, kernel, 'same')
rates.append(r)
# align them on the first trial:
nrates = len(rates[0])
for i in range(1, len(rates)):
rs = []
n = len(time[time <= period])
if n < 2:
n = 2
for k in range(1, 1 + n):
r = np.corrcoef(rates[0][:-k], rates[i][k:])[0, 1]
rs.append(r)
k = 1 + np.argmax(rs)
dt = time[k]
spikes[i] -= dt
print(f' shift trial {i} by {1000*dt:.0f}ms')
def load_am(cell_path, inx):
load = False
ams = []
index = 0
with open(cell_path / 'threefish-ams.dat') as sf:
for line in sf:
if load:
if line.startswith('# index '):
if len(ams) > 0:
ams[-1] = np.array(ams[-1])
return ams
elif line.startswith('# EOD rate '):
print(f' EODf = {line.split(':')[1].strip()}')
elif line.startswith('# Deltaf1 '):
print(f' Df1 = {line.split(':')[1].strip()}')
elif line.startswith('# Deltaf2 '):
print(f' DF2 = {line.split(':')[1].strip()}')
elif line.startswith('# trial:'):
if len(ams) > 0:
ams[-1] = np.array(ams[-1])
ams.append([])
elif len(line.strip()) > 0 and line[0] != '#':
time, am = line.split()
t = 0.001*float(time.strip())
a = float(am.strip())
ams[-1].append((t, a))
elif line.startswith('# index '):
index += 1
if inx == index:
load = True
print(f'no AM found at index {inx}')
def cosine(x, a, f, p, c):
return a*np.cos(2*np.pi*f*x + p) + c
def two_cosine(x, a1, f1, p1, a2, f2, p2, c):
return a1*np.cos(2*np.pi*f1*x + p1) + a2*np.cos(2*np.pi*f2*x + p2) + c
def am_phases(ams, eodf, df1, df2, t1, t2, t12):
twins = (t1, t2, t12)
dfs = ((df1,), (df2,), (df1, df2))
phases = np.zeros((len(ams), len(dfs) + 1))
for k in range(len(ams)):
t0 = 0
time = ams[k][:, 0]
am = ams[k][:, 1]
for i in range(len(twins)):
tw = twins[0]
t1 = t0 + tw
mask = (time >= t0) & (time <= t1)
tam = time[mask] - t0
aam = am[mask]
a = 0.5*(np.max(aam) - np.min(aam))
c = np.mean(aam)
tt = np.linspace(0, tw, 1000)
if len(dfs[i]) == 2:
popt = [a/2, dfs[i][0], 0, a/2, dfs[i][1], 0, c]
popt, _ = curve_fit(two_cosine, tam, aam, popt)
aa = two_cosine(tt, *popt)
phases[k, i] = popt[2] if popt[0] > 0 else popt[2] + np.pi
phases[k, i + 1] = popt[5] if popt[3] > 0 else popt[5] + np.pi
else:
popt = [a, dfs[i][0], 0, c]
popt, _ = curve_fit(cosine, tam, aam, popt)
aa = cosine(tt, *popt)
phases[k, i] = popt[2] if popt[0] > 0 else popt[2] + np.pi
t0 = t1
return phases
def align_spikes(spikes, freqs, phases):
f1, f2 = freqs
if f1 is None and f2 is None:
return spikes
p1 = phases[0]
p2 = phases[1]
if f2 is None:
df = f1
p = p1
else:
df = f2
p = p2
for i in range(len(spikes)):
spikes[i] += p[i]/2/np.pi/df
return spikes
def plot_symbols(ax, s):
def baseline_rate(spikes, t0, t1):
rates = []
for times in spikes:
c = np.sum((times > t0) & (times < t1))
rates.append(c/(t1 - t0))
return np.mean(rates)
def power_spectrum(spikes, tmax, dt=0.0005, nfft=512, p_ref=4000):
time = np.arange(0, tmax, dt)
if nfft > len(time):
print('nfft too large:', nfft, len(time))
freqs = np.fft.fftfreq(nfft, dt)
freqs = np.fft.fftshift(freqs)
segments = range(0, len(time) - nfft, nfft)
p_rr = np.zeros(len(freqs))
n = 0
for i, spiket in enumerate(spikes):
b, _ = np.histogram(spiket, time)
b = b / dt
for j, k in enumerate(segments):
fourier_r = np.fft.fft(b[k:k + nfft] - np.mean(b), n=nfft)
fourier_r = np.fft.fftshift(fourier_r)
p_rr += np.abs(fourier_r*np.conj(fourier_r))
n += 1
mask = freqs >= 0.0
freqs = freqs[mask]
scale = dt/nfft/n
p_rr = p_rr[mask]*scale
power = 10*np.log10(p_rr/p_ref)
return freqs, power
def plot_symbols(ax, s, freqs):
f1, f2 = freqs
ax.show_spines('')
ax.add_artist(plt.Rectangle((-1, -0.5), 2, 1, color=s.colors['black']))
ax.harrow(1.6, 0, 1.3, **s.asLine)
ax.set_xlim(-6, 14)
ax.set_ylim(-1, 1)
if f1 is None and f2 is None:
ax.text(3.5, 0, '$r$', va='center')
else:
ax.harrow(-2.8, 0, 1.3, **s.asLine)
if f2 is None:
ax.text(-3.2, 0, '$s_1(t)$', ha='right', va='center')
ax.text(3.3, 0, '$r + r_1(t)$', va='center')
elif f1 is None:
ax.text(-3.2, 0, '$s_2(t)$', ha='right', va='center')
ax.text(3.3, 0, '$r + r_2(t)$', va='center')
else:
ax.text(-3.2, 0, '$s_1(t) + s_2(t)$', ha='right', va='center')
ax.text(3.3, 0, '$\\ne r + r_1(t) + r_2(t)$', va='center')
def plot_stimulus(ax, s, tmax, eodf, f1, f2, c=0.1):
def plot_stimulus(ax, s, tmax, eodf, freqs, c=0.1):
time = np.arange(0, tmax, 0.0001)
eod = np.cos(2*np.pi*eodf*time)
am = np.ones(len(time))
ams = {}
f1, f2 = freqs
label = '$f_{EOD}$'
if f1 is not None:
eod += c*np.cos(2*np.pi*(eodf + f1)*time)
am += c*np.cos(2*np.pi*f1*time)
ams = s.lsF01
ams = s.lsF02
label += r' \& $f_1$'
if f2 is not None:
eod += c*np.cos(2*np.pi*(eodf + f2)*time)
am += c*np.cos(2*np.pi*f2*time)
ams = s.lsF02
ams = s.lsF01
label += r' \& $f_2$'
if f1 is not None and f2 is not None:
ams = s.lsF012
ams = s.lsF01_2
ax.show_spines('')
ax.plot(1000*time, am*eod, **s.lsStim)
ax.plot(1000*time, eod, **s.lsEOD)
if len(ams) > 0:
ax.plot(1000*time, am, **ams)
ax.set_xlim(0, 1000*tmax)
ax.set_ylim(-1.02 - 2*c, 1.02 + 2*c)
ax.text(0, 1.1, label, transform=ax.transAxes)
ax.text(0.5, 1.2, label, ha='center', transform=ax.transAxes)
def plot_raster(ax, s, spikes, tmin, tmax):
spikes_ms = [1000*(s[(s > tmin) & (s < tmax)] - tmin) for s in spikes]
ax.show_spines('')
ax.eventplot(spikes_ms, linelengths=0.9, **s.lsRaster)
ax.eventplot(spikes_ms, linelengths=0.8, **s.lsRaster)
ax.set_xlim(0, 1000*(tmax - tmin))
@ -123,71 +242,89 @@ def plot_rate(ax, s, spikes, tmin, tmax, sigma=0.002):
ax.show_spines('')
ax.plot(1000*time, r, **s.lsRate)
ax.set_xlim(0, 1000*(tmax - tmin))
ax.set_ylim(0, 500)
ax.set_ylim(-10, 550)
def plot_psd(ax, s, spikes, tmax, fmax, dt=0.0005, nfft=512):
time = np.arange(0, tmax, dt)
if nfft > len(time):
print('nfft too large:', nfft, len(time))
# power spectrum:
freqs = np.fft.fftfreq(nfft, dt)
freqs = np.fft.fftshift(freqs)
f0 = len(freqs)//4
f1 = 3*len(freqs)//4
segments = range(0, len(time) - nfft, nfft)
p_rr = np.zeros(len(freqs))
n = 0
for i, spiket in enumerate(spikes):
b, _ = np.histogram(spiket, time)
b = b / dt
for j, k in enumerate(segments):
fourier_r = np.fft.fft(b[k:k + nfft] - np.mean(b), n=nfft)
fourier_r = np.fft.fftshift(fourier_r)
p_rr += np.abs(fourier_r*np.conj(fourier_r))
n += 1
freqs = freqs[f0:f1]
scale = dt/nfft/n
p_rr = p_rr[f0:f1]*scale
def plot_psd(ax, s, freqs, power, fmax, dt=0.0005, nfft=512):
# plot:
mask = (freqs > 0) & (freqs <= fmax)
mask = freqs <= fmax
freqs = freqs[mask]
p_rr = p_rr[mask]
#print(np.max(p_rr))
p_ref = 4000
ax.plot(freqs, 10*np.log10(p_rr/p_ref), **s.lsPower)
power = power[mask]
ax.show_spines('b')
ax.plot(freqs, power, **s.lsPower)
ax.set_xlim(0, fmax)
ax.set_ylim(-20, 0)
ax.set_xlabel('Frequency', 'Hz')
def mark_freq(ax, freqs, power, f, label, style, xoffs=10, yoffs=0, toffs=0, angle=0):
i = np.argmin(np.abs(freqs - abs(f)))
p = power[i]
f = freqs[i]
ax.plot(f, p + 1 + yoffs, clip_on=False, **style)
if label:
yoffs += 3 + toffs
if angle > 0:
yoffs -= 1
ax.text(f - xoffs, p + yoffs, label, color=style['color'], rotation=angle)
if __name__ == '__main__':
spikes, eodf, df1, df2, t0, t1, t2, t12 = load_data(data_path / cell)
print(f'Loaded spike data for cell {cell}: ')
spikes, eodf, df1, df2, t0, t1, t2, t12, index = load_spikes(data_path / cell)
print(f'Loaded spike data for cell {cell} @ index {index}:')
print(f' EODf = {eodf:.1f}Hz')
print(f' Df1 = {df1:.1f}Hz')
print(f' Df2 = {df2:.1f}Hz')
print(f' {len(spikes)} trials')
print(f'Load AMs for cell {cell} @ index {index}:')
ams = load_am(data_path / cell, index)
phases = am_phases(ams, eodf, df1, df2, t1, t2, t12)
s = plot_style()
fig, axs = plt.subplots(5, 4, cmsize=(s.plot_width, 0.6*s.plot_width),
height_ratios=[1, 2, 1, 3, 6])
height_ratios=[1, 0, 2, 1.3, 3, 0.7, 5])
fig.subplots_adjust(leftm=3, rightm=4.5, topm=1.5, bottomm=4, wspace=0.4, hspace=0.4)
fmax = 250
tmin = 0.1
tmax = 0.2
tmin = 0.106
tmax = 0.206
twins = [[-t0, 0], [t1, t1 + t2], [0, t1], [t1 + t2, t1 + t2 + t12]]
freqs = [eodf, df2, df1, df2]
stim_freqs = [[None, None], [df2, None], [None, df1], [df1, df2]]
stim_phases = [[None, None], [phases[:, 1], None], [None, phases[:, 0]], [phases[:, 2], phases[:, 3]]]
base_rate = baseline_rate(spikes, *twins[0])
print(f'Baseline firing rate: {base_rate:.1f}Hz')
powers = []
for i in range(axs.shape[1]):
tstart, tend = twins[i]
plot_symbols(axs[0, i], s)
plot_stimulus(axs[1, i], s, tmax - tmin, eodf, *stim_freqs[i])
plot_symbols(axs[0, i], s, stim_freqs[i])
plot_stimulus(axs[1, i], s, tmax - tmin, eodf, stim_freqs[i])
sub_spikes = [times[(times >= tstart) & (times <= tend)] - tstart for times in spikes]
plot_psd(axs[4, i], s, sub_spikes, tend - tstart, fmax)
print(f'align spikes for frequency {freqs[i]:.0f}Hz:')
sub_spikes = align_spikes(sub_spikes, abs(1/freqs[i]))
freqs, power = power_spectrum(sub_spikes, tend - tstart)
powers.append(power)
plot_psd(axs[4, i], s, freqs, power, fmax)
sub_spikes = align_spikes(sub_spikes, stim_freqs[i], stim_phases[i])
plot_raster(axs[2, i], s, sub_spikes, tmin, tmax)
plot_rate(axs[3, i], s, sub_spikes, tmin, tmax)
#fig.savefig()
plt.show()
mark_freq(axs[4, 0], freqs, powers[0], base_rate, f'$r={base_rate:.0f}$\\,Hz', s.psF0, 30)
mark_freq(axs[4, 1], freqs, powers[1], df2, f'$\\Delta f_1=f_1 - f_{{EOD}}={abs(df2):.0f}$\\,Hz', s.psF02)
mark_freq(axs[4, 1], freqs, powers[1], 2*df2, f'$2\\Delta f_1={abs(2*df2):.0f}$\\,Hz', s.psF02)
mark_freq(axs[4, 2], freqs, powers[2], df1, '', s.psF0)
mark_freq(axs[4, 2], freqs, powers[2], df1, f'$\\Delta f_2=f_2 - f_{{EOD}}={abs(df1):.0f}$\\,Hz',
s.psF01, 130, 1.5)
mark_freq(axs[4, 3], freqs, powers[3], df2, '', s.psF02)
mark_freq(axs[4, 3], freqs, powers[3], 2*df2, '', s.psF02)
mark_freq(axs[4, 3], freqs, powers[3], df1, '', s.psF0)
mark_freq(axs[4, 3], freqs, powers[3], df1, '', s.psF01, 130, 1.5)
mark_freq(axs[4, 3], freqs, powers[3], abs(df1) + abs(df2) - 2,
f'$\\Delta f_1 + \\Delta f_2={abs(df1) + abs(df2):.0f}$\\,Hz', s.psF012, 20, angle=40)
mark_freq(axs[4, 3], freqs, powers[3], abs(df1) - abs(df2),
f'$\\Delta f_1 + \\Delta f_2={abs(df1) - abs(df2):.0f}$\\,Hz', s.psF01_2, 50, toffs=5, angle=40)
axs[3, 0].scalebars(-0.03, 0, 20, 500, 'ms', 'Hz')
axs[4, 0].yscalebar(-0.03, 0.5, 10, 'dB', va='center')
#fig.tag(axs.T)
fig.tag(axs[0])
fig.savefig()
#plt.show()
print()