def test_roman_pupil_against_file(): localpath = os.path.dirname(os.path.abspath(__file__)) fn = os.path.join(localpath, 'testdata', 'pupil_CGI-20200513_8k_binary_noF.png') pupil0 = imread(fn).astype(float) pupilFromFile = pupil0 / np.max(pupil0) Narray = pupilFromFile.shape[1] downSampFac = 16 Nbeam = 2 * 4027.25 NbeamDS = Nbeam / downSampFac NarrayDS = int(Narray / downSampFac) pupilFromFileDS = bin_downsample(pupilFromFile, downSampFac) # Generate pupil representation in FALCO shift = downSampFac / 2 - 0.5 changes = { "xShear": -0.5 / Nbeam - shift / Nbeam, "yShear": -52.85 / Nbeam - shift / Nbeam } pupilFromFALCODS = falco_gen_pupil_Roman_CGI_20200513( NbeamDS, 'pixel', changes) pupilFromFALCODS = pad_crop(pupilFromFALCODS, NarrayDS) diff = pupilFromFileDS - pupilFromFALCODS percentvalue = np.sum(np.abs(diff)) / np.sum(pupilFromFileDS) * 100 assert percentvalue < 0.1
def test_translation_and_rotation(): Nbeam = 100 centering = 'pixel' pupil = falco_gen_pupil_Roman_CGI_20200513(Nbeam, centering) # Translation test changes = {"xShear": -11 / 100, "yShear": 19 / 100} pupilOffset = falco_gen_pupil_Roman_CGI_20200513(Nbeam, centering, changes) # Test rotation (and translation) changes["clock_deg"] = 90 pupilRotOffset = falco_gen_pupil_Roman_CGI_20200513( Nbeam, centering, changes) pupilRotRecentered = np.roll( pupilRotOffset, [int(-Nbeam * changes["yShear"]), int(-Nbeam * changes["xShear"])], axis=(0, 1)) pupilRot = np.zeros_like(pupil) pupilRot[1::, 1::] = np.rot90(pupil[1::, 1::], -1) diff = pad_crop(pupilRot, pupilOffset.shape) - pupilRotRecentered assert np.max(np.abs(diff)) < 1e-4
def test_translation(): Nbeam = 200 inputs = {"Nbeam": Nbeam} pupil = falco_gen_pupil_LUVOIR_A_final(inputs) inputs.update({"xShear": -11 / 100, "yShear": 19 / 100}) pupilOffset = falco_gen_pupil_LUVOIR_A_final(inputs) pupilRecentered = np.roll( pupilOffset, [int(-Nbeam * inputs["yShear"]), int(-Nbeam * inputs["xShear"])], axis=(0, 1)) diff = pad_crop(pupil, pupilOffset.shape) - pupilRecentered assert np.max(np.abs(diff)) <= 1 / 33
def test_translation(): Nbeam = 100 centering = 'pixel' pupil = falco_gen_pupil_Roman_CGI_20200513(Nbeam, centering) changes = {"xShear": -11 / 100, "yShear": 19 / 100} pupilOffset = falco_gen_pupil_Roman_CGI_20200513(Nbeam, centering, changes) pupilRecentered = np.roll( pupilOffset, [int(-Nbeam * changes["yShear"]), int(-Nbeam * changes["xShear"])], axis=(0, 1)) diff = pad_crop(pupil, pupilOffset.shape) - pupilRecentered assert np.sum(np.abs(diff)) < 1e-8
def test_lyot_stop_translation(): Nbeam = 200 inputs = {"Nbeam": Nbeam, "flagLyot": True, "ID": 0.30, "OD": 0.80} pupil = falco_gen_pupil_LUVOIR_A_final(inputs) # Translation test inputs.update({"xShear": -11 / 100, "yShear": 19 / 100}) pupilOffset = falco_gen_pupil_LUVOIR_A_final(inputs) pupilRecentered = np.roll( pupilOffset, [int(-Nbeam * inputs["yShear"]), int(-Nbeam * inputs["xShear"])], axis=(0, 1)) diff = pad_crop(pupil, pupilOffset.shape) - pupilRecentered assert np.sum(np.abs(diff)) < 1e-8
def test_lyot_stop_translation_and_rotation(): Nbeam = 200 inputs = {"Nbeam": Nbeam, "flagLyot": True, "ID": 0.30, "OD": 0.80} pupil = falco_gen_pupil_LUVOIR_A_final(inputs) pupilRot = np.zeros_like(pupil) pupilRot[1::, 1::] = np.rot90(pupil[1::, 1::], -1) # Translation test inputs.update({"xShear": -11 / 100, "yShear": 19 / 100, "clock_deg": 90}) pupilRotOffset = falco_gen_pupil_LUVOIR_A_final(inputs) pupilRotRecentered = np.roll( pupilRotOffset, [int(-Nbeam * inputs["yShear"]), int(-Nbeam * inputs["xShear"])], axis=(0, 1)) diff = pad_crop(pupilRot, pupilRotOffset.shape) - pupilRotRecentered assert np.max(np.abs(diff)) < 1 / 33
def test_translation_annulus(): res = 6 inputs = {"pixresFPM": res, "rhoInner": 3, "rhoOuter": 10, } fpm = gen_annular_fpm(inputs) inputs.update({"xOffset": 5.5, "yOffset": -10}) fpmOffset = gen_annular_fpm(inputs) fpmRecentered = np.roll(fpmOffset, [int(-res*inputs["yOffset"]), int(-res*inputs["xOffset"])], axis=(0, 1)) fpmPad = pad_crop(fpm, fpmRecentered.shape) assert np.allclose(fpmPad, fpmRecentered, atol=1e-7)
def test_translation(): res = 6 inputs = { "pixresFPM": res, "rhoInner": 2.6, "rhoOuter": 9.4, "ang": 65, "clocking": 20, } fpm = gen_bowtie_fpm(inputs) inputs.update({"xOffset": 5.5, "yOffset": -10}) fpmOffset = gen_bowtie_fpm(inputs) fpmRecentered = np.roll( fpmOffset, [int(-res * inputs["yOffset"]), int(-res * inputs["xOffset"])], axis=(0, 1)) fpmPad = pad_crop(fpm, fpmRecentered.shape) assert np.allclose(fpmPad, fpmRecentered, atol=1e-7)
def test_rotation(): res = 6 inputs = { "pixresFPM": res, "rhoInner": 2.6, "rhoOuter": 9.4, "ang": 65, } fpm = gen_bowtie_fpm(inputs) fpmRot = np.zeros_like(fpm) fpmRot[1::, 1::] = np.rot90(fpm[1::, 1::], -1) inputs.update({"xOffset": 5.5, "yOffset": -10, "clocking": 90}) fpmRotOffset = gen_bowtie_fpm(inputs) fpmRotRecentered = np.roll( fpmRotOffset, [int(-res * inputs["yOffset"]), int(-res * inputs["xOffset"])], axis=(0, 1)) fpmRotPad = pad_crop(fpmRot, fpmRotRecentered.shape) assert np.allclose(fpmRotPad, fpmRotRecentered, atol=1e-4)
def full(mp, modvar, isNorm=True): """ Truth model used to generate images in simulation. Truth model used to generate images in simulation. Can include aberrations/errors that are unknown to the estimator and controller. This function is the wrapper for full models of any coronagraph type. Parameters ---------- mp : ModelParameters Structure containing optical model parameters modvar : ModelVariables Structure containing temporary optical model variables isNorm : bool If False, return an unnormalized image. If True, return a normalized image with the currently stored norm value. Returns ------- Eout : numpy ndarray 2-D electric field in final focal plane """ if type(mp) is not falco.config.ModelParameters: raise TypeError('Input "mp" must be of type ModelParameters') if hasattr(modvar, 'sbpIndex'): normFac = mp.Fend.full.I00[modvar.sbpIndex, modvar.wpsbpIndex] # Value to normalize the PSF. Set to 0 when finding the norm factor # Optional Keyword arguments if not isNorm: normFac = 0 # Set the wavelength if (hasattr(modvar, 'wvl')): # For FALCO or standalone use of full model wvl = modvar.wvl elif (hasattr(modvar, 'sbpIndex')): # For use in FALCO wvl = mp.full.lambdasMat[modvar.sbpIndex, modvar.wpsbpIndex] else: raise ValueError('Need to specify value or indices for wavelength.') """ Input E-fields """ if modvar.whichSource.lower() == 'offaxis': # Use for thput calculations TTphase = (-1.) * (2 * np.pi * (modvar.x_offset * mp.P2.full.XsDL + modvar.y_offset * mp.P2.full.YsDL)) Ett = np.exp(1j * TTphase * mp.lambda0 / wvl) Ein = Ett * np.squeeze(mp.P1.full.E[:, :, modvar.wpsbpIndex, modvar.sbpIndex]) else: # Default to using the starlight Ein = np.squeeze(mp.P1.full.E[:, :, modvar.wpsbpIndex, modvar.sbpIndex]) # Shift the source off-axis to compute the intensity normalization value. # This replaces the previous way of taking the FPM out in the optical model if normFac == 0: source_x_offset = mp.source_x_offset_norm # source offset in lambda0/D source_y_offset = mp.source_y_offset_norm # source offset in lambda0/D TTphase = (-1) * (2 * np.pi * (source_x_offset * mp.P2.full.XsDL + source_y_offset * mp.P2.full.YsDL)) Ett = np.exp(1j * TTphase * mp.lambda0 / wvl) Ein = Ett * np.squeeze(mp.P1.full.E[:, :, modvar.wpsbpIndex, modvar.sbpIndex]) # Apply a Zernike (in amplitude) at input pupil if specified if not (hasattr(modvar, 'zernIndex')): modvar.zernIndex = 1 if not modvar.zernIndex == 1: indsZnoll = modvar.zernIndex # Just send in 1 Zernike mode zernMat = np.squeeze( falco.zern.gen_norm_zern_maps(mp.P1.full.Nbeam, mp.centering, indsZnoll)) zernMat = pad_crop(zernMat, mp.P1.full.Narr) Ein = Ein * zernMat * ( 2 * np.pi / wvl) * mp.jac.Zcoef[mp.jac.zerns == modvar.zernIndex] # %% Pre-compute the FPM first for HLC if mp.layout.lower() == 'fourier' or mp.layout.lower() == 'proper': # ilam = (modvar.sbpIndex-1)*mp.Nwpsbp + modvar.wpsbpIndex if mp.coro.upper() == 'HLC': mp.F3.full.mask = falco.hlc.gen_fpm_from_LUT( mp, modvar.sbpIndex, modvar.wpsbpIndex, 'full') elif mp.layout.lower() == 'fpm_scale': if mp.coro.upper() == 'HLC': if mp.Nsbp > 1 and mp.Nwpsbp > 1: # Weird indexing is because interior wavelengths at # edges of sub-bands are the same, and the fpmCube # contains only the minimal set of masks. ilam = (modvar.sbpIndex-2)*mp.Nwpsbp + modvar.wpsbpIndex + \ (mp.Nsbp-modvar.sbpIndex+1) elif mp.Nsbp == 1 and mp.Nwpsbp > 1: ilam = modvar.wpsbpIndex elif mp.Nwpsbp == 1: ilam = modvar.sbpIndex mp.F3.full.mask = mp.full.fpmCube[:, :, ilam] # %% Select which optical layout's full model to use. if mp.layout.lower() == 'fourier': Eout = full_Fourier(mp, wvl, Ein, normFac) elif mp.layout.lower() == 'fpm_scale': # FPM scales with wavelength Eout = full_Fourier(mp, wvl, Ein, normFac, flagScaleFPM=True) elif mp.layout.lower() == 'proper': optval = copy.copy(vars(mp.full)) if any(mp.dm_ind == 1): optval['use_dm1'] = True optval['dm1'] = mp.dm1.V * mp.dm1.VtoH + mp.full.dm1FlatMap if any(mp.dm_ind == 2): optval['use_dm2'] = True optval['dm2'] = mp.dm2.V * mp.dm2.VtoH + mp.full.dm2FlatMap if normFac == 0: optval['xoffset'] = -mp.source_x_offset_norm optval['yoffset'] = -mp.source_y_offset_norm if mp.coro.upper() in ('VORTEX', 'VC', 'AVC'): optval['use_fpm'] = False optval['xoffset'] = 0 optval['yoffset'] = 0 pass # wavelength needs to be in microns instead of meters for PROPER [Eout, sampling_m] = proper.prop_run(mp.full.prescription, wvl * 1e6, mp.P1.full.Narr, QUIET=True, PASSVALUE=optval) if not normFac == 0: Eout = Eout / np.sqrt(normFac) del optval elif mp.layout.lower() in ('wfirst_phaseb_proper', 'roman_phasec_proper'): optval = copy.copy(vars(mp.full)) optval['use_dm1'] = True optval['use_dm2'] = True optval['dm1_m'] = mp.dm1.V * mp.dm1.VtoH + mp.full.dm1.flatmap optval['dm2_m'] = mp.dm2.V * mp.dm2.VtoH + mp.full.dm2.flatmap if normFac == 0: optval['source_x_offset'] = -mp.source_x_offset_norm optval['source_y_offset'] = -mp.source_y_offset_norm if mp.layout.lower() == 'wfirst_phaseb_proper': fn_proper = 'wfirst_phaseb' elif mp.layout.lower() == 'roman_phasec_proper': fn_proper = 'roman_phasec' [Eout, sampling] = proper.prop_run(fn_proper, wvl * 1e6, int(2**falco.util.nextpow2(mp.Fend.Nxi)), QUIET=True, PASSVALUE=optval) Eout = pad_crop(Eout, (mp.Fend.Nxi, mp.Fend.Nxi)) if not normFac == 0: Eout = Eout / np.sqrt(normFac) return Eout
def compact_general(mp, wvl, Ein, normFac, flagEval, flagScaleFPM=False): """ Compact model with a general-purpose optical layout. Used by estimator and controller. Simplified (aka compact) model used by estimator and controller. Does not include unknown aberrations of the full, "truth" model. This has a general optical layout that should work for most applications. Parameters ---------- mp : ModelParameters Structure containing optical model parameters wvl : float Wavelength of light [meters] Ein : numpy ndarray 2-D input electric field normFac : float Intensity normalization factor flagEval : bool Whether to use a higher resolution in final image plane for evaluation. flagScaleFPM : bool, optional Whether to scale the diameter of the FPM inversely with wavelength. Returns ------- Eout : numpy ndarray 2-D electric field in final focal plane """ if type(mp) is not falco.config.ModelParameters: raise TypeError('Input "mp" must be of type ModelParameters') check.is_bool(flagEval, 'flagEval') check.is_bool(flagScaleFPM, 'flagScaleFPM') mirrorFac = 2. # Phase change is twice the DM surface height. NdmPad = int(mp.compact.NdmPad) if mp.flagRotation: NrelayFactor = 1 else: NrelayFactor = 0 # zero out the number of relays if flagScaleFPM: fpmScaleFac = wvl / mp.lambda0 else: fpmScaleFac = 1.0 if (flagEval): # Higher resolution at final focal plane for eval dxi = mp.Fend.eval.dxi Nxi = mp.Fend.eval.Nxi deta = mp.Fend.eval.deta Neta = mp.Fend.eval.Neta else: # Otherwise use the detector resolution dxi = mp.Fend.dxi Nxi = mp.Fend.Nxi deta = mp.Fend.deta Neta = mp.Fend.Neta """ Masks and DM surfaces """ # ompute the DM surfaces for the current DM commands if any(mp.dm_ind == 1): DM1surf = falco.dm.gen_surf_from_act(mp.dm1, mp.dm1.compact.dx, NdmPad) else: DM1surf = np.zeros((NdmPad, NdmPad)) if any(mp.dm_ind == 2): DM2surf = falco.dm.gen_surf_from_act(mp.dm2, mp.dm2.compact.dx, NdmPad) else: DM2surf = np.zeros((NdmPad, NdmPad)) pupil = pad_crop(mp.P1.compact.mask, NdmPad) Ein = pad_crop(Ein, NdmPad) if (mp.flagDM1stop): DM1stop = pad_crop(mp.dm1.compact.mask, NdmPad) else: DM1stop = np.ones((NdmPad, NdmPad)) if (mp.flagDM2stop): DM2stop = pad_crop(mp.dm2.compact.mask, NdmPad) else: DM2stop = np.ones((NdmPad, NdmPad)) # if mp.useGPU: # log.warning('GPU support not yet implemented. Proceeding without GPU.') # This block is for BMC surface error testing if mp.flagDMwfe: if any(mp.dm_ind == 1): Edm1WFE = np.exp( 2 * np.pi * 1j / wvl * pad_crop(mp.dm1.compact.wfe, NdmPad, 'extrapval', 0)) else: Edm1WFE = np.ones((NdmPad, NdmPad)) if any(mp.dm_ind == 2): Edm2WFE = np.exp( 2 * np.pi * 1j / wvl * pad_crop(mp.dm2.compact.wfe, NdmPad, 'extrapval', 0)) else: Edm2WFE = np.ones((NdmPad, NdmPad)) else: Edm1WFE = np.ones((NdmPad, NdmPad)) Edm2WFE = np.ones((NdmPad, NdmPad)) """Propagation""" # Define pupil P1 and Propagate to pupil P2 EP1 = pupil * Ein # E-field at pupil plane P1 EP2 = falco.prop.relay(EP1, NrelayFactor * mp.Nrelay1to2, mp.centering) # Propagate from P2 to DM1, and apply DM1 surface and aperture stop if not (abs(mp.d_P2_dm1) == 0): # E-field arriving at DM1 Edm1 = falco.prop.ptp(EP2, mp.P2.compact.dx * NdmPad, wvl, mp.d_P2_dm1) else: Edm1 = EP2 # E-field leaving DM1 Edm1b = Edm1 * Edm1WFE * DM1stop * np.exp( mirrorFac * 2 * np.pi * 1j * DM1surf / wvl) # Propagate from DM1 to DM2, and apply DM2 surface and aperture stop Edm2 = falco.prop.ptp(Edm1b, mp.P2.compact.dx * NdmPad, wvl, mp.d_dm1_dm2) Edm2 *= Edm2WFE * DM2stop * np.exp( mirrorFac * 2 * np.pi * 1j * DM2surf / wvl) # Back-propagate to pupil P2 if (mp.d_P2_dm1 + mp.d_dm1_dm2 == 0): EP2eff = Edm2 else: EP2eff = falco.prop.ptp(Edm2, mp.P2.compact.dx * NdmPad, wvl, -1 * (mp.d_dm1_dm2 + mp.d_P2_dm1)) # Re-image to pupil P3 EP3 = falco.prop.relay(EP2eff, NrelayFactor * mp.Nrelay2to3, mp.centering) # Apply apodizer mask. if (mp.flagApod): EP3 = mp.P3.compact.mask * pad_crop(EP3, mp.P3.compact.Narr) """ Select propagation based on coronagraph type """ if mp.coro.upper() in ('LC', 'APLC', 'HLC', 'RODDIER'): # MFT from SP to FPM (i.e., P3 to F3) # E-field incident upon the FPM EF3inc = falco.prop.mft_p2f(EP3, mp.fl, wvl, mp.P2.compact.dx, fpmScaleFac * mp.F3.compact.dxi, mp.F3.compact.Nxi, fpmScaleFac * mp.F3.compact.deta, mp.F3.compact.Neta, mp.centering) # Apply (1-FPM) for Babinet's principle later if mp.coro.upper() == 'RODDIER': pass elif mp.coro.upper() == 'HLC': FPM = mp.F3.compact.mask # Complex transmission of the FPM transOuterFPM = FPM[0, 0] # Complex trans of points outside FPM EF3 = (transOuterFPM - FPM) * EF3inc # transOuterFPM instead of 1 because of the complex transmission of # the glass as well as the arbitrary phase shift. else: EF3 = (1. - mp.F3.compact.mask) * EF3inc # Use Babinet's principle at the Lyot plane. EP4noFPM = falco.prop.relay(EP3, NrelayFactor * mp.Nrelay3to4, mp.centering) EP4noFPM = pad_crop(EP4noFPM, mp.P4.compact.Narr) if mp.coro.upper() == 'HLC': EP4noFPM = transOuterFPM * EP4noFPM # MFT from FPM to Lyot Plane (i.e., F3 to P4) # Subtrahend term for Babinet's principle EP4sub = falco.prop.mft_f2p(EF3, mp.fl, wvl, fpmScaleFac * mp.F3.compact.dxi, fpmScaleFac * mp.F3.compact.deta, mp.P4.compact.dx, mp.P4.compact.Narr, mp.centering) EP4subRelay = falco.prop.relay(EP4sub, NrelayFactor * mp.Nrelay3to4 - 1, mp.centering) # Babinet's principle at P4 EP4 = (EP4noFPM - EP4subRelay) elif mp.coro.upper() == 'FLC' or mp.coro.upper() == 'SPLC': # MFT from SP to FPM (i.e., P3 to F3) # E-field incident upon the FPM EF3inc = falco.prop.mft_p2f(EP3, mp.fl, wvl, mp.P2.compact.dx, mp.F3.compact.dxi, mp.F3.compact.Nxi, mp.F3.compact.deta, mp.F3.compact.Neta, mp.centering) # Apply FPM EF3 = mp.F3.compact.mask * EF3inc # MFT from FPM to Lyot Plane (i.e., F3 to P4) EP4 = falco.prop.mft_f2p(EF3, mp.fl, wvl, mp.F3.compact.dxi, mp.F3.compact.deta, mp.P4.compact.dx, mp.P4.compact.Narr, mp.centering) EP4 = falco.prop.relay(EP4, NrelayFactor * mp.Nrelay3to4 - 1, mp.centering) elif mp.coro.upper() in ('VORTEX', 'VC', 'AVC'): # Get FPM charge if isinstance(mp.F3.VortexCharge, np.ndarray): # Passing an array for mp.F3.VortexCharge with # corresponding wavelengths mp.F3.VortexCharge_lambdas # represents a chromatic vortex FPM if mp.F3.VortexCharge.size == 1: charge = mp.F3.VortexCharge else: np.interp(wvl, mp.F3.VortexCharge_lambdas, mp.F3.VortexCharge, 'linear', 'extrap') elif isinstance(mp.F3.VortexCharge, (int, float)): # single value indicates fully achromatic mask charge = mp.F3.VortexCharge else: raise TypeError("mp.F3.VortexCharge must be int, float or numpy\ ndarray.") pass EP4 = falco.prop.mft_p2v2p(EP3, charge, mp.P1.compact.Nbeam / 2., 0.3, 5) EP4 = pad_crop(EP4, mp.P4.compact.Narr) # Undo the rotation inherent to falco.prop.mft_p2v2p.m if not mp.flagRotation: EP4 = falco.prop.relay(EP4, -1, mp.centering) else: raise ValueError("Value of mp.coro not recognized.") pass # Remove FPM completely if normalization value is being found for vortex if normFac == 0: if mp.coro.upper() in ('VORTEX', 'VC', 'AVC'): EP4 = falco.prop.relay(EP3, NrelayFactor * mp.Nrelay3to4, mp.centering) EP4 = pad_crop(EP4, mp.P4.compact.Narr) pass pass """ Back to common propagation any coronagraph type """ # Apply the Lyot stop EP4 = mp.P4.compact.croppedMask * EP4 # MFT to camera EP4 = falco.prop.relay(EP4, NrelayFactor * mp.NrelayFend, mp.centering) EFend = falco.prop.mft_p2f(EP4, mp.fl, wvl, mp.P4.compact.dx, dxi, Nxi, deta, Neta, mp.centering) # Don't apply FPM if normalization value is being found if normFac == 0: Eout = EFend # Don't normalize if normalization value is being found else: Eout = EFend / np.sqrt(normFac) # Apply normalization return Eout
def compact(mp, modvar, isNorm=True, isEvalMode=False): """ Simplified (aka compact) model used by estimator and controller. Simplified (aka compact) model used by estimator and controller. Does not include unknown aberrations of the full, "truth" model. This function is the wrapper for compact models of any coronagraph type. Parameters ---------- mp : ModelParameters Structure containing optical model parameters modvar : ModelVariables Structure containing temporary optical model variables isNorm : bool If False, return an unnormalized image. If True, return a normalized image with the currently stored norm value. isEvalMode : bool If set, uses a higher resolution in the focal plane for measuring performance metrics such as throughput. Returns ------- Eout : array_like 2-D electric field in final focal plane """ if type(mp) is not falco.config.ModelParameters: raise TypeError('Input "mp" must be of type ModelParameters') # Set default values of input parameters normFac = mp.Fend.compact.I00[modvar.sbpIndex] # Value to normalize PSF. flagEval = False # use a different res at final focal plane for eval modvar.wpsbpIndex = -1 # Dummy index since not needed in compact model # Optional Keyword arguments if not isNorm: normFac = 0. if isEvalMode: flagEval = True # Normalization factor for compact evaluation model if (isNorm and isEvalMode): normFac = mp.Fend.eval.I00[modvar.sbpIndex] # Value to normalize the PSF. Set to 0 when finding the norm factor # Set the wavelength if hasattr(modvar, 'wvl'): wvl = modvar.wvl else: wvl = mp.sbp_centers[modvar.sbpIndex] """ Input E-fields """ # Include the tip/tilt in the input wavefront # if(hasattr(mp,'ttx')): # %--Scale by wvl/lambda0 because ttx and tty are in lambda0/D # x_offset = mp.ttx(modvar.ttIndex)*(mp.lambda0/wvl); # y_offset = mp.tty(modvar.ttIndex)*(mp.lambda0/wvl); # # TTphase = (-1)*(2*np.pi*(x_offset*mp.P2.compact.XsDL + # y_offset*mp.P2.compact.YsDL)); # Ett = exp(1j*TTphase*mp.lambda0/wvl); # Ein = Ett.*mp.P1.compact.E(:,:,modvar.sbpIndex) if modvar.whichSource.lower() == 'offaxis': # Use for throughput calc TTphase = (-1) * (2 * np.pi * (modvar.x_offset * mp.P2.compact.XsDL + modvar.y_offset * mp.P2.compact.YsDL)) Ett = np.exp(1j * TTphase * mp.lambda0 / wvl) Ein = Ett * mp.P1.compact.E[:, :, modvar.sbpIndex] else: # Backward compatible with code without tip/tilt offsets in Jacobian Ein = mp.P1.compact.E[:, :, modvar.sbpIndex] # Shift the source off-axis to compute the intensity normalization value. # This replaces the previous way of taking the FPM out in optical model. if normFac == 0: # source offset in lambda0/D for normalization source_x_offset = mp.source_x_offset_norm source_y_offset = mp.source_y_offset_norm TTphase = (-1.) * (2 * np.pi * (source_x_offset * mp.P2.compact.XsDL + source_y_offset * mp.P2.compact.YsDL)) Ett = np.exp(1j * TTphase * mp.lambda0 / wvl) Ein = Ett * mp.P1.compact.E[:, :, modvar.sbpIndex] # Apply a Zernike (in amplitude) at input pupil if specified if not hasattr(modvar, 'zernIndex'): modvar.zernIndex = 1 # Only used for Zernike sensitivity control, which requires the perfect # E-field of the differential Zernike term. if not (modvar.zernIndex == 1): indsZnoll = modvar.zernIndex # Just send in 1 Zernike mode zernMat = np.squeeze( falco.zern.gen_norm_zern_maps(mp.P1.compact.Nbeam, mp.centering, indsZnoll)) zernMat = pad_crop(zernMat, mp.P1.compact.Narr) Ein = Ein * zernMat * (2 * np.pi * 1j / wvl) * mp.jac.Zcoef[ mp.jac.zerns == modvar.zernIndex] # Define what the complex-valued FPM is if the coro is some type of HLC. if mp.layout.lower() == 'fourier': if mp.coro.upper() in ('HLC', ): mp.F3.compact.mask = falco.hlc.gen_fpm_from_LUT( mp, modvar.sbpIndex, -1, 'compact') elif mp.layout.lower() in ('wfirst_phaseb_proper', 'roman_phasec_proper'): if mp.coro.upper() in ('HLC', ): mp.F3.compact.mask = mp.compact.fpmCube[:, :, modvar.sbpIndex] # Select which optical layout's compact model to use and get E-field if mp.layout.lower() == 'fourier' or mp.layout.lower() == 'proper': Eout = compact_general(mp, wvl, Ein, normFac, flagEval) elif mp.layout.lower() == 'fpm_scale': if mp.coro.upper() in ('HLC', 'SPLC'): Eout = compact_general(mp, wvl, Ein, normFac, flagEval, flagScaleFPM=True) elif mp.layout.lower() in ('wfirst_phaseb_proper', 'roman_phasec_proper'): if mp.coro.upper() == 'HLC': Eout = compact_general(mp, wvl, Ein, normFac, flagEval, flagScaleFPM=True) elif 'SP' in mp.coro.upper(): Eout = compact_general(mp, wvl, Ein, normFac, flagEval) return Eout
def lyot(mp, im, idm): """ Differential model used to compute the ctrl Jacobian for Lyot coronagraph. Specialized compact model used to compute the DM response matrix, aka the control Jacobian for a Lyot coronagraph. Can include an apodizer, making it an apodized pupil Lyot coronagraph (APLC). Does not include unknown aberrations of the full, "truth" model. This model propagates the first-order Taylor expansion of the phase from the poke of each actuator of the deformable mirror. Parameters ---------- mp : ModelParameters Structure containing optical model parameters Returns ------- Gzdl : numpy ndarray Complex-valued, 2-D array containing the Jacobian for the specified Zernike mode, DM number, and wavelength. """ modvar = falco.config.Object() # Initialize the new structure modvar.sbpIndex = mp.jac.sbp_inds[im] modvar.zernIndex = mp.jac.zern_inds[im] wvl = mp.sbp_centers[modvar.sbpIndex] mirrorFac = 2. # Phase change is twice the DM surface height. NdmPad = int(mp.compact.NdmPad) if mp.flagRotation: NrelayFactor = 1 else: NrelayFactor = 0 # zero out the number of relays if mp.coro.upper() in ('LC', 'APLC', 'FLC', 'SPLC'): fpm = mp.F3.compact.mask transOuterFPM = 1. # transmission of points outside the FPM. elif mp.coro.upper() in ('HLC', ): fpm = np.squeeze(mp.compact.fpmCube[:, :, modvar.sbpIndex]) # complex # Complex transmission of the points outside the FPM (just fused silica # with optional dielectric and no metal). transOuterFPM = fpm[0, 0] """Input E-fields""" Ein = np.squeeze(mp.P1.compact.E[:, :, modvar.sbpIndex]) # Apply a Zernike (in amplitude) at input pupil # Used only for Zernike sensitivity control, which requires the perfect # E-field of the differential Zernike term. if not (modvar.zernIndex == 1): indsZnoll = modvar.zernIndex # Just send in 1 Zernike mode zernMat = np.squeeze( falco.zern.gen_norm_zern_maps(mp.P1.compact.Nbeam, mp.centering, indsZnoll)) zernMat = pad_crop(zernMat, mp.P1.compact.Narr) Ein = Ein * zernMat * ( 2 * np.pi / wvl) * mp.jac.Zcoef[mp.jac.zerns == modvar.zernIndex] """ Masks and DM surfaces """ pupil = pad_crop(mp.P1.compact.mask, NdmPad) Ein = pad_crop(Ein, NdmPad) # Re-image the apodizer from pupil P3 back to pupil P2. if (mp.flagApod): apodReimaged = pad_crop(mp.P3.compact.mask, NdmPad) apodReimaged = fp.relay(apodReimaged, NrelayFactor * mp.Nrelay2to3, mp.centering) else: apodReimaged = np.ones((NdmPad, NdmPad)) # Compute the DM surfaces for the current DM commands if any(mp.dm_ind == 1): DM1surf = pad_crop(mp.dm1.compact.surfM, NdmPad) # DM1surf = falco.dm.gen_surf_from_act(mp.dm1, mp.dm1.compact.dx, NdmPad) else: DM1surf = np.zeros((NdmPad, NdmPad)) if any(mp.dm_ind == 2): DM2surf = pad_crop(mp.dm2.compact.surfM, NdmPad) # DM2surf = falco.dm.gen_surf_from_act(mp.dm2, mp.dm2.compact.dx, NdmPad) else: DM2surf = np.zeros((NdmPad, NdmPad)) if mp.flagDM1stop: DM1stop = pad_crop(mp.dm1.compact.mask, NdmPad) else: DM1stop = np.ones((NdmPad, NdmPad)) if (mp.flagDM2stop): DM2stop = pad_crop(mp.dm2.compact.mask, NdmPad) else: DM2stop = np.ones((NdmPad, NdmPad)) # This block is for BMC surface error testing if mp.flagDMwfe: # if(mp.flagDMwfe && (mp.P1.full.Nbeam==mp.P1.compact.Nbeam)) if any(mp.dm_ind == 1): Edm1WFE = np.exp( 2 * np.pi * 1j / wvl * pad_crop(mp.dm1.compact.wfe, NdmPad, 'extrapval', 0)) else: Edm1WFE = np.ones((NdmPad, NdmPad)) if any(mp.dm_ind == 2): Edm2WFE = np.exp( 2 * np.pi * 1j / wvl * pad_crop(mp.dm2.compact.wfe, NdmPad, 'extrapval', 0)) else: Edm2WFE = np.ones((NdmPad, NdmPad)) else: Edm1WFE = np.ones((NdmPad, NdmPad)) Edm2WFE = np.ones((NdmPad, NdmPad)) """Propagation""" # Define pupil P1 and Propagate to pupil P2 EP1 = pupil * Ein # E-field at pupil plane P1 EP2 = fp.relay(EP1, NrelayFactor * mp.Nrelay1to2, mp.centering) # Propagate from P2 to DM1, and apply DM1 surface and aperture stop if not abs(mp.d_P2_dm1) == 0: Edm1 = fp.ptp(EP2, mp.P2.compact.dx * NdmPad, wvl, mp.d_P2_dm1) else: Edm1 = EP2 Edm1out = Edm1 * Edm1WFE * DM1stop * np.exp( mirrorFac * 2 * np.pi * 1j * DM1surf / wvl) """ ---------- DM1 ---------- """ if idm == 1: Gzdl = np.zeros((mp.Fend.corr.Npix, mp.dm1.Nele), dtype=complex) # Two array sizes (at same resolution) of influence functions for MFT # and angular spectrum NboxPad1AS = int( mp.dm1.compact.NboxAS ) # array size for FFT-AS propagations from DM1->DM2->DM1 # Adjust the sub-array location of the influence function for the added zero padding mp.dm1.compact.xy_box_lowerLeft_AS = mp.dm1.compact.xy_box_lowerLeft -\ (mp.dm1.compact.NboxAS-mp.dm1.compact.Nbox)/2. if any(mp.dm_ind == 2): DM2surf = pad_crop(DM2surf, mp.dm1.compact.NdmPad) else: DM2surf = np.zeros((mp.dm1.compact.NdmPad, mp.dm1.compact.NdmPad)) if (mp.flagDM2stop): DM2stop = pad_crop(DM2stop, mp.dm1.compact.NdmPad) else: DM2stop = np.ones((mp.dm1.compact.NdmPad, mp.dm1.compact.NdmPad)) apodReimaged = pad_crop(apodReimaged, mp.dm1.compact.NdmPad) Edm1pad = pad_crop(Edm1out, mp.dm1.compact.NdmPad ) # Pad or crop for expected sub-array indexing Edm2WFEpad = pad_crop(Edm2WFE, mp.dm1.compact.NdmPad ) # Pad or crop for expected sub-array indexing # Propagate each actuator from DM1 through the optical system Gindex = 0 # initialize index counter for iact in mp.dm1.act_ele: # Compute only for influence functions that are not zeroed out if np.sum(np.abs(mp.dm1.compact.inf_datacube[:, :, iact])) > 1e-12: # x- and y- coordinate indices of the padded influence function in the full padded pupil x_box_AS_ind = np.arange( mp.dm1.compact.xy_box_lowerLeft_AS[0, iact], mp.dm1.compact.xy_box_lowerLeft_AS[0, iact] + NboxPad1AS, dtype=int) # x-indices in pupil arrays for the box y_box_AS_ind = np.arange( mp.dm1.compact.xy_box_lowerLeft_AS[1, iact], mp.dm1.compact.xy_box_lowerLeft_AS[1, iact] + NboxPad1AS, dtype=int) # y-indices in pupil arrays for the box indBoxAS = np.ix_(y_box_AS_ind, x_box_AS_ind) # x- and y- coordinates of the UN-padded influence function in the full padded pupil x_box = mp.dm1.compact.x_pupPad[ x_box_AS_ind] # full pupil x-coordinates of the box y_box = mp.dm1.compact.y_pupPad[ y_box_AS_ind] # full pupil y-coordinates of the box # Propagate from DM1 to DM2, and then back to P2 dEbox = (mirrorFac * 2 * np.pi * 1j / wvl) * pad_crop( (mp.dm1.VtoH.reshape(mp.dm1.Nact**2)[iact]) * np.squeeze( mp.dm1.compact.inf_datacube[:, :, iact]), NboxPad1AS ) # Pad influence function at DM1 for angular spectrum propagation. dEbox = fp.ptp( dEbox * Edm1pad[np.ix_(y_box_AS_ind, x_box_AS_ind)], mp.P2.compact.dx * NboxPad1AS, wvl, mp.d_dm1_dm2 ) # forward propagate to DM2 and apply DM2 E-field dEP2box = fp.ptp( dEbox * Edm2WFEpad[np.ix_(y_box_AS_ind, x_box_AS_ind)] * DM2stop[np.ix_(y_box_AS_ind, x_box_AS_ind)] * np.exp(mirrorFac * 2 * np.pi * 1j / wvl * DM2surf[np.ix_(y_box_AS_ind, x_box_AS_ind)]), mp.P2.compact.dx * NboxPad1AS, wvl, -1 * (mp.d_dm1_dm2 + mp.d_P2_dm1)) # back-propagate to DM1 # dEbox = fp.ptp_inf_func(dEbox*Edm1pad[np.ix_(y_box_AS_ind,x_box_AS_ind)], mp.P2.compact.dx*NboxPad1AS,wvl, mp.d_dm1_dm2, mp.dm1.dm_spacing, mp.propMethodPTP) # forward propagate to DM2 and apply DM2 E-field # dEP2box = fp.ptp_inf_func(dEbox.*Edm2WFEpad[np.ix_(y_box_AS_ind,x_box_AS_ind)]*DM2stop(y_box_AS_ind,x_box_AS_ind).*exp(mirrorFac*2*np.pi*1j/wvl*DM2surf(y_box_AS_ind,x_box_AS_ind)), mp.P2.compact.dx*NboxPad1AS,wvl,-1*(mp.d_dm1_dm2 + mp.d_P2_dm1), mp.dm1.dm_spacing, mp.propMethodPTP ) # back-propagate to DM1 # # To simulate going forward to the next pupil plane (with the apodizer) most efficiently, # First, back-propagate the apodizer (by rotating 180-degrees) to the previous pupil. # Second, negate the coordinates of the box used. dEP2box = apodReimaged[ indBoxAS] * dEP2box # Apply 180deg-rotated SP mask. dEP3box = np.rot90( dEP2box, k=NrelayFactor * 2 * mp.Nrelay2to3 ) # Forward propagate the cropped box by rotating 180 degrees mp.Nrelay2to3 times. # Negate and reverse coordinate values to effectively rotate by 180 degrees. No change if 360 degree rotation. if np.mod(NrelayFactor * mp.Nrelay2to3, 2) == 1: x_box = -1 * x_box[::-1] y_box = -1 * y_box[::-1] # Matrices for the MFT from the pupil P3 to the focal plane mask rect_mat_pre = (np.exp( -2 * np.pi * 1j * np.outer(mp.F3.compact.etas, y_box) / (wvl * mp.fl))) * np.sqrt( mp.P2.compact.dx * mp.P2.compact.dx) * np.sqrt( mp.F3.compact.dxi * mp.F3.compact.deta) / (wvl * mp.fl) rect_mat_post = (np.exp(-2 * np.pi * 1j * np.outer(x_box, mp.F3.compact.xis) / (wvl * mp.fl))) EF3inc = rect_mat_pre @ dEP3box @ rect_mat_post # MFT to FPM if mp.coro.upper() in ('LC', 'APLC', 'HLC'): # Propagate through (1 - FPM) for Babinet's principle EF3 = (transOuterFPM - fpm) * EF3inc # MFT to LS ("Sub" name for Subtrahend part of the Lyot-plane E-field) EP4sub = fp.mft_f2p(EF3, mp.fl, wvl, mp.F3.compact.dxi, mp.F3.compact.deta, mp.P4.compact.dx, mp.P4.compact.Narr, mp.centering) EP4sub = fp.relay(EP4sub, NrelayFactor * mp.Nrelay3to4 - 1, mp.centering) # Full Lyot plane pupil (for Babinet) EP4noFPM = np.zeros( (mp.dm1.compact.NdmPad, mp.dm1.compact.NdmPad), dtype=complex) EP4noFPM[ indBoxAS] = dEP2box # Propagating the E-field from P2 to P4 without masks gives the same E-field. EP4noFPM = fp.relay( EP4noFPM, NrelayFactor * (mp.Nrelay2to3 + mp.Nrelay3to4), mp.centering) # Get the correct orientation EP4noFPM = pad_crop( EP4noFPM, mp.P4.compact.Narr ) # Crop down to the size of the Lyot stop opening EP4 = transOuterFPM * EP4noFPM - EP4sub # Babinet's principle to get E-field at Lyot plane elif mp.coro.upper() in ('FLC', 'SPLC'): EF3 = fpm * EF3inc # Apply FPM # MFT to Lyot plane EP4 = fp.mft_f2p(EF3, mp.fl, wvl, mp.F3.compact.dxi, mp.F3.compact.deta, mp.P4.compact.dx, mp.P4.compact.Narr, mp.centering) EP4 = fp.relay(EP4, NrelayFactor * mp.Nrelay3to4 - 1, mp.centering) # Get the correct orientation EP4 *= mp.P4.compact.croppedMask # Apply Lyot stop # MFT to camera EP4 = fp.relay( EP4, NrelayFactor * mp.NrelayFend, mp.centering ) # Rotate the final image 180 degrees if necessary EFend = fp.mft_p2f(EP4, mp.fl, wvl, mp.P4.compact.dx, mp.Fend.dxi, mp.Fend.Nxi, mp.Fend.deta, mp.Fend.Neta, mp.centering) Gzdl[:, Gindex] = EFend[mp.Fend.corr.maskBool] / np.sqrt( mp.Fend.compact.I00[modvar.sbpIndex]) Gindex += 1 """ ---------- DM2 ---------- """ if idm == 2: Gzdl = np.zeros((mp.Fend.corr.Npix, mp.dm2.Nele), dtype=complex) # Two array sizes (at same resolution) of influence functions for MFT and angular spectrum NboxPad2AS = int(mp.dm2.compact.NboxAS) mp.dm2.compact.xy_box_lowerLeft_AS = mp.dm2.compact.xy_box_lowerLeft - ( NboxPad2AS - mp.dm2.compact.Nbox ) / 2 # Account for the padding of the influence function boxes apodReimaged = pad_crop(apodReimaged, mp.dm2.compact.NdmPad) DM2stopPad = pad_crop(DM2stop, mp.dm2.compact.NdmPad) Edm2WFEpad = pad_crop(Edm2WFE, mp.dm2.compact.NdmPad) # Propagate full field to DM2 before back-propagating in small boxes Edm2inc = pad_crop( fp.ptp(Edm1out, mp.compact.NdmPad * mp.P2.compact.dx, wvl, mp.d_dm1_dm2), mp.dm2.compact.NdmPad) # E-field incident upon DM2 Edm2inc = pad_crop(Edm2inc, mp.dm2.compact.NdmPad) Edm2 = DM2stopPad * Edm2WFEpad * Edm2inc * np.exp( mirrorFac * 2 * np.pi * 1j / wvl * pad_crop(DM2surf, mp.dm2.compact.NdmPad) ) # Initial E-field at DM2 including its own phase contribution # Propagate each actuator from DM2 through the rest of the optical system Gindex = 0 # initialize index counter for iact in mp.dm2.act_ele: if np.sum( np.abs(mp.dm2.compact.inf_datacube[:, :, iact]) ) > 1e-12: # Only compute for acutators specified for use or for influence functions that are not zeroed out # x- and y- coordinates of the padded influence function in the full padded pupil x_box_AS_ind = np.arange( mp.dm2.compact.xy_box_lowerLeft_AS[0, iact], mp.dm2.compact.xy_box_lowerLeft_AS[0, iact] + NboxPad2AS, dtype=int) # x-indices in pupil arrays for the box y_box_AS_ind = np.arange( mp.dm2.compact.xy_box_lowerLeft_AS[1, iact], mp.dm2.compact.xy_box_lowerLeft_AS[1, iact] + NboxPad2AS, dtype=int) # y-indices in pupil arrays for the box indBoxAS = np.ix_(y_box_AS_ind, x_box_AS_ind) # x- and y- coordinates of the UN-padded influence function in the full padded pupil x_box = mp.dm2.compact.x_pupPad[ x_box_AS_ind] # full pupil x-coordinates of the box y_box = mp.dm2.compact.y_pupPad[ y_box_AS_ind] # full pupil y-coordinates of the box dEbox = (mp.dm2.VtoH.reshape(mp.dm2.Nact**2)[iact]) * ( mirrorFac * 2 * np.pi * 1j / wvl) * pad_crop( np.squeeze(mp.dm2.compact.inf_datacube[:, :, iact]), NboxPad2AS) # the padded influence function at DM2 dEP2box = fp.ptp( dEbox * Edm2[indBoxAS], mp.P2.compact.dx * NboxPad2AS, wvl, -1 * (mp.d_dm1_dm2 + mp.d_P2_dm1)) # back-propagate to pupil P2 # dEP2box = ptp_inf_func(dEbox.*Edm2(y_box_AS_ind,x_box_AS_ind), mp.P2.compact.dx*NboxPad2AS,wvl,-1*(mp.d_dm1_dm2 + mp.d_P2_dm1), mp.dm2.dm_spacing, mp.propMethodPTP); # back-propagate to pupil P2 # To simulate going forward to the next pupil plane (with the apodizer) most efficiently, # First, back-propagate the apodizer (by rotating 180-degrees) to the previous pupil. # Second, negate the coordinates of the box used. dEP2box = apodReimaged[ indBoxAS] * dEP2box # Apply 180deg-rotated SP mask. dEP3box = np.rot90( dEP2box, k=2 * NrelayFactor * mp.Nrelay2to3 ) # Forward propagate the cropped box by rotating 180 degrees mp.Nrelay2to3 times. # Negate and rotate coordinates to effectively rotate by 180 degrees. No change if 360 degree rotation. if np.mod(NrelayFactor * mp.Nrelay2to3, 2) == 1: x_box = -1 * x_box[::-1] y_box = -1 * y_box[::-1] # Matrices for the MFT from the pupil P3 to the focal plane mask rect_mat_pre = np.exp( -2 * np.pi * 1j * np.outer(mp.F3.compact.etas, y_box) / (wvl * mp.fl)) * np.sqrt( mp.P2.compact.dx * mp.P2.compact.dx) * np.sqrt( mp.F3.compact.dxi * mp.F3.compact.deta) / (wvl * mp.fl) rect_mat_post = np.exp(-2 * np.pi * 1j * np.outer(x_box, mp.F3.compact.xis) / (wvl * mp.fl)) EF3inc = rect_mat_pre @ dEP3box @ rect_mat_post # MFT to FPM if mp.coro.upper() in ('LC', 'APLC', 'HLC'): # Propagate through (1 - fpm) for Babinet's principle EF3 = (transOuterFPM - fpm) * EF3inc # MFT to LS ("Sub" name for Subtrahend part of the Lyot-plane E-field) EP4sub = fp.mft_f2p( EF3, mp.fl, wvl, mp.F3.compact.dxi, mp.F3.compact.deta, mp.P4.compact.dx, mp.P4.compact.Narr, mp.centering ) # Subtrahend term for the Lyot plane E-field EP4sub = fp.relay( EP4sub, NrelayFactor * mp.Nrelay3to4 - 1, mp.centering) # Get the correct orientation EP4noFPM = np.zeros( (mp.dm2.compact.NdmPad, mp.dm2.compact.NdmPad), dtype=complex) EP4noFPM[ indBoxAS] = dEP2box # Propagating the E-field from P2 to P4 without masks gives the same E-field. EP4noFPM = fp.relay( EP4noFPM, NrelayFactor * (mp.Nrelay2to3 + mp.Nrelay3to4), mp.centering ) # Get the number or re-imaging relays between pupils P3 and P4. EP4noFPM = pad_crop( EP4noFPM, mp.P4.compact.Narr ) # Crop down to the size of the Lyot stop opening EP4 = transOuterFPM * EP4noFPM - EP4sub # Babinet's principle to get E-field at Lyot plane elif mp.coro.upper() in ('FLC', 'SPLC'): EF3 = fpm * EF3inc # Apply FPM # MFT to LS ("Sub" name for Subtrahend part of the Lyot-plane E-field) EP4 = fp.mft_f2p(EF3, mp.fl, wvl, mp.F3.compact.dxi, mp.F3.compact.deta, mp.P4.compact.dx, mp.P4.compact.Narr, mp.centering) EP4 = fp.relay(EP4, NrelayFactor * mp.Nrelay3to4 - 1, mp.centering) EP4 *= mp.P4.compact.croppedMask # Apply Lyot stop # MFT to detector EP4 = fp.relay( EP4, NrelayFactor * mp.NrelayFend, mp.centering ) # Rotate the final image 180 degrees if necessary EFend = fp.mft_p2f(EP4, mp.fl, wvl, mp.P4.compact.dx, mp.Fend.dxi, mp.Fend.Nxi, mp.Fend.deta, mp.Fend.Neta, mp.centering) Gzdl[:, Gindex] = EFend[mp.Fend.corr.maskBool] / np.sqrt( mp.Fend.compact.I00[modvar.sbpIndex]) Gindex += 1 """ ---------- DM9 (HLC only) ---------- """ if idm == 9: Gzdl = np.zeros((mp.Fend.corr.Npix, mp.dm9.Nele), dtype=complex) Nbox9 = int(mp.dm9.compact.Nbox) # Adjust the step size in the Jacobian, then divide back out. Used for # helping counteract effect of discretization. if not hasattr(mp.dm9, 'stepFac'): stepFac = 20 else: stepFac = mp.dm9.stepFac # Propagate from DM1 to DM2, and apply DM2 surface and aperture stop Edm2 = Edm2WFE * DM2stop * np.exp(mirrorFac*2*np.pi*1j*DM2surf/wvl) * \ fp.ptp(Edm1out, mp.P2.compact.dx*NdmPad, wvl, mp.d_dm1_dm2) # Back-propagate to pupil P2 dz2 = mp.d_P2_dm1 + mp.d_dm1_dm2 if dz2 < 10 * wvl: EP2eff = Edm2 else: EP2eff = fp.ptp(Edm2, mp.P2.compact.dx * NdmPad, wvl, -dz2) # Rotate 180 degrees mp.Nrelay2to3 times to go from pupil P2 to P3 EP3 = fp.relay(EP2eff, NrelayFactor * mp.Nrelay2to3, mp.centering) # Apply apodizer mask if mp.flagApod: EP3 = mp.P3.compact.mask * pad_crop(EP3, mp.P1.compact.Narr) # MFT from pupil P3 to FPM (at focus F3) EF3inc = fp.mft_p2f(EP3, mp.fl, wvl, mp.P2.compact.dx, mp.F3.compact.dxi, mp.F3.compact.Nxi, mp.F3.compact.deta, mp.F3.compact.Neta, mp.centering) EF3inc = pad_crop(EF3inc, mp.dm9.compact.NdmPad) # Coordinates for metal thickness and dielectric thickness DM8transIndAll = falco.hlc.discretize_fpm_surf( mp.dm8.surf, mp.t_metal_nm_vec, mp.dt_metal_nm) # All of the mask # Propagate each actuator from DM2 through the rest of the optical system Gindex = 0 # initialize index counter for iact in mp.dm9.act_ele: if np.sum( np.abs(mp.dm9.compact.inf_datacube[:, :, iact]) ) > 1e-12: # Only compute for acutators specified for use or for influence functions that are not zeroed out # xi- and eta- coordinates in the full FPM portion of the focal plane xyLL = mp.dm9.compact.xy_box_lowerLeft[:, iact] xi_box_ind = np.arange( xyLL[0], xyLL[0] + Nbox9, dtype=int) # xi-indices in focal arrays for the box eta_box_ind = np.arange( xyLL[1], xyLL[1] + Nbox9, dtype=int) # eta-indices in focal arrays for the box indBox = np.ix_(eta_box_ind, xi_box_ind) xi_box = mp.dm9.compact.x_pupPad[xi_box_ind] eta_box = mp.dm9.compact.y_pupPad[eta_box_ind] # Obtain values for the "poked" FPM's complex transmission (only in the sub-array where poked) Nxi = Nbox9 Neta = Nbox9 DM9surfCropNew = stepFac * mp.dm9.VtoH[ iact] * mp.dm9.compact.inf_datacube[:, :, iact] + mp.dm9.surf[ indBox] # New DM9 surface profile in the poked region (meters) DM9transInd = falco.hlc.discretize_fpm_surf( DM9surfCropNew, mp.t_diel_nm_vec, mp.dt_diel_nm) DM8transInd = DM8transIndAll[ indBox] # Cropped region of the FPM. # Look up table to compute complex transmission coefficient of the FPM at each pixel fpmPoked = np.zeros( (Neta, Nxi), dtype=complex ) # Initialize output array of FPM's complex transmission for ix in range(Nxi): for iy in range(Neta): ind_metal = DM8transInd[iy, ix] ind_diel = DM9transInd[iy, ix] fpmPoked[iy, ix] = mp.complexTransCompact[ind_diel, ind_metal, modvar.sbpIndex] dEF3box = ( (transOuterFPM - fpmPoked) - (transOuterFPM - fpm[indBox])) * EF3inc[ indBox] # Delta field (in a small region) at the FPM # Matrices for the MFT from the FPM stamp to the Lyot stop rect_mat_pre = np.exp(-2*np.pi*1j*np.outer(mp.P4.compact.ys, eta_box)/(wvl*mp.fl)) *\ np.sqrt(mp.P4.compact.dx*mp.P4.compact.dx)*np.sqrt(mp.F3.compact.dxi*mp.F3.compact.deta)/(wvl*mp.fl) rect_mat_post = np.exp(-2 * np.pi * 1j * np.outer(xi_box, mp.P4.compact.xs) / (wvl * mp.fl)) # MFT from FPM to Lyot stop (Nominal term transOuterFPM*EP4noFPM subtracts out to 0 since it ignores the FPM change). EP4 = 0 - rect_mat_pre @ dEF3box @ rect_mat_post # MFT from FPM (F3) to Lyot stop plane (P4) EP4 = fp.relay(EP4, NrelayFactor * mp.Nrelay3to4 - 1, mp.centering) EP4 = mp.P4.compact.croppedMask * EP4 # Apply Lyot stop # MFT to final focal plane EP4 = fp.relay(EP4, NrelayFactor * mp.NrelayFend, mp.centering) EFend = fp.mft_p2f(EP4, mp.fl, wvl, mp.P4.compact.dx, mp.Fend.dxi, mp.Fend.Nxi, mp.Fend.deta, mp.Fend.Neta, mp.centering) Gzdl[:, Gindex] = mp.dm9.act_sens / stepFac * mp.dm9.weight * EFend[ mp.Fend.corr.maskBool] / np.sqrt( mp.Fend.compact.I00[modvar.sbpIndex]) Gindex += 1 return Gzdl
def vortex(mp, im, idm): """ Differential model used to compute ctrl Jacobian for vortex coronagraph. Specialized compact model used to compute the DM response matrix, aka the control Jacobian for a vortex coronagraph. Can include an apodizer, making it an apodized vortex coronagraph (AVC). Does not include unknown aberrations of the full, "truth" model. This model propagates the first-order Taylor expansion of the phase from the poke of each actuator of the deformable mirror. Parameters ---------- mp : ModelParameters Structure containing optical model parameters Returns ------- Gzdl : numpy ndarray Complex-valued, 2-D array containing the Jacobian for the specified Zernike mode, DM number, and wavelength. """ modvar = falco.config.Object() # Initialize the new structure modvar.sbpIndex = mp.jac.sbp_inds[im] modvar.zernIndex = mp.jac.zern_inds[im] wvl = mp.sbp_centers[modvar.sbpIndex] mirrorFac = 2. # Phase change is twice the DM surface height. NdmPad = int(mp.compact.NdmPad) if mp.flagRotation: NrelayFactor = 1 else: NrelayFactor = 0 # zero out the number of relays # Minimum FPM resolution for Jacobian calculations (in pixels per lambda/D) minPadFacVortex = 8 # Get FPM charge if type(mp.F3.VortexCharge) == np.ndarray: # Passing an array for mp.F3.VortexCharge with # corresponding wavelengths mp.F3.VortexCharge_lambdas # represents a chromatic vortex FPM if mp.F3.VortexCharge.size == 1: charge = mp.F3.VortexCharge else: np.interp(wvl, mp.F3.VortexCharge_lambdas, mp.F3.VortexCharge, 'linear', 'extrap') elif type(mp.F3.VortexCharge) == int or type(mp.F3.VortexCharge) == float: # single value indicates fully achromatic mask charge = mp.F3.VortexCharge else: raise TypeError( "mp.F3.VortexCharge must be an int, float, or numpy ndarray.") """Input E-fields""" Ein = np.squeeze(mp.P1.compact.E[:, :, modvar.sbpIndex]) # Apply a Zernike (in amplitude) at input pupil # Used only for Zernike sensitivity control, which requires the perfect # E-field of the differential Zernike term. if not modvar.zernIndex == 1: indsZnoll = modvar.zernIndex # Just send in 1 Zernike mode zernMat = np.squeeze( falco.zern.gen_norm_zern_maps(mp.P1.compact.Nbeam, mp.centering, indsZnoll)) zernMat = pad_crop(zernMat, mp.P1.compact.Narr) Ein = Ein*zernMat*(2*np.pi/wvl) * \ mp.jac.Zcoef[mp.jac.zerns == modvar.zernIndex] """ Masks and DM surfaces """ pupil = pad_crop(mp.P1.compact.mask, NdmPad) Ein = pad_crop(Ein, NdmPad) # Re-image the apodizer from pupil P3 back to pupil P2. if (mp.flagApod): apodReimaged = pad_crop(mp.P3.compact.mask, NdmPad) apodReimaged = fp.relay(apodReimaged, NrelayFactor * mp.Nrelay2to3, mp.centering) else: apodReimaged = np.ones((NdmPad, NdmPad)) # Compute the DM surfaces for the current DM commands if any(mp.dm_ind == 1): DM1surf = pad_crop(mp.dm1.compact.surfM, NdmPad) # DM1surf = falco.dm.gen_surf_from_act(mp.dm1, mp.dm1.compact.dx, NdmPad) else: DM1surf = np.zeros((NdmPad, NdmPad)) if any(mp.dm_ind == 2): DM2surf = pad_crop(mp.dm2.compact.surfM, NdmPad) # DM2surf = falco.dm.gen_surf_from_act(mp.dm2, mp.dm2.compact.dx, NdmPad) else: DM2surf = np.zeros((NdmPad, NdmPad)) if (mp.flagDM1stop): DM1stop = pad_crop(mp.dm1.compact.mask, NdmPad) else: DM1stop = np.ones((NdmPad, NdmPad)) if (mp.flagDM2stop): DM2stop = pad_crop(mp.dm2.compact.mask, NdmPad) else: DM2stop = np.ones((NdmPad, NdmPad)) # This block is for BMC surface error testing if (mp.flagDMwfe): if any(mp.dm_ind == 1): Edm1WFE = np.exp( 2 * np.pi * 1j / wvl * pad_crop(mp.dm1.compact.wfe, NdmPad, 'extrapval', 0)) else: Edm1WFE = np.ones((NdmPad, NdmPad)) if any(mp.dm_ind == 2): Edm2WFE = np.exp( 2 * np.pi * 1j / wvl * pad_crop(mp.dm2.compact.wfe, NdmPad, 'extrapval', 0)) else: Edm2WFE = np.ones((NdmPad, NdmPad)) else: Edm1WFE = np.ones((NdmPad, NdmPad)) Edm2WFE = np.ones((NdmPad, NdmPad)) """Propagation""" # Define pupil P1 and Propagate to pupil P2 EP1 = pupil * Ein # E-field at pupil plane P1 EP2 = fp.relay(EP1, NrelayFactor * mp.Nrelay1to2, mp.centering) # Propagate from P2 to DM1, and apply DM1 surface and aperture stop if not (abs(mp.d_P2_dm1) == 0): # E-field arriving at DM1 Edm1 = fp.ptp(EP2, mp.P2.compact.dx * NdmPad, wvl, mp.d_P2_dm1) else: Edm1 = EP2 Edm1out = Edm1 * Edm1WFE * DM1stop * np.exp( mirrorFac * 2 * np.pi * 1j * DM1surf / wvl) """ ---------- DM1 ---------- """ if idm == 1: Gzdl = np.zeros((mp.Fend.corr.Npix, mp.dm1.Nele), dtype=complex) # Array size for planes P3, F3, and P4 Nfft1 = int(2**falco.util.nextpow2( np.max( np.array([ mp.dm1.compact.NdmPad, minPadFacVortex * mp.dm1.compact.Nbox ])))) # Don't crop--but do pad if necessary. # Generate vortex FPM with fftshift already applied fftshiftVortex = fftshift( falco.mask.falco_gen_vortex_mask(charge, Nfft1)) # Two array sizes (at same resolution) of influence functions for MFT and angular spectrum NboxPad1AS = int( mp.dm1.compact.NboxAS ) # array size for FFT-AS propagations from DM1->DM2->DM1 mp.dm1.compact.xy_box_lowerLeft_AS = mp.dm1.compact.xy_box_lowerLeft - ( mp.dm1.compact.NboxAS - mp.dm1.compact.Nbox ) / 2. # Adjust the sub-array location of the influence function for the added zero padding if any(mp.dm_ind == 2): DM2surf = pad_crop(DM2surf, mp.dm1.compact.NdmPad) else: DM2surf = np.zeros((mp.dm1.compact.NdmPad, mp.dm1.compact.NdmPad)) if (mp.flagDM2stop): DM2stop = pad_crop(DM2stop, mp.dm1.compact.NdmPad) else: DM2stop = np.ones((mp.dm1.compact.NdmPad, mp.dm1.compact.NdmPad)) apodReimaged = pad_crop(apodReimaged, mp.dm1.compact.NdmPad) Edm1pad = pad_crop(Edm1out, mp.dm1.compact.NdmPad ) # Pad or crop for expected sub-array indexing Edm2WFEpad = pad_crop(Edm2WFE, mp.dm1.compact.NdmPad ) # Pad or crop for expected sub-array indexing # Propagate each actuator from DM1 through the optical system Gindex = 0 # initialize index counter for iact in mp.dm1.act_ele: # Compute only for influence functions that are not zeroed out if np.sum(np.abs(mp.dm1.compact.inf_datacube[:, :, iact])) > 1e-12: # x- and y- coordinate indices of the padded influence function in the full padded pupil x_box_AS_ind = np.arange( mp.dm1.compact.xy_box_lowerLeft_AS[0, iact], mp.dm1.compact.xy_box_lowerLeft_AS[0, iact] + NboxPad1AS, dtype=int) # x-indices in pupil arrays for the box y_box_AS_ind = np.arange( mp.dm1.compact.xy_box_lowerLeft_AS[1, iact], mp.dm1.compact.xy_box_lowerLeft_AS[1, iact] + NboxPad1AS, dtype=int) # y-indices in pupil arrays for the box indBoxAS = np.ix_(y_box_AS_ind, x_box_AS_ind) # x- and y- coordinates of the UN-padded influence function in the full padded pupil x_box = mp.dm1.compact.x_pupPad[ x_box_AS_ind] # full pupil x-coordinates of the box y_box = mp.dm1.compact.y_pupPad[ y_box_AS_ind] # full pupil y-coordinates of the box # Propagate from DM1 to DM2, and then back to P2 dEbox = (mirrorFac * 2 * np.pi * 1j / wvl) * pad_crop( (mp.dm1.VtoH.reshape(mp.dm1.Nact**2)[iact]) * np.squeeze( mp.dm1.compact.inf_datacube[:, :, iact]), NboxPad1AS ) # Pad influence function at DM1 for angular spectrum propagation. dEbox = fp.ptp( dEbox * Edm1pad[indBoxAS], mp.P2.compact.dx * NboxPad1AS, wvl, mp.d_dm1_dm2 ) # forward propagate to DM2 and apply DM2 E-field dEP2box = fp.ptp( dEbox * Edm2WFEpad[indBoxAS] * DM2stop[indBoxAS] * np.exp( mirrorFac * 2 * np.pi * 1j / wvl * DM2surf[indBoxAS]), mp.P2.compact.dx * NboxPad1AS, wvl, -1 * (mp.d_dm1_dm2 + mp.d_P2_dm1)) # back-propagate to DM1 # dEbox = fp.ptp_inf_func(dEbox*Edm1pad[np.ix_(y_box_AS_ind,x_box_AS_ind)], mp.P2.compact.dx*NboxPad1AS,wvl, mp.d_dm1_dm2, mp.dm1.dm_spacing, mp.propMethodPTP) # forward propagate to DM2 and apply DM2 E-field # dEP2box = fp.ptp_inf_func(dEbox.*Edm2WFEpad[np.ix_(y_box_AS_ind,x_box_AS_ind)]*DM2stop(y_box_AS_ind,x_box_AS_ind).*exp(mirrorFac*2*np.pi*1j/wvl*DM2surf(y_box_AS_ind,x_box_AS_ind)), mp.P2.compact.dx*NboxPad1AS,wvl,-1*(mp.d_dm1_dm2 + mp.d_P2_dm1), mp.dm1.dm_spacing, mp.propMethodPTP ) # back-propagate to DM1 # # To simulate going forward to the next pupil plane (with the apodizer) most efficiently, # First, back-propagate the apodizer (by rotating 180-degrees) to the previous pupil. # Second, negate the coordinates of the box used. dEP2boxEff = apodReimaged[ indBoxAS] * dEP2box # Apply 180deg-rotated apodizer mask. # dEP3box = np.rot90(dEP2box,k=2*mp.Nrelay2to3) # Forward propagate the cropped box by rotating 180 degrees mp.Nrelay2to3 times. # # Negate and reverse coordinate values to effectively rotate by 180 degrees. No change if 360 degree rotation. # Re-insert the window around the influence function back into the full beam array. EP2eff = np.zeros( (mp.dm1.compact.NdmPad, mp.dm1.compact.NdmPad), dtype=complex) EP2eff[indBoxAS] = dEP2boxEff # Forward propagate from P2 (effective) to P3 EP3 = fp.relay(EP2eff, NrelayFactor * mp.Nrelay2to3, mp.centering) # Pad pupil P3 for FFT EP3pad = pad_crop(EP3, Nfft1) # FFT from P3 to Fend.and apply vortex EF3 = fftshiftVortex * fft2(fftshift(EP3pad)) / Nfft1 # FFT from Vortex FPM to Lyot Plane EP4 = fftshift(fft2(EF3)) / Nfft1 EP4 = fp.relay( EP4, NrelayFactor * mp.Nrelay3to4 - 1, mp.centering) # Add more re-imaging relays if necessary if (Nfft1 > mp.P4.compact.Narr): EP4 = mp.P4.compact.croppedMask * pad_crop( EP4, mp.P4.compact.Narr ) # Crop EP4 and then apply Lyot stop else: EP4 = pad_crop( mp.P4.compact.croppedMask, Nfft1) * EP4 # Crop the Lyot stop and then apply it. pass # MFT to camera EP4 = fp.relay( EP4, NrelayFactor * mp.NrelayFend, mp.centering ) # Rotate the final image 180 degrees if necessary EFend = fp.mft_p2f(EP4, mp.fl, wvl, mp.P4.compact.dx, mp.Fend.dxi, mp.Fend.Nxi, mp.Fend.deta, mp.Fend.Neta, mp.centering) Gzdl[:, Gindex] = EFend[mp.Fend.corr.maskBool] / np.sqrt( mp.Fend.compact.I00[modvar.sbpIndex]) Gindex += 1 """ ---------- DM2 ---------- """ if idm == 2: Gzdl = np.zeros((mp.Fend.corr.Npix, mp.dm2.Nele), dtype=complex) # Array size for planes P3, F3, and P4 Nfft2 = int(2**falco.util.nextpow2( np.max( np.array([ mp.dm2.compact.NdmPad, minPadFacVortex * mp.dm2.compact.Nbox ])))) # Don't crop--but do pad if necessary. # Generate vortex FPM with fftshift already applied fftshiftVortex = fftshift( falco.mask.falco_gen_vortex_mask(charge, Nfft2)) # Two array sizes (at same resolution) of influence functions for MFT and angular spectrum NboxPad2AS = int(mp.dm2.compact.NboxAS) mp.dm2.compact.xy_box_lowerLeft_AS = mp.dm2.compact.xy_box_lowerLeft - ( NboxPad2AS - mp.dm2.compact.Nbox ) / 2 # Account for the padding of the influence function boxes apodReimaged = pad_crop(apodReimaged, mp.dm2.compact.NdmPad) DM2stopPad = pad_crop(DM2stop, mp.dm2.compact.NdmPad) Edm2WFEpad = pad_crop(Edm2WFE, mp.dm2.compact.NdmPad) # Propagate full field to DM2 before back-propagating in small boxes Edm2inc = pad_crop( fp.ptp(Edm1out, mp.compact.NdmPad * mp.P2.compact.dx, wvl, mp.d_dm1_dm2), mp.dm2.compact.NdmPad) # E-field incident upon DM2 Edm2inc = pad_crop(Edm2inc, mp.dm2.compact.NdmPad) Edm2 = DM2stopPad * Edm2WFEpad * Edm2inc * np.exp( mirrorFac * 2 * np.pi * 1j / wvl * pad_crop(DM2surf, mp.dm2.compact.NdmPad) ) # Initial E-field at DM2 including its own phase contribution # Propagate each actuator from DM2 through the rest of the optical system Gindex = 0 # initialize index counter for iact in mp.dm2.act_ele: # Only compute for acutators specified for use or for influence functions that are not zeroed out if np.sum(np.abs(mp.dm2.compact.inf_datacube[:, :, iact])) > 1e-12: # x- and y- coordinates of the padded influence function in the full padded pupil x_box_AS_ind = np.arange( mp.dm2.compact.xy_box_lowerLeft_AS[0, iact], mp.dm2.compact.xy_box_lowerLeft_AS[0, iact] + NboxPad2AS, dtype=int) # x-indices in pupil arrays for the box y_box_AS_ind = np.arange( mp.dm2.compact.xy_box_lowerLeft_AS[1, iact], mp.dm2.compact.xy_box_lowerLeft_AS[1, iact] + NboxPad2AS, dtype=int) # y-indices in pupil arrays for the box indBoxAS = np.ix_(y_box_AS_ind, x_box_AS_ind) # # x- and y- coordinates of the UN-padded influence function in the full padded pupil # x_box = mp.dm2.compact.x_pupPad[x_box_AS_ind] # full pupil x-coordinates of the box # y_box = mp.dm2.compact.y_pupPad[y_box_AS_ind] # full pupil y-coordinates of the box dEbox = (mp.dm2.VtoH.reshape(mp.dm2.Nact**2)[iact]) * ( mirrorFac * 2 * np.pi * 1j / wvl) * pad_crop( np.squeeze(mp.dm2.compact.inf_datacube[:, :, iact]), NboxPad2AS) # the padded influence function at DM2 dEP2box = fp.ptp( dEbox * Edm2[indBoxAS], mp.P2.compact.dx * NboxPad2AS, wvl, -1 * (mp.d_dm1_dm2 + mp.d_P2_dm1)) # back-propagate to pupil P2 # dEP2box = ptp_inf_func(dEbox.*Edm2(y_box_AS_ind,x_box_AS_ind), mp.P2.compact.dx*NboxPad2AS,wvl,-1*(mp.d_dm1_dm2 + mp.d_P2_dm1), mp.dm2.dm_spacing, mp.propMethodPTP); # back-propagate to pupil P2 # To simulate going forward to the next pupil plane (with the apodizer) most efficiently, # First, back-propagate the apodizer (by rotating 180-degrees) to the previous pupil. # Second, negate the coordinates of the box used. dEP2boxEff = apodReimaged[indBoxAS] * dEP2box # dEP3box = np.rot90(dEP2box,k=2*mp.Nrelay2to3) # Forward propagate the cropped box by rotating 180 degrees mp.Nrelay2to3 times. # # Negate and rotate coordinates to effectively rotate by 180 degrees. No change if 360 degree rotation. # if np.mod(mp.Nrelay2to3,2)==1: # x_box = -1*x_box[::-1] # y_box = -1*y_box[::-1] EP2eff = np.zeros( (mp.dm2.compact.NdmPad, mp.dm2.compact.NdmPad), dtype=complex) EP2eff[indBoxAS] = dEP2boxEff # Forward propagate from P2 (effective) to P3 EP3 = fp.relay(EP2eff, NrelayFactor * mp.Nrelay2to3, mp.centering) # Pad pupil P3 for FFT EP3pad = pad_crop(EP3, Nfft2) # FFT from P3 to Fend.and apply vortex EF3 = fftshiftVortex * fft2(fftshift(EP3pad)) / Nfft2 # FFT from Vortex FPM to Lyot Plane EP4 = fftshift(fft2(EF3)) / Nfft2 EP4 = fp.relay(EP4, NrelayFactor * mp.Nrelay3to4 - 1, mp.centering) if (Nfft2 > mp.P4.compact.Narr): EP4 = mp.P4.compact.croppedMask * pad_crop( EP4, mp.P4.compact.Narr) else: EP4 = pad_crop(mp.P4.compact.croppedMask, Nfft2) * EP4 # MFT to detector EP4 = fp.relay(EP4, NrelayFactor * mp.NrelayFend, mp.centering) EFend = fp.mft_p2f(EP4, mp.fl, wvl, mp.P4.compact.dx, mp.Fend.dxi, mp.Fend.Nxi, mp.Fend.deta, mp.Fend.Neta, mp.centering) Gzdl[:, Gindex] = EFend[mp.Fend.corr.maskBool] / \ np.sqrt(mp.Fend.compact.I00[modvar.sbpIndex]) Gindex += 1 return Gzdl
def setup_fpm_cosine(mp): if mp.F3.full.res != mp.F3.compact.res: raise ValueError('Resolution at F3 must be same for cosine basis set.') # Centering of DM surfaces on array mp.dm8.centering = mp.centering mp.dm9.centering = mp.centering mp.dm9.compact = mp.dm9 mp.dm9.dxi = (mp.fl*mp.lambda0/mp.P2.D)/mp.F3.full.res # width of a pixel at the FPM in the full model (meters) mp.dm9.compact.dxi = (mp.fl*mp.lambda0/mp.P2.D)/mp.F3.compact.res # width of a pixel at the FPM in the compact model (meters) drCos = 1/mp.dm9.actres # Width and double-separation of the cosine rings [lambda0/D] Nrad = int(np.ceil(2*mp.dm9.actres*mp.F3.Rin)) # Generate datacube of influence functions, which are rings with radial cosine profile # Compact model mp.dm9.compact.NdmPad = ceil_even(1+2*mp.F3.Rin*mp.F3.compact.res) NbeamCompact = mp.dm9.compact.NdmPad mp.dm9.NdmPad = NbeamCompact mp.dm9.compact.Nbox = mp.dm9.compact.NdmPad # the modes take up the full array. # Normalized coordinates: Compact model if mp.centering == 'pixel': xc = np.arange(-mp.dm9.compact.NdmPad/2, mp.dm9.compact.NdmPad/2)/mp.F3.compact.res elif mp.centering == 'interpixel': xc = np.arange(-(mp.dm9.compact.NdmPad-1)/2, (mp.dm9.compact.NdmPad+1)/2)/mp.F3.compact.res [Xc, Yc] = np.meshgrid(xc, xc) Rc, THETAc = cart2pol(Xc, Yc) # Hyper-gaussian rolloff at edge of occulter hg_expon = 44 # Found empirically apRad = mp.F3.Rin/(mp.F3.Rin+0.1) # Found empirically OD = 1 mask = Rc <= mp.F3.Rin windowFull = mask*np.exp(-(Rc/mp.F3.Rin/(apRad*OD))**hg_expon) drSep = drCos/2 min_azimSize = mp.min_azimSize_dm9 # [microns] pow_arr = np.arange(2, 62, 2)*6 numdivCos = 2 countCos = 0 start_rad = int(np.floor(mp.dm9.actres/2))+1 for ri in range(start_rad, Nrad+1): for _iter in range(numdivCos+1): countCos += 1 numdivSin = 3 countSin = 0 start_rad = int(np.floor(mp.dm9.actres/2))+1 for ri in range(start_rad, Nrad+1): for _iter in range(numdivSin+1): countSin += 1 mp.dm9.NactTotal = Nrad + countCos + countSin mp.dm9.compact.inf_datacube = np.zeros((mp.dm9.compact.NdmPad, mp.dm9.compact.NdmPad, mp.dm9.NactTotal)) # Compute the ring influence functions for ri in range(1, Nrad+1): modeTemp = windowFull * (1 + (-1)**((ri+1) % 2) * np.cos(2*np.pi*(Rc*mp.dm9.actres-0.5)))/2 rMin = drSep*(ri - 1) rMax = drSep*(ri + 1) if ri == 1: # Fill in the center modeTemp[Rc < drSep] = 1 else: modeTemp[Rc < rMin] = 0 modeTemp[Rc > rMax] = 0 mp.dm9.compact.inf_datacube[:, :, ri-1] = modeTemp # for ri in range(1, Nrad+1): # infFunc = mp.dm9.compact.inf_datacube[:, :, ri-1] # plt.imshow(infFunc); plt.colorbar(); plt.pause(0.05) beamRad = NbeamCompact/2 if mp.centering == 'pixel': x = np.arange(-beamRad, beamRad) elif mp.centering == 'interpixel': x = np.arange((NbeamCompact-1)/2, (NbeamCompact+1)/2) X, Y = np.meshgrid(x, x) RHO, THETA = cart2pol(X, Y) THETA2 = THETA+np.pi/3*2 THETA3 = THETA+np.pi/3*4 THETA4 = THETA+np.pi/3 THETA5 = THETA+np.pi/3*3 THETA6 = np.fliplr(THETA) apRad = mp.F3.Rin/(mp.F3.Rin+0.1) # Found empirically OD = 1 mask = Rc <= mp.F3.Rin # Cosine basis numdiv = 2 count = 0 # index counter for ri in np.arange(start_rad, Nrad+1): modeTemp = windowFull * (1 + (-1)**((ri+1) % 2) * np.cos(2*np.pi*(Rc*mp.dm9.actres-0.5)))/2 rMin = drSep*(ri - 1) rMax = drSep*(ri + 1) if ri == 1: # Fill in the center modeTemp[Rc < drSep] = 1 else: modeTemp[Rc < rMin] = 0 modeTemp[Rc > rMax] = 0 for II in range(numdiv+1): # Choose power for number of lobes powmin = 2 * np.pi * rMin / min_azimSize * 18 aux = pow_arr - powmin aux[aux < 0] = np.Inf ind_mi = np.argmin(aux) power = pow_arr[ind_mi] # cosFull = np.cos(THETA*power) + 1 numdiv = int(power/6) dth = 2*np.pi/power th_arr = np.linspace(np.pi/2, np.pi/2+np.pi/3, numdiv+1) th_rev_arr = np.linspace(np.pi/2+np.pi/3, np.pi/2, numdiv+1) th = th_arr[II] th_rev = th_rev_arr[II] ind = np.logical_and(THETA < (th+dth/2), THETA > (th-dth/2)) ind_rev = np.logical_and(THETA4 < (th_rev+dth/2), THETA4 > (th_rev-dth/2)) ind2 = np.logical_and(THETA2 < (th+dth/2), THETA2 > (th-dth/2)) ind2_rev = np.logical_and(THETA5 < (th_rev+dth/2), THETA5 > (th_rev-dth/2)) ind3 = np.logical_and(THETA3 < (th+dth/2), THETA3 > (th-dth/2)) ind3_rev = np.logical_and(THETA6 < (th+dth/2-np.pi/2-np.pi/3/2), THETA6 > (th-dth/2-np.pi/2-np.pi/3/2)) indTot = np.logical_or(ind, ind2) indTot = np.logical_or(indTot, ind3) indTot = np.logical_or(indTot, ind_rev) indTot = np.logical_or(indTot, ind2_rev) indTot = np.logical_or(indTot, ind3_rev) cosII = cosFull * indTot * modeTemp / 2 mp.dm9.compact.inf_datacube[:, :, Nrad+count] = cosII count += 1 # Sin basis numdiv = 3 for ri in np.arange(start_rad, Nrad+1): modeTemp = windowFull * (1 + (-1)**((ri+1) % 2) * np.cos(2*np.pi*(Rc*mp.dm9.actres-0.5)))/2 rMin = drSep*(ri - 1) rMax = drSep*(ri + 1) if ri == 1: # Fill in the center modeTemp[Rc < drSep] = 1 else: modeTemp[Rc < rMin] = 0 modeTemp[Rc > rMax] = 0 for II in range(numdiv+1): # Choose power for number of lobes powmin = 2*np.pi*rMin/min_azimSize*18 aux = pow_arr - powmin aux[aux < 0] = np.Inf ind_mi = np.argmin(aux) power = pow_arr[ind_mi] # disp(['Number of lobes',num2str(pow)]) cosFull = -np.cos(THETA*power)+1 dth = 2*np.pi/power th_arr = np.linspace(np.pi/2, np.pi/2+np.pi/3+np.pi/6, numdiv+1) th = th_arr[II] + np.pi/12 if th < np.pi: ind = np.logical_and(THETA < (th+dth/2), THETA > (th-dth/2)) else: ind = np.fliplr(np.logical_and(THETA < (np.pi-th+dth/2), THETA > (np.pi-th-dth/2))) ind2 = np.logical_and(THETA2 < (th+dth/2), THETA2 > (th-dth/2)) ind3 = np.logical_and(THETA3 < (th+dth/2), THETA3 > (th-dth/2)) indTotsin = np.logical_or(ind, ind2) indTotsin = np.logical_or(indTotsin, ind3) cosII = cosFull * indTotsin * modeTemp / 2 mp.dm9.compact.inf_datacube[:, :, Nrad+count] = cosII count += 1 # numdiv = int(power/6) # dth = 2*np.pi/power # th_arr = np.linspace(np.pi/2-np.pi/2/power, np.pi/2+np.pi/3-np.pi/2/power, numdiv+1) # th_rev_arr = np.linspace(np.pi/2+np.pi/3+np.pi/2/power, np.pi/2+np.pi/2/power, numdiv+1) # th = th_arr[II] # th_rev = th_rev_arr[II] # ind = np.logical_and((THETA)<(th+dth/2), (THETA)>(th-dth/2)) # ind_rev = np.logical_and((THETA4)<(th_rev+dth/2), # (THETA4)>(th_rev-dth/2)) # ind2 = np.logical_and((THETA2)<(th+dth/2), (THETA2)>(th-dth/2)) # ind2_rev = np.logical_and((THETA5)<(th_rev+dth/2), # (THETA5)>(th_rev-dth/2)) # ind3 = np.logical_and((THETA3)<(th+dth/2), (THETA3)>(th-dth/2)) # ind3_rev = np.logical_and((THETA6)<(th+dth/2-np.pi/2-np.pi/3/2), # (THETA6)>(th-dth/2-np.pi/2-np.pi/3/2)) # indTot0 = np.logical_or(ind, ind2) # indTot0 = np.logical_or(indTot0, ind3); # indTot_rev = np.logical_or(ind_rev, ind2_rev) # indTot_rev = np.logical_or(indTot_rev, ind3_rev) # # % cosII = zeros(N); # sinII = sinFull*indTot0*modeTemp + sinFull_rev*indTot_rev*modeTemp # # % figure(102);imagesc(sinII);axis image; set(gca,'YDir', 'normal') # # % pause(0.1) # mp.dm9.compact.inf_datacube[:, :, Nrad+count] = sinII # count += 1 mp.dm9.inf_datacube = mp.dm9.compact.inf_datacube mp.dm9.NactTotal = mp.dm9.inf_datacube.shape[2] mp.dm9.VtoH = mp.dm9.VtoHavg*np.ones(mp.dm9.NactTotal) # for ri in range(mp.dm9.NactTotal): # infFunc = mp.dm9.compact.inf_datacube[:, :, ri] # plt.imshow(infFunc); plt.colorbar(); plt.pause(0.01) # infSum = np.sum(mp.dm9.inf_datacube[:, :, Nrad+countCos:-1], 2) # sin # infSum = np.sum(mp.dm9.inf_datacube[:, :, Nrad:Nrad+countCos], 2) # cos # infSum = np.sum(mp.dm9.inf_datacube[:, :, 0:Nrad], 2) # rings # infSum = np.sum(mp.dm9.inf_datacube[:, :, :], 2) # all # plt.figure(); plt.imshow(infSum); plt.colorbar(); plt.pause(1) # Lower-left pixel coordinates are all (1,1) since the Zernikes take up the full array. mp.dm9.xy_box_lowerLeft = np.zeros((2, mp.dm9.NactTotal)) mp.dm9.compact.xy_box_lowerLeft = np.zeros((2, mp.dm9.NactTotal)) mp.dm9.compact.Nbox = NbeamCompact mp.dm9.Nbox = NbeamCompact # Coordinates for the full FPM array [meters] if mp.centering == 'pixel': mp.dm9.compact.x_pupPad = np.arange(-mp.dm9.compact.NdmPad/2, (mp.dm9.compact.NdmPad/2)) * \ mp.dm9.compact.dxi elif mp.centering == 'interpixel': mp.dm9.compact.x_pupPad = np.arange(-(mp.dm9.compact.NdmPad-1)/2, (mp.dm9.compact.NdmPad+1)/2) * \ mp.dm9.compact.dxi mp.dm9.compact.y_pupPad = mp.dm9.compact.x_pupPad # Initial DM9 voltages if not hasattr(mp.dm9, 'V'): mp.dm9.V = np.zeros(mp.dm9.NactTotal) mp.dm9.V[0:Nrad] = mp.dm9.V0coef * np.ones(Nrad) else: mp.dm9.V = mp.DM9V0 mp.dm9.Vmin = np.min(mp.t_diel_nm_vec) # minimum thickness of FPM dielectric layer (nm) mp.dm9.Vmax = np.max(mp.t_diel_nm_vec) # maximum thickness (from one actuator, not of the facesheet) of FPM dielectric layer (nm) # OPTIONS FOR DEFINING DM8 (FPM Metal) mp.dm8.VtoHavg = 1e-9 # gain of DM8 (meters/Volt) mp.dm8.Vmin = np.min(mp.t_metal_nm_vec) # minimum thickness of FPM metal layer (nm) mp.dm8.Vmax = np.max(mp.t_metal_nm_vec) # maximum thickness (from one actuator, not of the facesheet) of FPM metal layer (nm) # DM8 Option 2: Set basis as a single nickel disk. mp.dm8.NactTotal = 1 mp.dm8.act_ele = 1 print('%d actuators in DM8.' % mp.dm8.NactTotal) mp.dm8.VtoH = mp.dm8.VtoHavg * np.ones(mp.dm8.NactTotal) # Gains: volts to meters in surface height; mp.dm8.xy_box_lowerLeft = np.array([0, 0]).reshape((2, 1)) mp.dm8.compact = mp.dm8 if not hasattr(mp.dm8, 'V'): # Initial DM8 voltages mp.dm8.V = mp.dm8.V0coef * np.ones(mp.dm8.NactTotal) else: mp.dm8.V = mp.DM8V0 # Don't define extra actuators and time: if not mp.F3.Rin == mp.F3.RinA: raise ValueError('Change mp.F3.Rin and mp.F3.RinA to be equal to avoid wasting time.') # Copy over some common values from DM9: mp.dm8.dxi = mp.dm9.dxi # Width of a pixel at the FPM in full model (meters) mp.dm8.NdmPad = mp.dm9.NdmPad mp.dm8.Nbox = mp.dm8.NdmPad mp.dm8.compact.dxi = mp.dm9.compact.dxi # Width of a pixel at the FPM in compact model (meters) mp.dm8.compact.NdmPad = mp.dm9.compact.NdmPad mp.dm8.compact.Nbox = mp.dm8.compact.NdmPad # Make or read in DM8 disk for the full model FPMgenInputs = {} FPMgenInputs['pixresFPM'] = mp.F3.full.res # pixels per lambda_c/D FPMgenInputs['rhoInner'] = mp.F3.Rin # radius of inner FPM amplitude spot (in lambda_c/D) FPMgenInputs['rhoOuter'] = np.Inf # radius of outer opaque FPM ring (in lambda_c/D) FPMgenInputs['FPMampFac'] = 0 # amplitude transmission of inner FPM spot FPMgenInputs['centering'] = mp.centering diskFull = np.round(pad_crop(1-falco.mask.gen_annular_FPM(FPMgenInputs), mp.dm8.NdmPad)) mp.dm8.inf_datacube = np.zeros((diskFull.shape[0], diskFull.shape[1], 1)) mp.dm8.inf_datacube[:, :, 0] = diskFull # Make or read in DM8 disk for the compact model FPMgenInputs['pixresFPM'] = mp.F3.compact.res # pixels per lambda_c/D diskCompact = np.round(pad_crop(1-falco.mask.gen_annular_FPM(FPMgenInputs), mp.dm8.compact.NdmPad)) mp.dm8.compact.inf_datacube = np.zeros((diskCompact.shape[0], diskCompact.shape[1], 1)) mp.dm8.compact.inf_datacube[:, :, 0] = diskCompact pass
def setup_fpm(mp): mp.dm9.Nact = ceil_even(2*mp.F3.Rin*mp.dm9.actres) # number of actuators across DM9 (if not in a hex grid) if mp.dm9.inf0name.lower() in '3x3': mp.dm9.inf0 = 1/4 * np.array([[1, 2, 1], [2, 4, 2], [1, 2, 1]]) # influence function mp.dm9.dx_inf0_act = 1/2 # number of inter-actuator widths per pixel # FPM resolution (pixels per lambda0/D) in the compact and full models. mp.F3.compact.res = mp.dm9.actres / mp.dm9.dx_inf0_act mp.F3.full.res = mp.dm9.actres / mp.dm9.dx_inf0_act elif mp.dm9.inf0name.lower() in 'lanczos3': N = 91 xs = np.arange(-(N-1)/2, (N+1)/2)/N*10*0.906 #(-(N-1)/2:(N-1)/2)/N *10*0.906 a = 3 Lx0 = a*np.sin(np.pi*xs)*np.sin(np.pi*xs/a)/(np.pi*xs)**2 Lx0[xs == 0] = 1 Lx = Lx0 Lx[xs >= a] = 0 Lx[xs <= -a] = 0 Lxy = Lx.T @ Lx # The 2-D Lanczos kernel Nhalf = np.ceil(N/2) Nrad = 30 LxyCrop = Lxy[Nhalf-Nrad-1:Nhalf+Nrad, Nhalf-Nrad-1:Nhalf+Nrad] mp.dm9.inf0 = LxyCrop # influence function mp.dm9.dx_inf0_act = 1/10 # number of inter-actuator widths per pixel elif mp.dm9.inf0name.lower() in 'xinetics': mp.dm9.inf0 = 1*fits.getdata('influence_dm5v2.fits') mp.dm9.dx_inf0_act = 1/10 # number of inter-actuator widths per pixel # DM8 and DM9 (Optimizable FPM) Setup # Centering of DM surfaces on array mp.dm8.centering = mp.centering mp.dm9.centering = mp.centering mp.dm9.compact = mp.dm9 if hasattr(mp, 'flagDM9inf3x3'): mp.dm9.xcent_dm = mp.dm9.Nact/2 - 1/2 mp.dm9.ycent_dm = mp.dm9.Nact/2 - 1/2 if mp.centering in 'interpixel': raise ValueError('The 3x3 influence function for DM9 requires a pixel-centered coordinate system.') else: mp.dm9.xcent_dm = mp.dm9.Nact/2 - 1/2 mp.dm9.ycent_dm = mp.dm9.Nact/2 - 1/2 mp.dm9.dm_spacing = 1/mp.dm9.actres*(mp.fl*mp.lambda0/mp.P2.D) # meters, pitch of DM actuators mp.dm9.compact = mp.dm9 mp.dm9.compact.dm_spacing = mp.dm9.dm_spacing # meters, pitch of DM actuators mp.dm9.dx_inf0 = (mp.dm9.dx_inf0_act)*mp.dm9.dm_spacing # meters, sampling of the influence function mp.dm9.compact.dx_inf0 = (mp.dm9.compact.dx_inf0_act)*mp.dm9.compact.dm_spacing # meters, sampling of the influence function mp.dm9.dxi = (mp.fl*mp.lambda0/mp.P2.D)/mp.F3.full.res # width of a pixel at the FPM in the full model (meters) mp.dm9.compact.dxi = (mp.fl*mp.lambda0/mp.P2.D)/mp.F3.compact.res # width of a pixel at the FPM in the compact model (meters) if mp.dm9.inf0.shape[0] == 3: fpm_inf_cube_3x3(mp.dm9) fpm_inf_cube_3x3(mp.dm9.compact) else: fpm_inf_cube(mp.dm9) fpm_inf_cube(mp.dm9.compact) # Zero out DM9 actuators too close to the outer edge (within mp.dm9.FPMbuffer lambda0/D of edge) r_cent_lam0D = mp.dm9.r_cent_act*mp.dm9.dm_spacing/(mp.dm9.dxi)/mp.F3.full.res ## mp.F3.RinA_inds = np.array([], dtype=int) mp.F3.RinAB_inds = np.array([], dtype=int) for ii in range(mp.dm9.NactTotal): # Zero out FPM actuators beyond the allowed radius (mp.F3.Rin) if r_cent_lam0D[ii] > mp.F3.Rin-mp.dm9.FPMbuffer: mp.dm9.inf_datacube[:, :, ii] = np.zeros_like(mp.dm9.inf_datacube[:, :, ii]) mp.dm9.compact.inf_datacube[:, :, ii] = np.zeros_like(mp.dm9.compact.inf_datacube[:, :, ii]) # Get the indices for the actuators within radius mp.F3.RinA if r_cent_lam0D[ii] <= mp.F3.RinA-mp.dm9.FPMbuffer: mp.F3.RinA_inds = np.append(mp.F3.RinA_inds, ii) else: # Get the indices for the actuators between radii mp.F3.RinA and mp.F3.Rin mp.F3.RinAB_inds = np.append(mp.F3.RinAB_inds, ii) print('%d actuators in DM9.' % mp.dm9.NactTotal) mp.dm9.ABfac = 1 # Gain factor between inner and outer FPM regions mp.dm9.VtoHavg = 1e-9 # gain of DM9 (meters/Volt) mp.dm9.VtoH = mp.dm9.VtoHavg * np.ones(mp.dm9.NactTotal) # Gains: volts to meters in surface height; mp.dm9.VtoH[mp.F3.RinAB_inds] = mp.dm9.ABfac * mp.dm9.VtoH[mp.F3.RinAB_inds] if not hasattr(mp.dm9, 'V'): # Initial DM9 voltages mp.dm9.V = np.zeros(mp.dm9.NactTotal) mp.dm9.V[mp.F3.RinA_inds] = mp.dm9.V0coef else: mp.dm9.V = mp.DM9V0 mp.dm9.Vmin = np.min(mp.t_diel_nm_vec) # minimum thickness of FPM dielectric layer (nm) mp.dm9.Vmax = np.max(mp.t_diel_nm_vec) # maximum thickness (from one actuator, not of the facesheet) of FPM dielectric layer (nm) # -OPTIONS FOR DEFINING DM8 (FPM Metal) mp.dm8.VtoHavg = 1e-9 # gain of DM8 (meters/Volt) mp.dm8.Vmin = np.min(mp.t_metal_nm_vec) # minimum thickness of FPM metal layer (nm) mp.dm8.Vmax = np.max(mp.t_metal_nm_vec) # maximum thickness (from one actuator, not of the facesheet) of FPM metal layer (nm) # DM8 Option 2: Set basis as a single nickel disk. mp.dm8.NactTotal = 1 mp.dm8.act_ele = 1 print('%d actuators in DM8.' % mp.dm8.NactTotal) mp.dm8.VtoH = mp.dm8.VtoHavg * np.ones(mp.dm8.NactTotal) # Gains: volts to meters in surface height; mp.dm8.xy_box_lowerLeft = np.array([0, 0]).reshape((2, 1)) mp.dm8.compact = mp.dm8 if not hasattr(mp.dm8, 'V'): # Initial DM8 voltages mp.dm8.V = mp.dm8.V0coef * np.ones(mp.dm8.NactTotal) else: mp.dm8.V = mp.DM8V0 # Don't define extra actuators and time: if not mp.F3.Rin == mp.F3.RinA: raise ValueError('Change mp.F3.Rin and mp.F3.RinA to be equal to avoid wasting time.') # Copy over some common values from DM9: mp.dm8.dxi = mp.dm9.dxi # Width of a pixel at the FPM in full model (meters) mp.dm8.NdmPad = mp.dm9.NdmPad mp.dm8.Nbox = mp.dm8.NdmPad mp.dm8.compact.dxi = mp.dm9.compact.dxi # Width of a pixel at the FPM in compact model (meters) mp.dm8.compact.NdmPad = mp.dm9.compact.NdmPad mp.dm8.compact.Nbox = mp.dm8.compact.NdmPad # Make or read in DM8 disk for the full model FPMgenInputs = {} FPMgenInputs['pixresFPM'] = mp.F3.full.res # pixels per lambda_c/D FPMgenInputs['rhoInner'] = mp.F3.Rin # radius of inner FPM amplitude spot (in lambda_c/D) FPMgenInputs['rhoOuter'] = np.Inf # radius of outer opaque FPM ring (in lambda_c/D) FPMgenInputs['FPMampFac'] = 0 # amplitude transmission of inner FPM spot FPMgenInputs['centering'] = mp.centering diskFull = np.round(pad_crop(1-falco.mask.gen_annular_FPM(FPMgenInputs), mp.dm8.NdmPad)) mp.dm8.inf_datacube = np.zeros((diskFull.shape[0], diskFull.shape[1], 1)) mp.dm8.inf_datacube[:, :, 0] = diskFull # Make or read in DM8 disk for the compact model FPMgenInputs['pixresFPM'] = mp.F3.compact.res # pixels per lambda_c/D diskCompact = np.round(pad_crop(1-falco.mask.gen_annular_FPM(FPMgenInputs), mp.dm8.compact.NdmPad)) mp.dm8.compact.inf_datacube = np.zeros((diskCompact.shape[0], diskCompact.shape[1], 1)) mp.dm8.compact.inf_datacube[:, :, 0] = diskCompact # Zero out parts of DM9 actuators that go outside the nickel disk. # Also apply the grayscale edge. DM8windowFull = diskFull DM8windowCompact = diskCompact for iact in range(mp.dm9.NactTotal): if np.sum(np.abs(mp.dm9.inf_datacube[:, :, iact])) >= 1e-8 and np.abs(mp.dm9.VtoH[iact]) >= 1e-13: y_box_ind = np.arange(mp.dm9.xy_box_lowerLeft[0, iact], mp.dm9.xy_box_lowerLeft[0, iact]+mp.dm9.Nbox, dtype=int) # x-indices in pupil arrays for the box x_box_ind = np.arange(mp.dm9.xy_box_lowerLeft[1, iact], mp.dm9.xy_box_lowerLeft[1, iact]+mp.dm9.Nbox, dtype=int) # y-indices in pupil arrays for the box mp.dm9.inf_datacube[:, :, iact] = DM8windowFull[np.ix_(x_box_ind, y_box_ind)] * mp.dm9.inf_datacube[:, :, iact] y_box_ind = np.arange(mp.dm9.compact.xy_box_lowerLeft[0, iact], mp.dm9.compact.xy_box_lowerLeft[0, iact]+mp.dm9.compact.Nbox, dtype=int) # x-indices in pupil arrays for the box x_box_ind = np.arange(mp.dm9.compact.xy_box_lowerLeft[1, iact], mp.dm9.compact.xy_box_lowerLeft[1, iact]+mp.dm9.compact.Nbox, dtype=int) # y-indices in pupil arrays for the box mp.dm9.compact.inf_datacube[:, :, iact] = DM8windowCompact[np.ix_(x_box_ind, y_box_ind)] * mp.dm9.compact.inf_datacube[:, :, iact] pass
def full_Fourier(mp, wvl, Ein, normFac, flagScaleFPM=False): """ Truth model with a simple layout used to generate images in simulation. Truth model used to generate images in simulation. Can include aberrations/errors that are unknown to the estimator and controller. This function uses the simplest model (FTs except for angular spectrum between DMs) for several coronagraph types. Parameters ---------- mp : ModelParameters Structure containing optical model parameters wvl : Wavelength Scalar value for the wavelength of the light in meters Ein : Electric field input 2-D electric field in the input pupil normFac : Normalization factor Scalar value of the PSF peak normalization factor to apply to the whole image. flagScaleFPM : bool, optional Whether to scale the diameter of the FPM inversely with wavelength. Returns ------- Eout : numpy ndarray 2-D electric field in final focal plane """ check.is_bool(flagScaleFPM, 'flagScaleFPM') mirrorFac = 2 # Phase change is twice the DM surface height in reflection NdmPad = int(mp.full.NdmPad) if mp.flagRotation: NrelayFactor = 1 else: NrelayFactor = 0 # zero out the number of relays if flagScaleFPM: fpmScaleFac = wvl / mp.lambda0 else: fpmScaleFac = 1.0 """ Masks and DM surfaces """ if any(mp.dm_ind == 1): DM1surf = falco.dm.gen_surf_from_act(mp.dm1, mp.dm1.dx, NdmPad) # try: # DM1surf = pad_crop(mp.dm1.surfM, NdmPad) # except AttributeError: # No surfM parameter exists, create DM surface # DM1surf = falco.dm.gen_surf_from_act(mp.dm1, mp.dm1.dx, NdmPad) else: DM1surf = np.zeros((NdmPad, NdmPad)) if any(mp.dm_ind == 2): DM2surf = falco.dm.gen_surf_from_act(mp.dm2, mp.dm2.dx, NdmPad) # try: # DM2surf = pad_crop(mp.dm2.surfM, NdmPad) # except AttributeError: # No surfM parameter exists, create DM surface # DM2surf = falco.dm.gen_surf_from_act(mp.dm2, mp.dm2.dx, NdmPad) else: DM2surf = np.zeros((NdmPad, NdmPad)) pupil = pad_crop(mp.P1.full.mask, NdmPad) Ein = pad_crop(Ein, NdmPad) if mp.flagDM1stop: DM1stop = pad_crop(mp.dm1.full.mask, NdmPad) else: DM1stop = np.ones((NdmPad, NdmPad)) if mp.flagDM2stop: DM2stop = pad_crop(mp.dm2.full.mask, NdmPad) else: DM2stop = np.ones((NdmPad, NdmPad)) if mp.flagDMwfe: pass # if(any(mp.dm_ind==1)); Edm1WFE = exp(2*np.pi*1j/wvl*pad_crop(mp.dm1.wfe,NdmPad,'extrapval',0)); else; Edm1WFE = ones(NdmPad); end # if(any(mp.dm_ind==2)); Edm2WFE = exp(2*np.pi*1j/wvl*pad_crop(mp.dm2.wfe,NdmPad,'extrapval',0)); else; Edm2WFE = ones(NdmPad); end else: Edm1WFE = np.ones((NdmPad, NdmPad)) Edm2WFE = np.ones((NdmPad, NdmPad)) """ Propagation: entrance pupil, 2 DMs, (optional) apodizer, FPM, LS, and final focal plane """ # Define pupil P1 and Propagate to pupil P2 EP1 = pupil * Ein # E-field at pupil plane P1 EP2 = falco.prop.relay(EP1, NrelayFactor * mp.Nrelay1to2, mp.centering) # Propagate from P2 to DM1, and apply DM1 surface and aperture stop if not abs(mp.d_P2_dm1) == 0: Edm1 = falco.prop.ptp(EP2, mp.P2.full.dx * NdmPad, wvl, mp.d_P2_dm1) else: Edm1 = EP2 # E-field arriving at DM1 Edm1b = Edm1 * Edm1WFE * DM1stop * np.exp( mirrorFac * 2 * np.pi * 1j * DM1surf / wvl) # Propagate from DM1 to DM2, and apply DM2 surface and aperture stop Edm2 = falco.prop.ptp(Edm1b, mp.P2.full.dx * NdmPad, wvl, mp.d_dm1_dm2) Edm2 *= Edm2WFE * DM2stop * np.exp( mirrorFac * 2 * np.pi * 1j * DM2surf / wvl) # Back-propagate to pupil P2 if (mp.d_P2_dm1 + mp.d_dm1_dm2 == 0): EP2eff = Edm2 # Do nothing if zero distance else: EP2eff = falco.prop.ptp(Edm2, mp.P2.full.dx * NdmPad, wvl, -1 * (mp.d_dm1_dm2 + mp.d_P2_dm1)) # Re-image to pupil P3 EP3 = falco.prop.relay(EP2eff, NrelayFactor * mp.Nrelay2to3, mp.centering) # Apply the apodizer mask (if there is one) if (mp.flagApod): EP3 = mp.P3.full.mask * pad_crop(EP3, mp.P3.full.Narr) # Propagations Specific to the Coronagraph Type if mp.coro.upper() in ('LC', 'APLC', 'RODDIER'): # MFT from apodizer plane to FPM (i.e., P3 to F3) EF3inc = falco.prop.mft_p2f(EP3, mp.fl, wvl, mp.P2.full.dx, fpmScaleFac * mp.F3.full.dxi, mp.F3.full.Nxi, fpmScaleFac * mp.F3.full.deta, mp.F3.full.Neta, mp.centering) # Apply (1-FPM) for Babinet's principle later if mp.coro.upper() == 'RODDIER': FPM = mp.F3.full.mask * np.exp( 1j * 2 * np.pi / wvl * (mp.F3.n(wvl) - 1) * mp.F3.t * mp.F3.full.mask.phzSupport) EF3 = (1. - FPM) * EF3inc # Apply (1-FPM) for Babinet's princ. later else: EF3 = (1. - mp.F3.full.mask) * EF3inc # Use Babinet's principle at the Lyot plane. EP4noFPM = falco.prop.relay(EP3, NrelayFactor * mp.Nrelay3to4, mp.centering) # MFT from FPM to Lyot Plane (i.e., F3 to P4) EP4subtrahend = falco.prop.mft_f2p(EF3, mp.fl, wvl, fpmScaleFac * mp.F3.full.dxi, mp.F3.full.deta, fpmScaleFac * mp.P4.full.dx, mp.P4.full.Narr, mp.centering) # Babinet's principle at P4 EP4 = pad_crop(EP4noFPM, mp.P4.full.Narr) - EP4subtrahend elif mp.coro.upper() == 'HLC': # Complex transmission of the points outside the FPM (just fused silica # with optional dielectric and no metal). t_Ti_base = 0 t_Ni_vec = [0] t_PMGI_vec = [1e-9 * mp.t_diel_bias_nm] # [meters] pol = 2 transOuterFPM, rCoef = falco.thinfilm.calc_complex_occulter( wvl, mp.aoi, t_Ti_base, t_Ni_vec, t_PMGI_vec, wvl * mp.F3.d0fac, pol) # MFT from apodizer plane to FPM (i.e., P3 to F3) EF3inc = falco.prop.mft_p2f(EP3, mp.fl, wvl, mp.P2.full.dx, mp.F3.full.dxi, mp.F3.full.Nxi, mp.F3.full.deta, mp.F3.full.Neta, mp.centering) # Apply (transOuterFPM-FPM) for Babinet's principle later EF3 = (transOuterFPM - mp.F3.full.mask) * EF3inc # Use Babinet's principle at the Lyot plane. # Propagate forward another pupil plane EP4noFPM = falco.prop.relay(EP3, NrelayFactor * mp.Nrelay3to4, mp.centering) # Apply the change from the FPM's outer complex transmission. EP4noFPM = transOuterFPM * pad_crop(EP4noFPM, mp.P4.full.Narr) # MFT from FPM to Lyot Plane (i.e., F3 to P4) # Subtrahend term for Babinet's principle EP4subtra = falco.prop.mft_f2p(EF3, mp.fl, wvl, mp.F3.full.dxi, mp.F3.full.deta, mp.P4.full.dx, mp.P4.full.Narr, mp.centering) # Babinet's principle at P4 EP4 = EP4noFPM - EP4subtra elif (mp.coro.upper() == 'SPLC' or mp.coro.upper() == 'FLC'): # MFT from apodizer plane to FPM (i.e., P3 to F3) EF3inc = falco.prop.mft_p2f(EP3, mp.fl, wvl, mp.P2.full.dx, fpmScaleFac * mp.F3.full.dxi, mp.F3.full.Nxi, fpmScaleFac * mp.F3.full.deta, mp.F3.full.Neta, mp.centering) EF3 = mp.F3.full.mask * EF3inc # MFT from FPM to Lyot Plane (i.e., F3 to P4) EP4 = falco.prop.mft_f2p(EF3, mp.fl, wvl, fpmScaleFac * mp.F3.full.dxi, fpmScaleFac * mp.F3.full.deta, mp.P4.full.dx, mp.P4.full.Narr, mp.centering) EP4 = falco.prop.relay(EP4, NrelayFactor * mp.Nrelay3to4 - 1, mp.centering) elif mp.coro.upper() in ('VORTEX', 'VC', 'AVC'): # Get FPM charge if isinstance(mp.F3.VortexCharge, np.ndarray): # Passing an array for mp.F3.VortexCharge with # corresponding wavelengths mp.F3.VortexCharge_lambdas # represents a chromatic vortex FPM if mp.F3.VortexCharge.size == 1: charge = mp.F3.VortexCharge else: np.interp(wvl, mp.F3.VortexCharge_lambdas, mp.F3.VortexCharge, 'linear', 'extrap') elif isinstance(mp.F3.VortexCharge, (int, float)): # single value indicates fully achromatic mask charge = mp.F3.VortexCharge else: raise TypeError("mp.F3.VortexCharge must be an int, float, or\ numpy ndarray.") pass EP4 = falco.prop.mft_p2v2p(EP3, charge, mp.P1.full.Nbeam / 2., 0.3, 5) EP4 = pad_crop(EP4, mp.P4.full.Narr) # Undo the rotation inherent to falco.prop.mft_p2v2p.m if not mp.flagRotation: EP4 = falco.prop.relay(EP4, -1, mp.centering) else: log.warning('The chosen coronagraph type is not included yet.') raise ValueError("Value of mp.coro not recognized.") pass # Remove FPM completely if normalization value being found for vortex if normFac == 0: if mp.coro.upper() in ('VORTEX', 'VC', 'AVC'): EP4 = falco.prop.relay(EP3, NrelayFactor * mp.Nrelay3to4, mp.centering) EP4 = pad_crop(EP4, mp.P4.full.Narr) pass pass """ Back to common propagation any coronagraph type """ # Apply the (cropped-down) Lyot stop EP4 *= mp.P4.full.croppedMask # MFT from Lyot Stop to final focal plane (i.e., P4 to Fend) EP4 = falco.prop.relay(EP4, NrelayFactor * mp.NrelayFend, mp.centering) EFend = falco.prop.mft_p2f(EP4, mp.fl, wvl, mp.P4.full.dx, mp.Fend.dxi, mp.Fend.Nxi, mp.Fend.deta, mp.Fend.Neta, mp.centering) # Don't apply FPM if normalization value is being found if normFac == 0: Eout = EFend # Don't normalize if normalization value is being found else: Eout = EFend / np.sqrt(normFac) # Apply normalization return Eout