def _test_cross_arbitrary_basis(testj=4, nterms=10, npix=500): """Verify the functions are orthogonal, by taking the integrals of a given mode times N other ones. This is a helper function for test_cross_arbitrary_basis. Parameters : -------------- testj : int Index of the Zernike-like polynomial to test against the others nterms : int Test that polynomial against those from 1 to this N npix : int Size of array to use for this test """ # choose a square aperture for the test square_aperture = optics.SquareAperture(size=1.).sample(npix=npix,grid_size=1.1) square_basis = zernike.arbitrary_basis(square_aperture,nterms=nterms) test_mode = square_basis[testj - 1] assert np.sum(np.isfinite(test_mode)) > 0, "Basis function calculation failure; all NaNs." for idx, array in enumerate(square_basis): j = idx + 1 if j == testj or j == 1: continue # discard piston term and self prod = array * test_mode wg = np.where(np.isfinite(prod)) cross_sum = np.abs(prod[wg].sum()) # Threshold was originally 1e-9, but we ended up getting 1.19e-9 on some machines (not always) # this seems acceptable, so relaxing criteria slightly assert cross_sum < 2e-9, ( "orthogonality failure, Sum[Mode(j={}) * Mode(j={})] = {} (> 2e-9)".format( j, testj, cross_sum) )
def _test_cross_arbitrary_basis(testj=4, nterms=10, npix=500): """Verify the functions are orthogonal, by taking the integrals of a given mode times N other ones. This is a helper function for test_cross_arbitrary_basis. Parameters : -------------- testj : int Index of the Zernike-like polynomial to test against the others nterms : int Test that polynomial against those from 1 to this N npix : int Size of array to use for this test """ # choose a square aperture for the test square_aperture = optics.SquareAperture(size=1.).sample(npix=npix,grid_size=1.1) square_basis = zernike.arbitrary_basis(square_aperture,nterms=nterms) test_mode = square_basis[testj - 1] assert np.sum(np.isfinite(test_mode)) > 0, "Basis function calculation failure; all NaNs." for idx, array in enumerate(square_basis): j = idx + 1 if j == testj or j == 1: continue # discard piston term and self prod = array * test_mode wg = np.where(np.isfinite(prod)) cross_sum = np.abs(prod[wg].sum()) # Threshold was originally 1e-9, but we ended up getting 1.19e-9 on some machines (not always) # this seems acceptable, so relaxing criteria slightly assert cross_sum < 2e-9, ( "orthogonality failure, Sum[Mode(j={}) * Mode(j={})] = {} (> 2e-9)".format( j, testj, cross_sum) )
def test_arbitrary_basis_rms(nterms=10, size=500): """Verify RMS(Zernike[n,m]) == 1.""" # choose a square aperture for the test square_aperture = optics.SquareAperture(size=1.).sample(npix=size,grid_size=1.1) square_basis = zernike.arbitrary_basis(square_aperture,nterms=nterms) assert np.nanstd(square_basis[0]) == 0.0, "Mode(j=0) has nonzero RMS" for j in range(1, nterms): rms = np.nanstd(square_basis[j]) # exclude masked pixels assert abs(1.0 - rms) < 0.001, "Mode(j={}) has RMS value of {}".format(j, rms)
def test_arbitrary_basis_rms(nterms=10, size=500): """Verify RMS(Zernike[n,m]) == 1.""" # choose a square aperture for the test square_aperture = optics.SquareAperture(size=1.).sample(npix=size,grid_size=1.1) square_basis = zernike.arbitrary_basis(square_aperture,nterms=nterms) assert np.nanstd(square_basis[0]) == 0.0, "Mode(j=0) has nonzero RMS" for j in range(1, nterms): rms = np.nanstd(square_basis[j]) # exclude masked pixels assert abs(1.0 - rms) < 0.001, "Mode(j={}) has RMS value of {}".format(j, rms)
def zernike_projection(beam, Nx=101, Nz=101, N_zern= 8): x = beam.getshonecol(1) z = beam.getshonecol(3) path = beam.getshonecol(13) x_lin = np.linspace(np.min(x), np.max(x), Nx) z_lin = np.linspace(np.min(z), np.max(z), Nx) X, Z = np.meshgrid(x_lin,z_lin) wfe = interpolate.griddata((x,z),path,(X,Z),method='linear') import sys import numpy sys.path.insert(0,"/Users/awojdyla/python/poppy/") from poppy import zernike # commented import astropy and import poppy_core in poppy repo #4 defocus mask2 = np.where(numpy.isnan(wfe)) wfe_proj = wfe.copy() wfe_proj[mask2] = 0 N_Zern = 8 #define an aperture for the Zernike basis (non-nans) aperture = wfe.copy()*0+1 aperture[mask2] = 0 ZZ = zernike.arbitrary_basis(aperture, nterms=N_Zern, rho=None, theta=None, outside=0) proj = np.zeros(N_Zern) for i in np.arange(N_Zern): Zerni = ZZ[i,:,:]/np.sqrt(np.sum(ZZ[i,:,:]*ZZ[i,:,:])) proj[i] = np.sum(wfe_proj*Zerni) proj[0]=0; fig = plt.stem(np.arange(N_Zern), proj*1e3) plt.title("Zernike projections") plt.xlabel("Noll index") plt.ylabel("coefficient [a.u.]") plt.show() return proj
def fit_dm_rotation_ellipticity(camstream, dmstream, dm_mask, nimages, cmd_value=0.5): # get tip/tilt terms zbasis_dm = zernike.arbitrary_basis(dm_mask, outside=0., nterms=3)[1:] # measure psf displacement from tip/tilt value = cmd_value # measure tt_ims = [] tt_ims.append(np.mean(camstream.grab_many(nimages), axis=0)) #ref sleep(1.0) for tt in zbasis_dm: dmstream.write((tt * value).astype(dmstream.buffer.dtype)) sleep(1.0) tt_ims.append(np.mean(camstream.grab_many(nimages), axis=0)) dmstream.write(np.zeros(dmstream.buffer.shape, dtype=dmstream.buffer.dtype)) # compute displacements tt_coms = [] for im in tt_ims: im_com = np.squeeze(np.where(im == im.max())) tt_coms.append(im_com) tt_coms = np.vstack(tt_coms) tt_coms -= tt_coms[0] # get angles of displacement angle1 = np.arctan2(tt_coms[1][0], tt_coms[1][1]) angle2 = np.arctan2(tt_coms[2][0], tt_coms[2][1]) - np.pi/2. mean_angle = (angle1 + angle2) / 2. logger.info(f'Found rotation: {mean_angle} rad, {np.rad2deg(mean_angle)} deg') # get ratio of displacement disp1 = np.sqrt(tt_coms[1][0]**2 + tt_coms[1][1]**2) disp2 = np.sqrt(tt_coms[2][0]**2 + tt_coms[2][1]**2) disp_ratio = disp1 / disp2 logger.info(f'Found x/y displacement ratio: {disp_ratio}') return mean_angle, disp_ratio
def get_control_matrix_from_hadamard_measurements(hmeas, hmodes, hval, dm_map, dm_mask, wfsthresh=0.5, dmthresh=0.5, ninterp=2, nmodes=None, deltas=False, remove_modes=None): ''' Given a measurement cube of WFS measurements of +/- hadamard modes with amplitude hval, return the reconstructed IF matrix, DMmodes, WFSmodes, DM and WFS maps/masks, and the control/reconstructor matrix ''' # get nact nact = np.count_nonzero(dm_map) # construct IF cube #print(hmeas.shape, hmodes.shape) ifcube = get_ifcube_from_hmeas(hmeas, hmodes, hval, nact=nact, deltas=deltas) # remove modes if requested if remove_modes is not None: mask = np.mean(ifcube, axis=0) != 0 modes = arbitrary_basis(mask, nterms=remove_modes, outside=0) ifcube = remove_modes_from_if(ifcube, modes, mask) # get DM and WFS maps and masks wfs_ctrl_map, wfs_ctrl_mask = get_wfs_ctrl_map_mask(ifcube, threshold=wfsthresh) dm_ctrl_map, dm_ctrl_mask = get_dm_ctrl_map_mask(ifcube, dm_map, dm_mask, threshold=dmthresh) # reduce ifcube to only include pixels in wfs_ctrl_mask ifcube_active = ifcube[:, wfs_ctrl_mask] ifcube_active -= np.mean(ifcube_active, axis=0) # remove the mean # get SVD wfsmodes, singvals, dmmodes = get_svd_from_ifcube(ifcube_active) #print(ifcube_active.shape, wfsmodes.shape, singvals.shape, dmmodes.shape) #return wfs_ctrl_map, wfs_ctrl_mask, dm_ctrl_map, dm_ctrl_mask, ifcube, wfsmodes, singvals, dmmodes # interpolate DM modes dmmodes_interp = interpolate_dm_modes(dmmodes, dm_ctrl_mask, dm_map, dm_mask, n=ninterp) # get control matrix ctrl = get_control_matrix(dmmodes_interp, wfsmodes, singvals, nmodes=nmodes) return { 'ifmat': ifcube_active, 'wfsmap': wfs_ctrl_map, 'wfsmask': wfs_ctrl_mask, 'dmmap': dm_ctrl_map, 'dmmask': dm_ctrl_mask, 'wfsmodes': wfsmodes, 'dmmodes': dmmodes_interp, 'singvals': singvals, 'ctrlmat': ctrl }
def projected_basis(nterms, angle, fill_fraction, actuator_mask): ''' Return a set of Zernike modes stretched such that they form ordinary Zernike shapes when deprojected. Values outside the beam footprint are extrapolated by evaluting the terms for rho > 1. Zernike modes are ordered by the Noll convention and normalized such that they have a unit RMS in the deprojected beam footprint. Parameters: nterms : int Number of terms in the basis. Always skips piston. angle : float Incidence angle of the beam on the DM, given in degrees. Angles are always assumed to be in the x-z plane. fill_fraction : float Fraction of the DM in the beam footprint (defined along the x axis) [=0.96 for the 2K] actuator_mask : str Path to FITS file with actuators mapped from 1 to n actuators. Returns: list of nterms modes of shape nact x act ''' from poppy.zernike import zernike_basis, arbitrary_basis if isinstance(actuator_mask, str): with fits.open(actuator_mask) as f: dm_mask = f[0].data.astype(bool) else: dm_mask = actuator_mask nact = dm_mask.shape[0] # generate the radial and theta arrays that account for geometrical # projection cos = np.cos(np.deg2rad(angle)) x = np.linspace( -(nact - 1) / nact, (nact - 1) / nact, num=nact, endpoint=True) / fill_fraction xx, yy = np.meshgrid(x, x) rho = np.sqrt(xx**2 + (yy / cos)**2) theta = np.arctan2(yy / cos, xx) # make the modes #zbasis = zernike_basis(nterms=nterms, npix=nact, outside=0., rho=rho, theta=theta) # generate nterms+1 because we're throwing away piston later zbasis = arbitrary_basis(aperture=rho <= 1.0, nterms=nterms + 1, outside=0., rho=rho, theta=theta) # find actuators outside the beam footprint footprint = zbasis[0] != 0 outside = dm_mask & ~footprint outyx = np.dstack(np.where(outside))[0] # interpolate from nearest neighbors for y, x in outyx: yneighbor, xneighbor = find_nearest((y, x), footprint, num=1) zbasis[:, y, x] = zbasis[:, yneighbor, xneighbor] # toss piston return zbasis[1:], footprint
def take_measurements_from_config(config_params, dm_cmds=None, client=None, dmstream=None, dmdivstream=None, camstream=None, darkim=None, restore_dm=True): skip_indi = config_params.get_param('diversity', 'skip_indi', str2bool) print('INDI STATE ', skip_indi) if (client is None) and (not skip_indi): # open indi client connection client = INDIClient('localhost', config_params.get_param('diversity', 'port', int)) client.start() if dmstream is None: # open shmims dmstream = ImageStream( config_params.get_param('diversity', 'dmchannel', str)) if dmdivstream is None: dmdivstream = ImageStream( config_params.get_param('diversity', 'dmdivchannel', str)) if camstream is None: camname = config_params.get_param('camera', 'name', str) camstream = ImageStream(camname) if darkim is None and (not skip_indi): # take a dark (eventually replace this with the INDI dark [needs some kind of check to see if we have a dark, I guess]) darkim = take_dark(camstream, client, camname, config_params.get_param('diversity', 'ndark', int)) else: darkim = None dmdelay = config_params.get_param('diversity', 'dmdelay', float) indidelay = config_params.get_param('diversity', 'indidelay', float) # measure div_type = config_params.get_param('diversity', 'type', str) if div_type.lower() == 'dm': # get defocus mode with fits.open(config_params.get_param('interaction', 'dm_mask', str)) as f: dm_mask = f[0].data zbasis = arbitrary_basis(dm_mask, nterms=4, outside=0) defocus_mode = zbasis[-1] imcube = measure_dm_diversity( camstream, dmstream, dmdivstream, defocus_mode, config_params.get_param('diversity', 'values', float), config_params.get_param('diversity', 'navg', float), darkim=darkim, dm_cmds=dm_cmds, dmdelay=dmdelay, indidelay=indidelay, restore_dm=restore_dm) else: # stage diversity positions = np.asarray( config_params.get_param('diversity', 'values', float)) + config_params.get_param( 'diversity', 'stage_focus', float) imcube = measure_stage_diversity( client, camstream, dmstream, config_params.get_param('diversity', 'camstage', str), positions, config_params.get_param('diversity', 'navg', float), darkim=darkim, dm_cmds=dm_cmds, dmdelay=dmdelay, final_position=positions[0], restore_dm=restore_dm) print(imcube.shape) # clip if needed shape = imcube.shape naxes = len(shape) N = config_params.get_param('estimation', 'N', int) if N < shape[-1]: # assume the camera image is square logger.info( f'Expected shape {N}x{N} but got shape {shape[-2:]}. Clipping to {N}x{N} about center of mass.' ) imcube_reduced = [] for im in imcube: if naxes == 4: # in this case, im is actually a cube # so define a slice around the mean center of mass # (e.g., response matrix measurements) im0 = np.mean(im, axis=0) com = center_of_mass(im0) totalslice = (slice(None), ) + slice_to_valid_shape( im0, com, N, return_slice=True) imcube_reduced.append(im[totalslice]) elif naxes == 3: # here, im is actually an image # (e.g., measurements for a single estimate) com = center_of_mass(im) newim = slice_to_valid_shape(im, com, N) imcube_reduced.append(newim) imcube = np.asarray(imcube_reduced) if N > shape[-1]: # assume the camera image is square logger.warning( f'Camera frames are smaller than expected. Expected {N}x{N} but got {shape[-2:]}.' ) return imcube