Source code for spharpy.samplings.interior

"""Calculation of interior stabilization points for open arrays."""
import numpy as np
import scipy.special as spspecial
from spharpy.special import spherical_bessel_zeros, spherical_harmonic
import pyfar as pf


[docs] def interior_stabilization_points(kr_max, resolution_factor=1): """Find stabilization points inside for an open spherical microphone array. The interior points stabilize the array response at the eigenfrequencies of the array. The algorithm is based on [#]_ and implemented following the Matlab code provided by Gilles Chardon on his homepage at [#]_. The stabilization points are independent of the sampling of the sphere and can therefore be combined with arbitrary spherical samplings. Parameters ---------- kr_max : float The maximum kr value to be considered. This will define the upper frequency limit of the array. resolution_factor : int Factor to increase the spatial resolution of the grid used to estimate the stabilization points. Default is ``1``. Returns ------- sampling_interior : Coordinates Coordinates of the stabilization points References ---------- .. [#] G. Chardon, W. Kreuzer, und M. Noisternig, "Design of spatial microphone arrays for sound field interpolation", IEEE Journal of Selected Topics in Signal Processing .. [#] https://gilleschardon.fr/jstsp_array/ """ x, y, z = find_interior_points(kr_max, resolution_factor=resolution_factor) return pf.Coordinates(x, y, z)
def find_eigenfrequencies(kr_max): """ Find the eigenfrequencies for the sphere from the spherical Bessel function. """ jn_zeros = spherical_bessel_zeros(kr_max, kr_max) eigenfrequencies = [] for idx in range(kr_max + 1): roots = jn_zeros[idx] roots = roots[roots < kr_max] if roots.size != 0: eigenfrequencies.append(roots) mults = np.arange(1, 2*len(eigenfrequencies)) return eigenfrequencies, mults def calculate_eigenspaces(kr_max, theta, phi, rad): """Calculate the eigenspaces for the corresponding eigenfrequencies of the sphere. Parameters ---------- kr_max : float The largest wave number to be included theta : array, float Azimuth angle phi : array, float Elevation angle rad : array, float Radius Returns ------- eigenspaces : list, ndarray, float List containing all eigenspaces """ eigenfrequencies, mults = find_eigenfrequencies(kr_max) subspaces = [] for u in range(len(eigenfrequencies)): subspaces.extend( sph_modes_matrix(u, root, theta, phi, rad) for root in eigenfrequencies[u]) return subspaces, mults def sph_modes_matrix(n_max, k, theta, phi, rad): """Build the matrix containing all spherical harmonic modes of the domain inside an open sphere for a specific order n_max. Parameters ---------- n_max : int Spherical harmonic order k : float Wave number theta : array, float Azimuth angle phi : array, float Elevation angle rad : array, float Radius Returns ------- modes : array, float A matrix with dimension [(...) x (2*n_max+1)] containing all spherical harmonic modes. Note ---- This function returns only coefficients for one order, but all degrees. """ n_coefficients = 2*n_max+1 meshgrid_shape = theta.shape B = spspecial.spherical_jn(n_max, rad.flatten()*k) * 4*np.pi * (1j)**n_max B = np.reshape(B, meshgrid_shape) M = np.zeros((*meshgrid_shape, n_coefficients), dtype=complex) for m in range(-n_max, n_max+1): Y_m = spherical_harmonic(n_max, m, phi.flatten(), theta.flatten()) M[:, :, :, m+n_max] = B * np.reshape(Y_m, meshgrid_shape) return M def ball_dot(S1, S2, radius, phi): """ Calculate the dot product of two spherical harmonic modes on a sphere. The dot product is defined as the integral over the sphere of the product of the two modes multiplied by a weighting function. The weighting function is defined as the square of the radius times the sine of the elevation angle. The weighting function is used to account for the spherical geometry. The integral is approximated by a sum over the spherical coordinates. The function is used to calculate the orthogonality of the spherical harmonic modes. Parameters ---------- S1 : array, complex The first spherical harmonic mode. S2 : array, complex The second spherical harmonic mode. radius : array, float The radius of the sphere. phi : array, float The elevation angle of the sphere. Returns ------- float The dot product of the two spherical harmonic modes. """ wphi = np.sin(phi) wr = radius**2 w = wr*wphi return np.sum(np.conj(S1) * S2 * w) / np.sum(w) def find_interior_points(k_max, resolution_factor=1): """Find the interior points of an open spherical microphone array. The interior points stabilize the array response at the eigenfrequencies of the array. The algorithm is based on [#]_ and implemented following the Matlab code provided by Gilles Chardon on his homepage at [#]_. The stabilization points are independent of the sampling of the sphere and can therefore be combined with arbitrary spherical samplings. Parameters ---------- k_max : float The maximum kr value to be considered. This will define the upper frequency limit of the array. resolution_factor : int Factor to increase the spatial resolution of the grid used to estimate the stabilization points. Returns ------- x, y, z : ndarray, float Coordinates of the stabilization points References ---------- .. [#] G. Chardon, W. Kreuzer, und M. Noisternig, "Design of spatial microphone arrays for sound field interpolation", IEEE Journal of Selected Topics in Signal Processing .. [#] https://gilleschardon.fr/jstsp_array/ """ resolution = 50 * resolution_factor vec_theta = np.linspace(0, 2 * np.pi, resolution*2) vec_phi = np.linspace(0, np.pi, resolution) vec_rad = np.linspace(0, 1, resolution) phi, theta, rad = np.meshgrid(vec_phi, vec_theta, vec_rad) meshgrid_shape = (resolution*2, resolution, resolution) subspaces, mults = calculate_eigenspaces(k_max, theta, phi, rad) max_mult = mults.max() idx_sel = [] for _ in range(max_mult): maxes = np.ones((*meshgrid_shape, len(subspaces))) * 1e3 for idx_space in range(len(subspaces)): if subspaces[idx_space].size > 0: maxes[:, :, :, idx_space] = 0.0 for v in range(subspaces[idx_space].shape[3]): vector = subspaces[idx_space][:, :, :, v] for www in range(v): vector = vector - ball_dot( subspaces[idx_space][:, :, :, www], vector, rad, phi) * \ subspaces[idx_space][:, :, :, www] vector = vector / np.sqrt(np.abs(ball_dot(vector, vector, rad, phi))) subspaces[idx_space][:, :, :, v] = vector maxes[:, :, :, idx_space] += np.abs(vector) ** 2 minmax = np.min(maxes, axis=3) argmax = np.argmax(minmax) argmax_unravel = np.unravel_index(argmax, meshgrid_shape) idx_sel.append(argmax) for idx_space in range(len(subspaces)): if subspaces[idx_space].shape[3] <= 1: subspaces[idx_space] = np.array([[[[]]]]) else: value_end = 0 www = -1 while value_end == 0: www += 1 vend = subspaces[idx_space][:, :, :, www].copy() value_end = vend[argmax_unravel] subspaces[idx_space][:, :, :, www] = \ subspaces[idx_space][:, :, :, -1] subspaces[idx_space][:, :, :, -1] = vend for v in range(subspaces[idx_space].shape[3] - 1): vv = subspaces[idx_space][:, :, :, v] value_vv = vv[argmax_unravel] subspaces[idx_space][:, :, :, v] = \ vv - vend / value_end * value_vv subspaces[idx_space] = subspaces[idx_space][:, :, :, :-1] idx_sel_unravel = np.unravel_index(idx_sel, meshgrid_shape) theta_opt = theta[idx_sel_unravel] phi_opt = phi[idx_sel_unravel] rad_opt = rad[idx_sel_unravel] x = rad_opt * np.cos(theta_opt) * np.sin(phi_opt) y = rad_opt * np.sin(theta_opt) * np.sin(phi_opt) z = rad_opt * np.cos(phi_opt) return x, y, z