Open yagizolmez opened 11 months ago
I would like to follow up on this. Could you assign me this issue, if this is a function you would want to have?
Hi @yagizolmez, please see the docs for contributing new code. The first step is to put your proposal to the mailing list.
Seems like Matlab's function is a wrapper. Some of the functionalities that are suggested are implemented in signal.hilbert or pandas. @yagizolmez, are you suggesting to create a wrapper?
Regardless of the algorithm, please make sure that you do not check matlab's sourcecode (not sure if it is even possible) but same with octave and other incompatible licensed sources. It is OK to have a similar function signature but we cannot accept a derivative of such codebase and/or algorithm.
@lucascolley I shared it in the email group, but the discussion seems to be happening here.
@gideonKogan I would be happy if you could share the functions in pandas. signal.hilbert indeed implements the 'analytic' option of the MATLAB function but does not implement the other methods ('rms', 'peak').
@ilayn Thank you for the warning. I will not check MATLAB's codebase.
Please let me know what you think. Happy New Year to all of you and thanks for commenting.
import pandas as pd
import numpy as np
fs = 1000
time = 10
freq = 1
window = 50
t = np.arange(time * fs) / fs
x = pd.Series((np.sin(2 * np.pi * freq * t) + 1) * np.random.normal(size=t.size))
ax = x.plot() #original
((x**2).rolling(window=window).mean()**0.5).plot(ax = ax) #rms
x.rolling(window=window).max().plot(ax = ax, grid=True) #peak
@ggkogan Thanks for sharing this. Your solution looks sound to me, but as far as I know scipy does not have a dependency on pandas. Would people be OK to add such dependency for this one function? @ilayn
Your code also needs to be slightly modified to support upper and lower envelopes.
We could also add additional envelope extraction methods that are not covered by the MATLAB function such as rectify and low pass filter (also known as linear envelope), which is commonly used in EMG analysis.
No, we will not be depending on pandas.
@ggkogan, @rkern I have written a function that depends only on numpy and scipy, that follows MATLAB's function signature, and seems to work well.
import numpy as np
from scipy.signal import hilbert,find_peaks
from scipy.interpolate import UnivariateSpline
def envelope(x,N = None,method = 'analytic'):
#Assert that x is a 1D numpy array
assert isinstance(x, np.ndarray) and x.ndim == 1, 'x must be a 1D NumPy array'
if method == 'analytic':
#Calculate the mean of x and remove it
x_mean = np.mean(x)
x_zero_mean = x-x_mean
#Take the absolute value of the Hilbert transform
#to calculate the analytical envelope of the zero mean
#version of x
zero_mean_envelope = np.abs(hilbert(x_zero_mean,N = N))
#Calculate and return upper and lower envelopes
return x_mean+zero_mean_envelope,x_mean-zero_mean_envelope
elif method == 'rms':
#Assert that N is specified and is an integer
#For the rms method N serves as window size
#for the moving average
assert N and isinstance(N,int), 'N must be an integer'
#Calculate the mean of x and remove it
x_mean = np.mean(x)
x_zero_mean = x-x_mean
#Calculate the RMS envelope of the zero mean version of x.
#Moving average is calculated using np.convolve
zero_mean_envelope = np.sqrt(np.convolve(x_zero_mean**2,np.ones(N)/N,mode = 'same'))
#Calculate and return upper and lower envelopes
return x_mean+zero_mean_envelope,x_mean-zero_mean_envelope
elif method == 'peak':
#Assert that N is specified and is an integer
#For the peak method N serves as the minimum number of
#samples that seperate local maxima
assert N and isinstance(N,int), 'N must be an integer'
#Calculate local maxima and minima which will serve
#as peaks for upper and lower envelopes respectively
peaks_upper,_ = find_peaks(x,distance = N)
peaks_lower,_ = find_peaks(-x,distance = N)
#Calculate upper and lower envelopes by interpolating
#peaks using a Univariate spline
upper_spline = UnivariateSpline(peaks_upper,x[peaks_upper])
lower_spline = UnivariateSpline(peaks_lower,x[peaks_lower])
upper_envelope = upper_spline(np.arange(x.shape[0]))
lower_envelope = lower_spline(np.arange(x.shape[0]))
#Return the envelopes
return upper_envelope,lower_envelope
else:
raise ValueError('%s is not a valid method' %(method))
Test for analytical envelope extraction:
import matplotlib.pyplot as plt
t = np.arange(0, 2, 1/2000)
signal = (1+0.5*np.cos(2*np.pi*1*t))*np.cos(2*np.pi*10*t)+5
upper,lower = envelope(signal)
plt.plot(t, signal)
plt.plot(t, upper)
plt.plot(t, lower)
plt.show()
Test for RMS envelope extraction:
t = np.arange(0, 2, 1/2000)
signal = (1+0.5*np.cos(2*np.pi*1*t))*np.random.normal(size = t.shape[0])+5
upper,lower = envelope(signal,N = 150,method = 'rms')
plt.plot(t, signal)
plt.plot(t, upper)
plt.plot(t, lower)
plt.show()
Test for peak envelope extraction
from scipy.io.wavfile import read
fs,signal = read('FK61_01.wav')
t = np.linspace(1,signal.shape[0],signal.shape[0])/fs
upper,lower = envelope(signal,N = 200,method = 'peak')
plt.plot(t, signal)
plt.plot(t, upper)
plt.plot(t, lower)
plt.show()
Please let me know what you think!
@yagizolmez your analytic filtering should be in time domain. There might appear other deviatios. I suggest you try to test for bit-exactness.
Are we sure that we want to replicate Matlab's function? Why?
I am not sure whether we should try to reduce running time by Cython implementation.
@ggkogan I do not understand your comment about the analytical filtering. I simply use signal.hilbert as you suggested in your first comment. I would be happy if you could be a bit more specific or provide some code.
I think using similar function signature with MATLAB would make it easier for people to migrate their codes into scipy. MATLAB Signal Processing Toolbox is unfortunately still more efficient than scipy.signal and it remains more widely used especially in the sciences. Many functions in scipy.signal have the same signature as their MATLAB counter parts (hilbert,butter,filtfilt,chirp etc).
Admittedly, I do not have much experience with cython. However, I would like to note that the code I provided does not have any python level loops. It relies on other functions in numpy and scipy. Considering that numpy is already partially written in C, I don't know how much of an improvement cython would provide.
I think using similar function signature with MATLAB would make it easier for people to migrate their codes into scipy
That is correct but we all are in a way, past matlab survivors. Being similar is good but if matlab signature is not good we don't insist on the affinity. This is particularly the case especially for matlab's legacy functions, wrong defaults, terrible option names and so on.
Admittedly, I do not have much experience with cython
Let's get there when it is needed; we can help you with all that. A bit of benchmarking would go a long way, even if it is done on a jupyter notebook with %timeit
magic.
@yagizolmez signal.hilbert gives envelope but if we try to reproduce the envelope you presented, we should implement in time domain. I suspect that the original implementation was designed to maintain causality, which is not maintained by using signal.hilbert.
@gideonKogan Do you suggest that I should implement the 'analytical' option with a causal filter? Causal filters introduce lags, so the filtered envelope would be lagged. I don't think we should do that.
@ilayn I personally like the MATLAB signature in this case, but I am open to suggestions. I will post the benchmarks shortly.
@ilayn I did some benchmarking on the code I have provided. %timeit gave the following outputs for the analytic, rms and peak respectively:
202 µs ± 16.3 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each) 210 µs ± 3.45 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each) 122 ms ± 18.2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
I have written the following code to benchmark the MATLAB function
%analytic
% Your code block here
t = 0:1/2000:2;
signal = (1 + 0.5 * cos(2 * pi * 1 * t)) .* cos(2 * pi * 10 * t) + 5;
% Number of iterations
numIterations = 1000;
% Measure execution time for multiple runs
totalTime = 0;
for i = 1:numIterations
tic;
upper = envelope(signal);
elapsedTime = toc;
totalTime = totalTime + elapsedTime;
end
% Calculate the average execution time
averageTime = totalTime / numIterations;
% Display the average execution time
fprintf('Average execution time of analytic over %d iterations: %.2f microseconds\n', numIterations, averageTime*10^6);
%rms
t = 0:1/2000:2; % Create a time vector from 0 to 2 with a step of 1/2000
signal = (1 + 0.5 * cos(2 * pi * 1 * t)) .* randn(size(t)) + 5; % Define the signal using random normal distribution
% Measure execution time for multiple runs
totalTime = 0;
for i = 1:numIterations
tic;
upper = envelope(signal,150,'rms');
elapsedTime = toc;
totalTime = totalTime + elapsedTime;
end
% Calculate the average execution time
averageTime = totalTime / numIterations;
% Display the average execution time
fprintf('Average execution time of rms over %d iterations: %.2f microseconds\n', numIterations, averageTime*10^6);
%peak
% Read the WAV file
[signal, fs] = audioread('FK61_01.wav');
% Create a time vector
t = (1:length(signal)) / fs;
% Measure execution time for multiple runs
totalTime = 0;
for i = 1:numIterations
tic;
upper = envelope(signal,200,'peak');
elapsedTime = toc;
totalTime = totalTime + elapsedTime;
end
% Calculate the average execution time
averageTime = totalTime / numIterations;
% Display the average execution time
fprintf('Average execution time of peak over %d iterations: %.2f milliseconds\n', numIterations, averageTime*10^3);
The output of this code was:
Average execution time of analytic over 1000 iterations: 572.22 microseconds Average execution time of rms over 1000 iterations: 130.67 microseconds Average execution time of peak over 1000 iterations: 40.49 milliseconds
The peak method seems to be 3x faster in MATLAB. Please let me know what you think.
@yagizolmez , I argue that we should decide if we want Matlab to be our reference. If we do, we should aim to bit-accurate. If we do not, we should decide how the function should be and ignore Matlab's implementation. As far as I understand, your motivation is copying Matlab, right? In such a case, we should consider the deviation...
Compatibility with MATLAB, either in terms of naming, function arguments, or bit-accuracy is an explicit non-goal of the scipy project. Looking at MATLAB is at most market research: we can infer from the presence of an envelope function with those three methods that having those three methods exposed is desirable. Once we get that nugget of information, we start from scratch and ignore the details of the MATLAB implementation. If warranted, we can sometimes use MATLAB to generate reference values for our tests, but bit-level accuracy is unnecessary (which is good, because it's unattainable in general).
I have worked on this a little, bit and I need some help. Please see the commit in my fork:
https://github.com/scipy/scipy/compare/main...yagizolmez:scipy:signal-envelope?expand=1
I have not opened a pull request yet, because I have some problems:
Traceback (most recent call last): File "/home/yagizolmez/miniforge3/envs/scipy-dev/lib/python3.11/site-packages/sphinx/events.py", line 97, in emit results.append(listener.handler(self.app, *args)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/yagizolmez/miniforge3/envs/scipy-dev/lib/python3.11/site-packages/pydata_sphinx_theme/init.py", line 145, in update_templates if theme_css_name in context["css_files"]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/yagizolmez/miniforge3/envs/scipy-dev/lib/python3.11/site-packages/sphinx/builders/html/_assets.py", line 40, in eq warnings.warn('The str interface for _CascadingStyleSheet objects is deprecated. ' sphinx.deprecation.RemovedInSphinx90Warning: The str interface for _CascadingStyleSheet objects is deprecated. Use css.filename instead.
The above exception was the direct cause of the following exception:
Traceback (most recent call last): File "/home/yagizolmez/miniforge3/envs/scipy-dev/lib/python3.11/site-packages/sphinx/cmd/build.py", line 298, in build_main app.build(args.force_all, args.filenames) File "/home/yagizolmez/miniforge3/envs/scipy-dev/lib/python3.11/site-packages/sphinx/application.py", line 355, in build self.builder.build_update() File "/home/yagizolmez/miniforge3/envs/scipy-dev/lib/python3.11/site-packages/sphinx/builders/init.py", line 293, in build_update self.build(to_build, File "/home/yagizolmez/miniforge3/envs/scipy-dev/lib/python3.11/site-packages/sphinx/builders/init.py", line 363, in build self.write(docnames, list(updated_docnames), method) File "/home/yagizolmez/miniforge3/envs/scipy-dev/lib/python3.11/site-packages/sphinx/builders/init.py", line 571, in write self._write_serial(sorted(docnames)) File "/home/yagizolmez/miniforge3/envs/scipy-dev/lib/python3.11/site-packages/sphinx/builders/init.py", line 581, in _write_serial self.write_doc(docname, doctree) File "/home/yagizolmez/miniforge3/envs/scipy-dev/lib/python3.11/site-packages/sphinx/builders/html/init.py", line 655, in write_doc self.handle_page(docname, ctx, event_arg=doctree) File "/home/yagizolmez/miniforge3/envs/scipy-dev/lib/python3.11/site-packages/sphinx/builders/html/init.py", line 1107, in handle_page newtmpl = self.app.emit_firstresult('html-page-context', pagename, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/yagizolmez/miniforge3/envs/scipy-dev/lib/python3.11/site-packages/sphinx/application.py", line 492, in emit_firstresult return self.events.emit_firstresult(event, args, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/yagizolmez/miniforge3/envs/scipy-dev/lib/python3.11/site-packages/sphinx/events.py", line 118, in emit_firstresult for result in self.emit(name, args, allowed_exceptions=allowed_exceptions): ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/yagizolmez/miniforge3/envs/scipy-dev/lib/python3.11/site-packages/sphinx/events.py", line 108, in emit raise ExtensionError(__("Handler %r for event %r threw an exception") % sphinx.errors.ExtensionError: Handler <function update_templates at 0x7fa2e5df1a80> for event 'html-page-context' threw an exception (exception: The str interface for _CascadingStyleSheet objects is deprecated. Use css.filename instead.)
I am thinking that I initially failed to create documentation for my function due to lines 4702,4703:
signal = (1+0.5np.cos(2np.pi1t))* np.random.normal(size = t.shape[0])+5
Later I fixed this, but I am still getting the error. This is my first time working with docstrings, so any help would be appreciated.
Is there any audio dataset in scipy that I can use for the documentation? I think the 'peak' method would be best demonstrated with audio.
I have tested all three methods locally using the examples I have provided earlier. I have also written some unit tests and added them to my commit. These tests only check for correct inputs, and they pass. Do you have any recommendations for any other tests?
In the documentation, I give the default value for the 'analytic' method as 'x.shape[0]' based on hilbert's default. Is this good practice? Should I change the code and set N = x.shape[0] for hilbert, so that my documentation would not be effected even if hilbert's default changes.
I am currently importing find_peaks within the envelope function. Doing this outside the function throws a circular import error. Is there a better way to handle circular imports? This is also something that I am not really familiar with.
These are my questions for now. Sorry for the long post. I am looking forward to your responses.
I also saw an error like this last night so chances are it is unrelated to your changes. I think it should be fixed by upgrading version of the theme after gh-16660 (in the meantime, I think downgrading your version of sphinx
might work).
I don't think so - see http://scipy.github.io/devdocs/reference/datasets.html
@lucascolley Downgrading sphinx by one version did not work. I tried deleting doc/build, and building the whole html again. This worked, but it is too time consuming. Also, the documentation still does not include the new envelope function I have added. Do you have any idea why this might be? My terminal's output is as follows:
💻 ninja -C /home/yagizolmez/git-repos/scipy/build -j8
ninja: Entering directory `/home/yagizolmez/git-repos/scipy/build'
[4/4] Generating scipy/generate-version with a custom command
Build OK
💻 meson install -C build --only-changed
Installing, see meson-install.log...
Installation OK
# for testing
# @echo installed scipy 58e580e matches git version 58e580e; exit 1
mkdir -p build/html build/doctrees
LANG=C /home/yagizolmez/miniforge3/envs/scipy-dev/bin/python -msphinx -WT --keep-going -b html -d build/doctrees -j1 source build/html
Running Sphinx v7.2.5
SciPy (VERSION 1.13.0.dev)
[autosummary] generating autosummary for: building/blas_lapack.rst, building/compilers_and_options.rst, building/cross_compilation.rst, building/distutils_equivalents.rst, building/index.rst, building/introspecting_a_build.rst, building/redistributable_binaries.rst, building/understanding_meson.rst, dev/api-dev/api-dev-toc.rst, dev/api-dev/array_api.rst, ..., tutorial/stats/discrete_zipf.rst, tutorial/stats/discrete_zipfian.rst, tutorial/stats/resampling.rst, tutorial/stats/sampling.rst, tutorial/stats/sampling_dau.rst, tutorial/stats/sampling_dgt.rst, tutorial/stats/sampling_hinv.rst, tutorial/stats/sampling_pinv.rst, tutorial/stats/sampling_srou.rst, tutorial/stats/sampling_tdr.rst
[autosummary] generating autosummary for: /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.LowLevelCallable.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.optimize.LbfgsInvHessProduct.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.optimize.OptimizeResult.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.optimize.RootResults.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.sparse.bsr_array.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.sparse.bsr_matrix.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.sparse.coo_array.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.sparse.coo_matrix.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.sparse.csc_array.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.sparse.csc_matrix.rst, ..., /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.sparse.csr_matrix.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.sparse.dia_array.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.sparse.dia_matrix.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.sparse.dok_array.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.sparse.dok_matrix.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.sparse.lil_array.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.sparse.lil_matrix.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.sparse.linalg.LaplacianNd.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.stats._result_classes.PearsonRResult.rst, /home/yagizolmez/git-repos/scipy/doc/source/reference/generated/scipy.stats._result_classes.TtestResult.rst
loading intersphinx inventory from https://docs.python.org/3/objects.inv...
loading intersphinx inventory from https://numpy.org/devdocs/objects.inv...
loading intersphinx inventory from https://numpy.org/neps/objects.inv...
loading intersphinx inventory from https://matplotlib.org/stable/objects.inv...
loading intersphinx inventory from https://asv.readthedocs.io/en/stable/objects.inv...
loading intersphinx inventory from https://www.statsmodels.org/stable/objects.inv...
myst v2.0.0: MdParserConfig(commonmark_only=False, gfm_only=False, enable_extensions=set(), disable_syntax=[], all_links_external=False, url_schemes=('http', 'https', 'mailto', 'ftp'), ref_domains=None, fence_as_directive=set(), number_code_blocks=[], title_to_header=False, heading_anchors=0, heading_slug_func=None, html_meta={}, footnote_transition=True, words_per_minute=200, substitutions={}, linkify_fuzzy_links=True, dmath_allow_labels=True, dmath_allow_space=True, dmath_allow_digits=True, dmath_double_inline=False, update_mathjax=True, mathjax_classes='tex2jax_process|mathjax_process|math|output_area', enable_checkboxes=False, suppress_warnings=[], highlight_code_blocks=True)
myst-nb v1.0.0: NbParserConfig(custom_formats={}, metadata_key='mystnb', cell_metadata_key='mystnb', kernel_rgx_aliases={}, eval_name_regex='^[a-zA-Z_][a-zA-Z0-9_]*$', execution_mode='auto', execution_cache_path='', execution_excludepatterns=(), execution_timeout=30, execution_in_temp=False, execution_allow_errors=False, execution_raise_on_error=False, execution_show_tb=False, merge_streams=False, render_plugin='default', remove_code_source=False, remove_code_outputs=False, code_prompt_show='Show code cell {type}', code_prompt_hide='Hide code cell {type}', number_source_lines=False, output_stderr='show', render_text_lexer='myst-ansi', render_error_lexer='ipythontb', render_image_options={}, render_figure_options={}, render_markdown_format='commonmark', output_folder='build', append_css=True, metadata_to_fm=False)
Using jupyter-cache at: /home/yagizolmez/git-repos/scipy/doc/build/.jupyter_cache
building [mo]: targets for 0 po files that are out of date
writing output...
building [html]: targets for 4539 source files that are out of date
updating environment: [new config] 4539 added, 0 changed, 0 removed
/home/yagizolmez/git-repos/scipy/doc/source/notebooks/interp_transition_guide.md: Executing notebook using local CWD [mystnb]
0.00s - Debugger warning: It seems that frozen modules are being used, which may
0.00s - make the debugger miss breakpoints. Please pass -Xfrozen_modules=off
0.00s - to python to disable frozen modules.
0.00s - Note: Debugging will proceed. Set PYDEVD_DISABLE_FILE_VALIDATION=1 to disable this validation.
0.00s - Debugger warning: It seems that frozen modules are being used, which may
0.00s - make the debugger miss breakpoints. Please pass -Xfrozen_modules=off
0.00s - to python to disable frozen modules.
0.00s - Note: Debugging will proceed. Set PYDEVD_DISABLE_FILE_VALIDATION=1 to disable this validation.
/home/yagizolmez/git-repos/scipy/doc/source/notebooks/interp_transition_guide.md: Executed notebook in 6.03 seconds [mystnb]
<string>:46: DeprecationWarning: invalid escape sequence '\,'dtr
<string>:46: DeprecationWarning: invalid escape sequence '\,'dtrc
<string>:50: DeprecationWarning: invalid escape sequence '\d'uber
<string>:30: DeprecationWarning: invalid escape sequence '\i'tairy
<string>:25: DeprecationWarning: invalid escape sequence '\i'ti0k0
<string>:25: DeprecationWarning: invalid escape sequence '\i'tj0y0
<string>:42: DeprecationWarning: invalid escape sequence '\c've
<string>:42: DeprecationWarning: invalid escape sequence '\c've
<string>:22: DeprecationWarning: invalid escape sequence '\P'dtr
<string>:50: DeprecationWarning: invalid escape sequence '\d'seudo_huber
<string>:46: DeprecationWarning: invalid escape sequence '\l'klmbda
<string>:19: DeprecationWarning: invalid escape sequence '\s'
reading sources... [100%] tutorial/stats/sampling_tdr
looking for now-outdated files... none found
pickling environment... done
checking consistency... done
preparing documents... done
copying assets... copying static files... done
copying extra files... done
done
writing output... [100%] tutorial/stats/sampling_tdr
generating indices... genindex done
writing additional pages... search done
copying images... [100%] ../build/plot_directive/tutorial/stats/sampling_srou-1.png
dumping search index in English (code: en)... done
dumping object inventory... done
build succeeded.
The HTML pages are in build/html.
the documentation still does not include the new envelope function I have added. Do you have any idea why this might be?
You need to add your new function to the toc in scipy/signal/__init__.py
I have completed the implementation and opened a pull request. I would be happy if you could review my code!
Enhancement request issues typically stay open until the enhancement has been merged (or decided against) 👍
Is your feature request related to a problem? Please describe.
It has come to my attention that scipy.signal does not have a dedicated function for extracting the envelope of a signal. It would be nice to have a function scipy.signal.envelope that implements several envelope extraction methods. MATLAB Signal Processing Toolbox has this functionality:
https://www.mathworks.com/help/signal/ref/envelope.html
Describe the solution you'd like.
I could implement this. I do not have experience contributing to open source, but this would be a good start.
Describe alternatives you've considered.
No response
Additional context (e.g. screenshots, GIFs)
No response