From 4b43e6a2ec418a1779638c64d7403e3b95bb6ac7 Mon Sep 17 00:00:00 2001 From: Christopher Holmes Date: Fri, 14 Aug 2026 16:14:12 -0400 Subject: [PATCH 1/4] Allow setting frequency units for psd In psd, allow user to specify the units for the sampling frequency of the analyzed array. psd previously assumed units to be Hz and displayed this on the y-axis label. Users can now specify other units. This change has no effect on the psd calculation. --- lib/matplotlib/axes/_axes.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 09aef856dc48..7ea346616a00 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -8111,7 +8111,7 @@ def ecdf(self, x, weights=None, *, complementary=False, @_api.make_keyword_only("3.10", "NFFT") @_preprocess_data(replace_names=["x"]) @_docstring.interpd - def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, + def psd(self, x, NFFT=None, Fs=None, Funits=None, Fc=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None, return_line=None, **kwargs): r""" @@ -8144,6 +8144,10 @@ def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, plot to reflect the frequency range used when a signal is acquired and then filtered and downsampled to baseband. + Funits : str, default: 'Hz' + Units for the sampling frequency *Fc*. It is used to label the + xaxis and yaxis. + return_line : bool, default: False Whether to include the line object plotted in the returned values. @@ -8194,6 +8198,8 @@ def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, """ if Fc is None: Fc = 0 + if Funits is None: + Funits = 'Hz' pxx, freqs = mlab.psd(x=x, NFFT=NFFT, Fs=Fs, detrend=detrend, window=window, noverlap=noverlap, pad_to=pad_to, @@ -8201,12 +8207,12 @@ def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, freqs += Fc if scale_by_freq in (None, True): - psd_units = 'dB/Hz' + psd_units = 'dB/%s' % Funits else: psd_units = 'dB' line = self.plot(freqs, 10 * np.log10(pxx), **kwargs) - self.set_xlabel('Frequency') + self.set_xlabel('Frequency (%s)' % Funits) self.set_ylabel('Power Spectral Density (%s)' % psd_units) self.grid(True) From 52fe0fa42ec4e38db550cc266f2595140fb5a5bd Mon Sep 17 00:00:00 2001 From: Christopher Holmes Date: Fri, 14 Aug 2026 16:51:06 -0400 Subject: [PATCH 2/4] Update psd documentation, example, type hinting, release notes --- .../development/32211_CDH.rst | 6 ++++ .../next_whats_new/new_psd_feature.rst | 30 +++++++++++++++++++ galleries/examples/statistics/psd_demo.py | 3 ++ lib/matplotlib/axes/_axes.py | 4 +-- lib/matplotlib/axes/_axes.pyi | 1 + lib/matplotlib/pyplot.py | 2 ++ 6 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 doc/api/next_api_changes/development/32211_CDH.rst create mode 100644 doc/release/next_whats_new/new_psd_feature.rst diff --git a/doc/api/next_api_changes/development/32211_CDH.rst b/doc/api/next_api_changes/development/32211_CDH.rst new file mode 100644 index 000000000000..c51e2e76c068 --- /dev/null +++ b/doc/api/next_api_changes/development/32211_CDH.rst @@ -0,0 +1,6 @@ +Power Spectral Density +~~~~~~~~~~~~~~~~~~~~~~ +An optional keyword *Funits* is added to `.Axes.psd` so +units that differ from the default 'Hz' can be specified. +This change is backward compatible (no change if the optional +keyword is omitted.) diff --git a/doc/release/next_whats_new/new_psd_feature.rst b/doc/release/next_whats_new/new_psd_feature.rst new file mode 100644 index 000000000000..4338354436c7 --- /dev/null +++ b/doc/release/next_whats_new/new_psd_feature.rst @@ -0,0 +1,30 @@ +Sampling frequency units can be specified for `.Axes.psd` +--------------------------------------------------------------- + +When creating a power spectral density (psd) plot, the units of the +sampling frequency can be specified. (Units were previously always +assumed to be Hz.) + +:: + + import matplotlib.pyplot as plt + import numpy as np + + # Sampling period in units of days + dt = 1/24 + + # Create example signal: sinusoid with red noise + np.random.seed(19680801) # Fixing random state for reproducibility + t = np.arange(0, 20, dt) + nse = np.random.randn(len(t)) + r = np.exp(-t / 0.05) + cnse = np.convolve(nse, r) * dt + cnse = cnse[:len(t)] + s = 0.1 * np.sin(2 * np.pi * t) + cnse + + # Show signal and power spectral density + fig, (ax0, ax1) = plt.subplots(2, 1, layout='constrained') + ax0.plot(t,s) + ax0.set(xlabel='Time (d)', ylabel='Signal') + ax1.psd(s, NFFT=256, Fs=1 / dt, Funits='cpd') + plt.show() diff --git a/galleries/examples/statistics/psd_demo.py b/galleries/examples/statistics/psd_demo.py index bf564df7542c..fd5c4646cb29 100644 --- a/galleries/examples/statistics/psd_demo.py +++ b/galleries/examples/statistics/psd_demo.py @@ -33,6 +33,9 @@ ax0.set_xlabel('Time (s)') ax0.set_ylabel('Signal') ax1.psd(s, NFFT=512, Fs=1 / dt) +# If dt had other units (e.g. days instead of seconds), +# then the units of Fs (e.g. cycles per day or cps) can be specified +# by the keyword Funits (e.g. Funits='cpd') in psd. plt.show() diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 7ea346616a00..789f9475ea5d 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -8145,8 +8145,8 @@ def psd(self, x, NFFT=None, Fs=None, Funits=None, Fc=None, detrend=None, and then filtered and downsampled to baseband. Funits : str, default: 'Hz' - Units for the sampling frequency *Fc*. It is used to label the - xaxis and yaxis. + Units for the sampling frequency *Fc*. It is used to label the + xaxis and yaxis. return_line : bool, default: False Whether to include the line object plotted in the returned values. diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 227eba9be6d4..a475d4154819 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -643,6 +643,7 @@ class Axes(_AxesBase): *, NFFT: int | None = ..., Fs: float | None = ..., + Funits: str | None = ..., Fc: int | None = ..., detrend: Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 8315056c81a2..5ad5874dbcae 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -4060,6 +4060,7 @@ def psd( x: ArrayLike, NFFT: int | None = None, Fs: float | None = None, + Funits: str | None = None, Fc: int | None = None, detrend: ( Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] | None @@ -4078,6 +4079,7 @@ def psd( x, NFFT=NFFT, Fs=Fs, + Funits=Funits, Fc=Fc, detrend=detrend, window=window, From 740c2497e8542bd4dce9afc8f00885f453f0240d Mon Sep 17 00:00:00 2001 From: Christopher Holmes Date: Sat, 15 Aug 2026 23:01:32 -0400 Subject: [PATCH 3/4] Rearrange psd arguments to minimize API impact --- lib/matplotlib/axes/_axes.py | 11 ++++++----- lib/matplotlib/axes/_axes.pyi | 2 +- lib/matplotlib/pyplot.py | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 789f9475ea5d..a01d3748a3bb 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -8111,9 +8111,10 @@ def ecdf(self, x, weights=None, *, complementary=False, @_api.make_keyword_only("3.10", "NFFT") @_preprocess_data(replace_names=["x"]) @_docstring.interpd - def psd(self, x, NFFT=None, Fs=None, Funits=None, Fc=None, detrend=None, + def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, pad_to=None, - sides=None, scale_by_freq=None, return_line=None, **kwargs): + sides=None, scale_by_freq=None, return_line=None, Funits=None, + **kwargs): r""" Plot the power spectral density. @@ -8144,13 +8145,13 @@ def psd(self, x, NFFT=None, Fs=None, Funits=None, Fc=None, detrend=None, plot to reflect the frequency range used when a signal is acquired and then filtered and downsampled to baseband. + return_line : bool, default: False + Whether to include the line object plotted in the returned values. + Funits : str, default: 'Hz' Units for the sampling frequency *Fc*. It is used to label the xaxis and yaxis. - return_line : bool, default: False - Whether to include the line object plotted in the returned values. - Returns ------- Pxx : 1-D array diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index a475d4154819..855ba30b07f8 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -643,7 +643,6 @@ class Axes(_AxesBase): *, NFFT: int | None = ..., Fs: float | None = ..., - Funits: str | None = ..., Fc: int | None = ..., detrend: Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] @@ -655,6 +654,7 @@ class Axes(_AxesBase): scale_by_freq: bool | None = ..., return_line: bool | None = ..., data: DataParamType = ..., + Funits: str | None = ..., **kwargs ) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, Line2D]: ... def csd( diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 5ad5874dbcae..6671e07af64c 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -4060,7 +4060,6 @@ def psd( x: ArrayLike, NFFT: int | None = None, Fs: float | None = None, - Funits: str | None = None, Fc: int | None = None, detrend: ( Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] | None @@ -4071,6 +4070,7 @@ def psd( sides: Literal["default", "onesided", "twosided"] | None = None, scale_by_freq: bool | None = None, return_line: bool | None = None, + Funits: str | None = None, *, data: DataParamType = None, **kwargs, @@ -4079,7 +4079,6 @@ def psd( x, NFFT=NFFT, Fs=Fs, - Funits=Funits, Fc=Fc, detrend=detrend, window=window, @@ -4088,6 +4087,7 @@ def psd( sides=sides, scale_by_freq=scale_by_freq, return_line=return_line, + Funits=Funits, **({"data": data} if data is not None else {}), **kwargs, ) From b7a56e226ac8f1e2e856d0c2f644388c52d92813 Mon Sep 17 00:00:00 2001 From: Christopher Holmes Date: Sun, 16 Aug 2026 23:27:21 -0400 Subject: [PATCH 4/4] Remove assumed frequency units (Hz) from comments --- lib/matplotlib/axes/_axes.py | 2 +- lib/matplotlib/mlab.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index a01d3748a3bb..2fdc21108094 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -8149,7 +8149,7 @@ def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, Whether to include the line object plotted in the returned values. Funits : str, default: 'Hz' - Units for the sampling frequency *Fc*. It is used to label the + Units for the sampling frequency *Fs*. It is used to label the xaxis and yaxis. Returns diff --git a/lib/matplotlib/mlab.py b/lib/matplotlib/mlab.py index a694308384c1..2063c9744c31 100644 --- a/lib/matplotlib/mlab.py +++ b/lib/matplotlib/mlab.py @@ -363,8 +363,8 @@ def _spectral_helper(x, y=None, NFFT=None, Fs=None, detrend_func=None, result[slc] *= scaling_factor # MATLAB divides by the sampling frequency so that density function - # has units of dB/Hz and can be integrated by the plotted frequency - # values. Perform the same scaling here. + # has units of V**2/Hz, if x is measured in units of V and the sampling + # frequency is measured in Hz. Perform the same scaling here. if scale_by_freq: result /= Fs # Scale the spectrum by the norm of the window to compensate for @@ -470,10 +470,10 @@ def _single_spectrum_helper( `.detrend_mean`. 'linear' calls `.detrend_linear`. scale_by_freq : bool, default: True - Whether the resulting density values should be scaled by the scaling - frequency, which gives density in units of 1/Hz. This allows for - integration over the returned frequency values. The default is True for - MATLAB compatibility.""") + Whether the resulting density values should be divided by the sampling + frequency, which gives density in units of 1/Hz, if the sampling rate + is measured in Hz. This allows for integration over the returned + frequency values. The default is True for MATLAB compatibility.""") @_docstring.interpd