├── hsh_signal ├── __init__.py ├── iter.py ├── pickling.py ├── hsh_data.py ├── am.py ├── ppg.py ├── chirp.py ├── envelope.py ├── alivecor.py ├── waveshape.py ├── dtw.py ├── heartseries.py ├── signal.py ├── filter.py └── ecg.py ├── gr_pll ├── __init__.py ├── gr │ ├── include │ │ └── gnuradio │ │ │ ├── analog │ │ │ ├── api.h │ │ │ └── pll_freqdet_cf.h │ │ │ ├── blocks │ │ │ ├── api.h │ │ │ └── control_loop.h │ │ │ ├── api.h │ │ │ ├── gr_complex.h │ │ │ ├── types.h │ │ │ ├── attributes.h │ │ │ └── math.h │ ├── pll_freqdet_cf_impl.h │ ├── control_loop.cc │ ├── pll_freqdet_cf_impl.cc │ └── fast_atan2f.cc └── pll.pyx ├── gr_firdes ├── __init__.py ├── gr │ ├── include │ │ └── gnuradio │ │ │ ├── fft │ │ │ ├── api.h │ │ │ └── window.h │ │ │ ├── filter │ │ │ └── api.h │ │ │ ├── api.h │ │ │ ├── gr_complex.h │ │ │ ├── types.h │ │ │ ├── attributes.h │ │ │ └── math.h │ └── window.cc ├── firdes.pxd └── firdes.pyx ├── .gitignore ├── pll-demod.png ├── release.sh ├── LICENSE.md ├── README.md ├── setup.py └── test ├── decode_alivecor.py ├── test_sqi_plot.py └── test_ppg.py /hsh_signal/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gr_pll/__init__.py: -------------------------------------------------------------------------------- 1 | from . import pll -------------------------------------------------------------------------------- /gr_firdes/__init__.py: -------------------------------------------------------------------------------- 1 | from . import firdes 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /.idea/ 3 | *.egg-info 4 | *.pyc 5 | *.so 6 | -------------------------------------------------------------------------------- /pll-demod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cidermole/hsh-signal/HEAD/pll-demod.png -------------------------------------------------------------------------------- /hsh_signal/iter.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | 4 | def pairwise(iterable): 5 | "s -> (s0,s1), (s1,s2), (s2, s3), ..." 6 | a, b = itertools.tee(iterable) 7 | next(b, None) 8 | return zip(a, b) 9 | -------------------------------------------------------------------------------- /hsh_signal/pickling.py: -------------------------------------------------------------------------------- 1 | import cPickle 2 | import gzip 3 | 4 | 5 | def save_zipped_pickle(obj, filename, protocol=-1): 6 | with gzip.open(filename, 'wb') as f: 7 | cPickle.dump(obj, f, protocol) 8 | 9 | 10 | def load_zipped_pickle(filename): 11 | with gzip.open(filename, 'rb') as f: 12 | loaded_object = cPickle.load(f) 13 | return loaded_object 14 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $# -lt 1 ]; then 4 | echo "usage: $0 " 5 | exit 1 6 | fi 7 | 8 | ver=$1 9 | #PKG=hsh_signal 10 | 11 | wd=$(basename $(dirname $(readlink -f $0))) 12 | 13 | cd .. 14 | mkdir $wd/release/$ver 15 | zip -r - $wd/hsh_signal $wd/gr_pll $wd/gr_firdes $wd/README.md $wd/setup.py -x '*.pyc' '*.so' > $wd/release/$ver/$wd.zip 16 | 17 | 18 | scp $wd/release/$ver/$wd.zip well:/var/www/$wd-$ver.zip 19 | scp $wd/release/$ver/$wd.zip well:/var/www/$wd.zip 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # hsh-signal 2 | 3 | Some parts are based on gnuradio, which is licensed GPLv3. Therefore, this is now simply GPLv3 itself. 4 | 5 | Copyright 2016 by David Madl 6 | 7 | hsh-signal is free software; you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation; either version 3, or (at your option) 10 | any later version. 11 | 12 | hsh-signal is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with hsh-signal; see the file COPYING. If not, write to 19 | the Free Software Foundation, Inc., 51 Franklin Street, 20 | Boston, MA 02110-1301, USA. 21 | 22 | -------------------------------------------------------------------------------- /gr_firdes/gr/include/gnuradio/fft/api.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Free Software Foundation, Inc. 3 | * 4 | * This file is part of GNU Radio 5 | * 6 | * GNU Radio is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3, or (at your option) 9 | * any later version. 10 | * 11 | * GNU Radio is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with GNU Radio; see the file COPYING. If not, write to 18 | * the Free Software Foundation, Inc., 51 Franklin Street, 19 | * Boston, MA 02110-1301, USA. 20 | */ 21 | 22 | #ifndef INCLUDED_FFT_API_H 23 | #define INCLUDED_FFT_API_H 24 | 25 | #include 26 | 27 | #ifdef gnuradio_fft_EXPORTS 28 | # define FFT_API __GR_ATTR_EXPORT 29 | #else 30 | # define FFT_API __GR_ATTR_IMPORT 31 | #endif 32 | 33 | #endif /* INCLUDED_FFT_API_H */ 34 | -------------------------------------------------------------------------------- /gr_firdes/gr/include/gnuradio/filter/api.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Free Software Foundation, Inc. 3 | * 4 | * This file is part of GNU Radio 5 | * 6 | * GNU Radio is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3, or (at your option) 9 | * any later version. 10 | * 11 | * GNU Radio is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with GNU Radio; see the file COPYING. If not, write to 18 | * the Free Software Foundation, Inc., 51 Franklin Street, 19 | * Boston, MA 02110-1301, USA. 20 | */ 21 | 22 | #ifndef INCLUDED_FILTER_API_H 23 | #define INCLUDED_FILTER_API_H 24 | 25 | #include 26 | 27 | #ifdef gnuradio_filter_EXPORTS 28 | # define FILTER_API __GR_ATTR_EXPORT 29 | #else 30 | # define FILTER_API __GR_ATTR_IMPORT 31 | #endif 32 | 33 | #endif /* INCLUDED_FILTER_API_H */ 34 | -------------------------------------------------------------------------------- /gr_pll/gr/include/gnuradio/analog/api.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Free Software Foundation, Inc. 3 | * 4 | * This file is part of GNU Radio 5 | * 6 | * GNU Radio is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3, or (at your option) 9 | * any later version. 10 | * 11 | * GNU Radio is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with GNU Radio; see the file COPYING. If not, write to 18 | * the Free Software Foundation, Inc., 51 Franklin Street, 19 | * Boston, MA 02110-1301, USA. 20 | */ 21 | 22 | #ifndef INCLUDED_ANALOG_API_H 23 | #define INCLUDED_ANALOG_API_H 24 | 25 | #include 26 | 27 | #ifdef gnuradio_analog_EXPORTS 28 | # define ANALOG_API __GR_ATTR_EXPORT 29 | #else 30 | # define ANALOG_API __GR_ATTR_IMPORT 31 | #endif 32 | 33 | #endif /* INCLUDED_ANALOG_API_H */ 34 | -------------------------------------------------------------------------------- /gr_pll/gr/include/gnuradio/blocks/api.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Free Software Foundation, Inc. 3 | * 4 | * This file is part of GNU Radio 5 | * 6 | * GNU Radio is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3, or (at your option) 9 | * any later version. 10 | * 11 | * GNU Radio is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with GNU Radio; see the file COPYING. If not, write to 18 | * the Free Software Foundation, Inc., 51 Franklin Street, 19 | * Boston, MA 02110-1301, USA. 20 | */ 21 | 22 | #ifndef INCLUDED_BLOCKS_API_H 23 | #define INCLUDED_BLOCKS_API_H 24 | 25 | #include 26 | 27 | #ifdef gnuradio_blocks_EXPORTS 28 | # define BLOCKS_API __GR_ATTR_EXPORT 29 | #else 30 | # define BLOCKS_API __GR_ATTR_IMPORT 31 | #endif 32 | 33 | #endif /* INCLUDED_BLOCKS_API_H */ 34 | -------------------------------------------------------------------------------- /gr_pll/gr/include/gnuradio/api.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2011 Free Software Foundation, Inc. 3 | * 4 | * This file is part of GNU Radio 5 | * 6 | * GNU Radio is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3, or (at your option) 9 | * any later version. 10 | * 11 | * GNU Radio is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with GNU Radio; see the file COPYING. If not, write to 18 | * the Free Software Foundation, Inc., 51 Franklin Street, 19 | * Boston, MA 02110-1301, USA. 20 | */ 21 | 22 | #ifndef INCLUDED_GR_RUNTIME_RUNTIME_API_H 23 | #define INCLUDED_GR_RUNTIME_RUNTIME_API_H 24 | 25 | #include 26 | 27 | #ifdef gnuradio_runtime_EXPORTS 28 | # define GR_RUNTIME_API __GR_ATTR_EXPORT 29 | #else 30 | # define GR_RUNTIME_API __GR_ATTR_IMPORT 31 | #endif 32 | 33 | #endif /* INCLUDED_GR_RUNTIME_RUNTIME_API_H */ 34 | -------------------------------------------------------------------------------- /gr_firdes/gr/include/gnuradio/api.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2011 Free Software Foundation, Inc. 3 | * 4 | * This file is part of GNU Radio 5 | * 6 | * GNU Radio is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3, or (at your option) 9 | * any later version. 10 | * 11 | * GNU Radio is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with GNU Radio; see the file COPYING. If not, write to 18 | * the Free Software Foundation, Inc., 51 Franklin Street, 19 | * Boston, MA 02110-1301, USA. 20 | */ 21 | 22 | #ifndef INCLUDED_GR_RUNTIME_RUNTIME_API_H 23 | #define INCLUDED_GR_RUNTIME_RUNTIME_API_H 24 | 25 | #include 26 | 27 | #ifdef gnuradio_runtime_EXPORTS 28 | # define GR_RUNTIME_API __GR_ATTR_EXPORT 29 | #else 30 | # define GR_RUNTIME_API __GR_ATTR_IMPORT 31 | #endif 32 | 33 | #endif /* INCLUDED_GR_RUNTIME_RUNTIME_API_H */ 34 | -------------------------------------------------------------------------------- /hsh_signal/hsh_data.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import json 3 | from datetime import datetime 4 | import time 5 | 6 | 7 | class PrettyFloat(float): 8 | def __repr__(self): 9 | # fix time floats to microsecond precision in JSON encoding sent to server. 10 | # for now, amplitudes are too precise. We could encode timestamps and amplitudes separately but that's overkill. 11 | return '%.6f' % self 12 | 13 | 14 | def pretty_floats(obj): 15 | if isinstance(obj, float) or isinstance(obj, np.float32): 16 | return PrettyFloat(obj) 17 | elif isinstance(obj, dict): 18 | return dict((k, pretty_floats(v)) for k, v in obj.items()) 19 | elif isinstance(obj, (list, tuple)): 20 | return map(pretty_floats, obj) 21 | return obj 22 | 23 | 24 | class MyJSONEncoder(json.JSONEncoder): 25 | def default(self, obj): 26 | if isinstance(obj, np.ndarray): 27 | return pretty_floats(obj.tolist()) 28 | elif isinstance(obj, datetime): 29 | return int(time.mktime(obj.timetuple())) 30 | else: 31 | return json.JSONEncoder.default(self, obj) 32 | 33 | def encode(self, obj): 34 | if isinstance(obj, (float, np.float32, dict, list, tuple)): 35 | return json.JSONEncoder.encode(self, pretty_floats(obj)) 36 | return json.JSONEncoder.encode(self, obj) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HeartShield signal processing 2 | 3 | This library was written during research on heart biosignals. 4 | 5 | ## FIR filters 6 | Most useful is probably the part with FIR filters, based on gnuradio's implementation of the Window Method for FIR Filter Design. 7 | 8 | See `hsh_signal/signal.py` for methods (low-pass, high-pass, etc.) directly applicable to an array of evenly-sampled signal values, or `hsh_signal/filter.py` for block-connectable filters intended for real-time applications. 9 | 10 | ## AliveCor decoder 11 | The file `hsh_signal/alivecor.py` implements a decoder for AliveCor's Kardia EKG (electrocardiograph). This device captures the raw electrical signal, which is then frequency-modulated onto an 18.8 kHz audio carrier that is transmitted to the microphone in a phone/tablet. 12 | 13 | Demodulation of the audio signal can be achieved via a phase-locked loop, with subsequent low-pass filter to remove high-frequency electrical noise, and a band-reject filter to remove 50 Hz mains noise. Note that for the US, you may need to change this filter to 60 Hz. 14 | 15 | ![Kardia EKG demodulator filter chain in gnuradio](pll-demod.png) 16 | 17 | -- David 18 | 19 | --- 20 | 21 | > All disease in this world is due to people who are knowingly doing wrong. 22 | > Acting in such a way hurts the relationship between yourself and other people. 23 | > Such acting turns justice on its head, because it hurts other people who would otherwise not have been involved in any way. 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | from distutils.extension import Extension 3 | 4 | try: 5 | from Cython.Build import cythonize 6 | 7 | USE_CYTHON = True 8 | except ImportError: 9 | USE_CYTHON = False 10 | 11 | 12 | def path_prefix(prefix, paths): 13 | return [prefix + p for p in paths] 14 | 15 | 16 | ext = '.pyx' if USE_CYTHON else '.cpp' 17 | 18 | extensions = [ 19 | Extension("gr_pll.pll", 20 | ["gr_pll/pll" + ext] + path_prefix("gr_pll/gr/", [ 21 | "fast_atan2f.cc", 22 | "control_loop.cc", 23 | "pll_freqdet_cf_impl.cc", 24 | ]), 25 | libraries=[], 26 | language="c++", 27 | extra_compile_args=["-std=c++11", "-Igr_pll/gr/include"], 28 | extra_link_args=[]), 29 | Extension("gr_firdes.firdes", 30 | ["gr_firdes/firdes" + ext] + path_prefix("gr_firdes/gr/", [ 31 | "firdes.cc", 32 | "window.cc", 33 | ]), 34 | libraries=[], 35 | language="c++", 36 | extra_compile_args=["-std=c++11", "-Igr_firdes/gr/include"], 37 | extra_link_args=[]) 38 | ] 39 | 40 | if USE_CYTHON: 41 | extensions = cythonize(extensions) 42 | 43 | setup( 44 | name='hsh-signal', 45 | version='0.1.5', 46 | packages=['hsh_signal', 'gr_pll', 'gr_firdes'], 47 | ext_modules=extensions 48 | ) 49 | 50 | # run as: 51 | # python setup.py build_ext --inplace [--rpath=...] 52 | 53 | # python setup.py develop 54 | 55 | # pip2 install --editable . 56 | -------------------------------------------------------------------------------- /gr_firdes/firdes.pxd: -------------------------------------------------------------------------------- 1 | from libcpp.vector cimport vector 2 | 3 | cdef extern from "" namespace "gr::filter::firdes": 4 | cpdef enum win_type: 5 | WIN_NONE = -1 #: don't use a window 6 | WIN_HAMMING = 0 #: Hamming window; max attenuation 53 dB 7 | WIN_HANN = 1 #: Hann window; max attenuation 44 dB 8 | WIN_BLACKMAN = 2 #: Blackman window; max attenuation 74 dB 9 | WIN_RECTANGULAR = 3 #: Basic rectangular window 10 | WIN_KAISER = 4 #: Kaiser window; max attenuation a function of beta, google it 11 | WIN_BLACKMAN_hARRIS = 5 #: Blackman-harris window 12 | WIN_BLACKMAN_HARRIS = 5 #: alias to WIN_BLACKMAN_hARRIS for capitalization consistency 13 | WIN_BARTLETT = 6 #: Barlett (triangular) window 14 | WIN_FLATTOP = 7 #: flat top window; useful in FFTs 15 | 16 | # static methods from class firdes 17 | vector[float] high_pass_2(double gain, double sampling_freq, double cutoff_freq, double transition_width, double attenuation_dB, win_type window, double beta) 18 | vector[float] low_pass_2(double gain, double sampling_freq, double cutoff_freq, double transition_width, double attenuation_dB, win_type window, double beta) 19 | vector[float] band_pass_2(double gain, double sampling_freq, double low_cutoff_freq, double high_cutoff_freq, double transition_width, double attenuation_dB, win_type window, double beta) 20 | vector[float] band_reject_2(double gain, double sampling_freq, double low_cutoff_freq, double high_cutoff_freq, double transition_width, double attenuation_dB, win_type window, double beta) 21 | vector[float] hilbert(unsigned int ntaps, win_type window, double beta) 22 | -------------------------------------------------------------------------------- /gr_firdes/gr/include/gnuradio/gr_complex.h: -------------------------------------------------------------------------------- 1 | /* -*- c++ -*- */ 2 | /* 3 | * Copyright 2004 Free Software Foundation, Inc. 4 | * 5 | * This file is part of GNU Radio 6 | * 7 | * GNU Radio is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 3, or (at your option) 10 | * any later version. 11 | * 12 | * GNU Radio is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with GNU Radio; see the file COPYING. If not, write to 19 | * the Free Software Foundation, Inc., 51 Franklin Street, 20 | * Boston, MA 02110-1301, USA. 21 | */ 22 | 23 | #ifndef INCLUDED_GR_COMPLEX_H 24 | #define INCLUDED_GR_COMPLEX_H 25 | 26 | #include 27 | typedef std::complex gr_complex; 28 | typedef std::complex gr_complexd; 29 | 30 | inline bool is_complex (gr_complex x) { (void) x; return true;} 31 | inline bool is_complex (gr_complexd x) { (void) x; return true;} 32 | inline bool is_complex (float x) { (void) x; return false;} 33 | inline bool is_complex (double x) { (void) x; return false;} 34 | inline bool is_complex (int x) { (void) x; return false;} 35 | inline bool is_complex (char x) { (void) x; return false;} 36 | inline bool is_complex (short x) { (void) x; return false;} 37 | 38 | // this doesn't really belong here, but there are worse places for it... 39 | 40 | #define CPPUNIT_ASSERT_COMPLEXES_EQUAL(expected,actual,delta) \ 41 | CPPUNIT_ASSERT_DOUBLES_EQUAL (expected.real(), actual.real(), delta); \ 42 | CPPUNIT_ASSERT_DOUBLES_EQUAL (expected.imag(), actual.imag(), delta); 43 | 44 | #endif /* INCLUDED_GR_COMPLEX_H */ 45 | 46 | -------------------------------------------------------------------------------- /gr_pll/gr/include/gnuradio/gr_complex.h: -------------------------------------------------------------------------------- 1 | /* -*- c++ -*- */ 2 | /* 3 | * Copyright 2004 Free Software Foundation, Inc. 4 | * 5 | * This file is part of GNU Radio 6 | * 7 | * GNU Radio is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 3, or (at your option) 10 | * any later version. 11 | * 12 | * GNU Radio is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with GNU Radio; see the file COPYING. If not, write to 19 | * the Free Software Foundation, Inc., 51 Franklin Street, 20 | * Boston, MA 02110-1301, USA. 21 | */ 22 | 23 | #ifndef INCLUDED_GR_COMPLEX_H 24 | #define INCLUDED_GR_COMPLEX_H 25 | 26 | #include 27 | typedef std::complex gr_complex; 28 | typedef std::complex gr_complexd; 29 | 30 | inline bool is_complex (gr_complex x) { (void) x; return true;} 31 | inline bool is_complex (gr_complexd x) { (void) x; return true;} 32 | inline bool is_complex (float x) { (void) x; return false;} 33 | inline bool is_complex (double x) { (void) x; return false;} 34 | inline bool is_complex (int x) { (void) x; return false;} 35 | inline bool is_complex (char x) { (void) x; return false;} 36 | inline bool is_complex (short x) { (void) x; return false;} 37 | 38 | // this doesn't really belong here, but there are worse places for it... 39 | 40 | #define CPPUNIT_ASSERT_COMPLEXES_EQUAL(expected,actual,delta) \ 41 | CPPUNIT_ASSERT_DOUBLES_EQUAL (expected.real(), actual.real(), delta); \ 42 | CPPUNIT_ASSERT_DOUBLES_EQUAL (expected.imag(), actual.imag(), delta); 43 | 44 | #endif /* INCLUDED_GR_COMPLEX_H */ 45 | 46 | -------------------------------------------------------------------------------- /test/decode_alivecor.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from hsh_signal.alivecor import decode_alivecor, AlivecorFilter, ChunkDataSource 3 | import matplotlib.pyplot as plt 4 | 5 | # test decode_alivecor 6 | 7 | def stretch(T, f, fps=48000): 8 | # T in samples, NOT sec 9 | t = np.arange(T) / float(fps) 10 | sig = np.cos(2*np.pi*f*t) 11 | return list(sig) 12 | 13 | 14 | def chirp(T, f1, f2, fps=48000): 15 | # T in samples, NOT sec 16 | t = np.arange(T) / float(fps) 17 | f = f1 + (t / t[-1]) * (f2 - f1) 18 | sig = np.cos(2*np.pi*f*t) 19 | return list(sig) 20 | 21 | """ 22 | plt.plot(chirp(5.0*30, 0.5, 3.0, fps=30)) 23 | plt.show() 24 | """ 25 | 26 | 27 | f_center = 18.8e3 28 | f_shift = 100 29 | 30 | sig = [] 31 | stretches = [(1.5, 0), (0.1, 1), (0.2, -1), (0.3, 1), (0.4, -1), (1.0, 0)] 32 | fps=48000 33 | 34 | for dur_sec, f_shift_sign in stretches: 35 | sig += stretch(int(fps*dur_sec), f=f_center+f_shift_sign*f_shift, fps=fps) 36 | 37 | sig = np.array(sig) * 1e-3 # amplitude correct 38 | 39 | chrp = chirp(0.5*fps, 100, 8000, fps=fps) 40 | #plt.plot(chrp) 41 | #plt.show() 42 | 43 | sig[len(chrp):2*len(chrp)] = np.array(chrp) * 1e-3 44 | sig[2*len(chrp):3*len(chrp)] = np.array(chrp) * 1e-3 45 | #sig[] 46 | 47 | #plt.plot(sig) 48 | #plt.show() 49 | 50 | 51 | from scipy.io import wavfile 52 | wavfile.write('all.wav', fps, (sig * 32767 * 1e3).astype(np.int16)) 53 | 54 | wavfile.write('sync.wav', fps, (sig[len(chrp):3*len(chrp)] * 32767 * 1e3).astype(np.int16)) 55 | 56 | sig_ecg = decode_alivecor(sig) 57 | 58 | 59 | mic = ChunkDataSource(data=sig, batch_size=179200, sampling_rate=fps) 60 | af = AlivecorFilter(mic.sampling_rate) 61 | 62 | # plus hilbert's 32 63 | print 'delays', af.lowpass.delay, af.bandreject.delay, (af.lowpass.delay + af.bandreject.delay + 32) 64 | 65 | print 'delay sec', (af.lowpass.delay + af.bandreject.delay + 32)/float(fps) 66 | 67 | plt.plot(np.arange(len(sig_ecg)) / float(fps), sig_ecg) 68 | plt.show() 69 | -------------------------------------------------------------------------------- /test/test_sqi_plot.py: -------------------------------------------------------------------------------- 1 | 2 | import glob 3 | import pickle 4 | from os.path import join, basename 5 | from hsh_signal.app_parser import AppData 6 | from hsh_signal.quality import QsqiPPG 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | 10 | 11 | DATA_DIR = '/mnt/hsh/src/testdata/datasets/david-getrr2-challenge-2' 12 | 13 | data_files = sorted(glob.glob(join(DATA_DIR, '*_ecg_ppg.b'))) 14 | 15 | #df = data_files[0] 16 | df = data_files[18] 17 | 18 | mf = basename(df).replace('_ecg_ppg', '_meta') 19 | print mf 20 | ad = AppData(mf) 21 | 22 | from hsh_beatdet.zong import ZongDetector 23 | 24 | zd = ZongDetector() 25 | zd.detect(ad.ppg_raw()) 26 | ppgz = zd.get_result() 27 | 28 | with open(df, 'rb') as fi: 29 | (ecg, ppg, ppg_raw, ecg_ibs, ppg_ibs) = pickle.load(fi) 30 | 31 | # not sure why necessary. 32 | ppg_dt = (ppg_raw.t[0] - ppg.t[0]) 33 | ppgz.shift(ppg_dt) 34 | 35 | errors = ppg.tbeats[ppg_ibs] - ecg.tbeats[ecg_ibs] 36 | 37 | #fig, ax = plt.subplots(4, sharex=True) 38 | #fig, ax = plt.subplots(2, sharex=True) 39 | #ecg.plot(ax[0]) 40 | #ppg.plot(ax[0], c='y') 41 | 42 | sq = QsqiPPG.from_heart_series(ppgz) 43 | 44 | sq.plot() 45 | plt.show() 46 | 47 | 48 | 49 | if False: 50 | def gauss(x, t_mu, t_sigma): 51 | a = 1.0 / (t_sigma * np.sqrt(2 * np.pi)) 52 | y = a * np.exp(-0.5 * (x - t_mu)**2 / t_sigma**2) 53 | return y 54 | 55 | 56 | s_min = sq.template 57 | from hsh_signal.quality import SLICE_FRONT 58 | 59 | weighting = gauss(np.arange(len(s_min)), len(s_min)*SLICE_FRONT, len(s_min)*0.4) 60 | weighting[:int(len(s_min)*SLICE_FRONT)] = 0.0 # blank weighting the previous beat 61 | weighting = weighting / np.sum(weighting) # * len(s_min) 62 | 63 | t1 = np.arange(len(sq.template)) / sq.fps 64 | plt.plot(t1, sq.template) 65 | plt.plot(t1, -weighting*10) 66 | plt.show() 67 | 68 | 69 | t1 = np.arange(len(sq.template))/sq.fps 70 | plt.plot(t1, sq.template) 71 | #plt.show() 72 | 73 | squ = QsqiPPG.from_heart_series(ppgz.upsample(10)) 74 | tu = np.arange(len(squ.template))/squ.fps 75 | plt.plot(tu-0.02, squ.template) 76 | plt.show() 77 | -------------------------------------------------------------------------------- /hsh_signal/am.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | from .filter import * 4 | 5 | 6 | class _AMDemod(FilterBlock): 7 | def __init__(self, loop_bw, max_freq, min_freq, sampling_rate, ntaps=65): 8 | """ 9 | real ---> | _AM | ---> real 10 | -> Hilbert -> PLL -> Multiply -> 11 | --------> 12 | 13 | You must use a low-pass afterwards, since this still contains frequency components from both sides. 14 | """ 15 | super(_AMDemod, self).__init__() 16 | self.hilbert = Hilbert(ntaps) 17 | self.pll = PLL(loop_bw, max_freq, min_freq, sampling_rate) 18 | self.delay = self.hilbert.delay 19 | 20 | def batch(self, x): 21 | """batch-process an array and return array of output values""" 22 | analytic = self.hilbert.batch(x) 23 | carrier = self.pll.batch_vco(analytic) 24 | demod = -np.real(carrier * analytic) 25 | return demod 26 | 27 | 28 | class AMFilter(SourceBlock): 29 | """Demodulates an AM audio signal.""" 30 | def __init__(self, fps, low_cutoff_freq, high_cutoff_freq, transition_width, min_freq=None, max_freq=None, loop_bw=None): 31 | super(AMFilter, self).__init__() 32 | self.sampling_rate = fps 33 | 34 | if min_freq is None: min_freq = low_cutoff_freq 35 | if max_freq is None: max_freq = high_cutoff_freq 36 | if loop_bw is None: loop_bw = (max_freq - min_freq) 37 | 38 | self.prefilter = Bandpass(low_cutoff_freq=low_cutoff_freq, high_cutoff_freq=high_cutoff_freq, transition_width=transition_width, sampling_rate=self.sampling_rate) 39 | self.demod = _AMDemod(loop_bw=loop_bw, max_freq=max_freq, min_freq=min_freq, sampling_rate=self.sampling_rate) 40 | self.lowpass = Lowpass(cutoff_freq=100, transition_width=5, sampling_rate=self.sampling_rate) 41 | 42 | self.delay = self.prefilter.delay + self.demod.delay + self.lowpass.delay 43 | 44 | connect(self.prefilter, self.demod, self.lowpass) 45 | 46 | def connect(self, consumer): 47 | # redirect the output 48 | self.lowpass.connect(consumer) 49 | 50 | def put(self, x): 51 | self.prefilter.put(x) 52 | -------------------------------------------------------------------------------- /gr_firdes/gr/include/gnuradio/types.h: -------------------------------------------------------------------------------- 1 | /* -*- c++ -*- */ 2 | /* 3 | * Copyright 2004,2013 Free Software Foundation, Inc. 4 | * 5 | * This file is part of GNU Radio 6 | * 7 | * GNU Radio is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 3, or (at your option) 10 | * any later version. 11 | * 12 | * GNU Radio is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with GNU Radio; see the file COPYING. If not, write to 19 | * the Free Software Foundation, Inc., 51 Franklin Street, 20 | * Boston, MA 02110-1301, USA. 21 | */ 22 | 23 | #ifndef INCLUDED_GR_TYPES_H 24 | #define INCLUDED_GR_TYPES_H 25 | 26 | #include 27 | #include 28 | #include 29 | #include // size_t 30 | 31 | #include 32 | 33 | typedef std::vector gr_vector_int; 34 | typedef std::vector gr_vector_uint; 35 | typedef std::vector gr_vector_float; 36 | typedef std::vector gr_vector_double; 37 | typedef std::vector gr_vector_void_star; 38 | typedef std::vector gr_vector_const_void_star; 39 | 40 | /* 41 | * #include must be placed beforehand 42 | * in the source file including gnuradio/types.h for 43 | * the following to work correctly 44 | */ 45 | #ifdef HAVE_STDINT_H 46 | #include 47 | typedef int16_t gr_int16; 48 | typedef int32_t gr_int32; 49 | typedef int64_t gr_int64; 50 | typedef uint16_t gr_uint16; 51 | typedef uint32_t gr_uint32; 52 | typedef uint64_t gr_uint64; 53 | #else 54 | /* 55 | * Note: these defaults may be wrong on 64-bit systems 56 | */ 57 | typedef short gr_int16; 58 | typedef int gr_int32; 59 | typedef long long gr_int64; 60 | typedef unsigned short gr_uint16; 61 | typedef unsigned int gr_uint32; 62 | typedef unsigned long long gr_uint64; 63 | #endif /* HAVE_STDINT_H */ 64 | 65 | #endif /* INCLUDED_GR_TYPES_H */ 66 | -------------------------------------------------------------------------------- /gr_pll/gr/include/gnuradio/types.h: -------------------------------------------------------------------------------- 1 | /* -*- c++ -*- */ 2 | /* 3 | * Copyright 2004,2013 Free Software Foundation, Inc. 4 | * 5 | * This file is part of GNU Radio 6 | * 7 | * GNU Radio is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 3, or (at your option) 10 | * any later version. 11 | * 12 | * GNU Radio is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with GNU Radio; see the file COPYING. If not, write to 19 | * the Free Software Foundation, Inc., 51 Franklin Street, 20 | * Boston, MA 02110-1301, USA. 21 | */ 22 | 23 | #ifndef INCLUDED_GR_TYPES_H 24 | #define INCLUDED_GR_TYPES_H 25 | 26 | #include 27 | #include 28 | #include 29 | #include // size_t 30 | 31 | #include 32 | 33 | typedef std::vector gr_vector_int; 34 | typedef std::vector gr_vector_uint; 35 | typedef std::vector gr_vector_float; 36 | typedef std::vector gr_vector_double; 37 | typedef std::vector gr_vector_void_star; 38 | typedef std::vector gr_vector_const_void_star; 39 | 40 | /* 41 | * #include must be placed beforehand 42 | * in the source file including gnuradio/types.h for 43 | * the following to work correctly 44 | */ 45 | #ifdef HAVE_STDINT_H 46 | #include 47 | typedef int16_t gr_int16; 48 | typedef int32_t gr_int32; 49 | typedef int64_t gr_int64; 50 | typedef uint16_t gr_uint16; 51 | typedef uint32_t gr_uint32; 52 | typedef uint64_t gr_uint64; 53 | #else 54 | /* 55 | * Note: these defaults may be wrong on 64-bit systems 56 | */ 57 | typedef short gr_int16; 58 | typedef int gr_int32; 59 | typedef long long gr_int64; 60 | typedef unsigned short gr_uint16; 61 | typedef unsigned int gr_uint32; 62 | typedef unsigned long long gr_uint64; 63 | #endif /* HAVE_STDINT_H */ 64 | 65 | #endif /* INCLUDED_GR_TYPES_H */ 66 | -------------------------------------------------------------------------------- /hsh_signal/ppg.py: -------------------------------------------------------------------------------- 1 | from brueser.brueser2 import brueser_beatdetect 2 | from .heartseries import HeartSeries 3 | from hsh_beatdet import beatdet_getrr_v1, beatdet_getrr_v2, beatdet_getrr_v2_fracidx, beatdet 4 | #from .signal import evenly_resample 5 | import numpy as np 6 | 7 | 8 | def ppg_beatdetect(ppg, debug=False): 9 | return ppg_beatdetect_brueser(ppg, debug) 10 | 11 | def ppg_beatdetect_getrr(ppg, type='fracidx', debug=False): 12 | # regular | fracidx 13 | # XXX: getrr() fps must be 30.0 currently, there is a subtle getrr() bug otherwise 14 | assert np.abs(ppg.fps - 30.0) < 1e-3 15 | fps = ppg.fps 16 | data = np.vstack((ppg.t, ppg.x)).T 17 | 18 | if type == 'fracidx': 19 | return beatdet(data, beatdet_getrr_v2_fracidx) 20 | else: 21 | return beatdet(data, beatdet_getrr_v2) 22 | #ibi, filtered, idxs, tbeats = beatdet(data, beatdet_getrr_v2) 23 | 24 | def ppg_beatdetect_brueser(ppg, debug=False): 25 | ibeats, ibis = brueser_beatdetect(ppg.x, ppg.fps) 26 | 27 | # remove beats too close to each other 28 | deltas = np.diff(np.insert(np.array(ibeats), 0, 0) / float(ppg.fps)) 29 | if debug: 30 | print 'ibeats', ibeats 31 | print 'deltas', deltas 32 | idupes = np.where(deltas <= 0.3)[0] 33 | if debug: print 'idupes', idupes 34 | ibeats = np.delete(ibeats, idupes) 35 | if debug: print 'ibeats after delete', ibeats 36 | 37 | ibeats = ibeats[np.arange(len(ibeats))] 38 | 39 | return HeartSeries(ppg.x, ibeats, fps=ppg.fps, lpad=ppg.lpad) 40 | 41 | 42 | ppg_dt, rel_fps_error = analyze_ecg_ppg_base(ecg, ppg, step=50e-3, debug=False) 43 | clock_bias = (1.0 + rel_fps_error) 44 | ppg_new = HeartSeries(ppg.x, ppg.ibeats, ppg.fps * clock_bias) 45 | 46 | 47 | 48 | from hsh_signal.heartseries import Series 49 | from hsh_signal.signal import grid_resample 50 | 51 | def make_series(mimic_ppg_curve): 52 | fps = 125.0 53 | x = -mimic_ppg_curve # in MIMIC PPGs, beats move in positive direction. in our PPGs, they are light intensities (they are reversed). 54 | t = np.arange(len(x)) / float(fps) 55 | ds = grid_resample(t, x, target_fps=30.0) 56 | return Series(ds[:,1], fps=30.0) 57 | 58 | ### 59 | 60 | from hsh_signal.waveshape import ppg_wave_foot 61 | from hsh_signal.ppg import ppg_beatdetect_getrr 62 | 63 | def make_footed(mimic_ppg_curve): 64 | ppg_raw = make_series(mimic_ppg_curve) 65 | ppg = ppg_beatdetect_getrr(ppg_raw) 66 | return ppg_wave_foot(ppg_raw, ppg) 67 | -------------------------------------------------------------------------------- /gr_pll/gr/pll_freqdet_cf_impl.h: -------------------------------------------------------------------------------- 1 | /* -*- c++ -*- */ 2 | /* 3 | * Copyright 2004,2011,2012 Free Software Foundation, Inc. 4 | * 5 | * This file is part of GNU Radio 6 | * 7 | * GNU Radio is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 3, or (at your option) 10 | * any later version. 11 | * 12 | * GNU Radio is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with GNU Radio; see the file COPYING. If not, write to 19 | * the Free Software Foundation, Inc., 51 Franklin Street, 20 | * Boston, MA 02110-1301, USA. 21 | */ 22 | 23 | #ifndef INCLUDED_ANALOG_PLL_FREQDET_CF_IMPL_H 24 | #define INCLUDED_ANALOG_PLL_FREQDET_CF_IMPL_H 25 | 26 | #include 27 | #include 28 | 29 | namespace gr { 30 | namespace analog { 31 | 32 | class pll_freqdet_cf_impl : public pll_freqdet_cf 33 | { 34 | private: 35 | float phase_detector(gr_complex sample,float ref_phase); 36 | 37 | public: 38 | pll_freqdet_cf_impl(float loop_bw, float max_freq, float min_freq); 39 | ~pll_freqdet_cf_impl(); 40 | 41 | float mod_2pi(float in); 42 | 43 | void set_loop_bandwidth(float bw); 44 | void set_damping_factor(float df); 45 | void set_alpha(float alpha); 46 | void set_beta(float beta); 47 | void set_frequency(float freq); 48 | void set_phase(float phase); 49 | void set_min_freq(float freq); 50 | void set_max_freq(float freq); 51 | 52 | float get_loop_bandwidth() const; 53 | float get_damping_factor() const; 54 | float get_alpha() const; 55 | float get_beta() const; 56 | float get_frequency() const; 57 | float get_phase() const; 58 | float get_min_freq() const; 59 | float get_max_freq() const; 60 | 61 | int work(int noutput_items, 62 | gr_vector_const_void_star &input_items, 63 | gr_vector_void_star &output_items); 64 | 65 | int work2_cc(int noutput_items, 66 | gr_vector_const_void_star &input_items, 67 | gr_vector_void_star &output_items); 68 | }; 69 | 70 | } /* namespace analog */ 71 | } /* namespace gr */ 72 | 73 | #endif /* INCLUDED_ANALOG_PLL_FREQDET_CF_IMPL_H */ 74 | -------------------------------------------------------------------------------- /hsh_signal/chirp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | from hsh_signal.heartseries import Series 5 | from hsh_signal.signal import lowpass_fft, filter_fft_ff, localmax, localmax_climb 6 | 7 | 8 | def chirp(T, f1, f2, fps=48000): 9 | # T in samples, NOT sec 10 | t = np.arange(T) / float(fps) 11 | f = f1 + (t / t[-1]) * (f2 - f1) 12 | sig = np.cos(2*np.pi*f*t) 13 | return sig 14 | 15 | 16 | def cross_corr(x1, x2): 17 | # x1 is the shorter chirp 18 | assert len(x1) <= len(x2) 19 | pad = len(x1)//2 20 | x1p, x2p = np.pad(x1, (pad, pad), mode='constant'), np.pad(x2, (2*pad, 2*pad-1), mode='constant') 21 | x1pi = x1p[::-1] 22 | #corr = np.convolve(x1p, x2pi, mode='valid') 23 | corr = filter_fft_ff(x2p, x1pi) 24 | # length = max(M, N) - min(M, N) + 1 25 | # since we are padded up by len(x1), len(corr) == len(x2) 26 | assert len(corr) == len(x2) 27 | 28 | #print 'len(x1), len(x2), len(x1p), len(x2p), len(corr)', len(x1), len(x2), len(x1p), len(x2p), len(corr) 29 | 30 | # normalize 31 | return corr / np.sqrt(np.sum(x1**2) * np.sum(x2**2)) 32 | 33 | 34 | 35 | class AudioChirpDetector(object): 36 | def __init__(self, track, debug=False): 37 | self.debug = debug 38 | self.track = track 39 | chrp = chirp(0.5*track.fps, 100, 8000, fps=track.fps)[:int(0.05*track.fps)+1] 40 | # ensure chrp_bit is even length (assumed by later code) 41 | self.chirp = chrp[:len(chrp)-len(chrp)%2] 42 | 43 | self.times, self.idxs = [], [] 44 | 45 | def chirp_times(self): 46 | track, chirp = self.track, self.chirp 47 | 48 | SMOOTHED_CORR_THRESHOLD = 0.08 49 | CORR_THRESHOLD = 0.35 50 | 51 | corr = cross_corr(chirp, track.x) 52 | 53 | smoothed_corr = lowpass_fft(np.abs(corr), track.fps, cf=100.0, tw=20.0) / np.max(np.abs(corr)) 54 | 55 | if self.debug: 56 | plt.plot(track.t, corr / np.max(np.abs(corr)), label='corr') 57 | plt.plot(track.t, smoothed_corr, label='smoothed_corr') 58 | plt.legend() 59 | plt.show() 60 | 61 | peak_locs = np.where( 62 | localmax(corr) & 63 | (smoothed_corr > SMOOTHED_CORR_THRESHOLD) & 64 | (corr / np.max(np.abs(corr)) > CORR_THRESHOLD) 65 | )[0] 66 | 67 | # look in +/- 5 ms of the smoothed_corr peak for real corr peak 68 | corr_peak_locs = np.unique(localmax_climb(corr, peak_locs, hwin=int(track.fps * 0.005))) 69 | 70 | times = track.t[corr_peak_locs] 71 | 72 | self.times, self.idxs = times, corr_peak_locs 73 | 74 | return self.times, self.idxs 75 | 76 | 77 | def audio_chirp_times(sig, fps): 78 | track = Series(sig, fps=fps) 79 | cd = AudioChirpDetector(track, debug=False) 80 | times, idxs = cd.chirp_times() 81 | times = times[:4] 82 | print 'audio_chirp_times() times', times 83 | #assert len(times) == 4 84 | 85 | dtimes = np.diff(times) 86 | 87 | assert np.abs(dtimes[0] - 0.5) < 1e-3 and np.abs(dtimes[2] - 0.5) < 1e-3 # sometimes last chirp missing?! but if it does, signal's crap. 88 | return times 89 | -------------------------------------------------------------------------------- /hsh_signal/envelope.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from .signal import cross_corr 3 | 4 | 5 | def envelopes_at_perc(slicez, perc=10): 6 | """:returns the lower and upper percentile envelopes of individual beat window slices.""" 7 | # slz = np.array(slicez) 8 | slz = slicez 9 | le, ue = np.percentile(slz, perc, axis=0), np.percentile(slz, (100 - perc), axis=0) 10 | return le, ue 11 | 12 | 13 | def envelopes_corr(slicez): 14 | """envelope similarity -- an 1d measure of general recording quality. useful for plotting.""" 15 | 16 | # p=45 means only 10% of all beats will be used -- that would be a very bad signal indeed. 17 | ps, xcs = [], [] 18 | for p in np.arange(1, 50, 1): 19 | le, ue = envelopes_at_perc(slicez, p) 20 | # non-zero indices: where zero-padding is not active. (would otherwise mess with mean removal) 21 | nzi = np.where((le != 0) | (ue != 0))[0] 22 | xc = cross_corr(le[nzi] - np.mean(le[nzi]), ue[nzi] - np.mean(ue[nzi])) 23 | ps.append(p) 24 | xcs.append(xc) 25 | return np.array(ps), np.array(xcs) 26 | 27 | 28 | def envelopes_perc_threshold(slicez, corr_threshold=0.8): 29 | """ 30 | :param slicez 2d np.array of slices, each zero-padded to the same length (for ECG, with centered R peak) 31 | :returns the smallest percentile such that envelopes_at_perc() returns envelopes which are still well correlated. 32 | """ 33 | #ENVELOPE_CORR_THRESHOLD = 0.8 34 | 35 | slicez = np.array(slicez) 36 | ps, xcs = envelopes_corr(slicez) 37 | igood = np.where(xcs > corr_threshold)[0] 38 | if len(igood) == 0: 39 | return 50 # both le,ue == exactly the median. 40 | return ps[igood[0]] 41 | 42 | """ 43 | 44 | from hsh_signal.ecg import beatdet_ecg 45 | 46 | ecg = beatdet_ecg(sig0.slice(slice(int(1500*fps), int(1510*fps)))) 47 | 48 | # ecg = 49 | 50 | 51 | from hsh_signal.quality import sqi_slices, sig_pad 52 | 53 | slicez = sqi_slices(ecg, method='fixed', slice_front=0.5, slice_back=-0.5) 54 | L = max([len(sl) for sl in slicez]) 55 | padded_slicez = [sig_pad(sl, L, side='center', mode='constant') for sl in slicez] 56 | 57 | ps, xcs = envelopes_corr(padded_slicez) 58 | 59 | plt.plot(ps, xcs) 60 | plt.show() 61 | """ 62 | 63 | 64 | 65 | def beat_penalty(sl, le, ue, mb, debug=False): 66 | # slice, lower envelope, upper envelope, median beat 67 | out_of_envelope = (sl > ue) | (sl < le) 68 | have_envelope = (le != 0) | (ue != 0) 69 | 70 | if debug: 71 | import matplotlib.pyplot as plt 72 | plt.plot(ue, c='r') 73 | plt.plot(sl, c='k') 74 | plt.plot(le, c='g') 75 | plt.plot(out_of_envelope * 0.2 + 0.4, c='m') 76 | plt.plot(have_envelope * 0.2 + 0.3, c='g') 77 | 78 | # frac_invalid = len(np.where(invalid)[0]) / float(len(np.where(have_envelope)[0])) 79 | # << would be another SQI metric on its own 80 | 81 | # where outside of envelopes, penalize with MSE vs. the median beat 82 | penalty = np.sqrt(np.mean(out_of_envelope * (sl - mb) ** 2)) 83 | median_energy = np.sqrt(np.mean(mb ** 2)) 84 | return penalty / median_energy 85 | 86 | 87 | def beat_penalty_threshold(le, ue, mb, debug=False): 88 | # idea: set threshold for noisy beats where a beat always eps above the normal percentile envelope 89 | # would be penalized. 90 | lp, up = beat_penalty(le - 1e-6, le, ue, mb), beat_penalty(ue + 1e-6, le, ue, mb) 91 | # these two should be similar, if distribution is not too skewed 92 | if debug: print lp, up 93 | return np.mean([lp, up]) 94 | 95 | 96 | """ 97 | # padded_slicez, see above 98 | 99 | perc = envelopes_perc_threshold(padded_slicez) 100 | le, ue = envelopes_at_perc(padded_slicez, perc) 101 | mb = np.median(padded_slicez, axis=0) 102 | 103 | bpt = beat_penalty_threshold(le, ue, mb) 104 | """ 105 | 106 | # see Untitled110.ipynb 107 | -------------------------------------------------------------------------------- /hsh_signal/alivecor.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | from .filter import * 4 | from .signal import highpass_fft 5 | from .heartseries import Series, HeartSeries 6 | from .ecg import scrub_ecg, NoisyECG 7 | 8 | import wave 9 | import numpy as np 10 | 11 | 12 | class AlivecorFilter(SourceBlock): 13 | """Demodulates an AliveCor ECG transmitted via FM audio signal.""" 14 | def __init__(self, fps): 15 | super(AlivecorFilter, self).__init__() 16 | self.sampling_rate = fps 17 | 18 | self.prefilter = Bandpass(low_cutoff_freq=17000, high_cutoff_freq=21000, transition_width=500, sampling_rate=self.sampling_rate) 19 | self.hilbert = Hilbert() 20 | # roughly center on the observed 18.8 kHz carrier 21 | self.pll = PLL(loop_bw=1500, max_freq=21000, min_freq=17000, sampling_rate=self.sampling_rate) 22 | # remove electrical noise: 23 | self.lowpass = Lowpass(cutoff_freq=100, transition_width=5, sampling_rate=self.sampling_rate) 24 | # reject mains noise: (Note that for the US, you may need to change this filter to 60 Hz.) 25 | self.bandreject = Bandreject(low_cutoff_freq=40, high_cutoff_freq=60, transition_width=3, sampling_rate=self.sampling_rate) 26 | #Logger.debug('lowpass taps={}, bandreject taps={}'.format(self.lowpass._ntaps, self.bandreject._ntaps)) 27 | 28 | self.delay = self.prefilter.delay + self.hilbert.delay + self.lowpass.delay + self.bandreject.delay 29 | 30 | connect(self.prefilter, self.hilbert, self.pll, self.lowpass, self.bandreject) #, self - but no. 31 | 32 | def connect(self, consumer): 33 | # redirect the output 34 | self.bandreject.connect(consumer) 35 | 36 | def put(self, x): 37 | self.prefilter.put(x) 38 | 39 | 40 | def decode_alivecor(signal, fps=48000, debug=False): 41 | alivecor = AlivecorFilter(fps) 42 | signal_padded = np.pad(signal, (0, alivecor.delay), mode='constant') # pad with trailing zeros to force returning complete ECG 43 | mic = ChunkDataSource(data=signal_padded, batch_size=179200, sampling_rate=fps) 44 | ecg = DataSink() 45 | #mic.connect(alivecor) 46 | #alivecor.connect(ecg) 47 | connect(mic, alivecor, ecg) 48 | 49 | # to do: use apply_filter() instead of stuff below 50 | 51 | # push through all the data 52 | prev_t = time.time() 53 | mic.start() 54 | while not mic.finished(): 55 | if time.time() > prev_t + 1.0 and debug: 56 | print 'progress: {} %'.format(mic.progress()) 57 | prev_t = time.time() 58 | mic.poll() 59 | mic.stop() 60 | 61 | return ecg.data[alivecor.delay:] # cut off leading filter delay (contains nonsense output) 62 | 63 | 64 | def load_raw_audio(file_name): 65 | """Returns (sampling_rate, samples) where samples is an array of floats""" 66 | wf = wave.open(file_name) 67 | nframes = wf.getnframes() 68 | buf = wf.readframes(nframes) 69 | arr = np.frombuffer(buf, dtype=np.int16) 70 | arr = arr.reshape((nframes, wf.getnchannels())).T 71 | 72 | assert(wf.getsampwidth() == 2) # for np.int16 to hold 73 | #assert(wf.getframerate() == 48000) # Android recs 48 kHz?! 74 | 75 | raw_audio = arr[0] / float(2**15) # assuming 16-bit wav file, left if stereo 76 | return raw_audio, wf.getframerate() 77 | 78 | 79 | def beatdet_alivecor(signal, fps=48000, lpad_t=0): 80 | """decode, scrub, and beatdetect AliveCor.""" 81 | #print 'ecg_fps=', ecg_fps 82 | ecg_raw = decode_alivecor(signal, fps=fps) 83 | ecg_dec_fps = 300 84 | ecg = ecg_raw[::int(fps/ecg_dec_fps)] 85 | ecg = highpass_fft(ecg, fps=ecg_dec_fps) 86 | ecg = Series(ecg, fps=ecg_dec_fps, lpad=lpad_t*ecg_dec_fps) 87 | 88 | scrubbed = scrub_ecg(ecg) 89 | ne = NoisyECG(scrubbed) 90 | ecg2 = HeartSeries(scrubbed.x, ne.beat_idxs, fps=ecg.fps, lpad=ecg.lpad) 91 | return ecg2 92 | 93 | 94 | def beatdet_ecg(series): 95 | ne = NoisyECG(series) 96 | return HeartSeries(series.x, ne.beat_idxs, fps=series.fps, lpad=series.lpad) 97 | -------------------------------------------------------------------------------- /hsh_signal/waveshape.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from hsh_signal.signal import localmax, lowpass, lowpass_fft 4 | from scipy.interpolate import interp1d 5 | 6 | 7 | def ppg_wave_foot(ppg_raw_l, ppg_l): 8 | """ 9 | :param ppg_raw_l: PPG signal, evenly spaced 10 | :param ppg_l: highpass filtered, beat detected PPG signal 11 | """ 12 | # 13 | # move beats to the wave foot 14 | # 15 | 16 | # highpass (`ppg()`) -> lowpass -> localmax 17 | # 18 | # to do: this is not methodologically thought through. 19 | # to do: testing (does it work cleanly under noisy conditions?) 20 | # 21 | ratio=10 # use same ratio for all! 22 | ppg_smoothed = ppg_l.upsample(ratio) 23 | ppg_smoothed.x = lowpass_fft(ppg_smoothed.x, ppg_smoothed.fps, cf=6.0, tw=0.5) 24 | 25 | # fill `ileft_min` with index of next local minimum to the left 26 | # for noise robustness, use some smoothing before 27 | #local_min = localmax(ppg.x) 28 | local_min = localmax(ppg_smoothed.x) 29 | ileft_min = np.arange(len(local_min)) * local_min 30 | for i in range(1, len(ileft_min)): 31 | if ileft_min[i] == 0: 32 | ileft_min[i] = ileft_min[i - 1] 33 | 34 | ppg_smoothed.ibeats = ileft_min[ppg_smoothed.ibeats.astype(int)] 35 | ppg_smoothed.tbeats = (ppg_smoothed.ibeats - ppg_smoothed.lpad) / float(ppg_smoothed.fps) 36 | 37 | # 38 | # hack to get beats onto ppg_raw 39 | # 40 | ppg_u = ppg_l.upsample(ratio) 41 | ppg_raw_tmp = ppg_raw_l.upsample(ratio) 42 | ppg_raw_u = ppg_l.upsample(ratio) 43 | ppg_raw_u.x = ppg_raw_tmp.x 44 | ppg_raw_u.ibeats = ppg_smoothed.ibeats 45 | ppg_raw_u.tbeats = ppg_smoothed.tbeats 46 | 47 | ppg_baselined = beat_baseline(ppg_raw_u, ppg_u) 48 | return ppg_baselined 49 | 50 | 51 | def beat_baseline(ppg_feet, ppg_beats): 52 | """ 53 | :param ppg_feet: raw ppg with beats located at the wave foot. 54 | :param ppg_beats: ppg with beats located on the edge as usual. 55 | """ 56 | ppg = ppg_feet 57 | xu = np.zeros(len(ppg.x)) 58 | fps = ppg.fps 59 | ibeats = ppg.ibeats.astype(int) 60 | 61 | # where long periods without beats, we need support from raw average (~baseline) 62 | ibis = np.diff(ppg.tbeats) 63 | median_ibi = np.median(ibis) 64 | long_ibis = np.where(ibis > 1.5 * median_ibi)[0] 65 | #print 'long_ibis', long_ibis 66 | ib_start = ppg.ibeats[long_ibis] 67 | ib_end = ppg.ibeats[np.clip(long_ibis + 1, 0, len(ppg.ibeats))] 68 | if len(ib_end) < len(ib_start): ib_end = np.insert(ib_end, len(ib_end), len(ppg.x) - 1) 69 | # iterate over long holes and fill them with fake ibis 70 | # to do: actually use raw average baseline, not some random value at a picked index 71 | extra_ibeats = [] 72 | for s, e in zip(ib_start, ib_end): 73 | #print 's,e', s, e, ppg.t[s], ppg.t[e] 74 | extra_ibeats += list(np.arange(s + fps * median_ibi, e - 0.5 * median_ibi * fps, fps * median_ibi).astype(int)) 75 | 76 | #print 'extra_ibeats', extra_ibeats, 'extra_tbeats', ppg.t[extra_ibeats] 77 | 78 | ibeats = np.array(sorted(list(ibeats) + list(extra_ibeats))) 79 | xu[ibeats] = ppg.x[ibeats] 80 | 81 | lf = interp1d(ibeats, xu[ibeats]) 82 | xul = lf(np.arange(ibeats[0], ibeats[-1])) 83 | xul = np.pad(xul, (ibeats[0], len(xu) - ibeats[-1]), mode='constant') 84 | # to do: should we lowpass filter here, to get rid of all higher freq components? 85 | 86 | f_cf = 1.8 87 | f_tw = f_cf / 2.0 88 | xu_filtered = lowpass_fft(xul, ppg.fps, cf=f_cf, tw=f_tw) # * scaling # * ratio 89 | 90 | if False: 91 | plt.plot(ppg.t, np.clip(xu_filtered, a_min=220, a_max=270), c='b') 92 | plt.plot(ppg.t, np.clip(ppg.x, a_min=200, a_max=270), c='r') 93 | plt.show() 94 | 95 | ppg_detrended = ppg.copy() 96 | ppg_detrended.x = ppg.x - xu_filtered 97 | ppg_detrended.tbeats, ppg_detrended.ibeats = ppg_beats.tbeats, ppg_beats.ibeats 98 | 99 | return ppg_detrended 100 | # return HeartSeries(xu, ppg.ibeats, ppg.fps, ppg.lpad) 101 | -------------------------------------------------------------------------------- /gr_pll/gr/include/gnuradio/analog/pll_freqdet_cf.h: -------------------------------------------------------------------------------- 1 | /* -*- c++ -*- */ 2 | /* 3 | * Copyright 2004,2011 Free Software Foundation, Inc. 4 | * 5 | * This file is part of GNU Radio 6 | * 7 | * GNU Radio is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 3, or (at your option) 10 | * any later version. 11 | * 12 | * GNU Radio is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with GNU Radio; see the file COPYING. If not, write to 19 | * the Free Software Foundation, Inc., 51 Franklin Street, 20 | * Boston, MA 02110-1301, USA. 21 | */ 22 | 23 | #ifndef INCLUDED_ANALOG_PLL_FREQDET_CF_H 24 | #define INCLUDED_ANALOG_PLL_FREQDET_CF_H 25 | 26 | //#include 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | //#include 33 | 34 | namespace gr { 35 | // surrogate 36 | class sync_block {}; 37 | 38 | namespace analog { 39 | 40 | /*! 41 | * \brief Implements a PLL which locks to the input frequency and outputs 42 | * an estimate of that frequency. Useful for FM Demod. 43 | * \ingroup synchronizers_blk 44 | * 45 | * \details 46 | * Input stream 0: complex 47 | * Output stream 0: float 48 | * 49 | * This PLL locks onto a [possibly noisy] reference carrier on 50 | * the input and outputs an estimate of that frequency in radians per sample. 51 | * All settings max_freq and min_freq are in terms of radians per sample, 52 | * NOT HERTZ. The loop bandwidth determins the lock range and should be set 53 | * around pi/200 -- 2pi/100. 54 | * \sa pll_refout_cc, pll_carriertracking_cc 55 | */ 56 | class ANALOG_API pll_freqdet_cf 57 | : virtual public sync_block, 58 | virtual public blocks::control_loop 59 | { 60 | public: 61 | // gr::analog::pll_freqdet_cf::sptr 62 | //typedef std::shared_ptr sptr; 63 | 64 | /* \brief Make PLL block that outputs the tracked signal's frequency. 65 | * 66 | * \param loop_bw: control loop's bandwidth parameter. 67 | * \param max_freq: maximum (normalized) frequency PLL will lock to. 68 | * \param min_freq: minimum (normalized) frequency PLL will lock to. 69 | */ 70 | static pll_freqdet_cf *make(float loop_bw, float max_freq, float min_freq); 71 | 72 | /** This is probably in sync_block in the original gnuradio. (David) */ 73 | virtual int work(int noutput_items, 74 | gr_vector_const_void_star &input_items, 75 | gr_vector_void_star &output_items) = 0; 76 | 77 | virtual int work2_cc(int noutput_items, 78 | gr_vector_const_void_star &input_items, 79 | gr_vector_void_star &output_items) = 0; 80 | 81 | virtual void set_loop_bandwidth(float bw) = 0; 82 | virtual void set_damping_factor(float df) = 0; 83 | virtual void set_alpha(float alpha) = 0; 84 | virtual void set_beta(float beta) = 0; 85 | virtual void set_frequency(float freq) = 0; 86 | virtual void set_phase(float phase) = 0; 87 | virtual void set_min_freq(float freq) = 0; 88 | virtual void set_max_freq(float freq) = 0; 89 | 90 | virtual float get_loop_bandwidth() const = 0; 91 | virtual float get_damping_factor() const = 0; 92 | virtual float get_alpha() const = 0; 93 | virtual float get_beta() const = 0; 94 | virtual float get_frequency() const = 0; 95 | virtual float get_phase() const = 0; 96 | virtual float get_min_freq() const = 0; 97 | virtual float get_max_freq() const = 0; 98 | }; 99 | 100 | } /* namespace analog */ 101 | } /* namespace gr */ 102 | 103 | #endif /* INCLUDED_ANALOG_PLL_FREQDET_CF_H */ 104 | -------------------------------------------------------------------------------- /gr_pll/gr/include/gnuradio/attributes.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Free Software Foundation, Inc. 3 | * 4 | * This file is part of GNU Radio 5 | * 6 | * GNU Radio is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3, or (at your option) 9 | * any later version. 10 | * 11 | * GNU Radio is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with GNU Radio; see the file COPYING. If not, write to 18 | * the Free Software Foundation, Inc., 51 Franklin Street, 19 | * Boston, MA 02110-1301, USA. 20 | */ 21 | 22 | #ifndef INCLUDED_GNURADIO_ATTRIBUTES_H 23 | #define INCLUDED_GNURADIO_ATTRIBUTES_H 24 | 25 | //////////////////////////////////////////////////////////////////////// 26 | // Cross-platform attribute macros 27 | //////////////////////////////////////////////////////////////////////// 28 | #if defined __GNUC__ 29 | # define __GR_ATTR_ALIGNED(x) __attribute__((aligned(x))) 30 | # define __GR_ATTR_UNUSED __attribute__((unused)) 31 | # define __GR_ATTR_INLINE __attribute__((always_inline)) 32 | # define __GR_ATTR_DEPRECATED __attribute__((deprecated)) 33 | # if __GNUC__ >= 4 34 | # define __GR_ATTR_EXPORT __attribute__((visibility("default"))) 35 | # define __GR_ATTR_IMPORT __attribute__((visibility("default"))) 36 | # else 37 | # define __GR_ATTR_EXPORT 38 | # define __GR_ATTR_IMPORT 39 | # endif 40 | #elif defined __clang__ 41 | # define __GR_ATTR_ALIGNED(x) __attribute__((aligned(x))) 42 | # define __GR_ATTR_UNUSED __attribute__((unused)) 43 | # define __GR_ATTR_INLINE __attribute__((always_inline)) 44 | # define __GR_ATTR_DEPRECATED __attribute__((deprecated)) 45 | # define __GR_ATTR_EXPORT __attribute__((visibility("default"))) 46 | # define __GR_ATTR_IMPORT __attribute__((visibility("default"))) 47 | #elif _MSC_VER 48 | # define __GR_ATTR_ALIGNED(x) __declspec(align(x)) 49 | # define __GR_ATTR_UNUSED 50 | # define __GR_ATTR_INLINE __forceinline 51 | # define __GR_ATTR_DEPRECATED __declspec(deprecated) 52 | # define __GR_ATTR_EXPORT __declspec(dllexport) 53 | # define __GR_ATTR_IMPORT __declspec(dllimport) 54 | #else 55 | # define __GR_ATTR_ALIGNED(x) 56 | # define __GR_ATTR_UNUSED 57 | # define __GR_ATTR_INLINE 58 | # define __GR_ATTR_DEPRECATED 59 | # define __GR_ATTR_EXPORT 60 | # define __GR_ATTR_IMPORT 61 | #endif 62 | 63 | //////////////////////////////////////////////////////////////////////// 64 | // define inline when building C 65 | //////////////////////////////////////////////////////////////////////// 66 | #if defined(_MSC_VER) && !defined(__cplusplus) && !defined(inline) 67 | # define inline __inline 68 | #endif 69 | 70 | //////////////////////////////////////////////////////////////////////// 71 | // suppress warnings 72 | //////////////////////////////////////////////////////////////////////// 73 | #ifdef _MSC_VER 74 | # pragma warning(disable: 4251) // class 'A' needs to have dll-interface to be used by clients of class 'B' 75 | # pragma warning(disable: 4275) // non dll-interface class ... used as base for dll-interface class ... 76 | # pragma warning(disable: 4244) // conversion from 'double' to 'float', possible loss of data 77 | # pragma warning(disable: 4305) // 'initializing' : truncation from 'double' to 'float' 78 | # pragma warning(disable: 4290) // C++ exception specification ignored except to indicate a function is not __declspec(nothrow) 79 | #endif 80 | 81 | //////////////////////////////////////////////////////////////////////// 82 | // implement cross-compiler VLA macros 83 | //////////////////////////////////////////////////////////////////////// 84 | #ifdef C99 85 | # define __GR_VLA(TYPE, buf, size) TYPE buf[size] 86 | # define __GR_VLA2D(TYPE, buf, size, size2) TYPE buf[size][size2] 87 | #else 88 | # define __GR_VLA(TYPE, buf, size) TYPE * buf = (TYPE *) alloca(sizeof(TYPE) * (size)) 89 | # define __GR_VLA2D(TYPE, buf, size, size2) TYPE ** buf = (TYPE **) alloca(sizeof(TYPE) * (size) * (size2)) 90 | #endif 91 | 92 | #endif /* INCLUDED_GNURADIO_ATTRIBUTES_H */ 93 | -------------------------------------------------------------------------------- /gr_firdes/gr/include/gnuradio/attributes.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Free Software Foundation, Inc. 3 | * 4 | * This file is part of GNU Radio 5 | * 6 | * GNU Radio is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 3, or (at your option) 9 | * any later version. 10 | * 11 | * GNU Radio is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with GNU Radio; see the file COPYING. If not, write to 18 | * the Free Software Foundation, Inc., 51 Franklin Street, 19 | * Boston, MA 02110-1301, USA. 20 | */ 21 | 22 | #ifndef INCLUDED_GNURADIO_ATTRIBUTES_H 23 | #define INCLUDED_GNURADIO_ATTRIBUTES_H 24 | 25 | //////////////////////////////////////////////////////////////////////// 26 | // Cross-platform attribute macros 27 | //////////////////////////////////////////////////////////////////////// 28 | #if defined __GNUC__ 29 | # define __GR_ATTR_ALIGNED(x) __attribute__((aligned(x))) 30 | # define __GR_ATTR_UNUSED __attribute__((unused)) 31 | # define __GR_ATTR_INLINE __attribute__((always_inline)) 32 | # define __GR_ATTR_DEPRECATED __attribute__((deprecated)) 33 | # if __GNUC__ >= 4 34 | # define __GR_ATTR_EXPORT __attribute__((visibility("default"))) 35 | # define __GR_ATTR_IMPORT __attribute__((visibility("default"))) 36 | # else 37 | # define __GR_ATTR_EXPORT 38 | # define __GR_ATTR_IMPORT 39 | # endif 40 | #elif defined __clang__ 41 | # define __GR_ATTR_ALIGNED(x) __attribute__((aligned(x))) 42 | # define __GR_ATTR_UNUSED __attribute__((unused)) 43 | # define __GR_ATTR_INLINE __attribute__((always_inline)) 44 | # define __GR_ATTR_DEPRECATED __attribute__((deprecated)) 45 | # define __GR_ATTR_EXPORT __attribute__((visibility("default"))) 46 | # define __GR_ATTR_IMPORT __attribute__((visibility("default"))) 47 | #elif _MSC_VER 48 | # define __GR_ATTR_ALIGNED(x) __declspec(align(x)) 49 | # define __GR_ATTR_UNUSED 50 | # define __GR_ATTR_INLINE __forceinline 51 | # define __GR_ATTR_DEPRECATED __declspec(deprecated) 52 | # define __GR_ATTR_EXPORT __declspec(dllexport) 53 | # define __GR_ATTR_IMPORT __declspec(dllimport) 54 | #else 55 | # define __GR_ATTR_ALIGNED(x) 56 | # define __GR_ATTR_UNUSED 57 | # define __GR_ATTR_INLINE 58 | # define __GR_ATTR_DEPRECATED 59 | # define __GR_ATTR_EXPORT 60 | # define __GR_ATTR_IMPORT 61 | #endif 62 | 63 | //////////////////////////////////////////////////////////////////////// 64 | // define inline when building C 65 | //////////////////////////////////////////////////////////////////////// 66 | #if defined(_MSC_VER) && !defined(__cplusplus) && !defined(inline) 67 | # define inline __inline 68 | #endif 69 | 70 | //////////////////////////////////////////////////////////////////////// 71 | // suppress warnings 72 | //////////////////////////////////////////////////////////////////////// 73 | #ifdef _MSC_VER 74 | # pragma warning(disable: 4251) // class 'A' needs to have dll-interface to be used by clients of class 'B' 75 | # pragma warning(disable: 4275) // non dll-interface class ... used as base for dll-interface class ... 76 | # pragma warning(disable: 4244) // conversion from 'double' to 'float', possible loss of data 77 | # pragma warning(disable: 4305) // 'initializing' : truncation from 'double' to 'float' 78 | # pragma warning(disable: 4290) // C++ exception specification ignored except to indicate a function is not __declspec(nothrow) 79 | #endif 80 | 81 | //////////////////////////////////////////////////////////////////////// 82 | // implement cross-compiler VLA macros 83 | //////////////////////////////////////////////////////////////////////// 84 | #ifdef C99 85 | # define __GR_VLA(TYPE, buf, size) TYPE buf[size] 86 | # define __GR_VLA2D(TYPE, buf, size, size2) TYPE buf[size][size2] 87 | #else 88 | # define __GR_VLA(TYPE, buf, size) TYPE * buf = (TYPE *) alloca(sizeof(TYPE) * (size)) 89 | # define __GR_VLA2D(TYPE, buf, size, size2) TYPE ** buf = (TYPE **) alloca(sizeof(TYPE) * (size) * (size2)) 90 | #endif 91 | 92 | #endif /* INCLUDED_GNURADIO_ATTRIBUTES_H */ 93 | -------------------------------------------------------------------------------- /hsh_signal/dtw.py: -------------------------------------------------------------------------------- 1 | from numpy import array, zeros, argmin, inf, equal 2 | from scipy.spatial.distance import cdist 3 | 4 | # https://raw.githubusercontent.com/pierre-rouanet/dtw/master/dtw.py 5 | 6 | def dtw(x, y, dist): 7 | """ 8 | Computes Dynamic Time Warping (DTW) of two sequences. 9 | 10 | :param array x: N1*M array 11 | :param array y: N2*M array 12 | :param func dist: distance used as cost measure 13 | 14 | Returns the minimum distance, the cost matrix, the accumulated cost matrix, and the wrap path. 15 | """ 16 | assert len(x) 17 | assert len(y) 18 | r, c = len(x), len(y) 19 | D0 = zeros((r + 1, c + 1)) 20 | D0[0, 1:] = inf 21 | D0[1:, 0] = inf 22 | D1 = D0[1:, 1:] # view 23 | for i in range(r): 24 | for j in range(c): 25 | D1[i, j] = dist(x[i], y[j]) 26 | C = D1.copy() 27 | for i in range(r): 28 | for j in range(c): 29 | D1[i, j] += min(D0[i, j], D0[i, j+1], D0[i+1, j]) 30 | if len(x)==1: 31 | path = zeros(len(y), dtype=int), range(len(y)) 32 | elif len(y) == 1: 33 | path = range(len(x)), zeros(len(x), dtype=int) 34 | else: 35 | path = _traceback(D0) 36 | return D1[-1, -1] / sum(D1.shape), C, D1, path 37 | 38 | def fastdtw(x, y, dist): 39 | """ 40 | Computes Dynamic Time Warping (DTW) of two sequences in a faster way. 41 | Instead of iterating through each element and calculating each distance, 42 | this uses the cdist function from scipy (https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.cdist.html) 43 | 44 | :param array x: N1*M array 45 | :param array y: N2*M array 46 | :param string or func dist: distance parameter for cdist. When string is given, cdist uses optimized functions for the distance metrics. 47 | If a string is passed, the distance function can be 'braycurtis', 'canberra', 'chebyshev', 'cityblock', 'correlation', 'cosine', 'dice', 'euclidean', 'hamming', 'jaccard', 'kulsinski', 'mahalanobis', 'matching', 'minkowski', 'rogerstanimoto', 'russellrao', 'seuclidean', 'sokalmichener', 'sokalsneath', 'sqeuclidean', 'wminkowski', 'yule'. 48 | Returns the minimum distance, the cost matrix, the accumulated cost matrix, and the wrap path. 49 | """ 50 | assert len(x) 51 | assert len(y) 52 | r, c = len(x), len(y) 53 | D0 = zeros((r + 1, c + 1)) 54 | D0[0, 1:] = inf 55 | D0[1:, 0] = inf 56 | D1 = D0[1:, 1:] 57 | D0[1:,1:] = cdist(x,y,dist) 58 | C = D1.copy() 59 | for i in range(r): 60 | for j in range(c): 61 | D1[i, j] += min(D0[i, j], D0[i, j+1], D0[i+1, j]) 62 | if len(x)==1: 63 | path = zeros(len(y)), range(len(y)) 64 | elif len(y) == 1: 65 | path = range(len(x)), zeros(len(x)) 66 | else: 67 | path = _traceback(D0) 68 | return D1[-1, -1] / sum(D1.shape), C, D1, path 69 | 70 | def _traceback(D): 71 | i, j = array(D.shape) - 2 72 | p, q = [i], [j] 73 | while ((i > 0) or (j > 0)): 74 | tb = argmin((D[i, j], D[i, j+1], D[i+1, j])) 75 | if (tb == 0): 76 | i -= 1 77 | j -= 1 78 | elif (tb == 1): 79 | i -= 1 80 | else: # (tb == 2): 81 | j -= 1 82 | p.insert(0, i) 83 | q.insert(0, j) 84 | return array(p), array(q) 85 | 86 | if __name__ == '__main__': 87 | if 0: # 1-D numeric 88 | from sklearn.metrics.pairwise import manhattan_distances 89 | x = [0, 0, 1, 1, 2, 4, 2, 1, 2, 0] 90 | y = [1, 1, 1, 2, 2, 2, 2, 3, 2, 0] 91 | dist_fun = manhattan_distances 92 | elif 0: # 2-D numeric 93 | from sklearn.metrics.pairwise import euclidean_distances 94 | x = [[0, 0], [0, 1], [1, 1], [1, 2], [2, 2], [4, 3], [2, 3], [1, 1], [2, 2], [0, 1]] 95 | y = [[1, 0], [1, 1], [1, 1], [2, 1], [4, 3], [4, 3], [2, 3], [3, 1], [1, 2], [1, 0]] 96 | dist_fun = euclidean_distances 97 | else: # 1-D list of strings 98 | from nltk.metrics.distance import edit_distance 99 | #x = ['we', 'shelled', 'clams', 'for', 'the', 'chowder'] 100 | #y = ['class', 'too'] 101 | x = ['i', 'soon', 'found', 'myself', 'muttering', 'to', 'the', 'walls'] 102 | y = ['see', 'drown', 'himself'] 103 | #x = 'we talked about the situation'.split() 104 | #y = 'we talked about the situation'.split() 105 | dist_fun = edit_distance 106 | dist, cost, acc, path = dtw(x, y, dist_fun) 107 | 108 | # vizualize 109 | from matplotlib import pyplot as plt 110 | plt.imshow(cost.T, origin='lower', cmap=plt.cm.Reds, interpolation='nearest') 111 | plt.plot(path[0], path[1], '-o') # relation 112 | plt.xticks(range(len(x)), x) 113 | plt.yticks(range(len(y)), y) 114 | plt.xlabel('x') 115 | plt.ylabel('y') 116 | plt.axis('tight') 117 | plt.title('Minimum distance: {}'.format(dist)) 118 | plt.show() 119 | -------------------------------------------------------------------------------- /test/test_ppg.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | if __name__ == '__main__': 5 | test_sig = np.array([-0.52, -0.50248776, -0.496, -0.48946329, -0.47663404, 6 | -0.47956118, -0.49914683, -0.52697608, -0.54956174, -0.57622024, 7 | -0.588, -0.589, -0.58490211, -0.57656062, -0.56504783, 8 | -0.54775528, -0.52980423, -0.51919466, -0.50770634, -0.49053504, 9 | -0.46875528, -0.46641435, -0.4612436, -0.457, -0.46948943, 10 | -0.49134316, -0.50775751, -0.52405061, -0.53956174, -0.54104839, 11 | -0.53175528, -0.529, -0.52360845, -0.50946162, -0.49658398, 12 | -0.48109566, -0.46770634, -0.46236485, -0.4435584, -0.42136374, 13 | -0.41134038, -0.40700056, -0.41902781, -0.45273637, -0.48156396, 14 | -0.49905061, -0.502, -0.49294994, -0.49, -0.49436652, 15 | -0.48938821, -0.47487597, -0.46712125, -0.458901, -0.45155951, 16 | -0.447, -0.43875306, -0.42280256, -0.41319355, -0.408, 17 | -0.41273415, -0.43290823, -0.46110345, -0.49029867, -0.51463849, 18 | -0.53219689, -0.54153838, -0.545, -0.54077864, -0.53321858, 19 | -0.531, -0.53087709, -0.52111902, -0.517, -0.514, 20 | -0.50458231, -0.49107008, -0.47885261, -0.48295439, -0.50442214, 21 | -0.53827697, -0.5649822, -0.58700445, -0.60234372, -0.606, 22 | -0.604, -0.59563181, -0.58714516, -0.57389766, -0.55282202, 23 | -0.5305317, -0.51384872, -0.4978743, -0.48148276, -0.46789989, 24 | -0.4577792, -0.46156618, -0.47724917, -0.49566518, -0.52232647, 25 | -0.54114905, -0.55173582, -0.55416796, -0.55041324, -0.53845495, 26 | -0.52507175, -0.519901, -0.50892102, -0.49323804, -0.47755506, 27 | -0.46621802, -0.46109455, -0.44538265, -0.42782314, -0.41607008, 28 | -0.42090712, -0.44364516, -0.46888598, -0.48229588, -0.48531869, 29 | -0.49248943, -0.497, -0.5, -0.5, -0.49765517, 30 | -0.48362736, -0.46194438, -0.4506307, -0.44128921, -0.43089544, 31 | -0.41540934, -0.40026474, -0.397, -0.39941824, -0.41239933, 32 | -0.43425306, -0.45588543, -0.47356841, -0.48968854, -0.49846719, 33 | -0.50190434, -0.504, -0.50375417, -0.49716685, -0.48882536, 34 | -0.47796774, -0.46421357, -0.45880089, -0.4544594, -0.44611791, 35 | -0.43477642, -0.429, -0.43390656, -0.45274416, -0.47576863, 36 | -0.49832759, -0.52181758, -0.53822803, -0.54795551, -0.554, 37 | -0.54968076, -0.54251001, -0.53467853, -0.51967408, -0.50499333, 38 | -0.49686485, -0.51465628, -0.50094216, -0.48325918, -0.4742881, 39 | -0.47810679, -0.49718465, -0.52631368, -0.55823359, -0.58425806, 40 | -0.60440211, -0.61710234, -0.62122191, -0.621, -0.62, 41 | -0.6155317, -0.60619021, -0.59477308, -0.58150723, -0.56591435, 42 | -0.54606062, -0.52996552, -0.5152119, -0.51340044, -0.53335428, 43 | -0.55276641, -0.57728643, -0.60191546, -0.62081535, -0.63012458, 44 | -0.633, -0.63346607, -0.63563682, -0.62657731, -0.61208676, 45 | -0.60070189, -0.5873604, -0.56856618, -0.54569355, -0.53050389, 46 | -0.51599444, -0.511, -0.52237709, -0.54812013, -0.58517186, 47 | -0.61599555, -0.62802725, -0.629802, -0.62526251, -0.62453949, 48 | -0.625, -0.61723804, -0.60784483, -0.59533259, -0.57603393, 49 | -0.54692436, -0.51065295, -0.48728365, -0.47877141, -0.46925918, 50 | -0.462, -0.47494216, -0.46548165, -0.48364961, -0.50140267, 51 | -0.509, -0.508, -0.508, -0.508, -0.50118242, 52 | -0.4863743, -0.46970412, -0.45960011, -0.44281313, -0.42015128, 53 | -0.40108454, -0.38140156, -0.36903893, -0.3649822, -0.37797108, 54 | -0.40399555, -0.42801669, -0.44769633, -0.46672414, -0.47651557, 55 | -0.47768632, -0.48185706, -0.47197219, -0.46660289, -0.4555228, 56 | -0.43783982, -0.41502447, -0.39335539, -0.393, -0.38588487, 57 | -0.36742492, -0.36143548, -0.36794105, -0.39234205, -0.42461402, 58 | -0.45323749, -0.47409121, -0.49435595, -0.5, -0.5, 59 | -0.5, -0.5, -0.5, -0.4879327, -0.46251057, 60 | -0.4342703, -0.41474861, -0.40240712, -0.39653281, -0.39627586, 61 | -0.40461735, -0.42387653, -0.45475083, -0.48224638, -0.51094994, 62 | -0.53298721, -0.5426663, -0.548, -0.55744494, -0.57630923, 63 | -0.56441935, -0.55231313, -0.54356952, -0.52488654, -0.50740267, 64 | -0.49315072, -0.4779188, -0.478, -0.48076418, -0.50547553]) 65 | 66 | from hsh_signal.heartseries import Series 67 | from hsh_signal.ppg import ppg_beatdetect_getrr 68 | test_ppg = Series(test_sig, fps=30.0) 69 | ppg = ppg_beatdetect_getrr(test_ppg) 70 | ppg_ref = ppg_beatdetect_getrr(test_ppg, type='regular') 71 | ppg_ref.tbeats += 1.0 # wtf TODO 72 | assert len(ppg.tbeats) > 1 73 | assert np.all(np.abs(np.diff(ppg_ref.tbeats - ppg.tbeats)) < 35e-3) 74 | -------------------------------------------------------------------------------- /gr_pll/pll.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | 3 | #from libcpp.memory cimport shared_ptr 4 | from libcpp.vector cimport vector 5 | from libcpp.complex cimport complex 6 | cimport numpy as np 7 | import numpy as np 8 | from math import pi 9 | 10 | ctypedef np.complex64_t complex64_t 11 | 12 | 13 | cdef extern from "" namespace "gr::analog": 14 | ctypedef vector[const void*] gr_vector_const_void_star 15 | ctypedef vector[void*] gr_vector_void_star 16 | ctypedef complex[float] gr_complex 17 | 18 | cdef cppclass pll_freqdet_cf: 19 | int work(int noutput_items, 20 | gr_vector_const_void_star &input_items, 21 | gr_vector_void_star &output_items) 22 | 23 | int work2_cc(int noutput_items, 24 | gr_vector_const_void_star &input_items, 25 | gr_vector_void_star &output_items) 26 | 27 | cdef extern from "" namespace "gr::analog::pll_freqdet_cf": 28 | # static method from class pll_freqdet_cf 29 | #shared_ptr[pll_freqdet_cf] make(float loop_bw, float max_freq, float min_freq) 30 | pll_freqdet_cf *make(float loop_bw, float max_freq, float min_freq) 31 | 32 | cdef class PLL: 33 | """ 34 | Implements a PLL which locks to the input frequency and outputs an estimate of that frequency. Useful for FM Demod. 35 | 36 | Input stream 0: complex Output stream 0: float 37 | 38 | This PLL locks onto a [possibly noisy] reference carrier on the input and outputs an estimate of that frequency 39 | in radians per sample. 40 | 41 | The loop bandwidth determines the lock range and should be set around sampling_rate * [pi/200; 2pi/100]. 42 | 43 | see http://gnuradio.org/doc/doxygen/classgr_1_1analog_1_1pll__freqdet__cf.html#details 44 | """ 45 | 46 | cdef int _i 47 | #cdef shared_ptr[pll_freqdet_cf] _ptr 48 | cdef pll_freqdet_cf *_ptr 49 | 50 | def __cinit__(self, loop_bw, max_freq, min_freq, sampling_rate): 51 | """ 52 | Make PLL block that outputs the tracked signal's frequency. 53 | 54 | :param loop_bw: loop bandwidth, determines the lock range and should be set around sampling_rate * [pi/200; 2pi/100]. 55 | :param max_freq: maximum frequency cap (Hz) 56 | :param min_freq: minimum frequency cap (Hz) 57 | :param sampling_rate: sampling rate (Hz) 58 | """ 59 | # internal settings are in terms of radians per sample, not Hz. 60 | # see https://en.wikipedia.org/wiki/Normalized_frequency_%28unit%29 61 | self._ptr = make(loop_bw * 2.0*pi / sampling_rate, max_freq * 2.0*pi / sampling_rate, min_freq * 2.0*pi / sampling_rate) 62 | 63 | def __dealloc__(self): 64 | del self._ptr 65 | 66 | def filter_cf(self, x): 67 | """ 68 | FM demodulator (complex -> float). 69 | :param x: array of complex numbers (analytic input signal) 70 | :returns: demodulated frequency values, as array of float numbers (I think these are in radians per sample -- normalize/convert back to Hz if required) 71 | """ 72 | 73 | # wrap to the exact data types required by _work(): 74 | pll_in = np.array(x, dtype=np.complex64) 75 | pll_out = np.zeros(len(pll_in), dtype=np.float32) 76 | self._work(pll_in, pll_out) 77 | return pll_out 78 | 79 | def filter_cc(self, x): 80 | """ 81 | Local oscillator for AM demodulator (complex -> complex). 82 | :param x: array of complex numbers (analytic input signal) 83 | :returns: VCO output, as array of complex numbers 84 | """ 85 | 86 | # wrap to the exact data types required by _work(): 87 | pll_in = np.array(x, dtype=np.complex64) 88 | pll_out = np.zeros(len(pll_in), dtype=np.complex64) 89 | self._work2_cc(pll_in, pll_out) 90 | return pll_out 91 | 92 | def _work(self, input_items, output_items): 93 | """ 94 | Outputs the tracked signal's frequency. 95 | 96 | :param input_items: complex[float] array 97 | :param output_items: float array 98 | :return: len(output_items) 99 | 100 | Example: 101 | 102 | In [12]: input=np.array([1+1j,1+2j], dtype=np.complex64) 103 | In [13]: output=np.zeros(2, dtype=np.float32) 104 | In [15]: pll.work(input, output) 105 | """ 106 | cdef complex64_t[:] input_view = input_items 107 | cdef float[:] output_view = output_items 108 | cdef vector[const void*] input 109 | cdef vector[void*] output 110 | input.push_back(&input_view[0]) 111 | output.push_back(&output_view[0]) 112 | return self._ptr.work(len(output_items), input, output) 113 | 114 | def _work2_cc(self, input_items, output_items): 115 | """ 116 | Outputs the VCO signal. 117 | 118 | :param input_items: complex[float] array 119 | :param output_items: complex[float] array 120 | :return: len(output_items) 121 | 122 | Example: 123 | 124 | In [12]: input=np.array([1+1j,1+2j], dtype=np.complex64) 125 | In [13]: output=np.zeros(2, dtype=np.float32) 126 | In [15]: pll.work(input, output) 127 | """ 128 | cdef complex64_t[:] input_view = input_items 129 | cdef complex64_t[:] output_view = output_items 130 | cdef vector[const void*] input 131 | cdef vector[void*] output 132 | input.push_back(&input_view[0]) 133 | output.push_back(&output_view[0]) 134 | return self._ptr.work2_cc(len(output_items), input, output) 135 | -------------------------------------------------------------------------------- /gr_pll/gr/control_loop.cc: -------------------------------------------------------------------------------- 1 | /* -*- c++ -*- */ 2 | /* 3 | * Copyright 2011,2013 Free Software Foundation, Inc. 4 | * 5 | * This file is part of GNU Radio 6 | * 7 | * GNU Radio is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 3, or (at your option) 10 | * any later version. 11 | * 12 | * GNU Radio is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with GNU Radio; see the file COPYING. If not, write to 19 | * the Free Software Foundation, Inc., 51 Franklin Street, 20 | * Boston, MA 02110-1301, USA. 21 | */ 22 | 23 | #ifdef HAVE_CONFIG_H 24 | #include "config.h" 25 | #endif 26 | 27 | #include 28 | #include 29 | #include 30 | 31 | namespace gr { 32 | namespace blocks { 33 | 34 | #define M_TWOPI (2.0f*M_PI) 35 | 36 | control_loop::control_loop(float loop_bw, 37 | float max_freq, float min_freq) 38 | : d_phase(0), d_freq(0), d_max_freq(max_freq), d_min_freq(min_freq) 39 | { 40 | // Set the damping factor for a critically damped system 41 | d_damping = sqrtf(2.0f)/2.0f; 42 | 43 | // Set the bandwidth, which will then call update_gains() 44 | set_loop_bandwidth(loop_bw); 45 | } 46 | 47 | control_loop::~control_loop() 48 | { 49 | } 50 | 51 | void 52 | control_loop::update_gains() 53 | { 54 | float denom = (1.0 + 2.0*d_damping*d_loop_bw + d_loop_bw*d_loop_bw); 55 | d_alpha = (4*d_damping*d_loop_bw) / denom; 56 | d_beta = (4*d_loop_bw*d_loop_bw) / denom; 57 | } 58 | 59 | void 60 | control_loop::advance_loop(float error) 61 | { 62 | d_freq = d_freq + d_beta * error; 63 | d_phase = d_phase + d_freq + d_alpha * error; 64 | } 65 | 66 | void 67 | control_loop::phase_wrap() 68 | { 69 | while(d_phase>M_TWOPI) 70 | d_phase -= M_TWOPI; 71 | while(d_phase<-M_TWOPI) 72 | d_phase += M_TWOPI; 73 | } 74 | 75 | void 76 | control_loop::frequency_limit() 77 | { 78 | if(d_freq > d_max_freq) 79 | d_freq = d_max_freq; 80 | else if(d_freq < d_min_freq) 81 | d_freq = d_min_freq; 82 | } 83 | 84 | /******************************************************************* 85 | * SET FUNCTIONS 86 | *******************************************************************/ 87 | 88 | void 89 | control_loop::set_loop_bandwidth(float bw) 90 | { 91 | if(bw < 0) { 92 | throw std::out_of_range ("control_loop: invalid bandwidth. Must be >= 0."); 93 | } 94 | 95 | d_loop_bw = bw; 96 | update_gains(); 97 | } 98 | 99 | void 100 | control_loop::set_damping_factor(float df) 101 | { 102 | if(df <= 0) { 103 | throw std::out_of_range ("control_loop: invalid damping factor. Must be > 0."); 104 | } 105 | 106 | d_damping = df; 107 | update_gains(); 108 | } 109 | 110 | void 111 | control_loop::set_alpha(float alpha) 112 | { 113 | if(alpha < 0 || alpha > 1.0) { 114 | throw std::out_of_range ("control_loop: invalid alpha. Must be in [0,1]."); 115 | } 116 | d_alpha = alpha; 117 | } 118 | 119 | void 120 | control_loop::set_beta(float beta) 121 | { 122 | if(beta < 0 || beta > 1.0) { 123 | throw std::out_of_range ("control_loop: invalid beta. Must be in [0,1]."); 124 | } 125 | d_beta = beta; 126 | } 127 | 128 | void 129 | control_loop::set_frequency(float freq) 130 | { 131 | if(freq > d_max_freq) 132 | d_freq = d_min_freq; 133 | else if(freq < d_min_freq) 134 | d_freq = d_max_freq; 135 | else 136 | d_freq = freq; 137 | } 138 | 139 | void 140 | control_loop::set_phase(float phase) 141 | { 142 | d_phase = phase; 143 | while(d_phase>M_TWOPI) 144 | d_phase -= M_TWOPI; 145 | while(d_phase<-M_TWOPI) 146 | d_phase += M_TWOPI; 147 | } 148 | 149 | void 150 | control_loop::set_max_freq(float freq) 151 | { 152 | d_max_freq = freq; 153 | } 154 | 155 | void 156 | control_loop::set_min_freq(float freq) 157 | { 158 | d_min_freq = freq; 159 | } 160 | 161 | /******************************************************************* 162 | * GET FUNCTIONS 163 | *******************************************************************/ 164 | 165 | float 166 | control_loop::get_loop_bandwidth() const 167 | { 168 | return d_loop_bw; 169 | } 170 | 171 | float 172 | control_loop::get_damping_factor() const 173 | { 174 | return d_damping; 175 | } 176 | 177 | float 178 | control_loop::get_alpha() const 179 | { 180 | return d_alpha; 181 | } 182 | 183 | float 184 | control_loop::get_beta() const 185 | { 186 | return d_beta; 187 | } 188 | 189 | float 190 | control_loop::get_frequency() const 191 | { 192 | return d_freq; 193 | } 194 | 195 | float 196 | control_loop::get_phase() const 197 | { 198 | return d_phase; 199 | } 200 | 201 | float 202 | control_loop::get_max_freq() const 203 | { 204 | return d_max_freq; 205 | } 206 | 207 | float 208 | control_loop::get_min_freq() const 209 | { 210 | return d_min_freq; 211 | } 212 | 213 | } /* namespace blocks */ 214 | } /* namespace gr */ 215 | 216 | int main() { return 0; } 217 | -------------------------------------------------------------------------------- /gr_pll/gr/include/gnuradio/math.h: -------------------------------------------------------------------------------- 1 | /* -*- c++ -*- */ 2 | /* 3 | * Copyright 2003,2005,2008,2013 Free Software Foundation, Inc. 4 | * 5 | * This file is part of GNU Radio 6 | * 7 | * GNU Radio is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 3, or (at your option) 10 | * any later version. 11 | * 12 | * GNU Radio is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with GNU Radio; see the file COPYING. If not, write to 19 | * the Free Software Foundation, Inc., 51 Franklin Street, 20 | * Boston, MA 02110-1301, USA. 21 | */ 22 | 23 | /* 24 | * mathematical odds and ends. 25 | */ 26 | 27 | #ifndef _GR_MATH_H_ 28 | #define _GR_MATH_H_ 29 | 30 | #include 31 | #include 32 | #include 33 | 34 | namespace gr { 35 | 36 | static inline bool 37 | is_power_of_2(long x) 38 | { 39 | return x != 0 && (x & (x-1)) == 0; 40 | } 41 | 42 | /*! 43 | * \brief Fast arc tangent using table lookup and linear interpolation 44 | * \ingroup misc 45 | * 46 | * \param y component of input vector 47 | * \param x component of input vector 48 | * \returns float angle angle of vector (x, y) in radians 49 | * 50 | * This function calculates the angle of the vector (x,y) based on a 51 | * table lookup and linear interpolation. The table uses a 256 point 52 | * table covering -45 to +45 degrees and uses symetry to determine 53 | * the final angle value in the range of -180 to 180 degrees. Note 54 | * that this function uses the small angle approximation for values 55 | * close to zero. This routine calculates the arc tangent with an 56 | * average error of +/- 0.045 degrees. 57 | */ 58 | GR_RUNTIME_API float fast_atan2f(float y, float x); 59 | 60 | static inline float 61 | fast_atan2f(gr_complex z) 62 | { 63 | return fast_atan2f(z.imag(), z.real()); 64 | } 65 | 66 | /* This bounds x by +/- clip without a branch */ 67 | static inline float 68 | branchless_clip(float x, float clip) 69 | { 70 | float x1 = fabsf(x+clip); 71 | float x2 = fabsf(x-clip); 72 | x1 -= x2; 73 | return 0.5*x1; 74 | } 75 | 76 | static inline float 77 | clip(float x, float clip) 78 | { 79 | float y = x; 80 | if(x > clip) 81 | y = clip; 82 | else if(x < -clip) 83 | y = -clip; 84 | return y; 85 | } 86 | 87 | // Slicer Functions 88 | static inline unsigned int 89 | binary_slicer(float x) 90 | { 91 | if(x >= 0) 92 | return 1; 93 | else 94 | return 0; 95 | } 96 | 97 | static inline unsigned int 98 | quad_45deg_slicer(float r, float i) 99 | { 100 | unsigned int ret = 0; 101 | if((r >= 0) && (i >= 0)) 102 | ret = 0; 103 | else if((r < 0) && (i >= 0)) 104 | ret = 1; 105 | else if((r < 0) && (i < 0)) 106 | ret = 2; 107 | else 108 | ret = 3; 109 | return ret; 110 | } 111 | 112 | static inline unsigned int 113 | quad_0deg_slicer(float r, float i) 114 | { 115 | unsigned int ret = 0; 116 | if(fabsf(r) > fabsf(i)) { 117 | if(r > 0) 118 | ret = 0; 119 | else 120 | ret = 2; 121 | } 122 | else { 123 | if(i > 0) 124 | ret = 1; 125 | else 126 | ret = 3; 127 | } 128 | 129 | return ret; 130 | } 131 | 132 | static inline unsigned int 133 | quad_45deg_slicer(gr_complex x) 134 | { 135 | return quad_45deg_slicer(x.real(), x.imag()); 136 | } 137 | 138 | static inline unsigned int 139 | quad_0deg_slicer(gr_complex x) 140 | { 141 | return quad_0deg_slicer(x.real(), x.imag()); 142 | } 143 | 144 | // Branchless Slicer Functions 145 | static inline unsigned int 146 | branchless_binary_slicer(float x) 147 | { 148 | return (x >= 0); 149 | } 150 | 151 | static inline unsigned int 152 | branchless_quad_0deg_slicer(float r, float i) 153 | { 154 | unsigned int ret = 0; 155 | ret = (fabsf(r) > fabsf(i)) * (((r < 0) << 0x1)); // either 0 (00) or 2 (10) 156 | ret |= (fabsf(i) > fabsf(r)) * (((i < 0) << 0x1) | 0x1); // either 1 (01) or 3 (11) 157 | 158 | return ret; 159 | } 160 | 161 | static inline unsigned int 162 | branchless_quad_0deg_slicer(gr_complex x) 163 | { 164 | return branchless_quad_0deg_slicer(x.real(), x.imag()); 165 | } 166 | 167 | static inline unsigned int 168 | branchless_quad_45deg_slicer(float r, float i) 169 | { 170 | char ret = (r <= 0); 171 | ret |= ((i <= 0) << 1); 172 | return (ret ^ ((ret & 0x2) >> 0x1)); 173 | } 174 | 175 | static inline unsigned int 176 | branchless_quad_45deg_slicer(gr_complex x) 177 | { 178 | return branchless_quad_45deg_slicer(x.real(), x.imag()); 179 | } 180 | 181 | /*! 182 | * \param x any value 183 | * \param pow2 must be a power of 2 184 | * \returns \p x rounded down to a multiple of \p pow2. 185 | */ 186 | static inline size_t 187 | p2_round_down(size_t x, size_t pow2) 188 | { 189 | return x & -pow2; 190 | } 191 | 192 | /*! 193 | * \param x any value 194 | * \param pow2 must be a power of 2 195 | * \returns \p x rounded up to a multiple of \p pow2. 196 | */ 197 | static inline size_t 198 | p2_round_up(size_t x, size_t pow2) 199 | { 200 | return p2_round_down(x + pow2 - 1, pow2); 201 | } 202 | 203 | /*! 204 | * \param x any value 205 | * \param pow2 must be a power of 2 206 | * \returns \p x modulo \p pow2. 207 | */ 208 | static inline size_t 209 | p2_modulo(size_t x, size_t pow2) 210 | { 211 | return x & (pow2 - 1); 212 | } 213 | 214 | /*! 215 | * \param x any value 216 | * \param pow2 must be a power of 2 217 | * \returns \p pow2 - (\p x modulo \p pow2). 218 | */ 219 | static inline size_t 220 | p2_modulo_neg(size_t x, size_t pow2) 221 | { 222 | return pow2 - p2_modulo(x, pow2); 223 | } 224 | 225 | } /* namespace gr */ 226 | 227 | #endif /* _GR_MATH_H_ */ 228 | -------------------------------------------------------------------------------- /gr_firdes/gr/include/gnuradio/math.h: -------------------------------------------------------------------------------- 1 | /* -*- c++ -*- */ 2 | /* 3 | * Copyright 2003,2005,2008,2013 Free Software Foundation, Inc. 4 | * 5 | * This file is part of GNU Radio 6 | * 7 | * GNU Radio is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 3, or (at your option) 10 | * any later version. 11 | * 12 | * GNU Radio is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with GNU Radio; see the file COPYING. If not, write to 19 | * the Free Software Foundation, Inc., 51 Franklin Street, 20 | * Boston, MA 02110-1301, USA. 21 | */ 22 | 23 | /* 24 | * mathematical odds and ends. 25 | */ 26 | 27 | #ifndef _GR_MATH_H_ 28 | #define _GR_MATH_H_ 29 | 30 | #include 31 | #include 32 | #include 33 | 34 | namespace gr { 35 | 36 | static inline bool 37 | is_power_of_2(long x) 38 | { 39 | return x != 0 && (x & (x-1)) == 0; 40 | } 41 | 42 | /*! 43 | * \brief Fast arc tangent using table lookup and linear interpolation 44 | * \ingroup misc 45 | * 46 | * \param y component of input vector 47 | * \param x component of input vector 48 | * \returns float angle angle of vector (x, y) in radians 49 | * 50 | * This function calculates the angle of the vector (x,y) based on a 51 | * table lookup and linear interpolation. The table uses a 256 point 52 | * table covering -45 to +45 degrees and uses symetry to determine 53 | * the final angle value in the range of -180 to 180 degrees. Note 54 | * that this function uses the small angle approximation for values 55 | * close to zero. This routine calculates the arc tangent with an 56 | * average error of +/- 0.045 degrees. 57 | */ 58 | GR_RUNTIME_API float fast_atan2f(float y, float x); 59 | 60 | static inline float 61 | fast_atan2f(gr_complex z) 62 | { 63 | return fast_atan2f(z.imag(), z.real()); 64 | } 65 | 66 | /* This bounds x by +/- clip without a branch */ 67 | static inline float 68 | branchless_clip(float x, float clip) 69 | { 70 | float x1 = fabsf(x+clip); 71 | float x2 = fabsf(x-clip); 72 | x1 -= x2; 73 | return 0.5*x1; 74 | } 75 | 76 | static inline float 77 | clip(float x, float clip) 78 | { 79 | float y = x; 80 | if(x > clip) 81 | y = clip; 82 | else if(x < -clip) 83 | y = -clip; 84 | return y; 85 | } 86 | 87 | // Slicer Functions 88 | static inline unsigned int 89 | binary_slicer(float x) 90 | { 91 | if(x >= 0) 92 | return 1; 93 | else 94 | return 0; 95 | } 96 | 97 | static inline unsigned int 98 | quad_45deg_slicer(float r, float i) 99 | { 100 | unsigned int ret = 0; 101 | if((r >= 0) && (i >= 0)) 102 | ret = 0; 103 | else if((r < 0) && (i >= 0)) 104 | ret = 1; 105 | else if((r < 0) && (i < 0)) 106 | ret = 2; 107 | else 108 | ret = 3; 109 | return ret; 110 | } 111 | 112 | static inline unsigned int 113 | quad_0deg_slicer(float r, float i) 114 | { 115 | unsigned int ret = 0; 116 | if(fabsf(r) > fabsf(i)) { 117 | if(r > 0) 118 | ret = 0; 119 | else 120 | ret = 2; 121 | } 122 | else { 123 | if(i > 0) 124 | ret = 1; 125 | else 126 | ret = 3; 127 | } 128 | 129 | return ret; 130 | } 131 | 132 | static inline unsigned int 133 | quad_45deg_slicer(gr_complex x) 134 | { 135 | return quad_45deg_slicer(x.real(), x.imag()); 136 | } 137 | 138 | static inline unsigned int 139 | quad_0deg_slicer(gr_complex x) 140 | { 141 | return quad_0deg_slicer(x.real(), x.imag()); 142 | } 143 | 144 | // Branchless Slicer Functions 145 | static inline unsigned int 146 | branchless_binary_slicer(float x) 147 | { 148 | return (x >= 0); 149 | } 150 | 151 | static inline unsigned int 152 | branchless_quad_0deg_slicer(float r, float i) 153 | { 154 | unsigned int ret = 0; 155 | ret = (fabsf(r) > fabsf(i)) * (((r < 0) << 0x1)); // either 0 (00) or 2 (10) 156 | ret |= (fabsf(i) > fabsf(r)) * (((i < 0) << 0x1) | 0x1); // either 1 (01) or 3 (11) 157 | 158 | return ret; 159 | } 160 | 161 | static inline unsigned int 162 | branchless_quad_0deg_slicer(gr_complex x) 163 | { 164 | return branchless_quad_0deg_slicer(x.real(), x.imag()); 165 | } 166 | 167 | static inline unsigned int 168 | branchless_quad_45deg_slicer(float r, float i) 169 | { 170 | char ret = (r <= 0); 171 | ret |= ((i <= 0) << 1); 172 | return (ret ^ ((ret & 0x2) >> 0x1)); 173 | } 174 | 175 | static inline unsigned int 176 | branchless_quad_45deg_slicer(gr_complex x) 177 | { 178 | return branchless_quad_45deg_slicer(x.real(), x.imag()); 179 | } 180 | 181 | /*! 182 | * \param x any value 183 | * \param pow2 must be a power of 2 184 | * \returns \p x rounded down to a multiple of \p pow2. 185 | */ 186 | static inline size_t 187 | p2_round_down(size_t x, size_t pow2) 188 | { 189 | return x & -pow2; 190 | } 191 | 192 | /*! 193 | * \param x any value 194 | * \param pow2 must be a power of 2 195 | * \returns \p x rounded up to a multiple of \p pow2. 196 | */ 197 | static inline size_t 198 | p2_round_up(size_t x, size_t pow2) 199 | { 200 | return p2_round_down(x + pow2 - 1, pow2); 201 | } 202 | 203 | /*! 204 | * \param x any value 205 | * \param pow2 must be a power of 2 206 | * \returns \p x modulo \p pow2. 207 | */ 208 | static inline size_t 209 | p2_modulo(size_t x, size_t pow2) 210 | { 211 | return x & (pow2 - 1); 212 | } 213 | 214 | /*! 215 | * \param x any value 216 | * \param pow2 must be a power of 2 217 | * \returns \p pow2 - (\p x modulo \p pow2). 218 | */ 219 | static inline size_t 220 | p2_modulo_neg(size_t x, size_t pow2) 221 | { 222 | return pow2 - p2_modulo(x, pow2); 223 | } 224 | 225 | } /* namespace gr */ 226 | 227 | #endif /* _GR_MATH_H_ */ 228 | -------------------------------------------------------------------------------- /gr_pll/gr/pll_freqdet_cf_impl.cc: -------------------------------------------------------------------------------- 1 | /* -*- c++ -*- */ 2 | /* 3 | * Copyright 2004,2010,2011 Free Software Foundation, Inc. 4 | * 5 | * This file is part of GNU Radio 6 | * 7 | * GNU Radio is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 3, or (at your option) 10 | * any later version. 11 | * 12 | * GNU Radio is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with GNU Radio; see the file COPYING. If not, write to 19 | * the Free Software Foundation, Inc., 51 Franklin Street, 20 | * Boston, MA 02110-1301, USA. 21 | */ 22 | 23 | #ifdef HAVE_CONFIG_H 24 | #include "config.h" 25 | #endif 26 | 27 | #include "pll_freqdet_cf_impl.h" 28 | //#include 29 | #include 30 | #include 31 | #include 32 | 33 | namespace gr { 34 | namespace analog { 35 | 36 | #ifndef M_TWOPI 37 | #define M_TWOPI (2.0f*M_PI) 38 | #endif 39 | 40 | //pll_freqdet_cf::sptr 41 | pll_freqdet_cf * 42 | pll_freqdet_cf::make(float loop_bw, float max_freq, float min_freq) 43 | { 44 | //return gnuradio::get_initial_sptr 45 | //return pll_freqdet_cf::sptr 46 | return 47 | (new pll_freqdet_cf_impl(loop_bw, max_freq, min_freq)); 48 | } 49 | 50 | pll_freqdet_cf_impl::pll_freqdet_cf_impl(float loop_bw, float max_freq, float min_freq) 51 | : /*sync_block("pll_freqdet_cf", 52 | io_signature::make(1, 1, sizeof(gr_complex)), 53 | io_signature::make(1, 1, sizeof(float))),*/ 54 | blocks::control_loop(loop_bw, max_freq, min_freq) 55 | { 56 | } 57 | 58 | pll_freqdet_cf_impl::~pll_freqdet_cf_impl() 59 | { 60 | } 61 | 62 | float 63 | pll_freqdet_cf_impl::mod_2pi(float in) 64 | { 65 | if(in > M_PI) 66 | return in - M_TWOPI; 67 | else if(in < -M_PI) 68 | return in + M_TWOPI; 69 | else 70 | return in; 71 | } 72 | 73 | float 74 | pll_freqdet_cf_impl::phase_detector(gr_complex sample, float ref_phase) 75 | { 76 | float sample_phase; 77 | sample_phase = gr::fast_atan2f(sample.imag(), sample.real()); 78 | return mod_2pi(sample_phase - ref_phase); 79 | } 80 | 81 | int 82 | pll_freqdet_cf_impl::work(int noutput_items, 83 | gr_vector_const_void_star &input_items, 84 | gr_vector_void_star &output_items) 85 | { 86 | const gr_complex *iptr = (gr_complex*)input_items[0]; 87 | float *optr = (float*)output_items[0]; 88 | 89 | float error; 90 | int size = noutput_items; 91 | 92 | while(size-- > 0) { 93 | *optr++ = d_freq; 94 | 95 | error = phase_detector(*iptr++, d_phase); 96 | 97 | advance_loop(error); 98 | phase_wrap(); 99 | frequency_limit(); 100 | } 101 | return noutput_items; 102 | } 103 | 104 | int 105 | pll_freqdet_cf_impl::work2_cc(int noutput_items, 106 | gr_vector_const_void_star &input_items, 107 | gr_vector_void_star &output_items) 108 | { 109 | const gr_complex *iptr = (gr_complex*)input_items[0]; 110 | gr_complex *optr = (gr_complex*)output_items[0]; 111 | 112 | float error; 113 | int size = noutput_items; 114 | 115 | while(size-- > 0) { 116 | *optr++ = std::polar(1.0f, d_phase); 117 | 118 | error = phase_detector(*iptr++, d_phase); 119 | 120 | advance_loop(error); 121 | phase_wrap(); 122 | frequency_limit(); 123 | } 124 | return noutput_items; 125 | } 126 | 127 | void 128 | pll_freqdet_cf_impl::set_loop_bandwidth(float bw) 129 | { 130 | blocks::control_loop::set_loop_bandwidth(bw); 131 | } 132 | 133 | void 134 | pll_freqdet_cf_impl::set_damping_factor(float df) 135 | { 136 | blocks::control_loop::set_damping_factor(df); 137 | } 138 | 139 | void 140 | pll_freqdet_cf_impl::set_alpha(float alpha) 141 | { 142 | blocks::control_loop::set_alpha(alpha); 143 | } 144 | 145 | void 146 | pll_freqdet_cf_impl::set_beta(float beta) 147 | { 148 | blocks::control_loop::set_beta(beta); 149 | } 150 | 151 | void 152 | pll_freqdet_cf_impl::set_frequency(float freq) 153 | { 154 | blocks::control_loop::set_frequency(freq); 155 | } 156 | 157 | void 158 | pll_freqdet_cf_impl::set_phase(float phase) 159 | { 160 | blocks::control_loop::set_phase(phase); 161 | } 162 | 163 | void 164 | pll_freqdet_cf_impl::set_min_freq(float freq) 165 | { 166 | blocks::control_loop::set_min_freq(freq); 167 | } 168 | 169 | void 170 | pll_freqdet_cf_impl::set_max_freq(float freq) 171 | { 172 | blocks::control_loop::set_max_freq(freq); 173 | } 174 | 175 | 176 | float 177 | pll_freqdet_cf_impl::get_loop_bandwidth() const 178 | { 179 | return blocks::control_loop::get_loop_bandwidth(); 180 | } 181 | 182 | float 183 | pll_freqdet_cf_impl::get_damping_factor() const 184 | { 185 | return blocks::control_loop::get_damping_factor(); 186 | } 187 | 188 | float 189 | pll_freqdet_cf_impl::get_alpha() const 190 | { 191 | return blocks::control_loop::get_alpha(); 192 | } 193 | 194 | float 195 | pll_freqdet_cf_impl::get_beta() const 196 | { 197 | return blocks::control_loop::get_beta(); 198 | } 199 | 200 | float 201 | pll_freqdet_cf_impl::get_frequency() const 202 | { 203 | return blocks::control_loop::get_frequency(); 204 | } 205 | 206 | float 207 | pll_freqdet_cf_impl::get_phase() const 208 | { 209 | return blocks::control_loop::get_phase(); 210 | } 211 | 212 | float 213 | pll_freqdet_cf_impl::get_min_freq() const 214 | { 215 | return blocks::control_loop::get_min_freq(); 216 | } 217 | 218 | float 219 | pll_freqdet_cf_impl::get_max_freq() const 220 | { 221 | return blocks::control_loop::get_max_freq(); 222 | } 223 | 224 | } /* namespace analog */ 225 | } /* namespace gr */ 226 | -------------------------------------------------------------------------------- /gr_firdes/firdes.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language = c++ 2 | 3 | """ 4 | Finite Impulse Response (FIR) filter design functions. 5 | 6 | see http://gnuradio.org/doc/doxygen/classgr_1_1filter_1_1firdes.html 7 | """ 8 | 9 | from firdes cimport high_pass_2 as c_high_pass_2 10 | from firdes cimport low_pass_2 as c_low_pass_2 11 | from firdes cimport band_pass_2 as c_band_pass_2 12 | from firdes cimport band_reject_2 as c_band_reject_2 13 | from firdes cimport hilbert as c_hilbert 14 | import numpy as np 15 | 16 | 17 | cdef class WinType: 18 | WIN_NONE = -1 #: don't use a window 19 | WIN_HAMMING = 0 #: Hamming window; max attenuation 53 dB 20 | WIN_HANN = 1 #: Hann window; max attenuation 44 dB 21 | WIN_BLACKMAN = 2 #: Blackman window; max attenuation 74 dB 22 | WIN_RECTANGULAR = 3 #: Basic rectangular window 23 | WIN_KAISER = 4 #: Kaiser window; max attenuation a function of beta, google it 24 | WIN_BLACKMAN_hARRIS = 5 #: Blackman-harris window 25 | WIN_BLACKMAN_HARRIS = 5 #: alias to WIN_BLACKMAN_hARRIS for capitalization consistency 26 | WIN_BARTLETT = 6 #: Barlett (triangular) window 27 | WIN_FLATTOP = 7 #: flat top window; useful in FFTs 28 | 29 | 30 | def high_pass_2(gain, sampling_freq, cutoff_freq, transition_width, attenuation_dB, window = WinType.WIN_HAMMING, beta = 6.76): 31 | """ 32 | Use "window method" to design a high-pass FIR filter. The 33 | normalized width of the transition band and the required stop band 34 | attenuation is what sets the number of taps required. Narrow --> more 35 | taps More attenuation --> more taps. The window type determines 36 | maximum attentuation and passband ripple. 37 | 38 | :param gain overall gain of filter (typically 1.0) 39 | :param sampling_freq sampling freq (Hz) 40 | :param cutoff_freq beginning of transition band (Hz) 41 | :param transition_width width of transition band (Hz) 42 | :param attenuation_dB required stopband attenuation (dB) 43 | :param window one of firdes::win_type 44 | :param beta parameter for Kaiser window 45 | 46 | :return a numpy.array() of filter taps (aka h[x], aka impulse response) 47 | """ 48 | cdef vector[float] c_taps = c_high_pass_2(gain, sampling_freq, cutoff_freq, transition_width, attenuation_dB, window, beta) 49 | cdef char *c_str = &c_taps[0] 50 | cdef Py_ssize_t length = c_taps.size() * sizeof(float) 51 | np_taps = np.fromstring(c_str[0:length], dtype=np.float32) 52 | return np_taps 53 | 54 | 55 | def low_pass_2(gain, sampling_freq, cutoff_freq, transition_width, attenuation_dB, window = WinType.WIN_HAMMING, beta = 6.76): 56 | """ 57 | Use "window method" to design a low-pass FIR filter. The 58 | normalized width of the transition band and the required stop band 59 | attenuation is what sets the number of taps required. Narrow --> more 60 | taps More attenuation --> more taps. The window type determines 61 | maximum attentuation and passband ripple. 62 | 63 | :param gain overall gain of filter (typically 1.0) 64 | :param sampling_freq sampling freq (Hz) 65 | :param cutoff_freq beginning of transition band (Hz) 66 | :param transition_width width of transition band (Hz) 67 | :param attenuation_dB required stopband attenuation (dB) 68 | :param window one of firdes::win_type 69 | :param beta parameter for Kaiser window 70 | 71 | :return a numpy.array() of filter taps (aka h[x], aka impulse response) 72 | """ 73 | cdef vector[float] c_taps = c_low_pass_2(gain, sampling_freq, cutoff_freq, transition_width, attenuation_dB, window, beta) 74 | cdef char *c_str = &c_taps[0] 75 | cdef Py_ssize_t length = c_taps.size() * sizeof(float) 76 | np_taps = np.fromstring(c_str[0:length], dtype=np.float32) 77 | return np_taps 78 | 79 | 80 | def band_pass_2(gain, sampling_freq, low_cutoff_freq, high_cutoff_freq, transition_width, attenuation_dB, window = WinType.WIN_HAMMING, beta = 6.76): 81 | """ 82 | Use "window method" to design a band-pass FIR filter. The 83 | normalized width of the transition band and the required stop band 84 | attenuation is what sets the number of taps required. Narrow --> more 85 | taps More attenuation --> more taps. The window type determines 86 | maximum attentuation and passband ripple. 87 | 88 | :param gain overall gain of filter (typically 1.0) 89 | :param sampling_freq sampling freq (Hz) 90 | :param low_cutoff_freq center of transition band (Hz) 91 | :param high_cutoff_freq center of transition band (Hz) 92 | :param transition_width width of transition band (Hz) 93 | :param attenuation_dB required stopband attenuation (dB) 94 | :param window one of firdes::win_type 95 | :param beta parameter for Kaiser window 96 | 97 | :return a numpy.array() of filter taps (aka h[x], aka impulse response) 98 | """ 99 | cdef vector[float] c_taps = c_band_pass_2(gain, sampling_freq, low_cutoff_freq, high_cutoff_freq, transition_width, attenuation_dB, window, beta) 100 | cdef char *c_str = &c_taps[0] 101 | cdef Py_ssize_t length = c_taps.size() * sizeof(float) 102 | np_taps = np.fromstring(c_str[0:length], dtype=np.float32) 103 | return np_taps 104 | 105 | 106 | def band_reject_2(gain, sampling_freq, low_cutoff_freq, high_cutoff_freq, transition_width, attenuation_dB, window = WinType.WIN_HAMMING, beta = 6.76): 107 | """ 108 | Use "window method" to design a band-reject FIR filter. The 109 | normalized width of the transition band and the required stop band 110 | attenuation is what sets the number of taps required. Narrow --> more 111 | taps More attenuation --> more taps. The window type determines 112 | maximum attentuation and passband ripple. 113 | 114 | :param gain overall gain of filter (typically 1.0) 115 | :param sampling_freq sampling freq (Hz) 116 | :param low_cutoff_freq center of transition band (Hz) 117 | :param high_cutoff_freq center of transition band (Hz) 118 | :param transition_width width of transition band (Hz) 119 | :param attenuation_dB required stopband attenuation (dB) 120 | :param window one of firdes::win_type 121 | :param beta parameter for Kaiser window 122 | 123 | :return a numpy.array() of filter taps (aka h[x], aka impulse response) 124 | """ 125 | cdef vector[float] c_taps = c_band_reject_2(gain, sampling_freq, low_cutoff_freq, high_cutoff_freq, transition_width, attenuation_dB, window, beta) 126 | cdef char *c_str = &c_taps[0] 127 | cdef Py_ssize_t length = c_taps.size() * sizeof(float) 128 | np_taps = np.fromstring(c_str[0:length], dtype=np.float32) 129 | return np_taps 130 | 131 | 132 | def hilbert(ntaps, window = WinType.WIN_RECTANGULAR, beta = 6.76): 133 | """ 134 | design a Hilbert Transform Filter 135 | 136 | :param ntaps number of taps, must be odd 137 | :param window one kind of firdes::win_type 138 | :param beta parameter for Kaiser window 139 | 140 | :return a numpy.array() of filter taps (aka h[x], aka impulse response) 141 | """ 142 | cdef vector[float] c_taps = c_hilbert(ntaps, window, beta) 143 | cdef char *c_str = &c_taps[0] 144 | cdef Py_ssize_t length = c_taps.size() * sizeof(float) 145 | np_taps = np.fromstring(c_str[0:length], dtype=np.float32) 146 | return np_taps 147 | -------------------------------------------------------------------------------- /gr_pll/gr/fast_atan2f.cc: -------------------------------------------------------------------------------- 1 | /* -*- c++ -*- */ 2 | /* 3 | * Copyright 2005,2013 Free Software Foundation, Inc. 4 | * 5 | * This file is part of GNU Radio 6 | * 7 | * GNU Radio is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 3, or (at your option) 10 | * any later version. 11 | * 12 | * GNU Radio is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with GNU Radio; see the file COPYING. If not, write to 19 | * the Free Software Foundation, Inc., 51 Franklin Street, 20 | * Boston, MA 02110-1301, USA. 21 | */ 22 | 23 | #include // declaration is in here 24 | #include 25 | 26 | namespace gr { 27 | 28 | /***************************************************************************/ 29 | /* Constant definitions */ 30 | /***************************************************************************/ 31 | 32 | #define TAN_MAP_RES 0.003921569 /* (smallest non-zero value in table) */ 33 | #define RAD_PER_DEG 0.017453293 34 | #define TAN_MAP_SIZE 255 35 | 36 | /* arctangents from 0 to pi/4 radians */ 37 | static float 38 | fast_atan_table[257] = { 39 | 0.000000e+00, 3.921549e-03, 7.842976e-03, 1.176416e-02, 40 | 1.568499e-02, 1.960533e-02, 2.352507e-02, 2.744409e-02, 41 | 3.136226e-02, 3.527947e-02, 3.919560e-02, 4.311053e-02, 42 | 4.702413e-02, 5.093629e-02, 5.484690e-02, 5.875582e-02, 43 | 6.266295e-02, 6.656816e-02, 7.047134e-02, 7.437238e-02, 44 | 7.827114e-02, 8.216752e-02, 8.606141e-02, 8.995267e-02, 45 | 9.384121e-02, 9.772691e-02, 1.016096e-01, 1.054893e-01, 46 | 1.093658e-01, 1.132390e-01, 1.171087e-01, 1.209750e-01, 47 | 1.248376e-01, 1.286965e-01, 1.325515e-01, 1.364026e-01, 48 | 1.402496e-01, 1.440924e-01, 1.479310e-01, 1.517652e-01, 49 | 1.555948e-01, 1.594199e-01, 1.632403e-01, 1.670559e-01, 50 | 1.708665e-01, 1.746722e-01, 1.784728e-01, 1.822681e-01, 51 | 1.860582e-01, 1.898428e-01, 1.936220e-01, 1.973956e-01, 52 | 2.011634e-01, 2.049255e-01, 2.086818e-01, 2.124320e-01, 53 | 2.161762e-01, 2.199143e-01, 2.236461e-01, 2.273716e-01, 54 | 2.310907e-01, 2.348033e-01, 2.385093e-01, 2.422086e-01, 55 | 2.459012e-01, 2.495869e-01, 2.532658e-01, 2.569376e-01, 56 | 2.606024e-01, 2.642600e-01, 2.679104e-01, 2.715535e-01, 57 | 2.751892e-01, 2.788175e-01, 2.824383e-01, 2.860514e-01, 58 | 2.896569e-01, 2.932547e-01, 2.968447e-01, 3.004268e-01, 59 | 3.040009e-01, 3.075671e-01, 3.111252e-01, 3.146752e-01, 60 | 3.182170e-01, 3.217506e-01, 3.252758e-01, 3.287927e-01, 61 | 3.323012e-01, 3.358012e-01, 3.392926e-01, 3.427755e-01, 62 | 3.462497e-01, 3.497153e-01, 3.531721e-01, 3.566201e-01, 63 | 3.600593e-01, 3.634896e-01, 3.669110e-01, 3.703234e-01, 64 | 3.737268e-01, 3.771211e-01, 3.805064e-01, 3.838825e-01, 65 | 3.872494e-01, 3.906070e-01, 3.939555e-01, 3.972946e-01, 66 | 4.006244e-01, 4.039448e-01, 4.072558e-01, 4.105574e-01, 67 | 4.138496e-01, 4.171322e-01, 4.204054e-01, 4.236689e-01, 68 | 4.269229e-01, 4.301673e-01, 4.334021e-01, 4.366272e-01, 69 | 4.398426e-01, 4.430483e-01, 4.462443e-01, 4.494306e-01, 70 | 4.526070e-01, 4.557738e-01, 4.589307e-01, 4.620778e-01, 71 | 4.652150e-01, 4.683424e-01, 4.714600e-01, 4.745676e-01, 72 | 4.776654e-01, 4.807532e-01, 4.838312e-01, 4.868992e-01, 73 | 4.899573e-01, 4.930055e-01, 4.960437e-01, 4.990719e-01, 74 | 5.020902e-01, 5.050985e-01, 5.080968e-01, 5.110852e-01, 75 | 5.140636e-01, 5.170320e-01, 5.199904e-01, 5.229388e-01, 76 | 5.258772e-01, 5.288056e-01, 5.317241e-01, 5.346325e-01, 77 | 5.375310e-01, 5.404195e-01, 5.432980e-01, 5.461666e-01, 78 | 5.490251e-01, 5.518738e-01, 5.547124e-01, 5.575411e-01, 79 | 5.603599e-01, 5.631687e-01, 5.659676e-01, 5.687566e-01, 80 | 5.715357e-01, 5.743048e-01, 5.770641e-01, 5.798135e-01, 81 | 5.825531e-01, 5.852828e-01, 5.880026e-01, 5.907126e-01, 82 | 5.934128e-01, 5.961032e-01, 5.987839e-01, 6.014547e-01, 83 | 6.041158e-01, 6.067672e-01, 6.094088e-01, 6.120407e-01, 84 | 6.146630e-01, 6.172755e-01, 6.198784e-01, 6.224717e-01, 85 | 6.250554e-01, 6.276294e-01, 6.301939e-01, 6.327488e-01, 86 | 6.352942e-01, 6.378301e-01, 6.403565e-01, 6.428734e-01, 87 | 6.453808e-01, 6.478788e-01, 6.503674e-01, 6.528466e-01, 88 | 6.553165e-01, 6.577770e-01, 6.602282e-01, 6.626701e-01, 89 | 6.651027e-01, 6.675261e-01, 6.699402e-01, 6.723452e-01, 90 | 6.747409e-01, 6.771276e-01, 6.795051e-01, 6.818735e-01, 91 | 6.842328e-01, 6.865831e-01, 6.889244e-01, 6.912567e-01, 92 | 6.935800e-01, 6.958943e-01, 6.981998e-01, 7.004964e-01, 93 | 7.027841e-01, 7.050630e-01, 7.073330e-01, 7.095943e-01, 94 | 7.118469e-01, 7.140907e-01, 7.163258e-01, 7.185523e-01, 95 | 7.207701e-01, 7.229794e-01, 7.251800e-01, 7.273721e-01, 96 | 7.295557e-01, 7.317307e-01, 7.338974e-01, 7.360555e-01, 97 | 7.382053e-01, 7.403467e-01, 7.424797e-01, 7.446045e-01, 98 | 7.467209e-01, 7.488291e-01, 7.509291e-01, 7.530208e-01, 99 | 7.551044e-01, 7.571798e-01, 7.592472e-01, 7.613064e-01, 100 | 7.633576e-01, 7.654008e-01, 7.674360e-01, 7.694633e-01, 101 | 7.714826e-01, 7.734940e-01, 7.754975e-01, 7.774932e-01, 102 | 7.794811e-01, 7.814612e-01, 7.834335e-01, 7.853982e-01, 103 | 7.853982e-01 104 | }; 105 | 106 | 107 | /***************************************************************************** 108 | Function: Arc tangent 109 | 110 | Syntax: angle = fast_atan2(y, x); 111 | float y y component of input vector 112 | float x x component of input vector 113 | float angle angle of vector (x, y) in radians 114 | 115 | Description: This function calculates the angle of the vector (x,y) 116 | based on a table lookup and linear interpolation. The table uses a 117 | 256 point table covering -45 to +45 degrees and uses symetry to 118 | determine the final angle value in the range of -180 to 180 119 | degrees. Note that this function uses the small angle approximation 120 | for values close to zero. This routine calculates the arc tangent 121 | with an average error of +/- 3.56e-5 degrees (6.21e-7 radians). 122 | *****************************************************************************/ 123 | 124 | float 125 | fast_atan2f(float y, float x) 126 | { 127 | float x_abs, y_abs, z; 128 | float alpha, angle, base_angle; 129 | int index; 130 | 131 | /* normalize to +/- 45 degree range */ 132 | y_abs = fabsf(y); 133 | x_abs = fabsf(x); 134 | /* don't divide by zero! */ 135 | if(!((y_abs > 0.0f) || (x_abs > 0.0f))) 136 | return 0.0; 137 | 138 | if(y_abs < x_abs) 139 | z = y_abs / x_abs; 140 | else 141 | z = x_abs / y_abs; 142 | 143 | /* when ratio approaches the table resolution, the angle is */ 144 | /* best approximated with the argument itself... */ 145 | if(z < TAN_MAP_RES) 146 | base_angle = z; 147 | else { 148 | /* find index and interpolation value */ 149 | alpha = z * (float)TAN_MAP_SIZE; 150 | index = ((int)alpha) & 0xff; 151 | alpha -= (float)index; 152 | /* determine base angle based on quadrant and */ 153 | /* add or subtract table value from base angle based on quadrant */ 154 | base_angle = fast_atan_table[index]; 155 | base_angle += (fast_atan_table[index + 1] - fast_atan_table[index]) * alpha; 156 | } 157 | 158 | if(x_abs > y_abs) { /* -45 -> 45 or 135 -> 225 */ 159 | if(x >= 0.0) { /* -45 -> 45 */ 160 | if(y >= 0.0) 161 | angle = base_angle; /* 0 -> 45, angle OK */ 162 | else 163 | angle = -base_angle; /* -45 -> 0, angle = -angle */ 164 | } 165 | else { /* 135 -> 180 or 180 -> -135 */ 166 | angle = 3.14159265358979323846; 167 | if(y >= 0.0) 168 | angle -= base_angle; /* 135 -> 180, angle = 180 - angle */ 169 | else 170 | angle = base_angle - angle; /* 180 -> -135, angle = angle - 180 */ 171 | } 172 | } 173 | else { /* 45 -> 135 or -135 -> -45 */ 174 | if(y >= 0.0) { /* 45 -> 135 */ 175 | angle = 1.57079632679489661923; 176 | if(x >= 0.0) 177 | angle -= base_angle; /* 45 -> 90, angle = 90 - angle */ 178 | else 179 | angle += base_angle; /* 90 -> 135, angle = 90 + angle */ 180 | } 181 | else { /* -135 -> -45 */ 182 | angle = -1.57079632679489661923; 183 | if(x >= 0.0) 184 | angle += base_angle; /* -90 -> -45, angle = -90 + angle */ 185 | else 186 | angle -= base_angle; /* -135 -> -90, angle = -90 - angle */ 187 | } 188 | } 189 | 190 | #ifdef ZERO_TO_TWOPI 191 | if (angle < 0) 192 | return (angle + TWOPI); 193 | else 194 | return (angle); 195 | #else 196 | return (angle); 197 | #endif 198 | } 199 | 200 | } /* namespace gr */ 201 | -------------------------------------------------------------------------------- /gr_firdes/gr/window.cc: -------------------------------------------------------------------------------- 1 | /* -*- c++ -*- */ 2 | /* 3 | * Copyright 2002,2007,2008,2012,2013 Free Software Foundation, Inc. 4 | * 5 | * This file is part of GNU Radio 6 | * 7 | * GNU Radio is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 3, or (at your option) 10 | * any later version. 11 | * 12 | * GNU Radio is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with GNU Radio; see the file COPYING. If not, write to 19 | * the Free Software Foundation, Inc., 51 Franklin Street, 20 | * Boston, MA 02110-1301, USA. 21 | */ 22 | 23 | #ifdef HAVE_CONFIG_H 24 | #include 25 | #endif 26 | 27 | #include 28 | #include 29 | 30 | namespace gr { 31 | namespace fft { 32 | 33 | #define IzeroEPSILON 1E-21 /* Max error acceptable in Izero */ 34 | 35 | static double Izero(double x) 36 | { 37 | double sum, u, halfx, temp; 38 | int n; 39 | 40 | sum = u = n = 1; 41 | halfx = x/2.0; 42 | do { 43 | temp = halfx/(double)n; 44 | n += 1; 45 | temp *= temp; 46 | u *= temp; 47 | sum += u; 48 | } while (u >= IzeroEPSILON*sum); 49 | return(sum); 50 | } 51 | 52 | double midn(int ntaps) 53 | { 54 | return ntaps/2.0; 55 | } 56 | 57 | double midm1(int ntaps) 58 | { 59 | return (ntaps - 1.0)/2.0; 60 | } 61 | 62 | double midp1(int ntaps) 63 | { 64 | return (ntaps + 1.0)/2.0; 65 | } 66 | 67 | double freq(int ntaps) 68 | { 69 | return 2.0*M_PI/ntaps; 70 | } 71 | 72 | double rate(int ntaps) 73 | { 74 | return 1.0/(ntaps >> 1); 75 | } 76 | 77 | double 78 | window::max_attenuation(win_type type, double beta) 79 | { 80 | switch(type) { 81 | case(WIN_HAMMING): return 53; break; 82 | case(WIN_HANN): return 44; break; 83 | case(WIN_BLACKMAN): return 74; break; 84 | case(WIN_RECTANGULAR): return 21; break; 85 | case(WIN_KAISER): return (beta/0.1102 + 8.7); break; 86 | case(WIN_BLACKMAN_hARRIS): return 92; break; 87 | case(WIN_BARTLETT): return 27; break; 88 | case(WIN_FLATTOP): return 93; break; 89 | default: 90 | throw std::out_of_range("window::max_attenuation: unknown window type provided."); 91 | } 92 | } 93 | 94 | std::vector 95 | window::coswindow(int ntaps, float c0, float c1, float c2) 96 | { 97 | std::vector taps(ntaps); 98 | float M = static_cast(ntaps - 1); 99 | 100 | for(int n = 0; n < ntaps; n++) 101 | taps[n] = c0 - c1*cosf((2.0f*M_PI*n)/M) + c2*cosf((4.0f*M_PI*n)/M); 102 | return taps; 103 | } 104 | 105 | std::vector 106 | window::coswindow(int ntaps, float c0, float c1, float c2, float c3) 107 | { 108 | std::vector taps(ntaps); 109 | float M = static_cast(ntaps - 1); 110 | 111 | for(int n = 0; n < ntaps; n++) 112 | taps[n] = c0 - c1*cosf((2.0f*M_PI*n)/M) + c2*cosf((4.0f*M_PI*n)/M) \ 113 | - c3*cosf((6.0f*M_PI*n)/M); 114 | return taps; 115 | } 116 | 117 | std::vector 118 | window::coswindow(int ntaps, float c0, float c1, float c2, float c3, float c4) 119 | { 120 | std::vector taps(ntaps); 121 | float M = static_cast(ntaps - 1); 122 | 123 | for(int n = 0; n < ntaps; n++) 124 | taps[n] = c0 - c1*cosf((2.0f*M_PI*n)/M) + c2*cosf((4.0f*M_PI*n)/M) \ 125 | - c3*cosf((6.0f*M_PI*n)/M) + c4*cosf((8.0f*M_PI*n)/M); 126 | return taps; 127 | } 128 | 129 | std::vector 130 | window::rectangular(int ntaps) 131 | { 132 | std::vector taps(ntaps); 133 | for(int n = 0; n < ntaps; n++) 134 | taps[n] = 1; 135 | return taps; 136 | } 137 | 138 | std::vector 139 | window::hamming(int ntaps) 140 | { 141 | std::vector taps(ntaps); 142 | float M = static_cast(ntaps - 1); 143 | 144 | for(int n = 0; n < ntaps; n++) 145 | taps[n] = 0.54 - 0.46 * cos((2 * M_PI * n) / M); 146 | return taps; 147 | } 148 | 149 | std::vector 150 | window::hann(int ntaps) 151 | { 152 | std::vector taps(ntaps); 153 | float M = static_cast(ntaps - 1); 154 | 155 | for(int n = 0; n < ntaps; n++) 156 | taps[n] = 0.5 - 0.5 * cos((2 * M_PI * n) / M); 157 | return taps; 158 | } 159 | 160 | std::vector 161 | window::hanning(int ntaps) 162 | { 163 | return hann(ntaps); 164 | } 165 | 166 | std::vector 167 | window::blackman(int ntaps) 168 | { 169 | return coswindow(ntaps, 0.42, 0.5, 0.08); 170 | } 171 | 172 | std::vector 173 | window::blackman2(int ntaps) 174 | { 175 | return coswindow(ntaps, 0.34401, 0.49755, 0.15844); 176 | } 177 | 178 | std::vector 179 | window::blackman3(int ntaps) 180 | { 181 | return coswindow(ntaps, 0.21747, 0.45325, 0.28256, 0.04672); 182 | } 183 | 184 | std::vector 185 | window::blackman4(int ntaps) 186 | { 187 | return coswindow(ntaps, 0.084037, 0.29145, 0.375696, 0.20762, 0.041194); 188 | } 189 | 190 | std::vector 191 | window::blackman_harris(int ntaps, int atten) 192 | { 193 | switch(atten) { 194 | case(61): return coswindow(ntaps, 0.42323, 0.49755, 0.07922); 195 | case(67): return coswindow(ntaps, 0.44959, 0.49364, 0.05677); 196 | case(74): return coswindow(ntaps, 0.40271, 0.49703, 0.09392, 0.00183); 197 | case(92): return coswindow(ntaps, 0.35875, 0.48829, 0.14128, 0.01168); 198 | default: 199 | throw std::out_of_range("window::blackman_harris: unknown attenuation value (must be 61, 67, 74, or 92)"); 200 | } 201 | } 202 | 203 | std::vector 204 | window::blackmanharris(int ntaps, int atten) 205 | { 206 | return blackman_harris(ntaps, atten); 207 | } 208 | 209 | std::vector 210 | window::nuttal(int ntaps) 211 | { 212 | return nuttall(ntaps); 213 | } 214 | 215 | std::vector 216 | window::nuttall(int ntaps) 217 | { 218 | return coswindow(ntaps, 0.3635819, 0.4891775, 0.1365995, 0.0106411); 219 | } 220 | 221 | std::vector 222 | window::blackman_nuttal(int ntaps) 223 | { 224 | return nuttall(ntaps); 225 | } 226 | 227 | std::vector 228 | window::blackman_nuttall(int ntaps) 229 | { 230 | return nuttall(ntaps); 231 | } 232 | 233 | std::vector 234 | window::nuttal_cfd(int ntaps) 235 | { 236 | return nuttall_cfd(ntaps); 237 | } 238 | 239 | std::vector 240 | window::nuttall_cfd(int ntaps) 241 | { 242 | return coswindow(ntaps, 0.355768, 0.487396, 0.144232, 0.012604); 243 | } 244 | 245 | std::vector 246 | window::flattop(int ntaps) 247 | { 248 | double scale = 4.63867; 249 | return coswindow(ntaps, 1.0/scale, 1.93/scale, 1.29/scale, 0.388/scale, 0.028/scale); 250 | } 251 | 252 | std::vector 253 | window::kaiser(int ntaps, double beta) 254 | { 255 | if(beta < 0) 256 | throw std::out_of_range("window::kaiser: beta must be >= 0"); 257 | 258 | std::vector taps(ntaps); 259 | 260 | double IBeta = 1.0/Izero(beta); 261 | double inm1 = 1.0/((double)(ntaps-1)); 262 | double temp; 263 | 264 | for(int i = 0; i < ntaps; i++) { 265 | temp = 2*i*inm1 - 1; 266 | taps[i] = Izero(beta*sqrt(1.0-temp*temp)) * IBeta; 267 | } 268 | return taps; 269 | } 270 | 271 | std::vector 272 | window::bartlett(int ntaps) 273 | { 274 | std::vector taps(ntaps); 275 | float M = static_cast(ntaps - 1); 276 | 277 | for(int n = 0; n < ntaps/2; n++) 278 | taps[n] = 2*n/M; 279 | for(int n = ntaps/2; n < ntaps; n++) 280 | taps[n] = 2 - 2*n/M; 281 | 282 | return taps; 283 | } 284 | 285 | std::vector 286 | window::welch(int ntaps) 287 | { 288 | std::vector taps(ntaps); 289 | double m1 = midm1(ntaps); 290 | double p1 = midp1(ntaps); 291 | for(int i = 0; i < midn(ntaps)+1; i++) { 292 | taps[i] = 1.0 - pow((i - m1) / p1, 2); 293 | taps[ntaps-i-1] = taps[i]; 294 | } 295 | return taps; 296 | } 297 | 298 | std::vector 299 | window::parzen(int ntaps) 300 | { 301 | std::vector taps(ntaps); 302 | double m1 = midm1(ntaps); 303 | double m = midn(ntaps); 304 | int i; 305 | for(i = ntaps/4; i < 3*ntaps/4; i++) { 306 | taps[i] = 1.0 - 6.0*pow((i-m1)/m, 2.0) * (1.0 - fabs(i-m1)/m); 307 | } 308 | for(; i < ntaps; i++) { 309 | taps[i] = 2.0 * pow(1.0 - fabs(i-m1)/m, 3.0); 310 | taps[ntaps-i-1] = taps[i]; 311 | } 312 | return taps; 313 | } 314 | 315 | std::vector 316 | window::exponential(int ntaps, double d) 317 | { 318 | if(d < 0) 319 | throw std::out_of_range("window::exponential: d must be >= 0"); 320 | 321 | double m1 = midm1(ntaps); 322 | double p = 1.0 / (8.69*ntaps/(2.0*d)); 323 | std::vector taps(ntaps); 324 | for(int i = 0; i < midn(ntaps)+1; i++) { 325 | taps[i] = exp(-fabs(i - m1)*p); 326 | taps[ntaps-i-1] = taps[i]; 327 | } 328 | return taps; 329 | } 330 | 331 | std::vector 332 | window::riemann(int ntaps) 333 | { 334 | double cx; 335 | double sr1 = freq(ntaps); 336 | double mid = midn(ntaps); 337 | std::vector taps(ntaps); 338 | for(int i = 0; i < mid; i++) { 339 | if(i == midn(ntaps)) { 340 | taps[i] = 1.0; 341 | taps[ntaps-i-1] = 1.0; 342 | } 343 | else { 344 | cx = sr1*(i - mid); 345 | taps[i] = sin(cx)/cx; 346 | taps[ntaps-i-1] = sin(cx)/cx; 347 | } 348 | } 349 | return taps; 350 | } 351 | 352 | std::vector 353 | window::build(win_type type, int ntaps, double beta) 354 | { 355 | switch (type) { 356 | case WIN_RECTANGULAR: return rectangular(ntaps); 357 | case WIN_HAMMING: return hamming(ntaps); 358 | case WIN_HANN: return hann(ntaps); 359 | case WIN_BLACKMAN: return blackman(ntaps); 360 | case WIN_BLACKMAN_hARRIS: return blackman_harris(ntaps); 361 | case WIN_KAISER: return kaiser(ntaps, beta); 362 | case WIN_BARTLETT: return bartlett(ntaps); 363 | case WIN_FLATTOP: return flattop(ntaps); 364 | default: 365 | throw std::out_of_range("window::build: type out of range"); 366 | } 367 | } 368 | 369 | } /* namespace fft */ 370 | } /* namespace gr */ 371 | -------------------------------------------------------------------------------- /gr_firdes/gr/include/gnuradio/fft/window.h: -------------------------------------------------------------------------------- 1 | /* -*- c++ -*- */ 2 | /* 3 | * Copyright 2002,2007,2008,2012,2013 Free Software Foundation, Inc. 4 | * 5 | * This file is part of GNU Radio 6 | * 7 | * GNU Radio is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 3, or (at your option) 10 | * any later version. 11 | * 12 | * GNU Radio is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with GNU Radio; see the file COPYING. If not, write to 19 | * the Free Software Foundation, Inc., 51 Franklin Street, 20 | * Boston, MA 02110-1301, USA. 21 | */ 22 | 23 | #ifndef INCLUDED_FFT_WINDOW_H 24 | #define INCLUDED_FFT_WINDOW_H 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | namespace gr { 32 | namespace fft { 33 | 34 | class FFT_API window { 35 | public: 36 | 37 | enum win_type { 38 | WIN_HAMMING = 0, //!< Hamming window; max attenuation 53 dB 39 | WIN_HANN = 1, //!< Hann window; max attenuation 44 dB 40 | WIN_BLACKMAN = 2, //!< Blackman window; max attenuation 74 dB 41 | WIN_RECTANGULAR = 3, //!< Basic rectangular window; max attenuation 21 dB 42 | WIN_KAISER = 4, //!< Kaiser window; max attenuation see window::max_attenuation 43 | WIN_BLACKMAN_hARRIS = 5, //!< Blackman-harris window; max attenuation 92 dB 44 | WIN_BLACKMAN_HARRIS = 5, //!< alias to WIN_BLACKMAN_hARRIS for capitalization consistency 45 | WIN_BARTLETT = 6, //!< Barlett (triangular) window; max attenuation 26 dB 46 | WIN_FLATTOP = 7, //!< flat top window; useful in FFTs; max attenuation 93 dB 47 | }; 48 | 49 | /*! 50 | * \brief Given a window::win_type, this tells you the maximum 51 | * attenuation you can expect. 52 | * 53 | * \details 54 | * For most windows, this is a set value. For the Kaiser window, 55 | * the attenuation is based on the value of beta. The actual 56 | * relationship is a piece-wise exponential relationship to 57 | * calculate beta from the desired attenuation and can be found 58 | * on page 542 of Oppenheim and Schafer (Discrete-Time Signal 59 | * Processing, 3rd edition). To simplify this function to solve 60 | * for A given beta, we use a linear form that is exact for 61 | * attenuation >= 50 dB. 62 | * 63 | * For an attenuation of 50 dB, beta = 4.55. 64 | * 65 | * For an attenuation of 70 dB, beta = 6.76. 66 | * 67 | * \param type The window::win_type enumeration of the window type. 68 | * \param beta Beta value only used for the Kaiser window. 69 | */ 70 | static double max_attenuation(win_type type, double beta=6.76); 71 | 72 | /*! 73 | * \brief Helper function to build cosine-based windows. 3-coefficient version. 74 | */ 75 | static std::vector coswindow(int ntaps, float c0, float c1, float c2); 76 | 77 | /*! 78 | * \brief Helper function to build cosine-based windows. 4-coefficient version. 79 | */ 80 | static std::vector coswindow(int ntaps, float c0, float c1, float c2, float c3); 81 | 82 | /*! 83 | * \brief Helper function to build cosine-based windows. 5-coefficient version. 84 | */ 85 | static std::vector coswindow(int ntaps, float c0, float c1, float c2, float c3, float c4); 86 | 87 | /*! 88 | * \brief Build a rectangular window. 89 | * 90 | * Taps are flat across the window. 91 | * 92 | * \param ntaps Number of coefficients in the window. 93 | */ 94 | static std::vector rectangular(int ntaps); 95 | 96 | /*! 97 | * \brief Build a Hamming window. 98 | * 99 | * See: 100 | *
101 |        *   A. V. Oppenheim and R. W. Schafer, "Discrete-Time
102 |        *   Signal Processing," Upper Saddle River, N.J.: Prentice
103 |        *   Hall, 2010, pp. 535-538.
104 |        * 
105 | * 106 | * \param ntaps Number of coefficients in the window. 107 | */ 108 | static std::vector hamming(int ntaps); 109 | 110 | /*! 111 | * \brief Build a Hann window (sometimes known as Hanning). 112 | * 113 | * See: 114 | *
115 |        *   A. V. Oppenheim and R. W. Schafer, "Discrete-Time
116 |        *   Signal Processing," Upper Saddle River, N.J.: Prentice
117 |        *   Hall, 2010, pp. 535-538.
118 |        * 
119 | * 120 | * \param ntaps Number of coefficients in the window. 121 | */ 122 | static std::vector hann(int ntaps); 123 | 124 | /*! 125 | * \brief Alias to build a Hann window. 126 | * 127 | * \param ntaps Number of coefficients in the window. 128 | */ 129 | static std::vector hanning(int ntaps); 130 | 131 | /*! 132 | * \brief Build an exact Blackman window. 133 | * 134 | * See: 135 | *
136 |        *   A. V. Oppenheim and R. W. Schafer, "Discrete-Time
137 |        *   Signal Processing," Upper Saddle River, N.J.: Prentice
138 |        *   Hall, 2010, pp. 535-538.
139 |        * 
140 | * 141 | * \param ntaps Number of coefficients in the window. 142 | */ 143 | static std::vector blackman(int ntaps); 144 | 145 | /*! 146 | * \brief Build Blackman window, variation 1. 147 | */ 148 | static std::vector blackman2(int ntaps); 149 | 150 | /*! 151 | * \brief Build Blackman window, variation 2. 152 | */ 153 | static std::vector blackman3(int ntaps); 154 | 155 | /*! 156 | * \brief Build Blackman window, variation 3. 157 | */ 158 | static std::vector blackman4(int ntaps); 159 | 160 | /*! 161 | * \brief Build a Blackman-harris window with a given attenuation. 162 | * 163 | *
164 |        *     f. j. harris, "On the use of windows for harmonic analysis
165 |        *     with the discrete Fourier transforms," Proc. IEEE, Vol. 66,
166 |        *     ppg. 51-83, Jan. 1978.
167 |        * 
168 | * 169 | * \param ntaps Number of coefficients in the window. 170 | 171 | * \param atten Attenuation factor. Must be [61, 67, 74, 92]. 172 | * See the above paper for details. 173 | */ 174 | static std::vector blackman_harris(int ntaps, int atten=92); 175 | 176 | /*! 177 | * Alias to gr::fft::window::blakcman_harris. 178 | */ 179 | static std::vector blackmanharris(int ntaps, int atten=92); 180 | 181 | /*! 182 | * \brief Build a Nuttall (or Blackman-Nuttall) window. 183 | * 184 | * See: http://en.wikipedia.org/wiki/Window_function#Blackman.E2.80.93Nuttall_window 185 | * 186 | * \param ntaps Number of coefficients in the window. 187 | */ 188 | static std::vector nuttall(int ntaps); 189 | 190 | /*! 191 | * Deprecated: use nuttall window instead. 192 | */ 193 | static std::vector nuttal(int ntaps); 194 | 195 | /*! 196 | * \brief Alias to the Nuttall window. 197 | * 198 | * \param ntaps Number of coefficients in the window. 199 | */ 200 | static std::vector blackman_nuttall(int ntaps); 201 | 202 | /*! 203 | * Deprecated: use blackman_nuttall window instead. 204 | */ 205 | static std::vector blackman_nuttal(int ntaps); 206 | 207 | /*! 208 | * \brief Build a Nuttall continuous first derivative window. 209 | * 210 | * See: http://en.wikipedia.org/wiki/Window_function#Nuttall_window.2C_continuous_first_derivative 211 | * 212 | * \param ntaps Number of coefficients in the window. 213 | */ 214 | static std::vector nuttall_cfd(int ntaps); 215 | 216 | /*! 217 | * Deprecated: use nuttall_cfd window instead. 218 | */ 219 | static std::vector nuttal_cfd(int ntaps); 220 | 221 | /*! 222 | * \brief Build a flat top window. 223 | * 224 | * See: http://en.wikipedia.org/wiki/Window_function#Flat_top_window 225 | * 226 | * \param ntaps Number of coefficients in the window. 227 | */ 228 | static std::vector flattop(int ntaps); 229 | 230 | /*! 231 | * \brief Build a Kaiser window with a given beta. 232 | * 233 | * See: 234 | *
235 |        *     A. V. Oppenheim and R. W. Schafer, "Discrete-Time
236 |        *     Signal Processing," Upper Saddle River, N.J.: Prentice
237 |        *     Hall, 2010, pp. 541-545.
238 |        * 
239 | * 240 | * \param ntaps Number of coefficients in the window. 241 | * \param beta Shaping parameter of the window. See the 242 | * discussion in Oppenheim and Schafer. 243 | */ 244 | static std::vector kaiser(int ntaps, double beta); 245 | 246 | /*! 247 | * \brief Build a Barlett (triangular) window. 248 | * 249 | * See: 250 | *
251 |        *   A. V. Oppenheim and R. W. Schafer, "Discrete-Time
252 |        *   Signal Processing," Upper Saddle River, N.J.: Prentice
253 |        *   Hall, 2010, pp. 535-538.
254 |        * 
255 | * 256 | * \param ntaps Number of coefficients in the window. 257 | */ 258 | static std::vector bartlett(int ntaps); 259 | 260 | static std::vector welch(int ntaps); 261 | 262 | /*! 263 | * \brief Build a Parzen (or de la Valle-Poussin) window. 264 | * 265 | * See: 266 | *
267 |        *   A. D. Poularikas, "Handbook of Formulas and Tables for
268 |        *   Signal Processing," Springer, Oct 28, 1998
269 |        * 
270 | * 271 | * \param ntaps Number of coefficients in the window. 272 | */ 273 | static std::vector parzen(int ntaps); 274 | 275 | /*! 276 | * \brief Build an exponential window with a given decay. 277 | * 278 | * See: http://en.wikipedia.org/wiki/Window_function#Exponential_or_Poisson_window 279 | * 280 | * \param ntaps Number of coefficients in the window. 281 | * \param d Decay of \p d dB over half the window length. 282 | */ 283 | static std::vector exponential(int ntaps, double d); 284 | 285 | /*! 286 | * \brief Build a Riemann window. 287 | * 288 | * See: 289 | *
290 |        *   A. D. Poularikas, "Handbook of Formulas and Tables for
291 |        *   Signal Processing," Springer, Oct 28, 1998
292 |        * 
293 | * 294 | * \param ntaps Number of coefficients in the window. 295 | */ 296 | static std::vector riemann(int ntaps); 297 | 298 | /*! 299 | * \brief Build a window using gr::fft::win_type to index the 300 | * type of window desired. 301 | * 302 | * \param type a gr::fft::win_type index for the type of window. 303 | * \param ntaps Number of coefficients in the window. 304 | * \param beta Used only for building Kaiser windows. 305 | */ 306 | static std::vector build(win_type type, int ntaps, double beta); 307 | }; 308 | 309 | } /* namespace fft */ 310 | } /* namespace gr */ 311 | 312 | #endif /* INCLUDED_FFT_WINDOW_H */ 313 | -------------------------------------------------------------------------------- /hsh_signal/heartseries.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import numpy as np 3 | from bisect import bisect_left 4 | import pickle 5 | 6 | from .signal import slices, cross_corr 7 | from .iter import pairwise 8 | 9 | 10 | class Series(object): 11 | def __init__(self, samples, fps, lpad=0): 12 | self.x = np.array(samples) 13 | self.t = (np.arange(len(self.x)) - lpad) / float(fps) 14 | self.fps = fps 15 | self.lpad = lpad 16 | 17 | def len_time(self): 18 | """for padded signal, returns the original unpadded length""" 19 | return float(len(self.x)-1 - 2*self.lpad) / float(self.fps) 20 | 21 | def pad(self, amount=None): 22 | """zero-pad left and right to 3x the length. Padding keeps self.t same in the signal range""" 23 | amount = len(self.x) if amount is None else int(amount) 24 | return Series(np.pad(self.x, ((amount, amount),), mode='constant'), self.fps, lpad=amount) 25 | 26 | def copy(self): 27 | return Series(self.x, self.fps, self.lpad) 28 | 29 | def upsample(self, ratio=10): 30 | ratio, fps = int(ratio), self.fps 31 | xu, iu = np.zeros(len(self.x)*ratio), np.arange(0, len(self.x)*ratio, ratio) 32 | for i in range(ratio): 33 | xu[iu+i] = np.array(self.x) 34 | import matplotlib.pyplot as plt 35 | #plt.plot(xu) 36 | from hsh_signal.signal import lowpass_fft 37 | #print fps*ratio 38 | xu = lowpass_fft(xu, fps*ratio, cf=(fps/2.0), tw=(fps/8.0)) # * ratio 39 | #plt.plot(xu) 40 | #plt.show() 41 | return Series(xu, fps*ratio, self.lpad*ratio) 42 | 43 | def slice(self, s): 44 | return Series(self.x[s], self.fps, self.lpad) 45 | 46 | def add_beats(self, ibeats): 47 | return HeartSeries(self.x, ibeats, self.fps, self.lpad) 48 | 49 | @staticmethod 50 | def load(filename): 51 | with open(filename, 'rb') as fi: 52 | return pickle.load(fi) 53 | 54 | def dump(self, filename): 55 | with open(filename, 'wb') as fo: 56 | pickle.dump(self, fo, protocol=2) 57 | 58 | def plot(self, plotter=None, dt=0.0, **kwargs): 59 | import matplotlib.pyplot as plt 60 | 61 | plotter = plt if plotter is None else plotter 62 | plotter.plot(self.t - dt, self.x, **kwargs) 63 | 64 | def stem(self, plotter=None, dt=0.0): 65 | import matplotlib.pyplot as plt 66 | 67 | plotter = plt if plotter is None else plotter 68 | plotter.stem(self.t - dt, self.x) 69 | 70 | 71 | class HeartSeries(Series): 72 | def __init__(self, samples, ibeats, fps, lpad=0): 73 | # ibeats may still be float, despite the name 74 | super(HeartSeries, self).__init__(samples, fps, lpad=lpad) 75 | ibeats = np.array(ibeats).flatten() 76 | self.ibeats = np.array(ibeats) 77 | self.tbeats = (ibeats - lpad) / float(fps) 78 | 79 | def pad(self, amount=None): 80 | amount = len(self.x) if amount is None else int(amount) 81 | series = np.pad(self.x, ((amount, amount),), mode='constant') 82 | ibeats = self.ibeats + amount 83 | return HeartSeries(series, ibeats, self.fps, lpad=amount) 84 | 85 | def copy(self): 86 | return HeartSeries(self.x, self.ibeats, self.fps, self.lpad) 87 | 88 | def slice(self, s): 89 | want_idxs = np.arange(len(self.x))[s] 90 | if len(want_idxs) == 0: 91 | return HeartSeries(np.zeros(0), np.zeros(0, dtype=int), self.fps, self.lpad) 92 | iib = np.where((self.ibeats >= want_idxs[0]) & (self.ibeats <= want_idxs[-1]))[0] 93 | return HeartSeries(self.x[s], self.ibeats[iib] - want_idxs[0], self.fps, self.lpad) 94 | 95 | def upsample(self, ratio=10): 96 | ratio, fps = int(ratio), self.fps 97 | xu, iu = np.zeros(len(self.x) * ratio), np.arange(0, len(self.x) * ratio, ratio) 98 | #xu[iu] = np.array(self.x) 99 | for i in range(ratio): 100 | xu[iu+i] = np.array(self.x) 101 | from hsh_signal.signal import lowpass_fft 102 | #xu = lowpass_fft(xu, fps * ratio, cf=(fps * ratio / 2.0), tw=(fps * ratio / 16.0)) * ratio 103 | xu = lowpass_fft(xu, fps * ratio, cf=(fps / 2.0), tw=(fps / 8.0)) # * ratio 104 | return HeartSeries(xu, self.ibeats * ratio, fps * ratio, self.lpad * ratio) 105 | 106 | def beat_baseline(self): 107 | xu = np.zeros(len(self.x)) 108 | fps = self.fps 109 | ibeats = self.ibeats.astype(int) 110 | 111 | 112 | # where long periods without beats, we need support from raw average (~baseline) 113 | ibis = np.diff(self.tbeats) 114 | median_ibi = np.median(ibis) 115 | long_ibis = np.where(ibis > 1.5 * median_ibi)[0] 116 | print 'long_ibis', long_ibis 117 | ib_start = self.ibeats[long_ibis] 118 | ib_end = self.ibeats[np.clip(long_ibis+1, 0, len(self.ibeats))] 119 | if len(ib_end) < len(ib_start): ib_end = np.insert(ib_end, len(ib_end), len(self.x)-1) 120 | # iterate over long holes and fill them with fake ibis 121 | # to do: actually use raw average baseline, not some random value at a picked index 122 | extra_ibeats = [] 123 | for s,e in zip(ib_start, ib_end): 124 | print 's,e',s,e,self.t[s],self.t[e] 125 | extra_ibeats += list(np.arange(s + fps * median_ibi, e - 0.5 * median_ibi * fps, fps * median_ibi).astype(int)) 126 | 127 | print 'extra_ibeats',extra_ibeats, 'extra_tbeats',self.t[extra_ibeats] 128 | 129 | ibeats = np.array(sorted(list(ibeats) + list(extra_ibeats))) 130 | xu[ibeats] = self.x[ibeats] 131 | 132 | from scipy.interpolate import interp1d 133 | #xu = interp1d(ibeats, self.x[ibeats], kind='linear', bounds_error=False, fill_value='extrapolate')(np.arange(len(xu))) 134 | #for i, j in pairwise(ibeats): 135 | #xu[i:j] = self.x[i] 136 | # boundary constants 137 | xu[0:ibeats[0]] = xu[ibeats[0]] 138 | xu[ibeats[-1]:] = xu[ibeats[-1]] 139 | from hsh_signal.signal import lowpass_fft 140 | scaling = float(len(xu))/len(ibeats) 141 | xu = lowpass_fft(xu, fps, cf=0.1, tw=0.05) * scaling # * ratio 142 | # plt.plot(xu) 143 | # plt.show() 144 | return HeartSeries(xu, self.ibeats, self.fps, self.lpad) 145 | 146 | def shift(self, dt): 147 | """add a time shift in seconds, moving the signal to the left.""" 148 | self.lpad += dt * self.fps 149 | self.t -= dt 150 | self.tbeats -= dt 151 | 152 | def plot(self, plotter=None, dt=0.0, **kwargs): 153 | import matplotlib.pyplot as plt 154 | 155 | plotter = plt if plotter is None else plotter 156 | plotter.plot(self.t - dt, self.x, **kwargs) 157 | 158 | self.scatter(plotter, dt) 159 | 160 | def scatter(self, plotter=None, dt=0.0, tbeats2=None, **kwargs): 161 | import matplotlib.pyplot as plt 162 | 163 | plotter = plt if plotter is None else plotter 164 | tbeats2 = self.tbeats if tbeats2 is None else tbeats2 165 | 166 | plotter.scatter(tbeats2 - dt, self.yt(tbeats2), **kwargs) 167 | 168 | def yt(self, t): 169 | """interpolated y value at a time falling between samples.""" 170 | if isinstance(t, (list, np.ndarray)): 171 | return [self.yt(e) for e in t] 172 | 173 | # TODO: check if change for ppgbeatannot.py broke something else 174 | t += self.lpad / float(self.fps) 175 | 176 | ib = t * self.fps 177 | if ib < 1.0 or ib > len(self.x) - 2: 178 | return self.x[max(min(int(ib), len(self.x)-1), 0)] 179 | xs = [int(np.floor(ib)), int(np.ceil(ib))] 180 | if xs[0] == xs[1]: 181 | xs[1] += 1 # happens if ib is precise integer 182 | ys = self.x[xs] 183 | return np.interp([ib], xs, ys)[0] 184 | 185 | def closest_beat(self, t): 186 | """find the ib of the beat closest to t""" 187 | ib = bisect_left(self.tbeats, t) 188 | ibl, ibr = max(ib-1, 0), min(ib+1, len(self.tbeats)-1) 189 | if ib == len(self.tbeats): 190 | ib -= 1 191 | idxs = [ibl,ib,ibr] 192 | cand_times = self.tbeats[idxs] 193 | #print 'closest_beat(t=',t,') cand_times=', cand_times 194 | return idxs[np.argmin(np.abs(cand_times - t))] 195 | 196 | def t2i(self, t): 197 | return (t - self.t[0]) * self.fps 198 | 199 | def aligned_iibeats_repeat(self, ecg, ppg_dt=0.0): 200 | ppg_ibs, ecg_ibs = [], [] 201 | for ib, t in enumerate(self.tbeats): 202 | ecg_ib = ecg.closest_beat(t - ppg_dt) 203 | ppg_ibs.append(ib) 204 | ecg_ibs.append(ecg_ib) 205 | assert len(ppg_ibs) == len(ecg_ibs) 206 | return ppg_ibs, ecg_ibs 207 | 208 | def aligned_iibeats(self, ecg, ppg_dt=0.0): 209 | """return all beats that align to a reference ecg. ppg is delayed by ppg_dt""" 210 | ppg_ibs, ecg_ibs = [], [] 211 | ecg_used_beats = np.zeros(len(ecg.tbeats), dtype=np.bool) 212 | for ib, t in enumerate(self.tbeats): 213 | ecg_ib = ecg.closest_beat(t - ppg_dt) 214 | #print 'PPG beat t=', t, 'found ECG beat t=', ecg.tbeats[ecg_ib], 'ecg_used=', ecg_used_beats[ecg_ib] 215 | 216 | # align in reverse to check if we are using something early, that can be used better later 217 | tbetter = self.tbeats[self.closest_beat(ecg.tbeats[ecg_ib])] 218 | #if ib == 0: 219 | # print 't', t - ppg_dt, 'tbetter', tbetter - ppg_dt 220 | better_aln_exists = np.abs(ecg.tbeats[ecg_ib] - t + ppg_dt) > np.abs(tbetter - ppg_dt - ecg.tbeats[ecg_ib]) 221 | if ecg_used_beats[ecg_ib] or better_aln_exists and tbetter > t: 222 | continue 223 | delta_t = (t - ppg_dt) - ecg.tbeats[ecg_ib] 224 | if np.abs(delta_t) > 0.15: 225 | continue 226 | ecg_used_beats[ecg_ib] = True 227 | ppg_ibs.append(ib) 228 | ecg_ibs.append(ecg_ib) 229 | assert len(ppg_ibs) == len(ecg_ibs) 230 | return ppg_ibs, ecg_ibs 231 | 232 | def snr(self, mode='median'): 233 | return self.beat_snr(mode) 234 | 235 | def beat_snr(self, mode='median'): 236 | """ 237 | signal-to-noise ratio. 238 | mode='neighbors': correlation with neighboring beats (returns range (-inf, inf) dB) 239 | mode='median': correlation with median beat (returns range (-inf, 0) dB) -- nope. can be >0 dB 240 | 241 | generally, for bad signals, 'neighbors' is simply 2 dB less than 'median'. 242 | 243 | :return SNR in dB 244 | """ 245 | 246 | #ibeats = self.ibeats 247 | ibeats = self.ibeats[np.where(self.tbeats > 5.0)[0]] # cut off leading noisy stuff... 248 | 249 | if len(ibeats) < 2: 250 | return -20.0 # pretend bad SNR if not enough beats were found. 251 | 252 | mean_ibi = np.mean(np.diff(ibeats)) 253 | slicez = slices(self.x, ibeats.astype(int), hwin=int(mean_ibi/2)) 254 | 255 | if mode == 'neighbors': 256 | # actually this is log(corr) and slightly different from SNR. 257 | corrs = [cross_corr(a, b) for a, b in pairwise(slicez)] 258 | return 10.0 * np.log10(np.mean(corrs)) 259 | elif mode == 'median': 260 | # median value from each timepoint (not a single one of any of the beats) 261 | median_beat = np.median(np.array(slicez), axis=0) 262 | num = [np.sum(median_beat**2) for sl in slicez] 263 | denom = [np.sum((sl - median_beat)**2) for sl in slicez] 264 | sns = [(n/d if d != 0.0 else np.inf) for n,d in zip(num, denom)] 265 | return 10.0 * np.log10(np.mean(sns)) 266 | # note: this is also affected by steep baseline wander (but so should be the zero-crossing time detection [test!]. so fine) 267 | else: 268 | raise ValueError('snr() unknown mode={}'.format(mode)) 269 | -------------------------------------------------------------------------------- /hsh_signal/signal.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import numpy as np 4 | from scipy import interpolate 5 | from scipy import signal 6 | from scipy.interpolate import interp1d 7 | 8 | 9 | def bpm2hz(f_bpm): 10 | return f_bpm / 60.0 11 | def hz2bpm(f_hz): 12 | return f_hz * 60.0 13 | 14 | 15 | def gauss(x, t_mu, t_sigma): 16 | a = 1.0 / (t_sigma * np.sqrt(2 * np.pi)) 17 | y = a * np.exp(-0.5 * (x - t_mu)**2 / t_sigma**2) 18 | return y 19 | 20 | 21 | def hilbert_fc(x): 22 | """Hilbert Transform: recover analytic signal (float -> complex)""" 23 | 24 | # gnuradio.filter.firdes.hilbert(ntaps=65) 25 | hilbert_taps_65 = (0.0, -0.020952552556991577, 0.0, -0.022397557273507118, 0.0, -0.024056635797023773, 0.0, -0.02598116546869278, 0.0, -0.028240399435162544, 0.0, -0.030929960310459137, 0.0, -0.0341857448220253, 0.0, -0.03820759803056717, 0.0, -0.04330194741487503, 0.0, -0.04996378347277641, 0.0, -0.05904810503125191, 0.0, -0.07216990739107132, 0.0, -0.09278988093137741, 0.0, -0.1299058347940445, 0.0, -0.21650972962379456, 0.0, -0.6495291590690613, 0.0, 0.6495291590690613, 0.0, 0.21650972962379456, 0.0, 0.1299058347940445, 0.0, 0.09278988093137741, 0.0, 0.07216990739107132, 0.0, 0.05904810503125191, 0.0, 0.04996378347277641, 0.0, 0.04330194741487503, 0.0, 0.03820759803056717, 0.0, 0.0341857448220253, 0.0, 0.030929960310459137, 0.0, 0.028240399435162544, 0.0, 0.02598116546869278, 0.0, 0.024056635797023773, 0.0, 0.022397557273507118, 0.0, 0.020952552556991577, 0.0) 26 | 27 | #x = np.array(x) 28 | xi = np.convolve(x, hilbert_taps_65, mode='same') # mode: for now, consistent with gnuradio 29 | return x + xi * 1j 30 | 31 | 32 | def nextpow2(n): 33 | m_f = np.log2(n) 34 | m_i = np.ceil(m_f) 35 | return int(2**int(m_i)) 36 | 37 | 38 | def filter_fft_ff(sig, taps): 39 | """ 40 | Applies a filter to a signal, implementing the overlap-save method (chunk up the signal) 41 | see https://en.wikipedia.org/wiki/Overlap%E2%80%93save_method 42 | 43 | This is equivalent to: np.convolve(x, taps, mode='valid') but more efficient for long signals 44 | """ 45 | 46 | assert(len(sig) > len(taps)) # expect a long signal 47 | # ^ we could try reversing sig and taps in this case, 48 | # but the caller would be surprised to get a longer return value than expected 49 | 50 | h = taps 51 | M = len(taps) 52 | overlap = M - 1 53 | N = nextpow2(4*overlap) 54 | step_size = N - overlap 55 | # TODO: keep filter's FFT around (cache it) instead of recomputing it every time. e.g. currently 15 ms (out of 50 ms) just for Bandreject's H 56 | H = np.fft.fft(h, N) 57 | x = np.concatenate([sig, np.zeros(N)]) # end padding, so the last step_size batch will surely cover the end 58 | y = np.zeros(len(x)) 59 | for pos in range(0, len(x)+1 - N, step_size): 60 | #print(pos) 61 | yt = np.fft.ifft(np.fft.fft(x[pos:pos+N], N) * H, N) 62 | y[pos:pos+step_size] = yt[M-1:N].real 63 | # cut back the end padding, and the overlap region where taps hang out of the signal 64 | #y = y[:-N-M//2] # what is wrong with this line?? 65 | y = y[0:len(sig)-len(taps)+1] 66 | 67 | # if latency is an issue, see "block convolver" (Bill Gardner) 68 | # here: http://dsp.stackexchange.com/questions/2537/do-fft-based-filtering-methods-add-intrinsic-latency-to-a-real-time-algorithm 69 | 70 | return y 71 | 72 | 73 | # copied heartbeat_localmax() from heartshield-kivy-app/utils.py, added some more docstring 74 | def localmax(d): 75 | """ 76 | Calculate the local maxima of a (heartbeat) signal vector 77 | (based on script from https://github.com/compmem/ptsa/blob/master/ptsa/emd.py). 78 | 79 | :param d: signal vector 80 | :returns: boolean vector with same length as 'd', True values indicating local maxima 81 | """ 82 | 83 | # this gets a value of -2 if it is an unambiguous local max 84 | # value -1 denotes that the run its a part of may contain a local max 85 | diffvec = np.r_[-np.inf,d,-np.inf] 86 | diffScore=np.diff(np.sign(np.diff(diffvec))) 87 | 88 | # Run length code with help from: 89 | # http://home.online.no/~pjacklam/matlab/doc/mtt/index.html 90 | # (this is all painfully complicated, but I did it in order to avoid loops...) 91 | 92 | # here calculate the position and length of each run 93 | runEndingPositions=np.r_[np.nonzero(d[0:-1]!=d[1:])[0],len(d)-1] 94 | runLengths = np.diff(np.r_[-1, runEndingPositions]) 95 | runStarts=runEndingPositions-runLengths + 1 96 | 97 | # Now concentrate on only the runs with length>1 98 | realRunStarts = runStarts[runLengths>1] 99 | realRunStops = runEndingPositions[runLengths>1] 100 | realRunLengths = runLengths[runLengths>1] 101 | 102 | # save only the runs that are local maxima 103 | maxRuns=(diffScore[realRunStarts]==-1) & (diffScore[realRunStops]==-1) 104 | 105 | # If a run is a local max, then count the middle position (rounded) as the 'max' 106 | maxRunMiddles=np.round(realRunStarts[maxRuns]+realRunLengths[maxRuns]/2.)-1 107 | 108 | # get all the maxima 109 | maxima=(diffScore==-2) 110 | maxima[maxRunMiddles.astype(np.int32)] = True 111 | 112 | return maxima 113 | 114 | 115 | def localmax_pos(x): 116 | """:returns: list of tuple(position, peak): [(t, x_t), ...]""" 117 | m = localmax(x) 118 | return [(t, x_t) for (t, x_t) in enumerate(x) if m[t]] 119 | 120 | 121 | def evenly_resample(times, series, target_fps=30.0): 122 | L = (times[-1] - times[0])*target_fps 123 | even_times = np.linspace(times[0], times[-1], L) 124 | series2 = np.interp(even_times, times, series) 125 | data = np.vstack((np.reshape(even_times, -1, 1), np.reshape(series2, -1, 1))).T 126 | return data 127 | 128 | 129 | def grid_resample(times, series, target_fps=30.0): 130 | """like `evenly_resample()`, but with precise grid spacing through tail padding.""" 131 | # add tail padding: 132 | times = np.hstack((times, [times[-1] + times[-1] - times[-2]])) 133 | series = np.hstack((series, [series[-1]])) 134 | # the trail padding above ensures precise grid spacing until the last sample. 135 | # Fractional index interpolation beyond the original end may leave a trailing constant value in the result output. 136 | 137 | # note: the original `evenly_resample()` result was never intended to be used on its own, decoupled from its timestamps. 138 | 139 | L = (times[-1] - times[0]) * target_fps 140 | even_times = np.linspace(times[0], times[-1], L, endpoint=False) 141 | series2 = np.interp(even_times, times, series) 142 | data = np.vstack((np.reshape(even_times, -1, 1), np.reshape(series2, -1, 1))).T 143 | return data 144 | 145 | # Example 3:1 extrapolation from [a, b, c]: 146 | # 147 | # [ a . . b . . c . . (c) ] 148 | # _ _ _ _ _ _ _ _ _ 149 | # 150 | # ^ these two interpolation points in the tail are constant, have no real support. 151 | 152 | 153 | def highpass(signal, fps, cf=0.5, tw=0.4): 154 | from gr_firdes import firdes 155 | cutoff_freq = cf 156 | transition_width = tw 157 | taps = firdes.high_pass_2(1.0, fps, cutoff_freq, transition_width, 60.0) 158 | if len(signal) == 0: return np.array([]) # workaround failing np.pad([]) 159 | return np.convolve(np.pad(signal, (len(taps)//2,len(taps)//2), 'edge'), taps, mode='valid') 160 | 161 | def lowpass(signal, fps, cf=3.0, tw=0.4): 162 | from gr_firdes import firdes 163 | cutoff_freq = cf 164 | transition_width = tw 165 | taps = firdes.low_pass_2(1.0, fps, cutoff_freq, transition_width, 60.0) 166 | if len(signal) == 0: return np.array([]) # workaround failing np.pad([]) 167 | return np.convolve(np.pad(signal, (len(taps)//2,len(taps)//2), 'edge'), taps, mode='valid') 168 | 169 | def highpass_fft(signal, fps, cf=0.5, tw=0.4): 170 | from filter import Highpass, apply_filter 171 | if len(signal) == 0: return np.array([]) 172 | return apply_filter(signal, Highpass(cf, tw, fps)) 173 | 174 | def lowpass_fft(signal, fps, cf=3.0, tw=0.4): 175 | from filter import Lowpass, apply_filter 176 | if len(signal) == 0: return np.array([]) 177 | return apply_filter(signal, Lowpass(cf, tw, fps)) 178 | 179 | def cross_corr(x, y): 180 | """normalized cross-correlation of the two signals of same length""" 181 | return np.sum(x * y) / np.sqrt(np.sum(x**2) * np.sum(y**2)) 182 | 183 | def slices(arr, loc, hwin): 184 | """Get slices from arr +/- hwin around indexes loc.""" 185 | ret = [] 186 | arrp = np.pad(arr, (hwin, hwin), mode='constant') # zero-pad 187 | locp = loc + hwin 188 | for lp in locp: 189 | ret.append(arrp[lp-hwin:lp+hwin+1]) 190 | return ret 191 | 192 | def localmax_climb(arr, loc, hwin): 193 | """Climb from loc to the local maxima, up to hwin to the left/right.""" 194 | # TODO: should be called localmax_win, since it's not really climbing but looking in a window. 195 | new_loc = [] 196 | arrp = np.pad(arr, (hwin, hwin), mode='constant') # zero-pad 197 | locp = loc + hwin 198 | for lp in locp: 199 | im = (lp-hwin) + np.argmax(arrp[lp-hwin:lp+hwin+1]) - hwin 200 | im = min(max(im, 0), len(arr)-1) # clip 201 | new_loc.append(im) 202 | return np.array(new_loc) 203 | 204 | 205 | def cubic_resample(series, fps_old=30, fps_new=300): 206 | fps_old, fps_new = float(fps_old), float(fps_new) 207 | t = np.linspace(0.0, len(series)/fps_old, len(series), endpoint=False) 208 | yinterp = interpolate.UnivariateSpline(t, series, s=0) 209 | tnew = np.arange(0.0, len(series)/fps_old, 1.0/fps_new) 210 | return np.clip(yinterp(tnew), a_min=np.min(series), a_max=np.max(series)) 211 | 212 | 213 | def win_max(sig, hwin): 214 | out = np.zeros(len(sig)) 215 | for i in range(len(sig)): 216 | s,e = max(i-hwin, 0), min(i+hwin+1, len(sig)) 217 | out[i] = np.max(sig[s:e]) 218 | return out 219 | 220 | 221 | def cwt_lowpass(x, fps, cf): 222 | num_wavelets = int(fps / cf) 223 | widths = np.arange(1, num_wavelets) 224 | cwt_mat = signal.cwt(x, signal.ricker, widths) 225 | c = np.mean(cwt_mat, axis=0) 226 | # renormalize 227 | orig_e, c_e = np.sqrt(np.sum(x*x)), np.sqrt(np.sum(c*c)) 228 | return c * (orig_e / c_e) 229 | 230 | 231 | def even_smooth(ii, x, length, fps, cf=2.0, tw=1.0): 232 | """ 233 | Turn an intermittently sampled signal into an evenly sampled one of `length`. 234 | Similar to `evenly_resample()` except this is index-based and smoothes afterwards. 235 | 236 | :param ii indices for the samples in `x` (may be fractional) 237 | :param x values sampled at uneven indices `ii` 238 | :param fps sampling rate 239 | :param cf cutoff frequency for subsequent low-pass filter 240 | :param tw transition width for subsequent low-pass filter 241 | """ 242 | # add bounds (constant value approximation) 243 | assert len(x) > 0 244 | ii = np.insert(ii, 0, -1) 245 | ii = np.insert(ii, len(ii), length) 246 | # + 1e-3 * std ... avoid interpolate division by zero? 247 | # x = np.insert(x, 0, x[0] + 1e-3 * np.std(x)) 248 | # x = np.insert(x, len(x), x[-1] + 1e-3 * np.std(x)) 249 | x = np.insert(x, 0, x[0]) 250 | x = np.insert(x, len(x), x[-1]) 251 | 252 | #print 'insert x[0]=', x[0], 'x[-1]=', x[-1] 253 | 254 | # interpolate it -> evenly sampled 255 | func = interp1d(ii, x, kind='linear') 256 | ix = func(np.arange(length)) 257 | 258 | #print 'resulting ix[0]=', ix[0], 'ix[1]=', ix[1], 'ix[-1]=', ix[-1] 259 | 260 | # smooth it 261 | return lowpass(ix, fps=fps, cf=cf, tw=tw) 262 | 263 | 264 | def seek_left_localmax(x, idxs, fps, win_len=0.3): 265 | """seek to the left of SSF-detected peaks `idxs`, to find the actual feet (localmax)""" 266 | ifeet = [] 267 | imax = np.where(localmax(x))[0] 268 | for i in idxs: 269 | ii = np.where(((i - imax) >= 0) & ((i - imax) <= int(fps * win_len)))[0] 270 | if len(ii) == 0: continue 271 | # find the first localmax on the left (not necessarily the tallest) 272 | ifeet.append(imax[ii][-1]) 273 | ifeet = np.array(ifeet) 274 | return ifeet 275 | 276 | 277 | def localmax_interp(x, idxs, hwin_size=None): 278 | """ 279 | Turn local maxima from integers to floats. 280 | :param x signal 281 | :param idxs integer indices of local maxima 282 | :param hwin_size optional integer window size. if given, do a localmax_climb() before to find the exact maxima so the result will be correct with rough estimates. 283 | :returns float indices into `x` 284 | """ 285 | idxs = localmax_climb(x, idxs, hwin_size) if hwin_size is not None else idxs 286 | assert np.all(idxs < len(x)) 287 | assert np.all(idxs >= 0) 288 | der = np.pad(np.diff(x), (1, 1), 'constant') 289 | new_idxs = [] 290 | for i in idxs: 291 | xp, fp = der[i:i+2], np.array([i,i+1]) 292 | ii = interp1d(xp, fp, bounds_error=False, fill_value=i)([0]) 293 | new_idxs.append(max(min(ii, i+1, len(x)-1), i)) 294 | return np.array(new_idxs) 295 | 296 | 297 | # 298 | # Indexing tools 299 | # 300 | 301 | 302 | def dirac(length, idxs, dtype=bool): 303 | """:returns an array with the specified `idxs` set to 1.""" 304 | arr = np.zeros(length, dtype=dtype) 305 | idxs = np.array(idxs) 306 | arr[idxs] = 1.0 307 | return arr 308 | 309 | 310 | def cohesive_ranges(idxs): 311 | if sorted(list(idxs)) != list(idxs): 312 | raise ValueError('idxs must be sorted') 313 | if len(idxs) == 0: 314 | return [] 315 | 316 | ranges = [] 317 | si, pi = idxs[0], idxs[0] 318 | for i in idxs[1:]: 319 | pi += 1 320 | if i != pi: 321 | ranges.append((si, pi)) 322 | si = i 323 | pi = i 324 | ranges.append((si, pi + 1)) 325 | 326 | return ranges 327 | -------------------------------------------------------------------------------- /hsh_signal/filter.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import numpy as np 4 | from .signal import filter_fft_ff 5 | from .iter import pairwise 6 | import time 7 | 8 | 9 | class FilterBlock(object): 10 | """Realtime batch-processing filter block interface.""" 11 | def __init__(self): 12 | self._consumer = None 13 | 14 | def connect(self, consumer): 15 | """connect to a consumer""" 16 | self._consumer = consumer 17 | 18 | def put(self, x): 19 | """batch-add an array and push results through consumers""" 20 | assert(self._consumer is not None) 21 | self._consumer.put(self.batch(x)) 22 | 23 | def batch(self, x): 24 | """batch-process an array and return array of output values""" 25 | raise NotImplementedError("override me: FilterBlock.batch()") 26 | 27 | 28 | class SourceBlock(FilterBlock): 29 | """Filter block providing data from some input hardware device.""" 30 | def batch(self, x): 31 | return x 32 | 33 | 34 | class SinkBlock(FilterBlock): 35 | """Filter block accepting data.""" 36 | def put(self, x): 37 | raise NotImplementedError("override me: SinkBlock.put()") 38 | 39 | 40 | class DataSink(SinkBlock): 41 | """Simply collects the data.""" 42 | def __init__(self, dtype=None): 43 | super(DataSink, self).__init__() 44 | self.data = np.array([], dtype=dtype) 45 | self.dtype = dtype 46 | 47 | def put(self, x): 48 | self.data = np.concatenate([self.data, x]) 49 | 50 | def reset(self): 51 | self.data = np.array([], dtype=self.dtype) 52 | 53 | 54 | class Delay(FilterBlock): 55 | """Simple time delay filter block. Initialized with zeros.""" 56 | def __init__(self, delay): 57 | """:param delay: delay in number of samples""" 58 | super(Delay, self).__init__() 59 | self._buffer = np.zeros(delay) 60 | self.delay = delay 61 | 62 | def batch(self, x): 63 | self._buffer = np.concatenate([self._buffer, x]) 64 | y = self._buffer[:-self.delay] 65 | self._buffer = self._buffer[-self.delay:] 66 | return y 67 | 68 | 69 | class PLL(FilterBlock): 70 | def __init__(self, loop_bw, max_freq, min_freq, sampling_rate): 71 | """ 72 | Make PLL block that outputs the tracked signal's frequency. 73 | 74 | :param loop_bw: loop bandwidth, determines the lock range 75 | :param max_freq: maximum frequency cap (Hz) 76 | :param min_freq: minimum frequency cap (Hz) 77 | :param sampling_rate: sampling rate (Hz) 78 | """ 79 | super(PLL, self).__init__() 80 | import gr_pll.pll as pll 81 | self._pll = pll.PLL(loop_bw, max_freq, min_freq, sampling_rate) 82 | 83 | def batch(self, x): 84 | """batch-process an array and return array of output values (frequency output)""" 85 | return self._pll.filter_cf(x) 86 | 87 | def batch_vco(self, x): 88 | """batch-process an array and return VCO output signal""" 89 | return self._pll.filter_cc(x) 90 | 91 | 92 | class FIRFilter(FilterBlock): 93 | """ 94 | Realtime FIR filter. 95 | 96 | Introduces a delay that depends on the filter parameters. 97 | Also, the first delay output samples are invalid. 98 | """ 99 | 100 | MODE_CONVOLVE = 1 101 | MODE_FFT_CONVOLVE = 2 102 | 103 | def __init__(self, taps, sampling_rate): 104 | """ 105 | :param taps: filter impulse response 106 | :param sampling_rate: sampling rate (Hz) 107 | """ 108 | super(FIRFilter, self).__init__() 109 | self._taps = taps # filter impulse response 110 | self.sampling_rate = sampling_rate 111 | self._ntaps = len(self._taps) 112 | self._ntaps_front = self._ntaps // 2 113 | self._ntaps_back = self._ntaps - self._ntaps_front # for odd ntaps, +1 at the back 114 | self._buffer_x = np.zeros(self._ntaps - 1) 115 | self.mode = FIRFilter.MODE_FFT_CONVOLVE 116 | 117 | @property 118 | def delay(self): 119 | """Filter delay in number of samples.""" 120 | return self._ntaps_front 121 | 122 | def batch(self, x): 123 | """batch-process an array and return array of output values""" 124 | self._buffer_x = np.concatenate([self._buffer_x, x]) 125 | 126 | # filter a slightly longer batch, to avoid boundary effects 127 | #print('len(self._buffer_x)=', len(self._buffer_x), 'len(self._taps)=', len(self._taps)) 128 | #Logger.info(str(('len(self._buffer_x)=', len(self._buffer_x), 'len(self._taps)=', len(self._taps)))) 129 | if self.mode == FIRFilter.MODE_CONVOLVE: 130 | # slow. for testing only 131 | filtered = np.convolve(self._buffer_x, self._taps, mode='valid') 132 | #filtered *= 0.1 133 | elif self.mode == FIRFilter.MODE_FFT_CONVOLVE: 134 | filtered = filter_fft_ff(self._buffer_x, self._taps) 135 | else: 136 | raise ValueError('invalid FIRFilter mode') 137 | 138 | # trim buffer: just keep trailing bit to include it into leading boundary of next batch 139 | self._buffer_x = self._buffer_x[-self._ntaps+1:] 140 | 141 | # cut off leading/trailing boundary effect areas 142 | # (note: introduces a delay of self.iphase) 143 | #return filtered[self._ntaps_back:-self._ntaps_front] 144 | return filtered 145 | 146 | 147 | class HilbertImag(FIRFilter): 148 | """Part of the Hilbert implementation, turns real part into imaginary part.""" 149 | def __init__(self, ntaps=65): 150 | """ 151 | :param ntaps number of taps, made odd if necessary 152 | """ 153 | ntaps += 1 - ntaps % 2 # ensure taps is odd 154 | from gr_firdes.firdes import hilbert 155 | super(HilbertImag, self).__init__(hilbert(ntaps), None) 156 | 157 | 158 | class Hilbert(FilterBlock): 159 | """ 160 | Hilbert transform turns a series of real-valued samples into complex-valued (analytic) samples. 161 | 162 | Realtime variant. 163 | Introduces a delay of ntaps//2 samples -- as opposed to hilbert_fc() which is not realtime. 164 | Also, the first ntaps//2 output samples are invalid. 165 | """ 166 | def __init__(self, ntaps=65): 167 | super(Hilbert, self).__init__() 168 | self.filter_imag = HilbertImag(ntaps) 169 | self.filter_real = Delay(self.filter_imag.delay) # delay so the two parts match up again 170 | self.delay = self.filter_imag.delay 171 | 172 | def add(self, sample): 173 | """ 174 | Append a single scalar sample value. 175 | :returns current filter output value 176 | """ 177 | #self.put(np.array([sample])) 178 | raise RuntimeError('not implemented anymore') 179 | 180 | def get(self): 181 | """:returns current filter output value""" 182 | raise RuntimeError('not implemented anymore') 183 | 184 | def batch(self, x): 185 | """batch-process an array and return array of output values""" 186 | real = self.filter_real.batch(x) 187 | imag = self.filter_imag.batch(x) 188 | #print(len(real), len(imag)) 189 | return real + imag * 1j 190 | 191 | 192 | class Lowpass(FIRFilter): 193 | """Realtime low-pass filter. Introduces a delay and outputs some initial invalid samples.""" 194 | def __init__(self, cutoff_freq, transition_width, sampling_rate): 195 | """ 196 | :param cutoff_freq: beginning of transition band (Hz) 197 | :param transition_width: width of transition band (Hz) 198 | :param sampling_rate: sampling rate (Hz) 199 | """ 200 | # design filter impulse response 201 | from gr_firdes.firdes import low_pass_2 202 | super(Lowpass, self).__init__(low_pass_2(1, sampling_rate, cutoff_freq, transition_width, 60), sampling_rate) 203 | 204 | 205 | class Highpass(FIRFilter): 206 | """Realtime high-pass filter. Introduces a delay and outputs some initial invalid samples.""" 207 | def __init__(self, cutoff_freq, transition_width, sampling_rate): 208 | """ 209 | :param cutoff_freq: beginning of transition band (Hz) 210 | :param transition_width: width of transition band (Hz) 211 | :param sampling_rate: sampling rate (Hz) 212 | """ 213 | # design filter impulse response 214 | from gr_firdes.firdes import high_pass_2 215 | super(Highpass, self).__init__(high_pass_2(1, sampling_rate, cutoff_freq, transition_width, 60), sampling_rate) 216 | 217 | 218 | class Bandpass(FIRFilter): 219 | """Realtime band-pass filter. Introduces a delay and outputs some initial invalid samples.""" 220 | def __init__(self, low_cutoff_freq, high_cutoff_freq, transition_width, sampling_rate): 221 | """ 222 | :param low_cutoff_freq: center of transition band (Hz) 223 | :param high_cutoff_freq: center of transition band (Hz) 224 | :param transition_width: width of transition band (Hz) 225 | :param sampling_rate: sampling rate (Hz) 226 | """ 227 | # design filter impulse response 228 | from gr_firdes.firdes import band_pass_2 229 | super(Bandpass, self).__init__(band_pass_2(1, sampling_rate, low_cutoff_freq, high_cutoff_freq, transition_width, 60), sampling_rate) 230 | 231 | 232 | class Bandreject(FIRFilter): 233 | """Realtime band-reject filter. Introduces a delay and outputs some initial invalid samples.""" 234 | def __init__(self, low_cutoff_freq, high_cutoff_freq, transition_width, sampling_rate): 235 | """ 236 | :param low_cutoff_freq: center of transition band (Hz) 237 | :param high_cutoff_freq: center of transition band (Hz) 238 | :param transition_width: width of transition band (Hz) 239 | :param sampling_rate: sampling rate (Hz) 240 | """ 241 | # design filter impulse response 242 | from gr_firdes.firdes import band_reject_2 243 | super(Bandreject, self).__init__(band_reject_2(1, sampling_rate, low_cutoff_freq, high_cutoff_freq, transition_width, 60), sampling_rate) 244 | 245 | def batch(self, x): 246 | before = time.time() 247 | res = super(Bandreject, self).batch(x) 248 | after = time.time() 249 | #Logger.debug('Bandreject: batch() took {} sec'.format(after-before)) 250 | return res 251 | 252 | 253 | class Splitter(object): 254 | def __init__(self): 255 | self._consumers = [] 256 | 257 | def connect(self, consumer): 258 | """add a consumer""" 259 | self._consumers.append(consumer) 260 | 261 | def put(self, x): 262 | """batch-put array to consumers""" 263 | for consumer in self._consumers: 264 | consumer.put(x) 265 | 266 | 267 | class Downsampler(FilterBlock): 268 | """Evenly spaced downsampler by an integer ratio.""" 269 | def __init__(self, ratio): 270 | super(Downsampler, self).__init__() 271 | self.ratio = ratio 272 | self.waitfor = 0 273 | def batch(self, x): 274 | if len(x) <= self.waitfor: 275 | self.waitfor -= len(x) 276 | return np.array([]) 277 | ret = x[self.waitfor + np.arange(0, len(x)-self.waitfor, self.ratio)] 278 | self.waitfor = self.ratio - len(x) % self.ratio 279 | # TODO: bugged somehow when used through apply_filter() 280 | # TODO: waitfor %= self.ratio 281 | return ret 282 | 283 | # d=Downsampler(2) 284 | # d.batch(np.array([1])) 285 | # Out[58]: 286 | # array([1]) 287 | # In [59]: 288 | # 289 | # d.batch(np.array([2])) 290 | # d.batch(np.array([2])) 291 | # Out[59]: 292 | # array([], dtype=float64) 293 | # In [60]: 294 | # 295 | # d.batch(np.array([3,4,5,6,7])) 296 | # d.batch(np.array([3,4,5,6,7])) 297 | # Out[60]: 298 | # array([3, 5, 7]) 299 | # In [61]: 300 | # 301 | # 9 302 | # d.batch(np.array([8,9])) 303 | # Out[61]: 304 | # array([9]) 305 | 306 | 307 | class RegroupBatches(FilterBlock): 308 | """Splits up large incoming batches into smaller chunks.""" 309 | def __init__(self, out_batch_size): 310 | super(RegroupBatches, self).__init__() 311 | self.out_batch_size = out_batch_size 312 | 313 | def batch(self, x): 314 | return x 315 | 316 | def put(self, x): 317 | assert(self._consumer is not None) 318 | starts = np.arange(0, len(x), self.out_batch_size) 319 | for s in starts: 320 | self._consumer.put(self.batch(x[s:s+self.out_batch_size])) 321 | 322 | 323 | class MixLocalOscillator(FilterBlock): 324 | """Local oscillator and mixer that mixes its output in.""" 325 | def __init__(self, fps, f0): 326 | super(MixLocalOscillator, self).__init__() 327 | self.fps, self.f0 = fps, f0 328 | self.t = 0.0 329 | 330 | def batch(self, x): 331 | t = self.t + np.arange(len(x))/float(self.fps) 332 | self.t = t[-1] + 1.0/float(self.fps) 333 | carrier = np.sin(2*np.pi*self.f0*t) 334 | return x * carrier 335 | 336 | 337 | class ChunkDataSource(SourceBlock): 338 | """ 339 | Fake Microphone signal source for testing. Provides a wav as audio. 340 | """ 341 | def __init__(self, data, batch_size, sampling_rate=44100): 342 | super(ChunkDataSource, self).__init__() 343 | self.sampling_rate = sampling_rate 344 | self._data = data 345 | self._batch_size = batch_size 346 | self._i = 0 347 | 348 | def poll(self): 349 | """Call this regulary in order to trigger the callback.""" 350 | # currently called with 30 fps in kivy -> could compute batch_size via sampling_rate 351 | #Logger.debug('FakeMic.poll()') 352 | before = time.time() 353 | self.put(self._data[self._i:self._i+self._batch_size]) 354 | after = time.time() 355 | #Logger.debug('FakeMic: poll() took {} sec'.format(after-before)) 356 | self._i += self._batch_size 357 | 358 | def progress(self): 359 | return float(self._i) / len(self._data) * 100.0 360 | 361 | def start(self): pass 362 | def stop(self): pass 363 | 364 | def finished(self): 365 | return self._i >= len(self._data) 366 | 367 | 368 | def apply_filter(signal, filter, debug=False): 369 | signal_padded = np.pad(signal, (0, filter.delay), mode='constant') # pad with trailing zeros to force returning complete ECG 370 | source = ChunkDataSource(data=signal_padded, batch_size=179200, sampling_rate=filter.sampling_rate) 371 | sink = DataSink() 372 | connect(source, filter, sink) 373 | 374 | # push through all the data 375 | prev_t = time.time() 376 | source.start() 377 | while not source.finished(): 378 | if time.time() > prev_t + 1.0 and debug: 379 | print 'progress: {} %'.format(source.progress()) 380 | prev_t = time.time() 381 | source.poll() 382 | source.stop() 383 | 384 | return sink.data[filter.delay:] # cut off leading filter delay (contains nonsense output) 385 | 386 | 387 | def connect(*args): 388 | """Connect several FilterBlock objects in a chain.""" 389 | for a, b in pairwise(args): 390 | a.connect(b) 391 | 392 | -------------------------------------------------------------------------------- /gr_pll/gr/include/gnuradio/blocks/control_loop.h: -------------------------------------------------------------------------------- 1 | /* -*- c++ -*- */ 2 | /* 3 | * Copyright 2011,2013 Free Software Foundation, Inc. 4 | * 5 | * This file is part of GNU Radio 6 | * 7 | * GNU Radio is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 3, or (at your option) 10 | * any later version. 11 | * 12 | * GNU Radio is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with GNU Radio; see the file COPYING. If not, write to 19 | * the Free Software Foundation, Inc., 51 Franklin Street, 20 | * Boston, MA 02110-1301, USA. 21 | */ 22 | 23 | #ifndef GR_BLOCKS_CONTROL_LOOP 24 | #define GR_BLOCKS_CONTROL_LOOP 25 | 26 | #include 27 | 28 | namespace gr { 29 | namespace blocks { 30 | 31 | /*! 32 | * \brief A second-order control loop implementation class. 33 | * 34 | * \details 35 | * This class implements a second order control loop and is 36 | * inteded to act as a parent class to blocks which need a control 37 | * loop (e.g., gr::digital::costas_loop_cc, 38 | * gr::analog::pll_refout_cc, etc.). It takes in a loop bandwidth 39 | * as well as a max and min frequency and provides the functions 40 | * that control the update of the loop. 41 | * 42 | * The loop works of alpha and beta gains. These gains are 43 | * calculated using the input loop bandwidth and a pre-set damping 44 | * factor. The damping factor can be changed using the 45 | * #set_damping_factor after the block is 46 | * constructed. The alpha and beta values can be set using their 47 | * respective #set_alpha or #set_beta functions if very precise 48 | * control over these is required. 49 | * 50 | * The class tracks both phase and frequency of a signal based on 51 | * an error signal. The error calculation is unique for each 52 | * algorithm and is calculated externally and passed to the 53 | * advance_loop function, which uses this to update its phase and 54 | * frequency estimates. 55 | * 56 | * This class also provides the functions #phase_wrap and 57 | * #frequency_limit to easily keep the phase and frequency 58 | * estimates within our set bounds (phase_wrap keeps it within 59 | * +/-2pi). 60 | */ 61 | class BLOCKS_API control_loop 62 | { 63 | protected: 64 | float d_phase, d_freq; 65 | float d_max_freq, d_min_freq; 66 | float d_damping, d_loop_bw; 67 | float d_alpha, d_beta; 68 | 69 | public: 70 | control_loop(void) {} 71 | control_loop(float loop_bw, float max_freq, float min_freq); 72 | virtual ~control_loop(); 73 | 74 | /*! \brief Update the system gains from the loop bandwidth and damping factor. 75 | * 76 | * \details 77 | * This function updates the system gains based on the loop 78 | * bandwidth and damping factor of the system. These two 79 | * factors can be set separately through their own set 80 | * functions. 81 | */ 82 | void update_gains(); 83 | 84 | /*! \brief Advance the control loop based on the current gain 85 | * settings and the inputted error signal. 86 | */ 87 | void advance_loop(float error); 88 | 89 | /*! \brief Keep the phase between -2pi and 2pi. 90 | * 91 | * \details 92 | * This function keeps the phase between -2pi and 2pi. If the 93 | * phase is greater than 2pi by d, it wraps around to be -2pi+d; 94 | * similarly if it is less than -2pi by d, it wraps around to 95 | * 2pi-d. 96 | * 97 | * This function should be called after advance_loop to keep the 98 | * phase in a good operating region. It is set as a separate 99 | * method in case another way is desired as this is fairly 100 | * heavy-handed. 101 | */ 102 | void phase_wrap(); 103 | 104 | /*! \brief Keep the frequency between d_min_freq and d_max_freq. 105 | * 106 | * \details 107 | * This function keeps the frequency between d_min_freq and 108 | * d_max_freq. If the frequency is greater than d_max_freq, it 109 | * is set to d_max_freq. If the frequency is less than 110 | * d_min_freq, it is set to d_min_freq. 111 | * 112 | * This function should be called after advance_loop to keep the 113 | * frequency in the specified region. It is set as a separate 114 | * method in case another way is desired as this is fairly 115 | * heavy-handed. 116 | */ 117 | void frequency_limit(); 118 | 119 | /******************************************************************* 120 | * SET FUNCTIONS 121 | *******************************************************************/ 122 | 123 | /*! 124 | * \brief Set the loop bandwidth. 125 | * 126 | * \details 127 | * Set the loop filter's bandwidth to \p bw. This should be 128 | * between 2*pi/200 and 2*pi/100 (in rads/samp). It must also be 129 | * a positive number. 130 | * 131 | * When a new damping factor is set, the gains, alpha and beta, 132 | * of the loop are recalculated by a call to update_gains(). 133 | * 134 | * \param bw (float) new bandwidth 135 | */ 136 | virtual void set_loop_bandwidth(float bw); 137 | 138 | /*! 139 | * \brief Set the loop damping factor. 140 | * 141 | * \details 142 | * Set the loop filter's damping factor to \p df. The damping 143 | * factor should be sqrt(2)/2.0 for critically damped systems. 144 | * Set it to anything else only if you know what you are 145 | * doing. It must be a number between 0 and 1. 146 | * 147 | * When a new damping factor is set, the gains, alpha and beta, 148 | * of the loop are recalculated by a call to update_gains(). 149 | * 150 | * \param df (float) new damping factor 151 | */ 152 | void set_damping_factor(float df); 153 | 154 | /*! 155 | * \brief Set the loop gain alpha. 156 | * 157 | * \details 158 | * Sets the loop filter's alpha gain parameter. 159 | * 160 | * This value should really only be set by adjusting the loop 161 | * bandwidth and damping factor. 162 | * 163 | * \param alpha (float) new alpha gain 164 | */ 165 | void set_alpha(float alpha); 166 | 167 | /*! 168 | * \brief Set the loop gain beta. 169 | * 170 | * \details 171 | * Sets the loop filter's beta gain parameter. 172 | * 173 | * This value should really only be set by adjusting the loop 174 | * bandwidth and damping factor. 175 | * 176 | * \param beta (float) new beta gain 177 | */ 178 | void set_beta(float beta); 179 | 180 | /*! 181 | * \brief Set the control loop's frequency. 182 | * 183 | * \details 184 | * Sets the control loop's frequency. While this is normally 185 | * updated by the inner loop of the algorithm, it could be 186 | * useful to manually initialize, set, or reset this under 187 | * certain circumstances. 188 | * 189 | * \param freq (float) new frequency 190 | */ 191 | void set_frequency(float freq); 192 | 193 | /*! 194 | * \brief Set the control loop's phase. 195 | * 196 | * \details 197 | * Sets the control loop's phase. While this is normally 198 | * updated by the inner loop of the algorithm, it could be 199 | * useful to manually initialize, set, or reset this under 200 | * certain circumstances. 201 | * 202 | * \param phase (float) new phase 203 | */ 204 | void set_phase(float phase); 205 | 206 | /*! 207 | * \brief Set the control loop's maximum frequency. 208 | * 209 | * \details 210 | * Set the maximum frequency the control loop can track. 211 | * 212 | * \param freq (float) new max frequency 213 | */ 214 | void set_max_freq(float freq); 215 | 216 | /*! 217 | * \brief Set the control loop's minimum frequency. 218 | * 219 | * \details 220 | * Set the minimum frequency the control loop can track. 221 | * 222 | * \param freq (float) new min frequency 223 | */ 224 | void set_min_freq(float freq); 225 | 226 | /******************************************************************* 227 | * GET FUNCTIONS 228 | *******************************************************************/ 229 | 230 | /*! 231 | * \brief Returns the loop bandwidth. 232 | */ 233 | float get_loop_bandwidth() const; 234 | 235 | /*! 236 | * \brief Returns the loop damping factor. 237 | */ 238 | float get_damping_factor() const; 239 | 240 | /*! 241 | * \brief Returns the loop gain alpha. 242 | */ 243 | float get_alpha() const; 244 | 245 | /*! 246 | * \brief Returns the loop gain beta. 247 | */ 248 | float get_beta() const; 249 | 250 | /*! 251 | * \brief Get the control loop's frequency estimate. 252 | */ 253 | float get_frequency() const; 254 | 255 | /*! 256 | * \brief Get the control loop's phase estimate. 257 | */ 258 | float get_phase() const; 259 | 260 | /*! 261 | * \brief Get the control loop's maximum frequency. 262 | */ 263 | float get_max_freq() const; 264 | 265 | /*! 266 | * \brief Get the control loop's minimum frequency. 267 | */ 268 | float get_min_freq() const; 269 | }; 270 | 271 | // This is a table of tanh(x) for x in [-2, 2] used in tanh_lut. 272 | static float 273 | tanh_lut_table[256] = { -0.96402758, -0.96290241, -0.96174273, -0.96054753, -0.95931576, 274 | -0.95804636, -0.95673822, -0.95539023, -0.95400122, -0.95257001, 275 | -0.95109539, -0.9495761 , -0.94801087, -0.94639839, -0.94473732, 276 | -0.94302627, -0.94126385, -0.93944862, -0.93757908, -0.93565374, 277 | -0.93367104, -0.93162941, -0.92952723, -0.92736284, -0.92513456, 278 | -0.92284066, -0.92047938, -0.91804891, -0.91554743, -0.91297305, 279 | -0.91032388, -0.90759795, -0.9047933 , -0.90190789, -0.89893968, 280 | -0.89588656, -0.89274642, -0.88951709, -0.88619637, -0.88278203, 281 | -0.87927182, -0.87566342, -0.87195453, -0.86814278, -0.86422579, 282 | -0.86020115, -0.85606642, -0.85181914, -0.84745683, -0.84297699, 283 | -0.83837709, -0.83365461, -0.82880699, -0.82383167, -0.81872609, 284 | -0.81348767, -0.80811385, -0.80260204, -0.7969497 , -0.79115425, 285 | -0.78521317, -0.77912392, -0.772884 , -0.76649093, -0.75994227, 286 | -0.75323562, -0.74636859, -0.73933889, -0.73214422, -0.7247824 , 287 | -0.71725127, -0.70954876, -0.70167287, -0.6936217 , -0.68539341, 288 | -0.67698629, -0.66839871, -0.65962916, -0.65067625, -0.64153871, 289 | -0.6322154 , -0.62270534, -0.61300768, -0.60312171, -0.59304692, 290 | -0.58278295, -0.57232959, -0.56168685, -0.55085493, -0.53983419, 291 | -0.52862523, -0.51722883, -0.50564601, -0.49387799, -0.48192623, 292 | -0.46979241, -0.45747844, -0.44498647, -0.4323189 , -0.41947836, 293 | -0.40646773, -0.39329014, -0.37994896, -0.36644782, -0.35279057, 294 | -0.33898135, -0.32502449, -0.31092459, -0.2966865 , -0.28231527, 295 | -0.26781621, -0.25319481, -0.23845682, -0.22360817, -0.208655 , 296 | -0.19360362, -0.17846056, -0.16323249, -0.14792623, -0.13254879, 297 | -0.11710727, -0.10160892, -0.08606109, -0.07047123, -0.05484686, 298 | -0.0391956 , -0.02352507, -0.00784298, 0.00784298, 0.02352507, 299 | 0.0391956 , 0.05484686, 0.07047123, 0.08606109, 0.10160892, 300 | 0.11710727, 0.13254879, 0.14792623, 0.16323249, 0.17846056, 301 | 0.19360362, 0.208655 , 0.22360817, 0.23845682, 0.25319481, 302 | 0.26781621, 0.28231527, 0.2966865 , 0.31092459, 0.32502449, 303 | 0.33898135, 0.35279057, 0.36644782, 0.37994896, 0.39329014, 304 | 0.40646773, 0.41947836, 0.4323189 , 0.44498647, 0.45747844, 305 | 0.46979241, 0.48192623, 0.49387799, 0.50564601, 0.51722883, 306 | 0.52862523, 0.53983419, 0.55085493, 0.56168685, 0.57232959, 307 | 0.58278295, 0.59304692, 0.60312171, 0.61300768, 0.62270534, 308 | 0.6322154 , 0.64153871, 0.65067625, 0.65962916, 0.66839871, 309 | 0.67698629, 0.68539341, 0.6936217 , 0.70167287, 0.70954876, 310 | 0.71725127, 0.7247824 , 0.73214422, 0.73933889, 0.74636859, 311 | 0.75323562, 0.75994227, 0.76649093, 0.772884 , 0.77912392, 312 | 0.78521317, 0.79115425, 0.7969497 , 0.80260204, 0.80811385, 313 | 0.81348767, 0.81872609, 0.82383167, 0.82880699, 0.83365461, 314 | 0.83837709, 0.84297699, 0.84745683, 0.85181914, 0.85606642, 315 | 0.86020115, 0.86422579, 0.86814278, 0.87195453, 0.87566342, 316 | 0.87927182, 0.88278203, 0.88619637, 0.88951709, 0.89274642, 317 | 0.89588656, 0.89893968, 0.90190789, 0.9047933 , 0.90759795, 318 | 0.91032388, 0.91297305, 0.91554743, 0.91804891, 0.92047938, 319 | 0.92284066, 0.92513456, 0.92736284, 0.92952723, 0.93162941, 320 | 0.93367104, 0.93565374, 0.93757908, 0.93944862, 0.94126385, 321 | 0.94302627, 0.94473732, 0.94639839, 0.94801087, 0.9495761 , 322 | 0.95109539, 0.95257001, 0.95400122, 0.95539023, 0.95673822, 323 | 0.95804636, 0.95931576, 0.96054753, 0.96174273, 0.96290241, 324 | 0.96402758 }; 325 | 326 | /*! 327 | * A look-up table (LUT) tanh calcuation. This function returns an 328 | * estimate to tanh(x) based on a 256-point LUT between -2 and 329 | * 2. If x < -2, it returns -1; if > 2, it retursn 1. 330 | * 331 | * This LUT form of the tanh is "hidden" in this code because it 332 | * is likely too coarse an estimate for any real uses of a 333 | * tanh. It is useful, however, in certain control loop 334 | * applications where the input is expected to be within these 335 | * bounds and the noise will be greater than the quanitzation of 336 | * this small LUT. For more accurate forms of tanh, see 337 | * volk_32f_tanh_32f. 338 | */ 339 | static inline float 340 | tanhf_lut(float x) 341 | { 342 | if(x > 2) 343 | return 1; 344 | else if(x <= -2) 345 | return -1; 346 | else { 347 | int index = 128 + 64*x; 348 | return tanh_lut_table[index]; 349 | } 350 | } 351 | 352 | } /* namespace blocks */ 353 | } /* namespace gr */ 354 | 355 | #endif /* GR_BLOCKS_CONTROL_LOOP */ 356 | -------------------------------------------------------------------------------- /hsh_signal/ecg.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import numpy as np 3 | from signal import localmax_climb, slices, cross_corr, hz2bpm 4 | import matplotlib.pyplot as plt 5 | from .heartseries import Series, HeartSeries 6 | from sklearn.linear_model import TheilSenRegressor 7 | 8 | 9 | from ishneholterlib import Holter 10 | from heartseries import Series 11 | 12 | from quality import sqi_slices, sig_pad 13 | from envelope import envelopes_perc_threshold, envelopes_at_perc 14 | from envelope import beat_penalty_threshold, beat_penalty 15 | 16 | from signal import cross_corr 17 | 18 | 19 | class NoisyECG(object): 20 | """TODO: this class should not leak out anywhere. Rename and fix external things that break. Then, redesign scrub_ecg()""" 21 | GOOD_BEAT_THRESHOLD = 0.5 #: normalized cross-correlation threshold for good beats, when compared vs. the median 22 | 23 | def __init__(self, ecg, debug=False): 24 | """:param ecg: heartseries.Series""" 25 | 26 | # needs package ecg-beat-detector 27 | # imported here, to avoid loading the big matrices (~ 2 sec) otherwise 28 | from kimqrsdetector.kimqrsdetector import QRSdetection 29 | self.QRSdetection = QRSdetection 30 | 31 | self.ecg = ecg 32 | self.median_ibi = 0.0 #: median inter-beat interval in secs 33 | self.good_beat_fraction = 0.0 #: fraction of Kim-detected beats which correlate well with the median beat 34 | self.beat_idxs, self.beat_times = self.beat_detect(debug=debug) 35 | 36 | def is_valid(self, debug=False): 37 | bpm = hz2bpm(1.0 / (self.median_ibi + 1e-6)) 38 | bpm_ok = bpm >= 30.0 and bpm <= 150.0 39 | enough_beats = len(self.beat_idxs) >= 5 # at least 5 good beats 40 | beat_hist_ok = self.good_beat_fraction > 0.5 # meh. not good enough?? 41 | 42 | if debug: 43 | print 'median_ibi={:.3f} -> bpm={:.1f}. len(beat_idxs)={} good_beat_fraction={:.2f}'.format(self.median_ibi, bpm, len(self.beat_idxs), self.good_beat_fraction) 44 | return bpm_ok and enough_beats and beat_hist_ok 45 | 46 | def slice_good(self, sl, median_beat): 47 | spectrum = np.abs(np.fft.fft(sl)**2) 48 | 49 | # around 1/8, there is a bottom in a clean signal (see plot of mean beat spectrum) 50 | lf_hf_db = 10.0 * np.log10(np.sum(spectrum[0:len(spectrum)//8]) / np.sum(spectrum[len(spectrum)//8:len(spectrum)//2])) 51 | 52 | # the slice has similar power like the median_beat 53 | power_ratio_db = 10.0 * np.log10(np.sum(sl**2) / np.sum(median_beat**2)) 54 | 55 | if False: 56 | plt.plot(spectrum, c='r') 57 | plt.plot(median_beat, c='k') 58 | plt.plot(sl, c='b') 59 | plt.title('slice_good() lf_hf={:.1f} dB slice/median power_ratio_db={:.1f} dB'.format(lf_hf_db, power_ratio_db)) 60 | plt.show() 61 | 62 | # the slice has similar power like the median_beat 63 | power_similar = -6.0 < power_ratio_db < 6.0 # 10 dB is a bit lenient. 6 dB would be better, but some baseline drift is larger. 64 | 65 | return lf_hf_db > 5.0 and power_similar 66 | 67 | def beat_detect(self, debug=False, outlierthreshold=0.001): 68 | # Heuristics: 69 | # * found enough beats 70 | # * median ibi is in plausible range (or even: some percentile of ibis is in plausible range) 71 | # * histogram of beat correlations is plausible (lots of good correlation) 72 | # 73 | # Good beats have positive correlation, e.g. rho > 0.5 with the median beat. 74 | 75 | ecg = self.ecg 76 | 77 | #ecg.x[:int(ecg.fps*5)] *= 0.0 # avoid the terrible swing 78 | 79 | # 80 | # Kim ECG beat detection 81 | # 82 | smoothsignal = np.array(ecg.x) 83 | 84 | # kill outliers 85 | mn, mx = np.min(smoothsignal), np.max(smoothsignal) 86 | m = min(abs(mn),abs(mx)) 87 | N = 100 88 | step = m/float(N) 89 | for i in range(N): 90 | n = len(np.where(smoothsignal<-m)[0]) + len(np.where(smoothsignal>m)[0]) 91 | if n > outlierthreshold*len(smoothsignal): 92 | break 93 | m -= step 94 | mn, mx = -m, m 95 | 96 | smoothsignal[smoothsignalmx] = mx 98 | smoothsignal[-10:] = 0 # extreme outlier in last few frames 99 | 100 | # adjust distribution to the one Kim has optimized for 101 | smoothsignal = (smoothsignal-np.mean(smoothsignal))/np.std(smoothsignal)*0.148213-0.191034 102 | loc, beattime = self.QRSdetection(smoothsignal, ecg.fps, ecg.t, ftype=0) 103 | loc = loc.flatten() 104 | 105 | # 106 | # check error Kim vs. localmax of R peaks 107 | # 108 | new_loc = localmax_climb(ecg.x, loc, hwin=int(0.02*ecg.fps)) # hwin = 20 ms 109 | peak_errs = (new_loc - loc) / float(ecg.fps) 110 | #print 'np.mean(peak_errs), np.std(peak_errs)', np.mean(peak_errs), np.std(peak_errs) 111 | 112 | ibis = np.diff(loc / float(ecg.fps)) 113 | median_ibi = np.median(ibis) 114 | 115 | # 116 | # filter beats by cross-correlation with median beat 117 | # 118 | ecg_slices = np.array(slices(ecg.x, loc, hwin=int(np.ceil(median_ibi * ecg.fps))//2)) 119 | # median value from each timepoint (not a single one of any of the beats) 120 | median_beat = np.median(ecg_slices, axis=0) 121 | if debug: 122 | plt.plot(np.arange(len(median_beat))/float(ecg.fps), median_beat) 123 | plt.title('median ECG beat') 124 | cross_corrs = [cross_corr(sl, median_beat) for sl in ecg_slices] 125 | 126 | spectrum_ok = np.array([self.slice_good(sl, median_beat) for sl in ecg_slices]) 127 | ccs_ok = np.array(cross_corrs) > NoisyECG.GOOD_BEAT_THRESHOLD 128 | 129 | good_loc_idxs = np.where(ccs_ok & spectrum_ok)[0] 130 | if debug: 131 | [plt.plot(np.arange(len(ecg_slices[i]))/float(ecg.fps), ecg_slices[i]) for i in range(1,len(ecg_slices)) if i in good_loc_idxs] 132 | plt.title('all good ECG beats with rho > {:.2f}'.format(NoisyECG.GOOD_BEAT_THRESHOLD)) 133 | plt.show() 134 | 135 | beat_idxs = loc[good_loc_idxs] 136 | beat_times = beattime[good_loc_idxs] 137 | 138 | self._beattime, self._cross_corrs = beattime, cross_corrs 139 | 140 | if debug: 141 | self.debug_plot() 142 | 143 | #self.cross_corrs = np.array(cross_corrs)[good_loc_idxs] 144 | self.median_ibi = median_ibi 145 | self.good_beat_fraction = float(len(good_loc_idxs)) / len(cross_corrs) 146 | return beat_idxs, beat_times 147 | 148 | def debug_plot(self): 149 | ecg = self.ecg 150 | 151 | beattime, cross_corrs = self._beattime, self._cross_corrs 152 | 153 | fig, ax = plt.subplots(2, sharex=True) 154 | 155 | ecg.plot(ax[0]) 156 | ax[0].scatter(self.beat_times, ecg.x[self.beat_idxs], c='r') 157 | 158 | ax[1].stem(beattime, cross_corrs) 159 | 160 | plt.title('beat correlation with median beat') 161 | plt.show() 162 | 163 | 164 | def baseline_energy(ecg): 165 | """The lowest energy level in dB(1) (should be where ECG signal is).""" 166 | sll = int(ecg.fps*1.0) # slice len 167 | idxs = np.arange(0, len(ecg.x)-sll, sll) 168 | slices = [ecg.x[i:i+sll] for i in idxs] 169 | #btt = idxs / float(ecg.fps) 170 | energies = [10.0*np.log10(np.mean(sl**2)) for sl in slices] 171 | energies_hist = list(sorted(energies)) 172 | return np.mean(energies_hist[:5]) # at least 5 clean ECG beats should be there, hopefully 173 | 174 | 175 | def scrub_ecg(ecg_in, THRESHOLD = 8.0): 176 | """ 177 | return an ecg signal where noisy bits are set to zero 178 | 179 | # nb. scrub_ecg() always kills an already-scrubbed lowpass'd ECG... :/ why? 180 | """ 181 | #ecg = ecg_in.copy() 182 | #THRESHOLD = 8.0 # dB above baseline_energy() 183 | ecg = Series(ecg_in.x, ecg_in.fps, ecg_in.lpad) 184 | #ecg.x = highpass(ecg.x, fps=ecg.fps, cf=2.0, tw=0.4) 185 | baseline_db = baseline_energy(ecg) 186 | hwin = int(ecg.fps*0.5) 187 | check_centers = np.arange(hwin, len(ecg.x)-hwin+1, int(ecg.fps*0.1)) # more densely spaced than hwin 188 | verdict = [] 189 | for c in check_centers: 190 | sl = ecg.x[c-hwin:c+hwin+1] 191 | energy_db = 10.0*np.log10(np.mean(sl**2)) 192 | verdict.append(energy_db < baseline_db + THRESHOLD) 193 | 194 | good_locs = np.where(verdict)[0] 195 | #flood_fill_width = int(ecg.fps*0.8) 196 | flood_fill_width = 5 # cf. check_centers step size 197 | for i in good_locs: 198 | for j in range(max(i - flood_fill_width, 0), min(i + flood_fill_width + 1, len(verdict))): 199 | verdict[j] = True 200 | 201 | for c, v in zip(check_centers, verdict): 202 | if not v: 203 | ecg.x[c-hwin:c+hwin+1] *= 0.0 # zero the noisy bits 204 | 205 | #ecg.x = np.clip(ecg.x, np.mean(ecg.x) - 10*np.std(ecg.x), np.mean(ecg.x) + 10*np.std(ecg.x)) 206 | 207 | return ecg #, check_centers, verdict 208 | 209 | 210 | def beatdet_ecg(ecg_in): 211 | """ 212 | UNTESTED 213 | 214 | beatdet_ecg(scrub_ecg(ecg)) 215 | 216 | @see beatdet_alivecor(signal, fps=48000, lpad_t=0) in hsh_signal.alivecor 217 | """ 218 | from kimqrsdetector.kimqrsdetector import QRSdetection 219 | smoothsignal = ecg_in.x 220 | # adjust distribution to the one Kim has optimized for 221 | smoothsignal = (smoothsignal-np.mean(smoothsignal))/np.std(smoothsignal)*0.148213-0.191034 222 | loc, beattime = QRSdetection(smoothsignal, ecg_in.fps, ecg_in.t, ftype=0) 223 | loc = loc.flatten() 224 | return HeartSeries(ecg_in.x, loc, fps=ecg_in.fps) 225 | 226 | 227 | def ecg_snr(raw, fps): 228 | """SNR of AliveCor ECG in raw audio. For quick (0.5 sec) checking whether audio contains ECG or not.""" 229 | win_size = int(2.0*fps) # 2sec window 230 | f1 = float(fps) / float(win_size) # FFT frequency spacing 231 | power_ecg, power_noise = 0.0, 0.0 232 | for sl in range(0,len(raw)-win_size,win_size): 233 | slf = np.fft.fft(raw[sl:sl+win_size]) 234 | # power in the AliveCor band (18.8 kHz) 235 | ecg_band = slf[int(18000/f1):int(19600/f1)] 236 | # compared to high-freq baseline noise 237 | noise_band = slf[int(16000/f1):int(17600/f1)] 238 | pe, pn = np.sqrt(np.sum(np.abs(ecg_band)**2)) / len(ecg_band), np.sqrt(np.sum(np.abs(noise_band)**2)) / len(noise_band) 239 | power_ecg += pe 240 | power_noise += pn 241 | # nb. division removes const factor of #windows 242 | if power_noise == 0.0 and power_ecg == 0.0: 243 | return -10.0 # nothing? pretend bad SNR 244 | return 20.0 * np.log10(power_ecg / power_noise) 245 | 246 | 247 | def fix_ecg_peaks(ecg, plt=None): 248 | ecg = ecg.copy() 249 | 250 | slopesize = int(ecg.fps / 45.0) 251 | 252 | # climb to maxima, and invert if necessary 253 | ecgidx = [max(i - slopesize, 0) + np.argmax(ecg.x[max(i - slopesize, 0):min(i + slopesize, len(ecg.x) - 1)]) for i in ecg.ibeats] 254 | beatheight = np.mean(ecg.x[ecgidx]) - np.mean(ecg.x) # average detected beat amplitude 255 | negecgidx = [max(i - slopesize, 0) + np.argmin(ecg.x[max(i - slopesize, 0):min(i + slopesize, len(ecg.x) - 1)]) for i in ecg.ibeats] 256 | negbeatheight = np.mean(ecg.x[negecgidx]) - np.mean(ecg.x) # average detected beat amplitude in the other direction 257 | if np.abs(negbeatheight) > np.abs(beatheight): # if the other direction has "higher" peaks, invert signal 258 | ecg.x *= -1 259 | ecgidx = negecgidx 260 | 261 | if plt != None: 262 | plt.plot(ecg.t, ecg.x) 263 | plt.scatter(ecg.t[ecg.ibeats], ecg.x[ecg.ibeats], 30, 'y') 264 | 265 | window = slopesize / 2 266 | fixed_indices, fixed_times = [], [] 267 | # loop through and linearly interpolate peak flanks 268 | for i in ecgidx: 269 | up_start = i 270 | while ecg.x[up_start] >= ecg.x[i] and up_start > i - slopesize: # make sure start is in trough, not still on peak / plateau 271 | up_start -= 1 272 | up_start -= slopesize 273 | while ecg.x[up_start + 1] <= ecg.x[up_start] and up_start < i - 1: # climb past noise (need to go up) 274 | up_start += 1 275 | up_end = i + 2 276 | while ecg.x[up_end - 1] >= ecg.x[up_end] and up_end > i + 1: # climb past noise (need to go up) 277 | up_end -= 1 278 | upidx = np.arange(up_start, up_end) # indices of upslope 279 | 280 | down_start = i 281 | down_end = i 282 | while ecg.x[down_end] >= ecg.x[i] and down_end < i + slopesize: # make sure end is in trough, not still on peak / plateau 283 | down_end += 1 284 | down_end += slopesize 285 | while ecg.x[down_start + 1] >= ecg.x[down_start] or ecg.x[down_start + 2] >= ecg.x[down_start] and down_start < down_end: # climb past noise (need to go down) 286 | down_start += 1 287 | while ecg.x[down_end - 1] <= ecg.x[down_end] and down_end > down_start: # climb past noise (need to go down) 288 | down_end -= 1 289 | downidx = np.arange(down_start, down_end) # indices of downslope 290 | 291 | if len(ecg.t[upidx]) <= 1 or len(ecg.t[downidx]) <= 1: # one or both flanks missing. just use max 292 | reali = i 293 | bestt = ecg.t[i] 294 | else: 295 | # interpolate flanks 296 | model1 = TheilSenRegressor().fit(ecg.t[upidx].reshape(-1, 1), ecg.x[upidx]) 297 | model2 = TheilSenRegressor().fit(ecg.t[downidx].reshape(-1, 1), ecg.x[downidx]) 298 | k1, d1 = model1.coef_[0], model1.intercept_ 299 | k2, d2 = model2.coef_[0], model2.intercept_ 300 | angle1, angle2 = np.arctan(k1), np.arctan(k2) 301 | if False: 302 | pass 303 | else: 304 | bestt = (d2 - d1) / (k1 - k2) # obtain intersection point (noise robust peak) 305 | 306 | if np.abs(bestt - ecg.t[i]) > slopesize or np.abs(angle1) < 0.1 or np.abs(angle2) < 0.1: # calculated intersection point is very far from max - something went wrong - reset 307 | print("fix_ecg_peaks WARNING: fixed beat is very far from actual maximum, or slopes suspiciously unsteep. Taking actual maximum to be safe") 308 | i = max(i - slopesize, 0) + np.argmax(ecg.x[max(i - slopesize, 0):min(i + slopesize, len(ecg.x) - 1)]) 309 | if plt != None: 310 | reali = i - window + np.argmin(np.abs(ecg.t[(i - window):(i + window)] - bestt)) 311 | plt.scatter(bestt, ecg.x[reali], 200, 'y') 312 | plt.scatter(ecg.t[i], ecg.x[i], 200, 'g') 313 | plt.plot([bestt, ecg.t[i]], [ecg.x[reali], ecg.x[i]], 'r', linewidth=2) 314 | 315 | reali = i 316 | bestt = ecg.t[i] 317 | else: 318 | reali = i - window + np.argmin(np.abs(ecg.t[(i - window):(i + window)] - bestt)) 319 | 320 | # store fixed times and indices 321 | fixed_indices.append(reali) 322 | fixed_times.append(bestt) 323 | 324 | if plt != None: 325 | # plot 326 | plt.plot(ecg.t[upidx], ecg.x[upidx], 'g') 327 | plt.plot(ecg.t[downidx], ecg.x[downidx], 'm') 328 | 329 | if len(upidx) > 1 and len(downidx) > 1: 330 | plt.plot(ecg.t[upidx], model1.predict(ecg.t[upidx].reshape(-1, 1)), '--k') 331 | plt.plot(ecg.t[downidx], model2.predict(ecg.t[downidx].reshape(-1, 1)), '--y') 332 | 333 | plt.scatter(ecg.t[reali], ecg.x[reali], 60, 'r') 334 | plt.scatter(bestt, ecg.x[reali], 90, 'k') 335 | 336 | ecg.tbeats = np.ravel(fixed_times) 337 | ecg.ibeats = np.ravel(fixed_indices).astype(int) 338 | 339 | return ecg 340 | 341 | 342 | def ecg_kept(raw): 343 | sig0 = raw 344 | 345 | fps = sig0.fps 346 | 347 | # print 'slice.x shape=', sig0.slice(slice(int(0*fps), int(ECG_DURATION*fps))).x.shape 348 | ### 349 | 350 | ecg = beatdet_ecg(sig0.slice(slice(int(0 * fps), int(ECG_DURATION * fps)))) 351 | ecg.lead = LEADS[ECG_LEAD] 352 | 353 | # scaling for plot 354 | pcs = np.array([np.percentile(ecg.x, 10), np.percentile(ecg.x, 90)]) 355 | pcs = (pcs - np.mean(pcs)) * 5.0 + np.mean(pcs) 356 | 357 | ### 358 | 359 | slicez = sqi_slices(ecg, method='fixed', slice_front=0.5, slice_back=-0.5) 360 | L = max([len(sl) for sl in slicez]) 361 | padded_slicez = np.array([sig_pad(sl, L, side='center', mode='constant') for sl in slicez]) 362 | 363 | ### 364 | 365 | perc = envelopes_perc_threshold(padded_slicez) 366 | le, ue = envelopes_at_perc(padded_slicez, perc) 367 | mb = np.median(padded_slicez, axis=0) 368 | 369 | bpt = beat_penalty_threshold(le, ue, mb) 370 | 371 | bps = np.array([beat_penalty(sl, le, ue, mb) for sl in padded_slicez]) 372 | 373 | bp_ok = bps < bpt 374 | 375 | ### 376 | 377 | template = np.median(padded_slicez, axis=0) 378 | corrs = np.array([cross_corr(sl, template) for sl in padded_slicez]) 379 | 380 | CORR_THRESHOLD = 0.8 381 | corr_ok = corrs > CORR_THRESHOLD 382 | 383 | ### 384 | 385 | # next beat is OK as well? 386 | bp_ibi_ok = bp_ok & np.roll(bp_ok, -1) 387 | corr_ibi_ok = corr_ok & np.roll(corr_ok, -1) 388 | 389 | igood = np.where(bp_ibi_ok & corr_ibi_ok)[0] 390 | --------------------------------------------------------------------------------