Skip to content

Commit bed0c4a

Browse files
authored
Merge pull request #6 from myokit/spectral
Added two spectral analysis methods
2 parents 2e544be + 6cecb6f commit bed0c4a

File tree

9 files changed

+201
-3
lines changed

9 files changed

+201
-3
lines changed

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,34 @@
33

44
# Datkit
55

6-
This module contains some simple methods that are useful when analysing time
6+
This module contains some simple methods that are useful when analysing time
77
series data.
88

9-
The code is tested on a recent version of Ubuntu & Python 3, but is so simple that it should work everywhere else too.
9+
The code is tested on a recent version of Ubuntu & Python 3, but is so simple
10+
that it should work everywhere else too.
11+
12+
## Installation
13+
14+
To install the latest release from PyPI, use
15+
16+
```
17+
pip install datkit
18+
```
19+
20+
## Installation for development
21+
22+
To install from the repo, use e.g.
23+
```
24+
python setup.py install -e .
25+
```
26+
27+
Tests can then be run with
28+
```
29+
python -m unittest
30+
```
31+
32+
And docs can be built with
33+
```
34+
cd docs
35+
make clean html
36+
```

datkit/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,9 @@
7272
moving_average,
7373
window_size,
7474
)
75+
76+
from ._spectral import ( # noqa
77+
amplitude_spectrum,
78+
power_spectral_density,
79+
)
80+

datkit/_spectral.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#
2+
# Methods to inspect time series in the frequency domain.
3+
# Note that much more advanced methods are available e.g. in scipy.
4+
#
5+
# This file is part of Datkit.
6+
# For copyright, sharing, and licensing, see https://github.com/myokit/datkit/
7+
#
8+
import numpy as np
9+
10+
import datkit
11+
12+
13+
def amplitude_spectrum(times, values):
14+
"""
15+
Calculates the amplitude spectrum of a regularly spaced time series
16+
``(times, current)``, returning a tuple ``(frequency, amplitude)``.
17+
18+
Example::
19+
20+
t = np.linspace(0, 10, 1000)
21+
v = 3 * np.sin(t * (2 * np.pi * 2)) + 10 * np.cos(t * (2 * np.pi * 3))
22+
f, a = amplitude_spectrum(t, v)
23+
24+
fig = plt.figure()
25+
ax = fig.add_subplot(2, 1, 1)
26+
ax.plot(t, v)
27+
ax = fig.add_subplot(2, 1, 2)
28+
ax.set_xlim(0, 10)
29+
ax.stem(f, a)
30+
plt.show()
31+
32+
"""
33+
dt = datkit.sampling_interval(times)
34+
n = len(times)
35+
# Select only the positive frequencies
36+
m = n // 2
37+
# FFT, normalised by number of point
38+
a = np.absolute(np.fft.fft(values)[:m]) / n
39+
# Only using one half, so multiply values of mirrored points by 2
40+
a[1:-1] *= 2
41+
# Get corresponding frequencies and return
42+
f = np.fft.fftfreq(n, dt)[:m]
43+
return f, a
44+
45+
46+
def power_spectral_density(times, values):
47+
"""
48+
Estimates the power spectral density of a regularly spaced time series
49+
``(times, current)``, returning a tuple ``(frequency, density)``.
50+
51+
For times in units "T" and values in units "V", the returned frequencies
52+
will be in units "F=1/T" and the densities in "V^2/F".
53+
54+
For a less noisy version, use ``scipy.signal.welch(values,
55+
1 / datkit.sampling_interval(times))`` instead.
56+
57+
Example::
58+
59+
t = np.linspace(0, 10, 1000)
60+
v = 5 * np.sin(t * (2 * np.pi * 2)) + 10 * np.cos(t * (2 * np.pi * 3))
61+
f, psd = power_spectral_density(t, v)
62+
63+
fig = plt.figure()
64+
ax = fig.add_subplot(2, 1, 1)
65+
ax.plot(t, v)
66+
ax = fig.add_subplot(2, 1, 2)
67+
ax.set_yscale('log')
68+
ax.set_xlim(0, 10)
69+
ax.plot(f, psd)
70+
plt.show()
71+
72+
"""
73+
dt = datkit.sampling_interval(times)
74+
n = len(times)
75+
m = n // 2
76+
p = np.absolute(np.fft.fft(values)[:m])**2 * dt / n
77+
p[1:-1] *= 2
78+
f = np.fft.fftfreq(n, dt)[:m]
79+
return f, p
80+

datkit/tests/test_spectral.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Tests the datkit spectral analysis methods.
4+
#
5+
# This file is part of Datkit.
6+
# For copyright, sharing, and licensing, see https://github.com/myokit/datkit/
7+
#
8+
import unittest
9+
10+
import numpy as np
11+
12+
import datkit as d
13+
14+
15+
class SpectralTest(unittest.TestCase):
16+
""" Tests methods from the hidden _spectral module. """
17+
18+
def test_amplitude_spectrum(self):
19+
20+
t = np.linspace(0, 10, 1000)
21+
v = 3 * np.sin(t * (2 * np.pi * 3)) + 10 * np.cos(t * (2 * np.pi * 7))
22+
f, a = d.amplitude_spectrum(t, v)
23+
self.assertAlmostEqual(f[30], 2.997)
24+
self.assertAlmostEqual(f[70], 6.993)
25+
self.assertLess(a[29], 0.1)
26+
self.assertAlmostEqual(a[30], 3, 0)
27+
self.assertLess(a[31], 0.1)
28+
self.assertLess(a[69], 1)
29+
self.assertAlmostEqual(a[70], 10, 0)
30+
self.assertLess(a[71], 1)
31+
32+
t = np.linspace(0, 10, 1235)
33+
v = 6 * np.sin(t * (2 * np.pi * 2)) + 4 * np.cos(t * (2 * np.pi * 5))
34+
f, a = d.amplitude_spectrum(t, v)
35+
self.assertAlmostEqual(f[20], 1.998, 3)
36+
self.assertAlmostEqual(f[50], 4.996, 3)
37+
self.assertLess(a[19], 0.11)
38+
self.assertAlmostEqual(a[20], 6, 0)
39+
self.assertLess(a[21], 0.11)
40+
self.assertLess(a[49], 0.2)
41+
self.assertAlmostEqual(a[50], 4, 0)
42+
self.assertLess(a[51], 0.2)
43+
44+
def test_power_spectral_density(self):
45+
46+
t = np.linspace(0, 10, 1000)
47+
v = 3 * np.sin(t * (2 * np.pi)) + 7 * np.cos(t * (2 * np.pi * 3))
48+
f, psd = d.power_spectral_density(t, v)
49+
self.assertAlmostEqual(f[10], 0.999, 3)
50+
self.assertAlmostEqual(f[30], 2.997, 3)
51+
52+
self.assertLess(psd[9], 0.01)
53+
self.assertAlmostEqual(psd[10], 45, 1)
54+
self.assertLess(psd[11], 0.01)
55+
self.assertLess(psd[29], 0.3)
56+
self.assertAlmostEqual(psd[30], 245, 0)
57+
self.assertLess(psd[31], 0.3)
58+
59+
60+
if __name__ == '__main__':
61+
unittest.main()

docs/source/checking_time_vectors.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
Checking time vectors
33
*********************
44

5+
The methods below can be used to test if sequences (typically numpy arrays)
6+
contain regularly sampled times.
7+
58
.. currentmodule:: datkit
69

710
.. autofunction:: is_increasing

docs/source/finding_points.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
Finding points
33
**************
44

5+
The methods listed here let you find indices corresponding to points in time,
6+
or perform aggregate functions (e.g. mean, min, max) on intervals within a time
7+
series.
8+
59
.. currentmodule:: datkit
610

711
Indices and values at specific times

docs/source/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ series data.
1010
.. toctree::
1111

1212
self
13+
checking_time_vectors
1314
finding_points
1415
smoothing
15-
checking_time_vectors
16+
spectral_analysis
1617

docs/source/smoothing.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Smoothing
33
*********
44

5+
The methods below can perform basic smoothing of noisy time series.
6+
57
.. currentmodule:: datkit
68

79
.. autofunction:: moving_average

docs/source/spectral_analysis.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
*****************
2+
Spectral analysis
3+
*****************
4+
5+
The methods listed below can be used to perform very rudimentary spectral (or
6+
frequency domain) analysis. They are most useful to make quick plots of the
7+
frequency content of a time series.
8+
9+
.. currentmodule:: datkit
10+
11+
.. autofunction:: amplitude_spectrum
12+
13+
.. autofunction:: power_spectral_density
14+

0 commit comments

Comments
 (0)