def test_shift_rotation_consistency(npix=30, grid_size = 1.5, angle=35, display=False): """Test shifting & rotation together for FITS and Analytic optics Do we get consistent behavior from each? Are the signs and order of operations consistent? Tests the fix for issue #275. """ import poppy if npix < 30: raise ValueError("Need npix>=30 for enough resolution for this test") # Create rectangle, rotated rect = poppy.RectangleAperture(shift_x=0.25, rotation=angle) rect_samp = rect.sample(grid_size=grid_size, npix=npix) # Create rectangle, turn into FITS, then rotate rect_fits = poppy.RectangleAperture().to_fits(grid_size=grid_size, npix=npix) rect2 = poppy.FITSOpticalElement(transmission=rect_fits, shift_x=0.25, rotation=angle) # Compare that they are consistent enough, meaning # no more than 1% pixel difference. That tolerance allows for the # imprecision of rotating low-res binary masks. diff = np.round(rect2.amplitude)-rect_samp assert np.abs(diff).sum() <= 0.01*rect_samp.sum(), "Shift and rotations differ unexpectedly" if display: plt.figure() plt.subplot(131) plt.imshow(rect_samp) plt.subplot(132) plt.imshow(np.round(rect2.amplitude)) plt.subplot(133) plt.imshow(diff)
def test_FITSOpticalElement(tmpdir): circ_fits = poppy.CircularAperture().to_fits(grid_size=3, npix=10) fn = str(tmpdir / "circle.fits") circ_fits.writeto(fn, overwrite=True) # Test passing aperture via file on disk foe = poppy.FITSOpticalElement(transmission=fn) assert foe.amplitude_file == fn assert np.allclose(foe.amplitude, circ_fits[0].data) # Test passing OPD via FITS object, along with unit conversion circ_fits[0].header['BUNIT'] = 'micron' # need unit for OPD foe = poppy.FITSOpticalElement(opd=circ_fits) assert foe.opd_file == 'supplied as fits.HDUList object' assert np.allclose(foe.opd, circ_fits[0].data * 1e-6) # make a cube rect_mask = poppy.RectangleAperture().sample(grid_size=3, npix=10) circ_mask = circ_fits[0].data circ_fits[0].data = np.stack([circ_mask, rect_mask]) circ_fits[0].header['BUNIT'] = 'nm' # need unit for OPD fn2 = str(tmpdir / "cube.fits") circ_fits.writeto(fn2, overwrite=True) # Test passing OPD as cube, with slice default, units of nanometers foe = poppy.FITSOpticalElement(opd=fn2) assert foe.opd_file == fn2 assert foe.opd_slice == 0 assert np.allclose(foe.opd, circ_mask * 1e-9) # Same cube but now we ask for the next slice foe = poppy.FITSOpticalElement(opd=(fn2, 1)) assert foe.opd_file == fn2 assert foe.opd_slice == 1 assert np.allclose(foe.opd, rect_mask * 1e-9)
def rectangle_intensity(rot=0, wd=2, ht=1, xshi=0, yshi=0): """Calculate light intensity for light going through a rectangular slit rectangle_intensity(rot = 0, wd= 2,ht = 1, xshi = 0,yshi = 0) rot : rotation in degrees wd : width in m ht : height in m xshi : x axis shift in m yshi : y axis shift in m """ ap = poppy.RectangleAperture(rotation=rot, width=wd, height=ht, shift_x=xshi, shift_y=yshi) ap.display(colorbar=False) osys = poppy.OpticalSystem() osys.add_pupil(ap) osys.add_detector(pixelscale=0.05, fov_arcsec=2.0) psf = osys.calc_psf(1e-6) plt.figure(figsize=(12, 8)) poppy.display_psf(psf, title="Diffraction Pattern")
def openspim_illumination(wavelength=500e-9, refr_index=1.333, laser_radius=1.2e-3, objective_na=0.3, objective_focal=18e-3, slit_opening=10e-3, pixelscale=635e-9, npix_fov=512, rel_thresh=None, simu_size=2048, oversample=16): """ Compute the illumination function of an OpenSPIM device Parameters ---------- wavelength : float, optional illumination wavelength in meters, by default 500e-9 refr_index : float, optional imaging medium refraction index, by default 1.333 laser_radius : float, optional source laser radius in meters, by default 1.2e-3 objective_na : float, optional illumination objective NA, by default 0.3 objective_focal : float, optional illumination objective focal length in meters, by default 18e-3 slit_opening : float, optional vertical slit opening in meters, by default 10e-3 pixelscale : float, optional target pixelscale in meters per pixel, by default 1.3e-3/2048 npix_fov : int, optional target size in pixels, by default 512 rel_thresh: float, optional relative threshold to crop the beam thickness if a full row is below this theshold, all rows after are removed will be computed as compared to the maximum pixel simu_size : int, optional size of the arrays used for simulation, by default 2048 oversample : int, optional oversampling used for the simulation (must be increased sith simu_size), by default 16 Returns ------- array [ZXY] the illumination function """ pixel_width = 1 wavelength *= u.m laser_radius *= u.m objective_focal *= u.m pixelscale *= (u.m / u.pixel) slit_opening *= u.m noop = poppy.ScalarTransmission() beam_ratio = 1 / oversample fov_pixels = npix_fov * u.pixel detector = poppy.FresnelOpticalSystem() detector.add_detector(fov_pixels=fov_pixels, pixelscale=pixelscale) # We approximate the objective aperture with a square one to make it separable # Given the shape of the wavefront, we estimate the generated error to be negligible objective_radius = math.tan(math.asin( objective_na / refr_index)) * objective_focal objective_aperture = poppy.RectangleAperture(name='objective aperture', width=2 * objective_radius, height=2 * objective_radius) objective_lens = poppy.QuadraticLens(f_lens=objective_focal, name='objective lens') obj_aperture = poppy.FresnelOpticalSystem() obj_aperture.add_optic(objective_aperture, objective_focal) # Implement the objective lens separately to be able to account for refractive index change obj_lens = poppy.FresnelOpticalSystem() obj_lens.add_optic(objective_lens) # Computed as following: going through T1 then CLens then T2 # is equivalent to going through CLens with focal/4 # Then the radius is computed as the Fourier transform of the input beam, per 2F lens system w0_y = (12.5e-3 * u.m * wavelength) / (2 * np.pi**2 * laser_radius) laser_shape_y = poppy.GaussianAperture(w=w0_y, pupil_diam=5 * w0_y) path_y = poppy.FresnelOpticalSystem(pupil_diameter=2 * w0_y, npix=pixel_width, beam_ratio=beam_ratio) path_y.add_optic(laser_shape_y) # Going through T1, slit and T2 is equivalent to going through a half-sized slit, # then propagating 1/4 the distance # Since we use 1D propagation, we can increase oversampling a lot for better results laser_shape_z = poppy.GaussianAperture(w=laser_radius, pupil_diam=slit_opening / 2) slit = poppy.RectangleAperture(name='Slit', width=slit_opening / 2, height=slit_opening / 2) path_z = poppy.FresnelOpticalSystem(pupil_diameter=slit_opening / 2, npix=pixel_width, beam_ratio=beam_ratio) path_z.add_optic(laser_shape_z) path_z.add_optic(slit) path_z.add_optic(noop, 0.25 * 100e-3 * u.m) # Propagate 1D signals wf_z = path_z.input_wavefront(wavelength=wavelength) create_wf_1d(wf_z, upsampling=simu_size) path_z.propagate(wf_z) wf_y = path_y.input_wavefront(wavelength=wavelength) create_wf_1d(wf_y, upsampling=simu_size, scale=10) path_y.propagate(wf_y) obj_aperture.propagate(wf_z) obj_aperture.propagate(wf_y) wf_z.wavelength /= refr_index wf_y.wavelength /= refr_index obj_lens.propagate(wf_z) obj_lens.propagate(wf_y) illumination = np.empty((npix_fov, npix_fov, npix_fov), dtype=wf_z.intensity.dtype) # Make sure it is centered even if pixels are odd or even offset = 0 if npix_fov % 2 else 0.5 for pix in range(npix_fov): pixel = pix - npix_fov // 2 + offset distance = pixel * pixelscale * u.pixel psf = poppy.FresnelOpticalSystem() psf.add_optic(noop, objective_focal + distance) wfc_y = wf_y.copy() wfc_z = wf_z.copy() psf.propagate(wfc_y) psf.propagate(wfc_z) resample_wavefront(wfc_y, pixelscale, fov_pixels) resample_wavefront(wfc_z, pixelscale, fov_pixels) mix = wf_mix(wfc_y, wfc_z) mix.normalize() illumination[:, pix, :] = mix.intensity if rel_thresh is not None: illumination = utils.threshold_crop(illumination, rel_thresh, 0) return illumination / illumination.sum(0).mean()