plotpress 0.23.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
plotpress/_spectral.py ADDED
@@ -0,0 +1,154 @@
1
+ """Spectral estimators for the signal-processing plot methods.
2
+
3
+ Pure-NumPy Welch-averaged power / cross spectral density, coherence, single-shot
4
+ spectra, and lagged correlation. The conventions (segmenting, Hann window,
5
+ mean detrend, one-sided scaling) follow matplotlib's ``mlab`` closely enough
6
+ that the resulting plots line up -- without pulling in SciPy.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import numpy as np
12
+
13
+
14
+ def _as_segments(x, NFFT, noverlap):
15
+ """Split ``x`` into overlapping length-``NFFT`` rows, zero-padded if short."""
16
+ x = np.asarray(x, float).ravel()
17
+ if x.size < NFFT:
18
+ x = np.concatenate([x, np.zeros(NFFT - x.size)])
19
+ step = NFFT - noverlap
20
+ if step <= 0:
21
+ raise ValueError("noverlap must be less than NFFT")
22
+ n_seg = 1 + (x.size - NFFT) // step
23
+ idx = np.arange(NFFT)[None, :] + step * np.arange(n_seg)[:, None]
24
+ return x[idx], step # (n_seg, NFFT)
25
+
26
+
27
+ def _windowed_fft(x, NFFT, Fs, noverlap, window, detrend):
28
+ """Detrended, windowed rFFT of every segment. Returns (Z, freqs, win, step)."""
29
+ segs, step = _as_segments(x, NFFT, noverlap)
30
+ if detrend:
31
+ segs = segs - segs.mean(axis=1, keepdims=True)
32
+ win = window(NFFT) if callable(window) else np.asarray(window, float)
33
+ Z = np.fft.rfft(segs * win, n=NFFT, axis=1)
34
+ freqs = np.fft.rfftfreq(NFFT, d=1.0 / Fs)
35
+ return Z, freqs, win, step
36
+
37
+
38
+ def _onesided_double(P, NFFT):
39
+ """Double the non-DC / non-Nyquist bins for a one-sided spectrum (in place)."""
40
+ if NFFT % 2 == 0:
41
+ P[..., 1:-1] *= 2.0
42
+ else:
43
+ P[..., 1:] *= 2.0
44
+ return P
45
+
46
+
47
+ def psd(x, NFFT, Fs, noverlap, window, detrend):
48
+ """One-sided power spectral density by Welch averaging."""
49
+ Z, freqs, win, _ = _windowed_fft(x, NFFT, Fs, noverlap, window, detrend)
50
+ scale = 1.0 / (Fs * (win ** 2).sum())
51
+ Pxx = _onesided_double(np.abs(Z) ** 2 * scale, NFFT).mean(axis=0)
52
+ return Pxx, freqs
53
+
54
+
55
+ def csd(x, y, NFFT, Fs, noverlap, window, detrend):
56
+ """One-sided cross spectral density ``Pxy`` (complex)."""
57
+ Zx, freqs, win, _ = _windowed_fft(x, NFFT, Fs, noverlap, window, detrend)
58
+ Zy, _, _, _ = _windowed_fft(y, NFFT, Fs, noverlap, window, detrend)
59
+ scale = 1.0 / (Fs * (win ** 2).sum())
60
+ Pxy = _onesided_double(Zx * np.conj(Zy) * scale, NFFT).mean(axis=0)
61
+ return Pxy, freqs
62
+
63
+
64
+ def cohere(x, y, NFFT, Fs, noverlap, window, detrend):
65
+ """Magnitude-squared coherence ``|Pxy|^2 / (Pxx Pyy)`` in ``[0, 1]``.
66
+
67
+ Coherence is only meaningful when the estimate averages several segments;
68
+ a single segment makes it identically 1 (as in matplotlib).
69
+ """
70
+ Zx, freqs, win, _ = _windowed_fft(x, NFFT, Fs, noverlap, window, detrend)
71
+ Zy, _, _, _ = _windowed_fft(y, NFFT, Fs, noverlap, window, detrend)
72
+ scale = 1.0 / (Fs * (win ** 2).sum())
73
+ Pxx = _onesided_double(np.abs(Zx) ** 2 * scale, NFFT).mean(axis=0)
74
+ Pyy = _onesided_double(np.abs(Zy) ** 2 * scale, NFFT).mean(axis=0)
75
+ Pxy = _onesided_double(Zx * np.conj(Zy) * scale, NFFT).mean(axis=0)
76
+ # An empty/all-zero x and y (zero-padded by _as_segments rather than
77
+ # rejected -- a real segment length is still needed either way) makes
78
+ # Pxx == Pyy == 0 everywhere: 0/0, mathematically undefined coherence,
79
+ # not a bug -- silence the resulting "invalid value" noise rather than
80
+ # rejecting a case NFFT-segmenting already treats as legitimate.
81
+ with np.errstate(invalid="ignore"):
82
+ Cxy = np.abs(Pxy) ** 2 / (Pxx * Pyy)
83
+ return Cxy, freqs
84
+
85
+
86
+ def specgram(x, NFFT, Fs, noverlap, window, detrend):
87
+ """Spectrogram: one-sided power per segment. Returns (P, freqs, t).
88
+
89
+ ``P`` has shape ``(n_freqs, n_segments)`` -- ready for ``imshow``.
90
+ """
91
+ Z, freqs, win, step = _windowed_fft(x, NFFT, Fs, noverlap, window, detrend)
92
+ scale = 1.0 / (Fs * (win ** 2).sum())
93
+ P = _onesided_double(np.abs(Z) ** 2 * scale, NFFT) # (n_seg, n_freq)
94
+ t = (np.arange(P.shape[0]) * step + NFFT / 2.0) / Fs
95
+ return P.T, freqs, t
96
+
97
+
98
+ def _single_spectrum(x, Fs, window, detrend):
99
+ """Windowed rFFT of the whole signal (no segmenting)."""
100
+ x = np.asarray(x, float).ravel()
101
+ if x.size == 0:
102
+ # x.mean() on an empty array below leaks a raw "Mean of empty
103
+ # slice" RuntimeWarning before np.fft eventually raises its own
104
+ # clear error a few lines further on -- raise that same "nothing
105
+ # to do" error here instead, without the noise in front of it.
106
+ raise ValueError("magnitude_spectrum()/angle_spectrum()/phase_spectrum(): x must not be empty")
107
+ if detrend:
108
+ x = x - x.mean()
109
+ n = x.size
110
+ win = window(n) if callable(window) else np.asarray(window, float)
111
+ Z = np.fft.rfft(x * win)
112
+ freqs = np.fft.rfftfreq(n, d=1.0 / Fs)
113
+ return Z, freqs, win, n
114
+
115
+
116
+ def magnitude_spectrum(x, Fs, window, detrend):
117
+ """One-sided magnitude spectrum ``|X(f)|``."""
118
+ Z, freqs, win, n = _single_spectrum(x, Fs, window, detrend)
119
+ mag = _onesided_double(np.abs(Z) / win.sum(), n)
120
+ return mag, freqs
121
+
122
+
123
+ def angle_spectrum(x, Fs, window, detrend):
124
+ """Wrapped phase spectrum in radians (``-pi..pi``)."""
125
+ Z, freqs, _, _ = _single_spectrum(x, Fs, window, detrend)
126
+ return np.angle(Z), freqs
127
+
128
+
129
+ def phase_spectrum(x, Fs, window, detrend):
130
+ """Unwrapped phase spectrum in radians."""
131
+ Z, freqs, _, _ = _single_spectrum(x, Fs, window, detrend)
132
+ return np.unwrap(np.angle(Z)), freqs
133
+
134
+
135
+ def correlation(x, y, detrend, normed, maxlags):
136
+ """Lagged cross-correlation. Returns ``(lags, c)`` over ``+-maxlags``."""
137
+ x = np.asarray(x, float).ravel()
138
+ y = np.asarray(y, float).ravel()
139
+ n = x.size
140
+ if y.size != n:
141
+ raise ValueError("x and y must be the same length")
142
+ if detrend:
143
+ x = x - x.mean()
144
+ y = y - y.mean()
145
+ c = np.correlate(x, y, mode="full")
146
+ if normed:
147
+ c = c / (np.sqrt(np.dot(x, x) * np.dot(y, y)) or 1.0)
148
+ if maxlags is None:
149
+ maxlags = n - 1
150
+ if not 0 <= maxlags < n:
151
+ raise ValueError("maxlags must be in 0..len(x)-1")
152
+ lags = np.arange(-maxlags, maxlags + 1)
153
+ c = c[n - 1 - maxlags:n + maxlags]
154
+ return lags, c
plotpress/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.23.2"