def __call__(self, colat_idx, lon_idx, f, r): """ Interpolate function samples at order `N`. Parameters ---------- colat_idx : :py:class:`~numpy.ndarray` (N_colat,) polar support indices from :py:func:`~imot_tools.math.sphere.grid.equal_angle`. lon_idx : :py:class:`~numpy.ndarray` (N_lon,) azimuthal support indices from :py:func:`~imot_tools.math.sphere.grid.equal_angle`. f : :py:class:`~numpy.ndarray` (L, N_colat, N_lon) zonal function values at support points. (float or complex) r : :py:class:`~numpy.ndarray` (3, N_px) evaluation points. Returns ------- f_interp : :py:class:`~numpy.ndarray` (L, N_px) function values at specified coordinates. """ N_colat = colat_idx.size if not (colat_idx.shape == (N_colat, )): raise ValueError( "Parameter[colat_idx] must have shape (N_colat,).") N_lon = lon_idx.size if not (lon_idx.shape == (N_lon, )): raise ValueError("Parameter[lon_idx] must have shape (N_lon,).") L = len(f) if not (f.shape == (L, N_colat, N_lon)): raise ValueError( "Parameter[f] must have shape (L, N_colat, N_lon).") if not ((r.ndim == 2) and (r.shape[0] == 3)): raise ValueError("Parameter[r] must have shape (3, N_px).") # Apply weights directly onto `f` to avoid memory blow-up. _, _, colat, lon = grid.equal_angle(self._N) a = np.arange(self._N + 1) weight = (np.sum(np.sin((2 * a + 1) * colat[colat_idx]) / (2 * a + 1), axis=1) * np.sin(colat[colat_idx, 0]) * ((2 * np.pi) / ((self._N + 1)**2))) # (N_colat,) fw = f * weight.reshape((1, N_colat, 1)) # (L, N_colat, N_lon) f_interp = super().__call__( weight=np.broadcast_to([1], (N_colat * N_lon, )), support=transform.pol2cart(1, colat[colat_idx, :], lon[:, lon_idx]).reshape((3, -1)), f=fw.reshape((L, -1)), r=r, sparsity_mask=None, ) return f_interp
def spherical(direction, FoV, size): """ Spherical pixel grid. Parameters ---------- direction : :py:class:`~numpy.ndarray` (3,) vector around which the grid is centered. FoV : float Span of the grid [rad] centered at `direction`. size : :py:class:`~numpy.ndarray` (N_height, N_width) The grid will consist of `N_height` concentric circles around `direction`, each containing `N_width` pixels. Returns ------- XYZ : :py:class:`~numpy.ndarray` (3, N_height, N_width) pixel grid. """ direction = np.array(direction, dtype=float) direction /= linalg.norm(direction) if not (0 < np.rad2deg(FoV) <= 179): raise ValueError("Parameter[FoV] must be in (0, 179] degrees.") size = np.array(size, copy=False) if np.any(size <= 0): raise ValueError("Parameter[size] must contain positive entries.") N_height, N_width = size colat, lon = np.meshgrid(np.linspace(0, FoV / 2, N_height), np.linspace(0, 2 * np.pi, N_width), indexing="ij") XYZ = transform.pol2cart(1, colat, lon) # Center grid at 'direction' _, dir_colat, _ = transform.cart2pol(*direction) R_axis = np.cross([0, 0, 1], direction) if np.allclose(R_axis, 0): # R_axis is in span(E_z), so we must manually set R R = np.eye(3) if direction[2] < 0: R[2, 2] = -1 else: R = ilinalg.rot(axis=R_axis, angle=dir_colat) XYZ = np.tensordot(R, XYZ, axes=1) return XYZ
def spherical_geometry(): """ Spherical 3D microphone array geometry. Radius: 0.20[m] Returns ------- XYZ : :py:class:`~numpy.ndarray` (3, N_antenna) Cartesian coordinates. """ N_antenna = 64 n = np.arange(N_antenna) colat = np.arccos(1 - (2 * n + 1) / N_antenna) lon = (4 * np.pi * n) / (1 + np.sqrt(5)) r = 0.2 XYZ = np.stack(transform.pol2cart(r, colat, lon), axis=0) return XYZ
def from_circular_distribution(direction, FoV, N_src): """ Distribute `N_src` sources on a circle centered at `direction`. Parameters ---------- direction : :py:class:`~numpy.ndarray` (3,) direction in the sky. FoV : float Spherical angle [rad] of the sky centered at `direction` from which sources are extracted. N_src : int Number of dominant sources to extract. Returns ------- sky_model : :py:class:`~deepwave.tools.data_gen.source.SkyModel` Sky model. """ if not (0 < FoV < 2 * np.pi): raise ValueError('Parameter[FoV] must lie in (0, 360) degrees.') colat = FoV / 4 lon = np.linspace(0, 2 * np.pi, N_src, endpoint=False) XYZ = np.stack(transform.pol2cart(1, colat, lon), axis=0) # Center grid at 'direction' _, dir_colat, _ = transform.cart2pol(*direction) R_axis = np.cross([0, 0, 1], direction) if np.allclose(R_axis, 0): # R_axis is in span(E_z), so we must manually set R R = np.eye(3) if direction[2] < 0: R[2, 2] = -1 else: R = pylinalg.rot(axis=R_axis, angle=dir_colat) XYZ = np.tensordot(R, XYZ, axes=1) I = np.ones((N_src, )) sky_model = SkyModel(XYZ, I) return sky_model
def _from_fits(cls, primary_hdu, image_hdu): """ Load image from Header Descriptor Units. Parameters ---------- primary_hdu : :py:class:`~astropy.io.fits.PrimaryHDU` image_hdu : :py:class:`~astropy.io.fits.ImageHDU` Returns ------- I : :py:class:`~imot_tools.io.s2image.Image` """ # PrimaryHDU: grid specification. colat, lon = primary_hdu.data grid = transform.pol2cart(1, colat, lon) # ImageHDU: extract data cube. data = image_hdu.data I = cls(data=data, grid=grid) return I
def sky(f): """ Extract Sky Model from file name. Parameters ========== f : :py:class:`~pathlib.Path` .wav file with name of the form "<characters>_spkr[012]_angle[0:360:2].wav" Returns ======= sky_model : :py:class:`~deepwave.tools.data_gen.source.SkyModel` Container with source information. `sky_model.intensity` will contain the speaker number {0, 1, 2}. """ pattern = r'.+_spkr(\d+)_angle(\d+)\.wav' match = re.search(pattern, str(f)) if match: idx_speaker, idx_angle = map(int, match.group(1, 2)) if idx_speaker not in (0, 1, 2): raise ValueError(f'speaker number {idx_speaker} is out of bounds.') if idx_angle not in np.arange(0, 360, 2): raise ValueError(f'angle number {idx_angle} is out of bounds.') m_speaker = speaker_map() colat = data()['sources'][m_speaker[idx_speaker]]['colatitude'][str( idx_angle)] lon = data()['sources'][m_speaker[idx_speaker]]['azimuth'][str( idx_angle)] src_xyz = transform.pol2cart(1, colat, lon) src_I = np.r_[idx_speaker] sky_model = source.SkyModel(src_xyz, src_I) return sky_model else: raise ValueError('Parameter[f] does not have the right form.')
def simulate_dataset(N_sample, N_src, XYZ, R, wl, src_mask, intensity=None, rate=None): """ Generate APGD dataset. Parameters ---------- N_sample : int Number of images to generate. N_src : int Number of sources present per image. XYZ : :py:class:`~numpy.ndarray` (3, N_antenna) microphone positions. R : :py:class:`~numpy.ndarray` (3, N_px) pixel grid. wl : float Wavelength [m] of plane wave. src_mask : :py:class:`~numpy.ndarray` (N_px,) boolean mask saying near which pixels it is possible to place sources. intensity : float If present, generate equi-amplitude sources. rate : float If present, generate rayleigh-amplitude sources. Returns ------- D : :py:class:`~acoustic_camera.nn.DataSet` (N_sample,) dataset Note ---- Either `intensity` or `rate` must be specified, not both. """ if not (N_sample > 0): raise ValueError('Paremeter[N_sample] must be positive.') if not (N_src > 0): raise ValueError('Paremeter[N_src] must be positive.') if not ((XYZ.ndim == 2) and (XYZ.shape[0] == 3)): raise ValueError('Parameter[XYZ]: expected (3, N_antenna) array.') N_antenna = XYZ.shape[1] if not ((R.ndim == 2) and (R.shape[0] == 3)): raise ValueError('Parameter[R]: expected (3, N_px) array.') N_px = R.shape[1] if wl < 0: raise ValueError('Parameter[wl] is out of bounds.') if not ((src_mask.ndim == 1) and (src_mask.size == N_px)): raise ValueError('Parameter[src_mask]: expected (N_px,) boolean array.') if (((intensity is None) and (rate is None)) or ((intensity is not None) and (rate is not None))): raise ValueError('One of Parameters[intensity, rate] must be specified.') if (intensity is not None) and (intensity <= 0): raise ValueError('Parameter[intensity] must be positive.') if (rate is not None) and (rate <= 0): raise ValueError('Parameter[rate] must be positive.') vis_gen = statistics.VisibilityGenerator(T=50e-3, fs=48000, SNR=10) A = phased_array.steering_operator(XYZ, R, wl) sampler = nn.Sampler(N_antenna, N_px) N_data = sampler._N_cell data = np.zeros((N_sample, N_data)) ground_truth = [None] * N_sample apgd_gamma = 0.5 apgd_lambda_ = np.zeros(N_sample) apgd_N_iter = np.zeros(N_sample) apgd_tts = np.zeros(N_sample) for i in range(N_sample): logging.info(f'Generate APGD image {i + 1}/{N_sample}.') ### Create synthetic sky if (intensity is not None): sky_I = intensity * np.ones(N_src) elif (rate is not None): sky_I = stats.rayleigh.rvs(scale=rate, size=N_src) sky_XYZ = R[:, src_mask][:, np.random.randint(0, np.sum(src_mask), size=N_src)] ## Randomize positions slightly to not fall straight onto grid. _, sky_colat, sky_lon = transform.cart2pol(*sky_XYZ) px_pitch = np.arccos(np.clip(R[:, 0] @ R[:, 1:], -1, 1)).min() colat_noise, lon_noise = 0.1 * px_pitch * np.random.randn(2, N_src) sky_XYZ = np.stack(transform.pol2cart(1, sky_colat + colat_noise, sky_lon + lon_noise), axis=0) sky_model = source.SkyModel(sky_XYZ, sky_I) S = vis_gen(XYZ, wl, sky_model) # Normalize `S` spectrum for scale invariance. S_D, S_V = linalg.eigh(S) if S_D.max() <= 0: S_D[:] = 0 else: S_D = np.clip(S_D / S_D.max(), 0, None) S = (S_V * S_D) @ S_V.conj().T I_apgd = apgd.solve(S, A, lambda_=None, gamma=apgd_gamma, L=None, d=50, x0=None, eps=1e-3, N_iter_max=200, verbosity='NONE', # 'LOW', momentum=True) data[i] = sampler.encode(S=S, I=I_apgd['sol']) ground_truth[i] = sky_model apgd_lambda_[i] = I_apgd['lambda_'] apgd_N_iter[i] = I_apgd['niter'] apgd_tts[i] = I_apgd['time'] D = nn.DataSet(data, XYZ, R, wl, ground_truth, apgd_lambda_, apgd_gamma, apgd_N_iter, apgd_tts) return D
def fibonacci(N, direction=None, FoV=None): r""" (Region-limited) near-uniform sampling on the sphere. Parameters ---------- N : int Order of the grid, i.e. there will be :math:`4 (N + 1)^{2}` points on the sphere. direction : :py:class:`~numpy.ndarray` (3,) vector around which the grid is centered. If :py:obj:`None`, then the grid covers the entire sphere. FoV : float Span of the grid [rad] centered at `direction`. This parameter is ignored if `direction` left unspecified. Returns ------- XYZ : :py:class:`~numpy.ndarray` (3, N_px) sample points. `N_px == 4*(N+1)**2` if `direction` left unspecified. Examples -------- Sampling a zonal function :math:`f(r): \mathbb{S}^{2} \to \mathbb{C}` of order :math:`N` on the sphere: .. testsetup:: import numpy as np from imot_tools.math.sphere.grid import fibonacci .. doctest:: >>> N = 2 >>> XYZ = fibonacci(N) >>> np.around(XYZ, 2) array([[ 0.23, -0.29, 0.04, 0.36, -0.65, 0.61, -0.2 , -0.37, 0.8 , -0.81, 0.39, 0.28, -0.82, 0.95, -0.56, -0.13, 0.76, -1. , 0.71, -0.05, -0.63, 0.97, -0.79, 0.21, 0.46, -0.87, 0.8 , -0.33, -0.27, 0.68, -0.7 , 0.36, 0.1 , -0.4 , 0.4 , -0.16], [ 0. , -0.27, 0.51, -0.47, 0.12, 0.39, -0.74, 0.72, -0.29, -0.34, 0.82, -0.89, 0.48, 0.21, -0.8 , 0.98, -0.64, -0.04, 0.71, -1. , 0.76, -0.13, -0.55, 0.93, -0.81, 0.28, 0.37, -0.78, 0.76, -0.36, -0.18, 0.56, -0.58, 0.31, 0.03, -0.17], [ 0.97, 0.92, 0.86, 0.81, 0.75, 0.69, 0.64, 0.58, 0.53, 0.47, 0.42, 0.36, 0.31, 0.25, 0.19, 0.14, 0.08, 0.03, -0.03, -0.08, -0.14, -0.19, -0.25, -0.31, -0.36, -0.42, -0.47, -0.53, -0.58, -0.64, -0.69, -0.75, -0.81, -0.86, -0.92, -0.97]]) Sampling a zonal function :math:`f(r): \mathbb{S}^{2} \to \mathbb{C}` of order :math:`N` on *part* of the sphere: .. doctest:: >>> N = 2 >>> direction = np.r_[1, 0, 0] >>> FoV = np.deg2rad(90) >>> XYZ = fibonacci(N, direction, FoV) >>> np.around(XYZ, 2) array([[ 0.8 , 0.95, 0.76, 0.71, 0.97, 0.8 ], [-0.29, 0.21, -0.64, 0.71, -0.13, 0.37], [ 0.53, 0.25, 0.08, -0.03, -0.19, -0.47]]) Notes ----- The sample positions on the unit sphere are given (in radians) by [2]_: .. math:: \cos(\theta_{q}) & = 1 - \frac{2 q + 1}{4 (N + 1)^{2}}, \qquad & q \in \{ 0, \ldots, 4 (N + 1)^{2} - 1 \}, \phi_{q} & = \frac{4 \pi}{1 + \sqrt{5}} q, \qquad & q \in \{ 0, \ldots, 4 (N + 1)^{2} - 1 \}. .. [2] B. Rafaely, "Fundamentals of Spherical Array Processing", Springer 2015 """ if direction is not None: direction = np.array(direction, dtype=float) direction /= linalg.norm(direction) if FoV is not None: if not (0 < np.rad2deg(FoV) < 360): raise ValueError("Parameter[FoV] must be in (0, 360) degrees.") else: raise ValueError( "Parameter[FoV] must be specified if Parameter[direction] provided." ) if N < 0: raise ValueError("Parameter[N] must be non-negative.") N_px = 4 * (N + 1)**2 n = np.arange(N_px) colat = np.arccos(1 - (2 * n + 1) / N_px) lon = (4 * np.pi * n) / (1 + np.sqrt(5)) XYZ = np.stack(transform.pol2cart(1, colat, lon), axis=0) if direction is not None: # region-limited case. # TODO: highly inefficient to generate the grid this way! min_similarity = np.cos(FoV / 2) mask = (direction @ XYZ) >= min_similarity XYZ = XYZ[:, mask] return XYZ