diff --git a/README.rst b/README.rst index 4ff99a5..496aec8 100644 --- a/README.rst +++ b/README.rst @@ -26,37 +26,32 @@ The following external libraries are required: Installation ------------ -For performance and convenience reasons we highly recommend to use -`Conda`_ (miniconda for simplicity) to manage your Python installation. -Once installed, you can use the following steps to create a new environment -with the *sfa* toolbox. +For performance and convenience reasons we highly recommend to use `Conda`_ (miniconda for simplicity) to manage your Python installation. Once installed, you can use the following steps to receive and use *sfa*, depending on your use case: -#. Create new Conda environment from the specified requirements: - ``conda env create --file environment.yml`` +A. By cloning (or downloading) the repository and setting up a new environment **[recommended]**: -#. Activate the environment: - ``source activate sfa`` + ``git clone https://github.com/AppliedAcousticsChalmers/sound_field_analysis-py.git`` -#. Install *sfa* from **either** source: + ``cd sound_field_analysis-py/`` - By cloning (or downloading) the repository **[recommended]**: + Create a new Conda environment from the specified requirements: + ``conda env create --file environment.yml`` - ``git clone https://github.com/AppliedAcousticsChalmers/sound_field_analysis-py.git`` + Activate the environment: + ``source activate sfa`` - ``cd sound_field_analysis-py/`` + **Optional:** Install additional requirements in case you want to locally run the *Jupyter Notebooks* with examples: + ``conda env update --file environment_jupyter.yml`` - ``pip install -e .`` +B. From PyPI **[NOT recommended - code outdated]**: - From `conda-forge`_ channel **[not recommended - code currently outdated]**: + Install into an existing environment (without example *Jupyter Notebooks*): + ``pip install sound_field_analysis`` - ``conda install -c conda-forge sound_field_analysis`` +C. From `conda-forge`_ channel **[NOT recommended - code outdated]**: - From PyPI **[Not recommended - code currently outdated]**: - - ``pip install sound_field_analysis`` - -#. **Optional:** Install additional requirements in case you want to locally run *Jupyter Notebooks*: - ``conda env update --file environment_jupyter.yml`` + Install into an existing environment: + ``conda install -c conda-forge sound_field_analysis`` Documentation @@ -113,6 +108,9 @@ miro or `SOFA`_ files. Version history --------------- +*v2019.11.6* + * Update of internal documentation and string formatting + *v2019.8.15* * Change of version number scheme to CalVer * Improvement of Exp4 diff --git a/contribute.md b/contribute.md index dbda0c2..aab4c7b 100644 --- a/contribute.md +++ b/contribute.md @@ -2,12 +2,13 @@ ## Dev environment Given you have [anaconda](https://www.continuum.io/downloads) installed, run the following commands to clone the repository into a new folder `sound_field_analysis-py`, install necessary tools into a new conda environment and activate it: ``` -git clone https://github.com/QULab/sound_field_analysis-py.git -conda create -n sfa_dev scipy numpy plotly pip ipython -source activate sfa_dev -pip install -e sound_field_analysis-py +git clone https://github.com/AppliedAcousticsChalmers/sound_field_analysis-py.git +cd sound_field_analysis-py/ +conda env create --file environment.yml +source activate sfa ``` -You can now work on the *sfa* toolbox inside the `sound_field_analysis-py` folder. Using `ipython`, you may use the following magic commands to ensure reload on any changes inside the package: + +You can now work on the *sfa* toolbox inside the `sound_field_analysis-py` folder. Using `ipython`, you may use the following magic commands to ensure reload on any changes inside the package: ``` %load_ext autoreload %autoreload 2 @@ -15,16 +16,14 @@ You can now work on the *sfa* toolbox inside the `sound_field_analysis-py` fold ## Documentation If you want to compile the documentation (pdf and/or html), you need to additionally install sphinx and sphinx_rtd_theme and clone the gh-pages branch: - ``` -conda install sphinx sphinx_rtd_theme -git clone --single-branch --branch gh-pages https://github.com/QULab/sound_field_analysis-py.git sound_field_analysis-docs +conda env update --file environment_gh-pages.yml +git clone --single-branch --branch gh-pages https://github.com/AppliedAcousticsChalmers/sound_field_analysis-py.git sound_field_analysis-docs ``` Now you can compile the pdf readme (given you have latex installed) and html pages by running `make latexpdf` or `make html` from the `sound_field_analysis-py\doc` directory. If you decide on a different folder structure, you may edit the following line in `doc/Makefile` to decide on where to move the html documentation: - ``` HTMLDIR = ../../sound_field_analysis-docs ``` diff --git a/environment_gh-pages.yml b/environment_gh-pages.yml new file mode 100644 index 0000000..a1f1f0a --- /dev/null +++ b/environment_gh-pages.yml @@ -0,0 +1,5 @@ +channels: + - defaults +dependencies: + - sphinx + - sphinx_rtd_theme diff --git a/setup.py b/setup.py index 85807fb..363b4f7 100644 --- a/setup.py +++ b/setup.py @@ -1,23 +1,33 @@ -from setuptools import setup - -version = open("sound_field_analysis/_version.py").readlines()[-1].split()[-1].strip("\"'") +from setuptools import find_packages, setup setup( name='sound_field_analysis', - version=version, - description='Analyze, visualize and process sound field data recorded by spherical microphone arrays.', url='https://github.com/AppliedAcousticsChalmers/sound_field_analysis-py/', + version=open('sound_field_analysis/_version.py').readlines()[-1].split()[-1].strip('"\''), + license='GPLv3', + # license='MIT', + + description='Analyze, visualize and process sound field data recorded by spherical microphone arrays.', + long_description=open('README.rst', mode='r', encoding='utf-8').read(), + keywords='sound field analysis spherical microphone array', + author='QU Lab / Christoph Hohnerlein', + # author='Chalmers University of Technology / Jens Ahrens', author_email='christoph.hohnerlein@qu.tu-berlin.de', - license='GPLv3', + # author_email='jens.ahrens@chalmers.se', + classifiers=[ 'Development Status :: 4 - Beta', + # 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Science/Research', 'Topic :: Multimedia :: Sound/Audio', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Programming Language :: Python :: 3', + # 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3.7', + 'Operating System :: OS Independent', ], - keywords='sound field analysis spherical microphone array', + + python_requires='>=3.7', install_requires=[ 'scipy', 'numpy', @@ -27,9 +37,11 @@ package_data={ 'examples': ['examples'], }, + extras_require={ 'plotting': ['plotly'], 'examples': ['jupyter'], }, - packages=['sound_field_analysis'], + + packages=find_packages(), ) diff --git a/sound_field_analysis/__init__.py b/sound_field_analysis/__init__.py index c47989b..de91de5 100644 --- a/sound_field_analysis/__init__.py +++ b/sound_field_analysis/__init__.py @@ -2,7 +2,7 @@ from ._version import __version__ -__all__ = ["io", "gen", "process", "sph", "utils", "lebedev"] +__all__ = ['io', 'gen', 'process', 'sph', 'utils', 'lebedev'] try: import plotly diff --git a/sound_field_analysis/_version.py b/sound_field_analysis/_version.py index 60a66a6..48058d5 100644 --- a/sound_field_analysis/_version.py +++ b/sound_field_analysis/_version.py @@ -1,2 +1,2 @@ """Version information.""" -__version__ = '2019.8.15' +__version__ = '2019.11.7' diff --git a/sound_field_analysis/gen.py b/sound_field_analysis/gen.py index bef2d9f..59ec815 100644 --- a/sound_field_analysis/gen.py +++ b/sound_field_analysis/gen.py @@ -6,7 +6,7 @@ `gauss_grid` Gauss-Legendre quadrature grid and weights `lebedev` - Lebedev quadrature grid and weigths + Lebedev quadrature grid and weights `radial_filter` Modal radial filter `radial_filter_fullspec` @@ -20,8 +20,8 @@ from scipy.special import spherical_jn from .io import ArrayConfiguration, SphericalGrid -from .process import spatFT, iSpatFT -from .sph import sph_harm, sph_harm_all, cart2sph, array_extrapolation, kr, sphankel2, dsphankel2 +from .process import iSpatFT, spatFT +from .sph import array_extrapolation, cart2sph, dsphankel2, kr, sph_harm, sph_harm_all, sphankel2 def whiteNoise(fftData, noiseLevel=80): @@ -101,7 +101,7 @@ def lebedev(max_order=None, degree=None): Returns ------- gridData : array_like - Lebedev quadrature positions and weigths: [AZ, EL, W] + Lebedev quadrature positions and weights: [AZ, EL, W] """ if max_order is None and not degree: raise ValueError('Either a maximum order or a degree have to be given.') @@ -117,8 +117,7 @@ def lebedev(max_order=None, degree=None): raise ValueError('Maximum order can only be between 0 and 11.') if degree not in allowed_degrees: - raise ValueError('{} is an invalid quadrature degree. Choose one of the following: {}'.format( - degree, allowed_degrees)) + raise ValueError(f'{degree} is an invalid quadrature degree. Choose one of the following: {allowed_degrees}') from . import lebedev leb = lebedev.genGrid(degree) @@ -336,8 +335,8 @@ def sampled_wave(order, fs, NFFT, array_configuration, # TODO : Investigate if limit_order works as intended if max_order_fullspec > limit_order: - print('Requested wave front needs a minimum order of {} but was limited to order {}'.format( - max_order_fullspec, limit_order)) + print(f'Requested wave front needs a minimum order of {max_order_fullspec} but was limited to order ' + f'{limit_order}') Pnm = ideal_wave(min(max_order_fullspec, limit_order), fs, wave_azimuth, wave_colatitude, array_configuration, wavetype, distance, NFFT) Pnm_resampled = spatFT(iSpatFT(Pnm, gridData), gridData, order_max=order) diff --git a/sound_field_analysis/io.py b/sound_field_analysis/io.py index 723afc7..f19788b 100644 --- a/sound_field_analysis/io.py +++ b/sound_field_analysis/io.py @@ -1,6 +1,5 @@ -""" -Input-Output functions""" - +"""Input-Output functions""" +import sys from collections import namedtuple import numpy as _np @@ -48,10 +47,7 @@ def __new__(cls, array_radius, array_type, transducer_type, scatter_radius=None, return self def __repr__(self): - return 'ArrayConfiguration(\n' + ',\n'.join( - ' {0} = {1}'.format(name, repr(data).replace('\n', '\n ')) - for name, data in - zip(['array_radius', 'array_type', 'transducer_type', 'scatter_radius', 'dual_radius'], self)) + ')' + return utils.get_named_tuple__repr__(self) class TimeSignal(namedtuple('TimeSignal', 'signal fs delay')): @@ -84,9 +80,7 @@ def __new__(cls, signal, fs, delay=None): return self def __repr__(self): - return 'TimeSignal(\n' + ',\n'.join( - ' {0} = {1}'.format(name, repr(data).replace('\n', '\n ')) - for name, data in zip(['signal', 'fs', 'delay'], self)) + ')' + return utils.get_named_tuple__repr__(self) class SphericalGrid(namedtuple('SphericalGrid', 'azimuth colatitude radius weight')): @@ -120,9 +114,7 @@ def __new__(cls, azimuth, colatitude, radius=None, weight=None): return self def __repr__(self): - return 'SphericalGrid(\n' + ',\n'.join( - ' {0} = {1}'.format(name, repr(data).replace('\n', '\n ')) - for name, data in zip(['azimuth', 'colatitude', 'radius', 'weight'], self)) + ')' + return utils.get_named_tuple__repr__(self) class ArraySignal(namedtuple('ArraySignal', 'signal grid center_signal configuration temperature')): @@ -154,9 +146,7 @@ def __new__(cls, signal, grid, center_signal=None, configuration=None, temperatu return self def __repr__(self): - return 'ArraySignal(\n' + ',\n'.join( - ' {0} = {1}'.format(name, repr(data).replace('\n', '\n ')) - for name, data in zip(['signal', 'grid', 'center_signal', 'configuration', 'temperature'], self)) + ')' + return utils.get_named_tuple__repr__(self) class HrirSignal(namedtuple('HrirSignal', 'l r grid center_signal')): @@ -188,9 +178,7 @@ def __new__(cls, l, r, grid, center_signal=None): return self def __repr__(self): - return 'HrirSignal(\n' + ',\n'.join( - ' {0} = {1}'.format(name, repr(data).replace('\n', '\n ')) - for name, data in zip(['l', 'r', 'grid', 'center_signal'], self)) + ')' + return utils.get_named_tuple__repr__(self) def read_miro_struct(file_name, channel='irChOne', transducer_type='omni', scatter_radius=None, @@ -236,7 +224,7 @@ def read_miro_struct(file_name, channel='irChOne', transducer_type='omni', scatt center_signal = TimeSignal(signal=_np.squeeze(current_data['irCenter']).T, fs=_np.squeeze(current_data['fs'])) except KeyError: - print('WARNING: Center signal not included in miro struct, use extended miro_to_struct.m!') + print('WARNING: Center signal not included in miro struct, use extended miro_to_struct.m!', file=sys.stderr) center_signal = None mic_grid = SphericalGrid(azimuth=_np.squeeze(current_data['azimuth']), @@ -245,8 +233,8 @@ def read_miro_struct(file_name, channel='irChOne', transducer_type='omni', scatt weight=_np.squeeze(current_data['quadWeight'])) if (mic_grid.colatitude < 0).any(): - print("WARNING: The 'colatitude' data contains negative values, which is an indication that it is actually " - "elevation") + print('WARNING: The "colatitude" data contains negative values, which is an indication that it is actually ' + 'elevation', file=sys.stderr) if _np.squeeze(current_data['scatterer']): sphere_config = 'rigid' @@ -383,7 +371,7 @@ def empty_time_signal(no_of_signals, signal_length): .air_temperature Average temperature in [C] """ return _np.rec.array(_np.zeros(no_of_signals, - dtype=[('signal', str(signal_length) + 'f8'), + dtype=[('signal', f'{signal_length}f8'), ('fs', 'f8'), ('azimuth', 'f8'), ('colatitude', 'f8'), @@ -447,8 +435,6 @@ def write_SSR_IRs(filename, time_data_l, time_data_r, wavformat='float32'): ValueError in case integer format should be exported and amplitude exceeds 1.0 """ - import sys - # make lower case and remove spaces wavformat = wavformat.lower().strip() diff --git a/sound_field_analysis/lebedev.py b/sound_field_analysis/lebedev.py index 137862b..969de9a 100755 --- a/sound_field_analysis/lebedev.py +++ b/sound_field_analysis/lebedev.py @@ -1,4 +1,5 @@ -"""Generate Lebedev grid and coefficients +""" +Generate Lebedev grid and coefficients This module only exposes the function `lebGrid = lebedev.genGrid(degree)`. lebGrid is a named tuple containing the coordinates .x, .y, .z and the weights .w @@ -211,4 +212,4 @@ def genGrid(n): lebGrid.w = leb[:, 3] return lebGrid except KeyError: - raise ValueError('No grid available for degree', n) + raise ValueError(f'No grid available for degree {n}') diff --git a/sound_field_analysis/plot.py b/sound_field_analysis/plot.py index 47b3e44..6311ca1 100644 --- a/sound_field_analysis/plot.py +++ b/sound_field_analysis/plot.py @@ -1,31 +1,35 @@ -"""Plotting functions +""" +Plotting functions Helps visualizing spherical microphone data. """ from collections import namedtuple import numpy as _np import plotly.graph_objs as go -from plotly import offline as plotly_off -from plotly import subplots +from plotly import offline as plotly_off, subplots from .process import plane_wave_decomp -from .utils import env_info, progress_bar, current_time +from .utils import current_time, env_info, progress_bar def showTrace(trace, layout=None, title=None): - """ Wrapper around plotlys offline .plot() function + """ Wrapper around Plotly's offline .plot() function Parameters ---------- trace : plotly_trace - Plotly generated trace to be displayed offline - # colorize : Bool, optional - # Toggles bw / colored plot [Default: True] + Plotly generated trace to be displayed offline + layout : plotly.graph_objs.Layout, optional + Layout of plot to be displayed offline + title : str, optional + Title of plot to be displayed offline + # colorize : bool, optional + # Toggles bw / colored plot [Default: True] Returns ------- fig : plotly_fig_handle - JSON representation of generated figure + JSON representation of generated figure """ if layout is None: layout = go.Layout( @@ -47,12 +51,12 @@ def showTrace(trace, layout=None, title=None): if title is not None: fig.layout.update(title=title) - filename = title + '.html' + filename = f'{title}.html' else: try: - filename = fig.layout.title + '.html' + filename = f'{fig.layout.title}.html' except TypeError: - filename = str(current_time()) + '.html' + filename = f'{current_time()}.html' # if colorize: # data[0].autocolorscale = False @@ -72,20 +76,20 @@ def makeMTX(spat_coeffs, radial_filter, kr_IDX, viz_order=None, stepsize_deg=1): Parameters ---------- spat_coeffs : array_like - Spatial fourier coefficients + Spatial fourier coefficients radial_filter : array_like - Modal radial filters + Modal radial filters kr_IDX : int - Index of kr to be computed + Index of kr to be computed viz_order : int, optional - Order of the spatial fourier transform [Default: Highest available] + Order of the spatial fourier transform [Default: Highest available] stepsize_deg : float, optional - Integer Factor to increase the resolution. [Default: 1] + Integer Factor to increase the resolution. [Default: 1] Returns ------- mtxData : array_like - Plane wave decomposition (frequency domain) + Plane wave decomposition (frequency domain) Note ---- @@ -107,18 +111,18 @@ def makeFullMTX(Pnm, dn, kr, viz_order=None): Parameters ---------- Pnm : array_like - Spatial Fourier Coefficients (e.g. from S/T/C) + Spatial Fourier Coefficients (e.g. from S/T/C) dn : array_like - Modal Radial Filters (e.g. from M/F) + Modal Radial Filters (e.g. from M/F) kr : array_like - kr-vector + kr-vector viz_order : int, optional - Order of the spatial fourier tplane_wave_decompransform [Default: Highest available] + Order of the spatial fourier tplane_wave_decompransform [Default: Highest available] Returns ------- vizMtx : array_like - Computed visualization matrix over all kr + Computed visualization matrix over all kr """ kr = _np.asarray(kr) if not viz_order: @@ -138,14 +142,14 @@ def normalizeMTX(MTX, logScale=False): Parameters ---------- MTX : array_like - Matrix to be normalized - logScale : bool - Toggle conversion logScale [Default: False] + Matrix to be normalized + logScale : bool, optional + Toggle conversion logScale [Default: False] Returns ------- MTX : array_liked - Normalized Matrix + Normalized Matrix """ MTX -= MTX.min() MTX /= MTX.max() @@ -161,6 +165,7 @@ def normalizeMTX(MTX, logScale=False): def genSphCoords(): """ Generates cartesian (x,y,z) and spherical (theta, phi) coordinates of a sphere + Returns ------- coords : named tuple @@ -184,12 +189,12 @@ def sph2cartMTX(vizMTX): Parameters ---------- vizMTX : array_like - [180 x 360] matrix that hold amplitude information over phi and theta + [180 x 360] matrix that hold amplitude information over phi and theta Returns ------- V : named_tuple - Contains .xs, .ys, .zs cartesian coordinates + Contains .xs, .ys, .zs cartesian coordinates """ rs = _np.abs(vizMTX.reshape((181, -1)).T) @@ -207,12 +212,12 @@ def genShape(vizMTX): Parameters ---------- vizMTX : array_like - Matrix holding spherical data for visualization + Matrix holding spherical data for visualization Returns ------- T : plotly_trace - Trace of desired shape + Trace of desired shape TODO ---- @@ -238,12 +243,12 @@ def genSphere(vizMTX): Parameters ---------- vizMTX : array_like - Matrix holding spherical data for visualization + Matrix holding spherical data for visualization Returns ------- T : plotly_trace - Trace of desired sphere + Trace of desired sphere """ coords = genSphCoords() @@ -265,12 +270,12 @@ def genFlat(vizMTX): Parameters ---------- vizMTX : array_like - Matrix holding spherical data for visualization + Matrix holding spherical data for visualization Returns ------- T : plotly_trace - Trace of desired surface + Trace of desired surface TODO ---- @@ -295,16 +300,18 @@ def genVisual(vizMTX, style='shape', normalize=True, logScale=False): Parameters ---------- vizMTX : array_like - Matrix holding spherical data for visualization + Matrix holding spherical data for visualization style : string{'shape', 'sphere', 'flat'}, optional - Style of visualization. [Default: 'Shape'] - normalize : Bool, optional - Toggle normalization of data to [-1 ... 1] [Default: True] + Style of visualization. [Default: 'Shape'] + normalize : bool, optional + Toggle normalization of data to [-1 ... 1] [Default: True] + logScale : bool, optional + Toggle conversion logScale [Default: False] Returns ------- T : plotly_trace - Trace of desired visualization + Trace of desired visualization """ vizMTX = _np.abs(vizMTX) # Can we be sure to only need the abs? if normalize: @@ -317,7 +324,7 @@ def genVisual(vizMTX, style='shape', normalize=True, logScale=False): elif style == 'flat': return genFlat(vizMTX) else: - raise ValueError('Provided style "' + style + '" not available. Try sphere, shape or flat.') + raise ValueError(f'Provided style "{style}" not available. Try sphere, shape or flat.') def layout_2D(viz_type=None, title=None): @@ -388,15 +395,15 @@ def plot2D(data, title=None, viz_type=None, fs=44100, line_names=None): Parameters ---------- data : array_like - Data to be plotted, separated along the first dimension (rows) + Data to be plotted, separated along the first dimension (rows) title : str, optional - Add title to be displayed on plot + Add title to be displayed on plot viz_type : str{None, 'Time', 'ETC', 'LinFFT', 'LogFFT'}, optional - Type of data to be displayed [Default: None] + Type of data to be displayed [Default: None] fs : int, optional - Sampling rate in Hz [Default: 44100] + Sampling rate in Hz [Default: 44100] line_names : list of str, optional - Add legend to be displayed on plot, with one entry for each data row [Default: None] + Add legend to be displayed on plot, with one entry for each data row [Default: None] """ viz_type = viz_type.strip().upper() # remove whitespaces and make upper case @@ -413,11 +420,15 @@ def plot3D(vizMTX, style='shape', layout=None, normalize=True, logScale=False): Parameters ---------- vizMTX : array_like - Matrix holding spherical data for visualization + Matrix holding spherical data for visualization + layout : plotly.graph_objs.Layout, optional + Layout of plot to be displayed offline style : string{'shape', 'sphere', 'flat'}, optional - Style of visualization. [Default: 'shape'] - normalize : Bool, optional - Toggle normalization of data to [-1 ... 1] [Default: True] + Style of visualization. [Default: 'shape'] + normalize : bool, optional + Toggle normalization of data to [-1 ... 1] [Default: True] + logScale : bool, optional + Toggle conversion logScale [Default: False] # TODO # ---- @@ -464,13 +475,13 @@ def plot3Dgrid(rows, cols, viz_data, style, normalize=True, title=None): cur_row = int(rows[IDX]) cur_col = int(cols[IDX]) fig.add_trace(genVisual(viz_data[IDX], style=style, normalize=normalize), cur_row, cur_col) - fig.layout['scene' + str(IDX + 1)].update(layout_3D) + fig.layout[f'scene{IDX + 1:d}'].update(layout_3D) if title is not None: fig.layout.update(title=title) - filename = title + '.html' + filename = f'{title}.html' else: - filename = str(current_time()) + '.html' + filename = f'{current_time()}.html' if env_info() == 'jupyter_notebook': plotly_off.iplot(fig) diff --git a/sound_field_analysis/process.py b/sound_field_analysis/process.py index ec3f21b..e684052 100644 --- a/sound_field_analysis/process.py +++ b/sound_field_analysis/process.py @@ -23,6 +23,7 @@ `plane_wave_decomp` Plane Wave Decomposition """ +import sys import numpy as _np from scipy.linalg import lstsq @@ -69,16 +70,16 @@ def BEMA(Pnm, center_sig, dn, transition, avg_band_width=1, fade=True, max_order transition = int(_np.floor(transition)) - # computing spatiotemporal Image Imn - Imn = _np.zeros((Pnm.shape[0], 1), dtype=complex) # preallocating the spectral image matrix - start_avg_bin = int(_np.floor(transition / (_np.power(2, avg_band_width)))) # first bin for averaging + # computing spatio-temporal Image Imn + Imn = _np.zeros((Pnm.shape[0], 1), dtype=complex) # pre-allocating the spectral image matrix + start_avg_bin = int(_np.floor(transition / (_np.power(2, avg_band_width)))) # first bin for averaging modeCnt = 0 avgPower = 0 for n in range(0, max_order + 1): # sh orders for m in range(0, 2 * n + 1): # modes - for bin in range(start_avg_bin - 1, transition): # bins - synthBin = Pnm[modeCnt, bin] * dn[n, bin] + for inBin in range(start_avg_bin - 1, transition): # bins + synthBin = Pnm[modeCnt, inBin] * dn[n, inBin] Imn[modeCnt, 0] += synthBin avgPower += _np.abs(synthBin) ** 2 modeCnt += 1 @@ -97,8 +98,8 @@ def BEMA(Pnm, center_sig, dn, transition, avg_band_width=1, fade=True, max_order modeCnt = 0 for n in range(0, max_order + 1): for m in range(0, 2 * n + 1): - for bin in range(start_avg_bin - 1, Pnm_synth.shape[1]): - Pnm_synth[modeCnt, bin] = Imn[modeCnt, 0] * (1 / dn[n, bin]) * center_sig[0, bin] + for inBin in range(start_avg_bin - 1, Pnm_synth.shape[1]): + Pnm_synth[modeCnt, inBin] = Imn[modeCnt, 0] * (1 / dn[n, inBin]) * center_sig[0, inBin] modeCnt += 1 # Phase correction (Eq. 16) @@ -365,8 +366,8 @@ def plane_wave_decomp(order, wave_direction, field_coeffs, radial_filter, weight max_order = int(_np.floor(_np.sqrt(NMDeliveredSize) - 1)) if order > max_order: - raise ValueError('The provided coefficients deliver a maximum order of ' + str(max_order) + ' but order ' + str( - order) + ' was requested.') + raise ValueError( + f'The provided coefficients deliver a maximum order of {max_order} but order {order} was requested.') gaincorrection = 4 * _np.pi / ((order + 1) ** 2) @@ -546,15 +547,15 @@ def sfe(Pnm_kra, kra, krb, problem='interior'): exp = jn_krb / jn_kra if _np.any(_np.abs(exp) > 1e2): # 40dB - print('WARNING: Extrapolation might be unstable for one or more frequencies/orders!') + print('WARNING: Extrapolation might be unstable for one or more frequencies/orders!', file=sys.stderr) elif problem == 'exterior': hn_kra = _np.sqrt(_np.pi / (2 * kra)) * hankel1(nvector + 0.5, kra) hn_krb = _np.sqrt(_np.pi / (2 * krb)) * hankel1(nvector + 0.5, krb) exp = hn_krb / hn_kra else: - raise ValueError('Problem selector ' + problem + 'not recognized. Please either choose "interior" [Default] ' - 'or "exterior".') + raise ValueError( + f'Problem selector {problem} not recognized. Please either choose "interior" [Default] or "exterior".') return Pnm_kra * exp.T @@ -614,7 +615,8 @@ def wdr(Pnm, xAngle, yAngle, zAngle): PnmRot: array_like Rotated spatial Fourier coefficients """ - print('!WARNING. Wigner-D Rotation is not yet implemented. Continuing with un-rotated coefficients!') + print('!WARNING. Wigner-D Rotation is not yet implemented. Continuing with un-rotated coefficients!', + file=sys.stderr) return Pnm diff --git a/sound_field_analysis/utils.py b/sound_field_analysis/utils.py index 2c69cf0..a6ef6e9 100644 --- a/sound_field_analysis/utils.py +++ b/sound_field_analysis/utils.py @@ -43,15 +43,14 @@ def progress_bar(curIDX, maxIDX=None, description='Progress'): Clarify what's taking time """ if maxIDX is None: - print('\r' + description + ': ' + next(spinner), end='', flush=True) + print(f'\r{description}: {next(spinner)}', end='', flush=True) else: maxIDX = (int(maxIDX) - 1) if maxIDX == 0: amount_done = 1 else: amount_done = curIDX / maxIDX - print('\r' + description + ': [{0:50s}] {1:.1f}%'.format( - '#' * int(amount_done * 50), amount_done * 100), end="", flush=True) + print(f'\r{description}: [{"#" * int(amount_done * 50):50s}] {amount_done * 100:.1f}%', end='', flush=True) if amount_done >= 1: print('\n') @@ -193,7 +192,6 @@ def _is_grid_cartesian(grid): grid_values = grid_values.T.filled(0).copy() # transform into regular `numpy.ndarray` # TODO: validation of data units against individual convention should be done in `pysofaconventions` - print(grid_info) if _is_grid_spherical(grid_info): # given spherical degrees with elevation # transform into spherical radians with colatitude @@ -318,3 +316,9 @@ def zero_pad_fd(data_fd, target_length_td): def current_time(): return datetime.now() + + +def get_named_tuple__repr__(namedtuple): + # noinspection PyProtectedMember + fields_str = f',\n\t'.join(f'{f} = {repr(v)}' for f, v in zip(namedtuple._fields, namedtuple)) + return f'{namedtuple.__class__.__name__}(\n\t{fields_str})'