sfstoolbox / sfs-python

SFS Toolbox for Python

Home Page:https://sfs-python.readthedocs.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Amplitude normalization factor in point_25d functions

pizqleh opened this issue · comments

Describe the problem/bug
Apparently, the driving function given by wfs.point_25d and nfchoa.point_25d is not consistent with the common normalization of the point source model for the loudspeakers. It seems that it has an extra factor of n_loudspeakers / (2 pi radius).

To Reproduce

import numpy as np
import matplotlib.pyplot as plt
import sfs


### MANUAL OUTILS.
def row_wise_norm2(array):
    return np.sum(np.abs(array) ** 2, axis=-1) ** (1. / 2)


def g3d(x, x0, f, c=343):
    r = row_wise_norm2(x - x0)
    k = 2 * np.pi * f / c
    return np.e ** (-1j * k * r) / (4 * np.pi * r)


def plot(image, name):
    maximum = np.nanpercentile(np.ravel(image), 99)
    im = plt.imshow(np.flip(image, axis=0),
                    cmap=plt.cm.RdBu_r,
                    vmax=maximum,
                    vmin=-maximum,
                    extent=[start_x, end_x, start_y, end_y],
                    interpolation='spline16')
    plt.colorbar(im)
    plt.title(name)
    plt.show()

# Loudspeakers
radius = 1
n_loud = 50
loud_positions = np.asarray([[radius * np.cos(2 * np.pi * i / n_loud),
                              radius * np.sin(2 * np.pi * i / n_loud)] for i in range(n_loud)])

# Mesh.
spacing = 0.01
n_mesh = int(3 * radius / spacing) + 1
start_x, end_x, start_y, end_y = 1.5 * radius * np.asarray([-1, 1, -1, 1])  # From left to right and from top to bottom.
mesh_x, mesh_y = np.mgrid[start_x:end_x:n_mesh * 1j, start_y:end_y:n_mesh * 1j]
points = np.vstack([mesh_x.ravel(), mesh_y.ravel()]).T

# Point source attributes.
position = np.asarray([10, 0, 0])
frequency = 343

# G transfer function.
G = np.asarray([g3d(points, loud_positions[i], frequency) for i in range(n_loud)]).T


### POINT SOURCE.
# Sfs way to generate point source.
grid = sfs.util.xyz_grid([start_x, end_x], [start_y, end_y], 0, spacing=spacing)
point_source_sfs = np.real(sfs.fd.source.point(2 * np.pi * frequency, position, grid))
plot(point_source_sfs, 'Point Source (sfs)')

# Manual way to generate point source.
point_source_manual = np.real(g3d(points, position[:-1], 343)).reshape(n_mesh, n_mesh).T
plot(point_source_manual, 'Point Source (manual)')

# Plot of difference.
plot(point_source_manual - point_source_sfs, 'Point Source Difference (manual - sfs)')


## SOUND FIELD SYNTHESIS.
array = sfs.array.circular(n_loud, radius)
omega = 2 * np.pi * frequency
normalization = n_loud / (2 * np.pi * radius)

## HOA.
d_hoa, s_hoa, ss_hoa = sfs.fd.nfchoa.point_25d(omega, array.x, radius, position)
d_hoa /= normalization

# Sfs way to display the HOA sound field.
u_hoa_sfs = np.real(sfs.fd.synthesize(d_hoa, np.ones(n_loud), array, ss_hoa, grid=grid)) * normalization
plot(u_hoa_sfs, 'HOA (sfs)')

# Manual way to display the HOA sound field.
u_hoa_manual = np.real(G @ d_hoa).reshape(n_mesh, n_mesh).T
plot(u_hoa_manual, 'HOA (manual)')

# Plot difference.
plot(u_hoa_sfs - u_hoa_manual, 'HOA difference (manual - sfs)')
plot(point_source_sfs - u_hoa_sfs, 'HOA - point source')


## WFS.
d_wfs, s_wfs, ss_wfs = sfs.fd.wfs.point_25d(omega, array.x, array.n, position)
d_wfs /= normalization

# Sfs way to display the WFS sound field.
u_wfs_sfs = np.real(sfs.fd.synthesize(d_wfs, s_wfs, array, ss_wfs, grid=grid)) * normalization
plot(u_wfs_sfs, 'WFS (sfs)')

# Manual way to display the WFS sound field.
u_wfs_manual = np.real(G @ (s_wfs * d_wfs)).reshape(n_mesh, n_mesh).T
plot(u_wfs_manual, 'WFS (sfs)')

# Plot difference.
plot(u_wfs_sfs - u_wfs_manual, 'WFS difference (manual - sfs)')
plot(point_source_sfs - u_wfs_sfs, 'WFS - point source')

Expected behavior
One can see that there is no problem with the point source case. However, in HOA and WFS, there is a multiplying factor between the sound fields generated manually (multiplying the driving function by the G transfer function) and the sound fields generated by fd.synthesize. I think that this should not happen, because in both cases the driving function was the same: the one given by wfs.point_25d or nfchoa.point_25d. Moreover, the ratio between the sound fields should be 1, but it is 7.957747154594, which is an approximation to n_loud / (2 pi radius). (In the example radius = 1 and n_loud=50, but one can try examples varying the radius and the number of loudspeakers to see the trend).

It's wierd, because the sound field given by fd.synthesize is actually the correct one (is the one that ressembles the point source case), and the sound field generated manually is too large (7.9577 times). But we are generating the sound field manually under the common point source mode. So one can infere that is in the driving function where there is an extra factor of (n_loud / (2 pi radius)) that actually is then compensated inside fd.synthesize by an other factor of 2 pi radius/ n_loud.

It's something wrong in this analysis?
Thank you in advance,

Pedro Izquierdo L.

Thanks for bringing this up.

The given code example is not straightforwardly working with radius not equal 1, since the grid sizes then get different and thus subtracting of these arrays is not working. In general, for finding bugs it is a good idea to choose rather odd numbers for parameters. Multiplication with 1 might mask certain important details.

Anyway, your actual problem seems to be the following: since the sum G @ d_hoa operation should approximate the SFS integral, we need to include weights of the SSD. In your case of circular array we need the arc length between two speakers
ssd_weight = radius * 2*np.pi/n_loud which then should be included in G @ d_hoa * ssd_weight. If we have not regular sampling of the SSD, we obviously need to deal with an array of weights. This is always stored in an sfs generated array.a. The synthesize() then handles this weighting, see ssd.a -> a

p = 0
    for x, n, a, d, weight in zip(ssd.x, ssd.n, ssd.a, d, weights):
        if weight != 0:
            p += a * weight * d * secondary_source_function(x, n, **kwargs)

In a manual version this should be implemented as well. Then the sound fields should match.

I see.. this fully explains the phenomena. Thank you very much!