def uniform(direction, FoV, size): """ Uniform 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 : array-like(int) (N_height, N_width) 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 lim = np.sin(FoV / 2) Y, X = np.meshgrid(np.linspace(-lim, lim, N_height), np.linspace(-lim, lim, N_width), indexing="ij") Z = 1 - X**2 - Y**2 X[Z < 0], Y[Z < 0], Z[Z < 0] = 0, 0, 0 Z = np.sqrt(Z) XYZ = np.stack([X, Y, Z], axis=0) # Center grid at 'direction' _, dir_colat, dir_lon = transform.cart2pol(*direction) R1 = ilinalg.rot(axis=[0, 0, 1], angle=dir_lon) R2_axis = np.cross([0, 0, 1], direction) if np.allclose(R2_axis, 0): # R2_axis is in span(E_z), so we must manually set R2. R2 = np.eye(3) if direction[2] < 0: R2[2, 2] = -1 else: R2 = ilinalg.rot(axis=R2_axis, angle=dir_colat) R = R2 @ R1 XYZ = np.tensordot(R, XYZ, axes=1) return XYZ
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 _PrimaryHDU(self): """ Generate primary Header Descriptor Unit (HDU) for FITS export. Returns ------- hdu : :py:class:`~astropy.io.fits.PrimaryHDU` """ metadata = dict(IMG_TYPE=(self.__class__.__name__, "Image subclass")) # grid: stored as angles to reduce file size. _, colat, lon = transform.cart2pol(*self._grid) coordinates = np.stack([colat, lon], axis=0) hdu = fits.PrimaryHDU(data=coordinates) for k, v in metadata.items(): hdu.header[k] = v return hdu
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 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 equal_angle(N, direction=None, FoV=None): r""" (Region-limited) open grid of Equal-Angle sample-points 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 ------- q : :py:class:`~numpy.ndarray` (N_height,) polar indices. l : :py:class:`~numpy.ndarray` (N_width,) azimuthal indices. colat : :py:class:`~numpy.ndarray` (N_height, 1) polar angles [rad]. `N_height == 2N+2` in the whole-sphere case. lon : :py:class:`~numpy.ndarray` (1, N_width) azimuthal angles [rad]. `N_width == 2N+2` in the whole-sphere case. 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 equal_angle .. doctest:: >>> N = 3 >>> _, _, colat, lon = equal_angle(N) >>> np.around(colat, 2) array([[0.2 ], [0.59], [0.98], [1.37], [1.77], [2.16], [2.55], [2.95]]) >>> np.around(lon, 2) array([[0. , 0.79, 1.57, 2.36, 3.14, 3.93, 4.71, 5.5 ]]) Sampling a zonal function :math:`f(r): \mathbb{S}^{2} \to \mathbb{C}` of order :math:`N` on *part* of the sphere: .. doctest:: >>> N = 3 >>> direction = np.r_[0, 1, 0] >>> FoV = np.deg2rad(90) >>> q, l, colat, lon = equal_angle(N, direction, FoV) >>> q array([2, 3, 4, 5]) >>> np.around(colat, 2) array([[0.98], [1.37], [1.77], [2.16]]) >>> l array([1, 2, 3]) >>> np.around(lon, 2) array([[0.79, 1.57, 2.36]]) Notes ----- * The sample positions on the unit sphere are given (in radians) by [1]_: .. math:: \theta_{q} & = \frac{\pi}{2 N + 2} \left( q + \frac{1}{2} \right), \qquad & q \in \{ 0, \ldots, 2 N + 1 \}, \phi_{l} & = \frac{2 \pi}{2N + 2} l, \qquad & l \in \{ 0, \ldots, 2 N + 1 \}. * Longitudinal range may be erroneous if direction too close to [1, 0, 0]. .. [1] 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 np.allclose(np.cross([0, 0, 1], direction), 0): raise ValueError( "Generating Equal-Angle grids centered at poles currently not supported." ) # Why? Because the grid layout is spatially incorrect in this degenerate case. if FoV is not None: if not (0 < np.rad2deg(FoV) <= 179): raise ValueError("Parameter[FoV] must be in (0, 179] degrees.") else: raise ValueError( "Parameter[FoV] must be specified if Parameter[direction] provided." ) if N <= 0: raise ValueError("Parameter[N] must be non-negative.") def ea_sample(N: int): _2N2 = 2 * N + 2 q, l = np.ogrid[:_2N2, :_2N2] colat = (np.pi / _2N2) * (0.5 + q) lon = (2 * np.pi / _2N2) * l return colat, lon colat_full, lon_full = ea_sample(N) q_full = np.arange(colat_full.size) l_full = np.arange(lon_full.size) if direction is None: # full-sphere case return q_full, l_full, colat_full, lon_full else: _, dir_colat, dir_lon = transform.cart2pol(*direction) lim_lon = dir_lon + (FoV / 2) * np.r_[-1, 1] lim_lon = coord.Angle(lim_lon * u.rad).wrap_at(360 * u.deg).to_value( u.rad) lim_colat = dir_colat + (FoV / 2) * np.r_[-1, 1] lim_colat = (max(np.deg2rad(0.5), lim_colat[0]), min(lim_colat[1], np.deg2rad(179.5))) q_mask = (lim_colat[0] <= colat_full) & (colat_full <= lim_colat[1]) if lim_lon[0] < lim_lon[1]: l_mask = (lim_lon[0] <= lon_full) & (lon_full <= lim_lon[1]) else: l_mask = (lim_lon[0] <= lon_full) | (lon_full <= lim_lon[1]) q_mask = np.reshape(q_mask, (-1, )) l_mask = np.reshape(l_mask, (-1, )) q, l = q_full[q_mask], l_full[l_mask] colat, lon = colat_full[q_mask, :], lon_full[:, l_mask] return q, l, colat, lon