def general_gamma_binary(x, y, i1, i2): k = 2 * np.pi / wavelength beta = mas2rad(sep) th = np.deg2rad(theta) phi1 = k * x * beta * np.cos(th) / 2 phi2 = k * y * beta * np.sin(th) / 2 out = i1 * np.exp(-1j * (phi1 + phi2)) out += i2 * np.exp(1j * (phi1 + phi2)) return out / (i1 + i2)
def test_pynfft(): pixels = 128 wavel = 1.5e-6 plate_scale = 1.2 # mas / pixel x = np.arange(pixels) - pixels // 2 xx, yy = np.meshgrid(x, x) image = np.zeros_like(xx) rho = np.sqrt(xx ** 2 + yy ** 2) image = image + 1.0 * (rho < 5) mask = np.random.normal(0, 6, [100, 2]) def uv_samples(mask): N = mask.shape[0] uv = np.zeros([N * (N - 1) // 2, 2]) k = 0 for i in range(N): for j in range(i + 1, N): uv[k, 0] = mask[i, 0] - mask[j, 0] uv[k, 1] = mask[i, 1] - mask[j, 1] k += 1 return uv uv = uv_samples(mask) start = time.time() ndft = NDFTM(uv, wavel, pixels, plate_scale) vis1 = np.einsum("ij, j -> i", ndft, np.ravel(image)) end = time.time() - start print(f"Took {end:.4f} second to compute NDFTM") phase = np.exp(-1j * np.pi / wavel * mas2rad(plate_scale) * (uv[:, 0] + uv[:, 1])) start = time.time() plan = NFFT([pixels, pixels], uv.shape[0], n=[pixels, pixels]) plan.x = uv / wavel * mas2rad(plate_scale) plan.f_hat = image plan.precompute() plan.trafo() vis = plan.f.copy() * phase end = time.time() - start print(f"Took {end:.4f} seconds to compute NFFT") assert np.allclose(np.abs(vis), np.abs(vis1), rtol=1e-5) assert np.allclose(np.sin(np.angle(vis) - np.angle(vis1)), np.zeros_like(vis), atol=1e-5)
def NDFTM(coords, wavelength, pixels, plate_scale, inv=False, dprec=True): ''' (modified from F. Martinache Xara project) Computes the Non uniform 2D Discrete Fourier Transform Matrix to transform flattened 2D image into complex Fourier coefficient. parameters: ---------- - coords : vector of baseline (u,v) coordinates where to compute the FT (usually coords=Baselines.UVC) - wavelength: wavelength of light observed (in meters) - pixels: number of pixels of mage grid (on a side) - plate_scale: Plate scale of the camera in mas/pixel (that is 206265/(1000 * f[mm] * pixel_density[pixel/mm]) or simply FOV [mas] / pixels where FOV stands for field of view and f is the focal length) Options: ------ - inv : Boolean (default=False) : True -> computes inverse DFT matrix - dprec : double precision (default=True) For an image of size pixels^2, the computation requires what can be a fairly large (N_UV x pixels^2) auxilliary matrix. Consider using pyNFFT (or equivalent) for Non uniform Fast Fourier Transform. Example: -------- >> B = exorim.operators.Baselines(mask_coordinates) >> A = NDFTM(B.UVC, wavelength, pixels, FOV / pixels) ''' m2pix = mas2rad(plate_scale) * pixels / wavelength # meter to pixels i2pi = 1j * 2 * np.pi mydtype = np.complex64 if dprec is True: mydtype = np.complex128 xx, yy = pixel_grid(pixels) uvc = coords * m2pix # scale correctly uv and xy so that ux + vy is in RAD nuv = uvc.shape[0] if inv is True: WW = np.zeros((pixels**2, nuv), dtype=mydtype) for i in range(nuv): # Inverse is scaled correctly with the 4 pi^2 (2D transform) WW[:, i] = 1. / 4. / np.pi**2 * np.exp( i2pi * (uvc[i, 0] * xx.flatten() + uvc[i, 1] * yy.flatten()) / float(pixels)) else: WW = np.zeros((nuv, pixels**2), dtype=mydtype) for i in range(nuv): WW[i] = np.exp( -i2pi * (uvc[i, 0] * xx.flatten() + uvc[i, 1] * yy.flatten()) / float(pixels)) return WW
def gamma_binary(x, y, i1, i2): k = 2 * np.pi / wavelength beta = mas2rad(sep) out = (i1 - i2) / (i1 + i2) * np.exp(1j * k * x * beta / 2) out += 2 * i2 / (i1 + i2) * np.cos(np.pi * k * x * beta / 2) return out
def scale2freq(x): return 1/mas2rad(x)
def main(): # Defines physical variables np.random.seed(42) N = 21 L = 6 radius = 20 # pixels lam_circle = 4*radius pixels = 64 wavel = 0.5e-6 var = 5 # mask x = (L + np.random.normal(0, var, N)) * np.cos(2 * np.pi * np.arange(N) / N) y = (L + np.random.normal(0, var, N)) * np.sin(2 * np.pi * np.arange(N) / N) circle_mask = np.array([x, y]).T mask = np.random.normal(0, L, (N, 2)) # selected_mask = circle_mask selected_mask = mask B = Operators(selected_mask, wavel) rho = np.sqrt(B.UVC[:, 0]**2 + B.UVC[:, 1]**2)/wavel theta = rad2mas(1/rho) print(mode(theta)[0][0]) print(np.std(theta)) # this favors high frequencies over large ones. # plate_scale = (mode(theta)[0][0] + 2*np.std(theta))/pixels plate_scale = (np.median(theta) + 1*np.std(theta))/pixels # plate_scale = np.max(theta)/pixels # plate_scale = 5*np.std(theta)/pixels # plate_scale = np.min(theta)/10 print(plate_scale) d_theta = mas2rad(plate_scale) sampled_scales = rad2mas(1/rho) #/plate_scale print(f"Largest structure: {sampled_scales.max():.0f}") print(f"Smallest structure: {sampled_scales.min():.0f}") print(f"xi = {(rad2mas(1/2/rho.min())/pixels)}") sampling_frequency = 1/plate_scale signal_frequency = 1/(mas2rad(plate_scale*lam_circle)) nyquist_frequency = 0.5*signal_frequency # half of pixel/RAD, plate_scale is the sampling rate # Estimated sampling rate estimated_rate = np.diff(np.sort(rho), 1).mean() rho_th = np.linspace(rho.min(), rho.max(), 1000) image_coords = np.arange(pixels) - pixels / 2. xx, yy = np.meshgrid(image_coords, image_coords) image1 = np.zeros_like(xx) rho_squared = (xx) ** 2 + (yy) ** 2 a = radius * mas2rad(plate_scale) image1 += np.sqrt(rho_squared) < radius A = NDFTM(B.UVC, wavel, pixels, plate_scale) * d_theta**2 V = A.dot(image1.ravel()) x = 2*np.pi*a*rho_th V_th = j1(x)/x * 2*np.pi * a**2 V_th_f = interp1d(rho_th, np.abs(V_th)) def freq2scale(x): return rad2mas(1/ x)#/plate_scale def scale2freq(x): return 1/mas2rad(x) fig = plt.figure(figsize=(10, 8)) fig.suptitle(r"Comparison between analytic and discrete FT of circ($2\rho/a$)") frame1 = fig.add_axes((.1, .3, .8, .6)) _frame1 = frame1.twiny() _frame1.set_xlabel(r"$\theta$ [pixels]") _frame1.xaxis.set_major_formatter(FuncFormatter(lambda x, pos: f"{freq2scale(x)/plate_scale:.1f}")) frame1.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: f"{x/0.5/a**2/2/np.pi:.1f}")) plt.plot(rho, np.abs(V), "ko", label=r"$\mathcal{F} X$") plt.plot(rho_th, np.abs(V_th), "r-", label=r"$a J_1(2\pi a \rho) / \rho$") # frame1.axvline(signal_frequency, color="b") plt.axvline(signal_frequency, color="b") plt.annotate(f"Circle radius = a = {rad2mas(a):.2f} mas", xy=(0.6, 0.8), xycoords="axes fraction", color="k") plt.annotate(r"Sampling frequency = %.2f mas$^{-1}$" % (sampling_frequency), xy=(0.6, 0.65), xycoords="axes fraction", color="b") plt.annotate(r"Nyquist frequency = %.2f mas$^{-1}$" % (nyquist_frequency/rad2mas(1)), xy=(0.6, 0.6), xycoords="axes fraction", color="k") frame1.set_ylabel(r"$|\gamma| = \frac{|V|}{a^2\pi }$") frame1.set_xticklabels([]) # Remove x-tic labels for the first frame # plt.annotate("") plt.xlim(rho.min(), rho.max()) plt.legend() frame2 = fig.add_axes((.1, .1, .8, .2)) frame2.xaxis.set_major_formatter(FuncFormatter(lambda x,pos: f"{x/rad2mas(1):.2f}")) plt.plot(rho, np.abs(np.abs(V) - V_th_f(rho))/V_th_f(rho) * 100, 'ok') plt.yscale("log") plt.ylabel("Error %") plt.xlabel(r"$\rho$ (mas$^{-1}$)") # plt.xlabel(r"Baseline (m)") plt.xlim(rho.min(), rho.max()) # plt.savefig(os.path.join(results_dir, "test_fourier_transform_circle.png"), bbox_inches="tight") # it is the baselines that are distributed with this pdf, so we change scale def rho_pdf(rho): x = rho*wavel return x/L * np.exp(-x**2/L**2/4) / 2 plt.figure() # theta = sampled_scales #/plate_scale plt.hist(np.sort(sampled_scales)[:-3], bins=50, density=True) # print(f"mean theta = {freq2scale(2*gamma(3/2)/wavel)/plate_scale:.2f}") # print(f"var theta = {freq2scale((4 - 4*gamma(3/2)**2)/wavel)/plate_scale:.2f}") # print(f"skewness theta = {freq2scale(8*gamma(5/2)/wavel)/plate_scale:.2f}") # plt.plot(theta, rho_pdf(rho/2), "ko") plt.axvline(rad2mas(2*a), color="r") plt.axvline(pixels*plate_scale, color="g") plt.axvline(plate_scale, color="g") plt.xlabel(r"$\Delta \theta$ sampled [mas]") plt.annotate(f"Circle diameter = {rad2mas(2*a):.2f} mas", xy=(0.65, 0.9), xycoords="axes fraction", color="r") plt.annotate(f"Image size = {pixels} pixels", xy=(0.65, 0.5), xycoords="axes fraction", color="g") plt.annotate(f"Median = {np.median(theta):.2f} mas", xy=(0.65, 0.8), xycoords="axes fraction", color="k") plt.annotate(f"Mean = {np.mean(theta):.2f} mas", xy=(0.65, 0.7), xycoords="axes fraction", color="k") plt.annotate(f"std = {np.std(theta):.2f} mas", xy=(0.65, 0.6), xycoords="axes fraction", color="k") plt.title("Baseline sampling of image dimensions in pixels") # plt.savefig(os.path.join(results_dir, "image_pixels_sampling.png")) plt.show()
return uv def pulse(x, y, pdim): rm = 0.5 * pdim return j1(rm * np.sqrt(x**2 + y**2)) / np.sqrt(x**2 + y**2) / rm**2 uv = uv_samples(mask) start = time.time() ndft = NDFTM(uv, wavel, pixels, plate_scale) vis1 = np.einsum("ij, j -> i", ndft, np.ravel(image)) end = time.time() - start print(f"Took {end:.4f} second to compute NDFTM") m2pix = mas2rad(plate_scale) * pixels / wavel phase = np.exp(-1j * np.pi / wavel * mas2rad(plate_scale) * (uv[:, 0] + uv[:, 1])) start = time.time() plan = NFFT([pixels, pixels], uv.shape[0], n=[pixels, pixels]) plan.x = uv / wavel * mas2rad(plate_scale) plan.f_hat = image plan.precompute() plan.trafo() vis = plan.f.copy() * phase end = time.time() - start print( f"Took {end:.4f} seconds to compute NFFT" ) # usually at least 5x faster, scales better with large number of pixels # print(np.abs(vis)) # print(np.abs(vis1))