HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

20+ functions for generating different waveforms

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
waveformsdifferentgeneratingforfunctions

Problem

I added a guard clause to the functions in scipy.signal.windows, but the way they are currently written means the same 11 lines are now repeated in every function.

Each function has a unique core and a unique docstring, then some common guards and preconditioning around it:

if int(M) != M or M < 0:
    raise ValueError('Window length M must be a non-negative integer')
if M == 0:
    return np.array([])
if M == 1:
    return np.ones(1, 'd')
odd = M % 2
if not sym and not odd:
    M = M + 1

...

if not sym and not odd:
    w = w[:-1]
return w


So I thought I'd factor out the common code to make it more DRY. However I can't find a really elegant way to do it:

  • Moving the redundant code into helper functions doesn't work because the early exits don't work nested inside another function. Adding more code to catch the early exits kind of defeats the purpose.



  • Wrapping them in a decorator changes the function signature from blackman(M, sym=True) to blackman(M, *args), since some window functions have different numbers of arguments than others.



  • Splitting the unique guts of each function out into their own _core functions would work, but seems a little mangled, with the actual code being separated from the docstrings.



The complete file is here. Removed parts to make it less lengthy, while still showing some docstrings and functions with different numbers of arguments:

```
"""The suite of window functions."""
from __future__ import division, print_function, absolute_import

import warnings

import numpy as np
from scipy import fftpack, linalg, special
from scipy._lib.six import string_types

__all__ = ['boxcar', 'triang', 'parzen', 'bohman', 'blackman', 'nuttall',
'blackmanharris', 'flattop', 'bartlett', 'hanning', 'barthann',
'hamming', 'kaiser', 'gaussian', 'general_gaussian', 'chebwin',
'slepian', 'cosine', 'hann', 'exponential', 'tukey', 'get_window']
...

def triang(M, sym=True):
"""Return a triangu

Solution

I do believe that the decorator way is the proper one. There are some tradeoff using it but you can easily overcome your main concern using functools.wraps: it will reuse the name, docstring and signature of the decorated function for the wrapper.

The wrapper within the decorator should be aware of both M and sym; this is where things can get tricky depending on how you want things to behave.

A first approach could be:

from functools import wraps

def argument_checker(func):
    @wraps(func)
    def wrapper(M, *args, sym=True, **kwargs):
        if int(M) != M or M < 0:
            raise ValueError('Window length M must be a non-negative integer')
        if M == 0:
            return np.array([])
        if M == 1:
            return np.ones(1, 'd')

        if not sym and not M % 2:
            return func(M + 1, *args, **kwargs)[:-1]
        else:
            return func(M, *args, **kwargs)
    return wrapper


I just modified the boilerplate code to group the two if not sym and not odd into a single if as there is func that abstract the window building. However this solution is Python 3 only as the signature of wrapper will raise a SyntaxError in Python 2, and as regard to your imports from __future__, I'm assuming Python 2 here. So you might want:

def argument_checker(func):
    @wraps(func)
    def wrapper(M, *args, **kwargs):
        sym = kwargs.pop('sym', True)
        ...


But this poses the issue of sym needing to be a keyword argument, and not a positional one. Again, this can be made more explicit using Python 3 syntax for keyword-only arguments:

def triang(M, *, sym=True):
    ...


or

def tukey(M, alpha=0.5, *, sym=True):
    ...


Which, when called naturally using triang(150, False), for instance, will raise TypeError: triang() takes 1 positional argument but 2 were given.

So I'd go for this second version and keep the current signature of the functions for now, but if you can use Python 3 instead, it will be made much more explicit. I would, however, remove the *args parameter from the wrapper to make it accept only keyword arguments.

But then, there is the case of tukey that add an other layer of check into the checks. To handle that properly, it is easier to split the decorator into two, more meaningful, tasks:

from functools import wraps

def argument_checker(func):
    @wraps(func)
    def wrapper(M, **kwargs):
        if int(M) != M or M < 0:
            raise ValueError('Window length M must be a non-negative integer')
        if M == 0:
            return np.array([])
        if M == 1:
            return np.ones(1, 'd')

        return func(M, **kwargs)
    return wrapper

def parity_handler(func):
    @wraps(func)
    def wrapper(M, **kwargs):
        sym = kwargs.get('sym', True)
        if not sym and not M % 2:
            return func(M + 1, **kwargs)[:-1]
        else:
            return func(M, **kwargs)
    return wrapper


So you can

@argument_checker
@parity_handler
def triang(M, sym=True):
    ...


But most importantly:

def alpha_handler(func):
    @wraps(func)
    def wrapper(M, **kwargs):
        alpha = kwargs.get('alpha', 0.5)
        if alpha = 1.0:
            return hann(M, sym=kwargs.get('sym', True))
        return func(M, **kwargs)
    return wrapper

@argument_checker
@alpha_handler
@parity_handler
def tukey(...


And the whole module could become:

```
"""The suite of window functions."""
from __future__ import division, print_function, absolute_import

import warnings
from functools import wraps

import numpy as np
from scipy import fftpack, linalg, special
from scipy._lib.six import string_types

__all__ = ['boxcar', 'triang', 'parzen', 'bohman', 'blackman', 'nuttall',
'blackmanharris', 'flattop', 'bartlett', 'hanning', 'barthann',
'hamming', 'kaiser', 'gaussian', 'general_gaussian', 'chebwin',
'slepian', 'cosine', 'hann', 'exponential', 'tukey', 'get_window']
...

def argument_checker(func):
@wraps(func)
def wrapper(M, **kwargs):
if int(M) != M or M >> from scipy import signal
>>> from scipy.fftpack import fft, fftshift
>>> import matplotlib.pyplot as plt

>>> window = signal.triang(51)
>>> plt.plot(window)
>>> plt.title("Triangular window")
>>> plt.ylabel("Amplitude")
>>> plt.xlabel("Sample")

>>> plt.figure()
>>> A = fft(window, 2048) / (len(window)/2.0)
>>> freq = np.linspace(-0.5, 0.5, len(A))
>>> response = 20 * np.log10(np.abs(fftshift(A / abs(A).max())))
>>> plt.plot(freq, response)
>>> plt.axis([-0.5, 0.5, -120, 0])
>>> plt.title("Frequency response of the triangular window")
>>> plt.ylabel("Normalized magnitude [dB]")
>>> plt.xlabel("Normalized frequency [cycles per sample]")

"""
n = np.arange(1, (M + 1) // 2 + 1)
if M % 2 == 0:
w = (2 * n - 1.0) / M
w = np.r_[w, w[::-1]]
else:
w = 2 * n / (M + 1.0)

Code Snippets

from functools import wraps


def argument_checker(func):
    @wraps(func)
    def wrapper(M, *args, sym=True, **kwargs):
        if int(M) != M or M < 0:
            raise ValueError('Window length M must be a non-negative integer')
        if M == 0:
            return np.array([])
        if M == 1:
            return np.ones(1, 'd')

        if not sym and not M % 2:
            return func(M + 1, *args, **kwargs)[:-1]
        else:
            return func(M, *args, **kwargs)
    return wrapper
def argument_checker(func):
    @wraps(func)
    def wrapper(M, *args, **kwargs):
        sym = kwargs.pop('sym', True)
        ...
def triang(M, *, sym=True):
    ...
def tukey(M, alpha=0.5, *, sym=True):
    ...
from functools import wraps


def argument_checker(func):
    @wraps(func)
    def wrapper(M, **kwargs):
        if int(M) != M or M < 0:
            raise ValueError('Window length M must be a non-negative integer')
        if M == 0:
            return np.array([])
        if M == 1:
            return np.ones(1, 'd')

        return func(M, **kwargs)
    return wrapper


def parity_handler(func):
    @wraps(func)
    def wrapper(M, **kwargs):
        sym = kwargs.get('sym', True)
        if not sym and not M % 2:
            return func(M + 1, **kwargs)[:-1]
        else:
            return func(M, **kwargs)
    return wrapper

Context

StackExchange Code Review Q#135910, answer score: 6

Revisions (0)

No revisions yet.