示例#1
0
    def test_add_incompatible_wavefronts():
        """ Test we can't add wavefronts to incompatible things. Verifies fix of #308 """
        import poppy
        import astropy.units as u
        n = 10
        w1 = poppy.Wavefront(npix=n)
        w2 = poppy.Wavefront(npix=n)
        w3 = poppy.Wavefront(npix=n, diam=1 * u.m)
        w4 = poppy.Wavefront(npix=n, pixelscale=1 * u.arcsec / u.pixel)
        w5 = poppy.Wavefront(npix=n * 2)

        fw1 = poppy.FresnelWavefront(1.0 * u.m, npix=n, oversample=1)

        # test a valid addition works:
        output = w1 + w2
        assert isinstance(output,
                          w1.__class__), "couldn't add compatible wavefronts"

        # test the invalid additions are caught:
        inputs_and_errors = ((
            3, "Wavefronts can only be summed with other Wavefronts"
        ), (
            fw1,
            "Wavefronts can only be summed with other Wavefronts of the same class"
        ), (
            w3, "Wavefronts can only be added if they have the same pixelscale"
        ), (
            w4, "Wavefronts can only be added if they have the same pixelscale"
        ), (w5,
            "Wavefronts can only be added if they have the same size and shape"
            ))
        for test_input, expected_error in inputs_and_errors:
            with pytest.raises(ValueError) as excinfo:
                output = w1 + test_input
            assert _exception_message_starts_with(excinfo, expected_error)
示例#2
0
def test_TipTiltStage(display=False, verbose=False):
    """ Test tip tilt stage moves the PSF by the requested amount
    """
    ap = poppy.HexagonAperture(flattoflat=0.75 * u.m)
    det = poppy.Detector(pixelscale=0.1 * u.arcsec / u.pix, fov_pixels=128)

    tt = poppy.active_optics.TipTiltStage(ap, include_factor_of_two=False)

    wave = poppy.Wavefront(npix=128, diam=1 * u.m)

    trans = ap.get_transmission(wave)
    assert np.allclose(tt.get_transmission(wave),
                       trans), "Transmission does not match expectations"
    assert np.allclose(tt.get_opd(wave),
                       0), "OPD without tilt does not match expectation"

    for tx, ty in ((0 * u.arcsec, 1 * u.arcsec), (1 * u.arcsec, 0 * u.arcsec),
                   (-0.23 * u.arcsec, 0.65 * u.arcsec)):
        for include_factor_of_two in [True, False]:

            if verbose:
                print(
                    f"Testing {tx}, {ty}, with include_factor_of_two={include_factor_of_two}"
                )

            tt.include_factor_of_two = include_factor_of_two
            tt.set_tip_tilt(tx, ty)

            wave = poppy.Wavefront(npix=64, diam=1 * u.m)
            wave *= ap
            wave *= tt

            if display:
                plt.figure()
                wave.display(what='both')
                plt.suptitle(f"Wavefront with {tx}, {ty}")

            wave.propagate_to(det)

            if display:
                plt.figure()
                wave.display()
                plt.title(f"PSF with {tx}, {ty}")

            cen = poppy.measure_centroid(wave.as_fits(),
                                         boxsize=5,
                                         relativeto='center',
                                         units='arcsec')

            factor = 2 if include_factor_of_two else 1
            assert np.isclose(cen[1] * u.arcsec, tx * factor,
                              atol=1e-4), "X pos not as expected"
            assert np.isclose(
                cen[0] * u.arcsec, ty * factor, atol=1e-4
            ), f"Y pos not as expected: {cen[0]*u.arcsec}, {ty*factor}"
def test_ShackHartmannWFS():
    """ 
	Test Shack Hartmann Wavefront Sensor class functionality. Show that spot centroid measurement changes when optical system reflects off a deformed mirror
	"""
    wavelength = 635 * u.nm
    # define nominal shack harttmann wavefront sensor:
    shwfs = sub_sampled_optics.ShackHartmannWavefrontSensor()
    dm_size = shwfs.lenslet_pitch * 24

    # setup flat wavefront and calculate nominal spot locations on SHWFS
    wf_flat = poppy.Wavefront(
        diam=dm_size,
        wavelength=wavelength,
        npix=int((shwfs.lenslet_pitch / shwfs.pixel_pitch).value *
                 shwfs.n_lenslets * 2))
    wf_flat *= poppy.CircularAperture(radius=dm_size / 2)
    # sample flat wavefront:
    shwfs.sample_wf(wf_flat)
    shwfs.get_psfs()
    flat_centroid_list = shwfs.get_centroids()

    ## define DM
    act_x = 2
    act_y = 2
    stroke = .3e-6
    dm_actuator_pitch = dm_size / 4
    dm = poppy.dms.ContinuousDeformableMirror(
        dm_shape=(4, 4),
        actuator_spacing=dm_actuator_pitch,
        radius=dm_size / 2,
        include_factor_of_two=True)
    dm.set_actuator(act_x, act_y, stroke)

    # define Wavefront object for simulation, reflect off DM
    wf = poppy.Wavefront(diam=dm_size,
                         wavelength=wavelength,
                         npix=int(
                             (shwfs.lenslet_pitch / shwfs.pixel_pitch).value *
                             shwfs.n_lenslets * 2))
    wf *= poppy.CircularAperture(radius=dm_size / 2)
    wf *= dm

    #sample actual wf and propagate to detector:
    shwfs.sample_wf(wf)
    shwfs.get_psfs()
    # reconstruct wavefront and ensure that it is nonzero after reflecting off of deformed DM
    reconstruction = shwfs.reconstruct_wavefront(flat_centroid_list).value

    assert np.count_nonzero(
        reconstruction
    ) > 0, "Wavefront reconstruction was not non-zero as expected for input DM actuation"

    return np.count_nonzero(reconstruction) > 0
示例#4
0
def test_wavefront_rot90_vs_ndimagerotate_consistency(plot=False):
    """Test that rotating a Wavefront via either of the two
    methods yields consistent results. This compares an exact
    90 degree rotation and an interpolating not-quite-90-deg rotation.
    Both methods should rotate counterclockwise and consistently.
    """
    letterf = poppy.optics.LetterFAperture()
    wave = poppy.Wavefront(diam=3 * u.m, npix=128)
    wave *= letterf
    wave2 = wave.copy()

    wave.rotate(90)
    wave2.rotate(89.99999)

    assert np.allclose(wave2.intensity, wave.intensity, atol=1e-5), "Inconsistent results from the two rotation methods"

    from poppy.tests.test_sign_conventions import brighter_top_half, brighter_left_half

    assert brighter_left_half(wave.intensity), "Rotated wavefront orientation not as expected"
    assert not brighter_top_half(wave.intensity), "Rotated wavefront orientation not as expected"

    if plot:
        fig, axes = plt.subplots(figsize=(10, 5), ncols=2)
        wave.display(ax=axes[0])
        wave2.display(ax=axes[1])
        axes[0].set_title("Rot90")
        axes[1].set_title("ndimage rotate(89.9999)")
示例#5
0
def test_pupil_orientations_before_and_after_focus(plot=False,
                                                   npix_pupil=128,
                                                   npix_fov=128):
    """ Verify pupil orientations before and after focus, and signs of thin lens defocus

    1. A negative weak lens produces images (before focus) that have consistent orientation with the exit pupil
    2. A positive weak lens produces images (after focus) that have the opposite orientation as the exit pupil
    3. Images with the same magnitude but opposite signs of defocus should be 180 degree rotations of one another.

    """

    wave0 = poppy.Wavefront(diam=3 * u.m, npix=npix_pupil)
    wave0 *= poppy.LetterFAperture()
    wave1 = wave0.copy()
    wave2 = wave0.copy()

    wave1 *= poppy.ThinLens(nwaves=-5)
    wave1.propagate_to(
        poppy.Detector(fov_pixels=npix_fov,
                       pixelscale=0.03 * u.arcsec / u.pixel))

    wave2 *= poppy.ThinLens(nwaves=+5)
    wave2.propagate_to(
        poppy.Detector(fov_pixels=npix_fov,
                       pixelscale=0.03 * u.arcsec / u.pixel))

    if plot:
        fig, axes = plt.subplots(figsize=(15, 6), ncols=3)
        plt.suptitle("Before and After Focus sign test (Fresnel propagation)",
                     fontweight='bold')

        wave0.display(ax=axes[0])
        wave1.display(imagecrop=fov,
                      title='Intensity at plane before focus',
                      scale='log',
                      ax=axes[1])
        wave2.display(imagecrop=fov,
                      title='Intensity at plane after focus',
                      scale='log',
                      ax=axes[2])

    # check entrance pupil orientation
    assert brighter_top_half(wave0.intensity) and brighter_left_half(
        wave0.intensity), "Letter F should be brighter at top and left"
    # check orientation of what should be an image before focus
    assert brighter_top_half(wave1.intensity) and brighter_left_half(
        wave1.intensity
    ), "Image with negative lens (before focus) should have same orientation as the pupil "
    # check orientation of what should be an image after focus
    assert (not brighter_top_half(wave2.intensity)) and (
        not brighter_left_half(wave2.intensity)
    ), "Image with positive lens (after focus) should have opposite orientation as the pupil "
    # check consistency of defocus diffraction pattern on either side of focus, just with opposite signs (for this no-WFE case)
    assert np.allclose(
        wave1.intensity, np.rot90(wave2.intensity, 2)
    ), "Positive and negative weak lenses should be 180 degree rotation of one another"
示例#6
0
    def decorated_getphasor(optic,wave):
        #newwave_shape = (wave.shape[0]*_grayscale_pixels, wave.shape[1]*_grayscale_pixels)

        # TODO FIXME extend this to work for both image and pupil planes?
        oversampled_wave = poppy.Wavefront( wavelength=wave.wavelength,
            npix=wave.shape[0]*_grayscale_pixels,
            diam=wave.diam,
            oversample=wave.oversample)

        oversampled_phasor = optic.getPhasor(oversampled_wave)

        rebinned_phasor = poppy.utils.rebin_array(oversampled_wave, (_grayscale_pixels,_grayscale_pixels))
        return rebinned_phasor
示例#7
0
def _patched_inputWavefront(self, wavelength=2e-6):
    """Create a Wavefront object suitable for sending through a given optical system, based on
    the size of the first optical plane, assumed to be a pupil.

    If the first optical element is an Analytic pupil (i.e. has no pixel scale) then
    an array of 1024x1024 will be created (not including oversampling).

    Uses self.source_offset to assign an off-axis tilt, if requested.

    Parameters
    ----------
    wavelength : float
        Wavelength in meters

    Returns
    -------
    wavefront : poppy.Wavefront instance
        A wavefront appropriate for passing through this optical system.

    """

    if hasattr(self, 'npix'):
        npix = int(self.npix)
    else:
        npix = self.planes[0].shape[0] if self.planes[
            0].shape is not None else 1024
    diam = self.planes[0].pupil_diam if hasattr(self.planes[0],
                                                'pupil_diam') else 8

    inwave = poppy.Wavefront(wavelength=wavelength,
                             npix=npix,
                             diam=diam,
                             oversample=self.oversample)
    poppy.poppy_core._log.debug(
        "Creating input wavefront with wavelength=%f, npix=%d, pixel scale=%f meters/pixel"
        % (wavelength, npix, diam / npix))

    if np.abs(self.source_offset_r) > 0:
        offset_x = self.source_offset_r * -np.sin(
            self.source_offset_theta * np.pi /
            180)  # convert to offset X,Y in arcsec
        offset_y = self.source_offset_r * np.cos(
            self.source_offset_theta * np.pi /
            180)  # using the usual astronomical angle convention
        inwave.tilt(Xangle=offset_x, Yangle=offset_y)
        poppy.poppy_core._log.debug(
            "Tilted input wavefront by theta_X=%f, theta_Y=%f arcsec" %
            (offset_x, offset_y))
    return inwave
示例#8
0
def test_wfe_and_opd_have_consistent_signs():
    """ Verify that the sign and amplitude of wavefront error matches that of an optic's OPD,
    for cases that do NOT encounter phase wrapping.

    """

    for opd_amount in (100 * u.nm, -0.25 * u.micron, 1e-8 * u.m):
        constant_opd = poppy.ScalarOpticalPathDifference(opd=opd_amount)
        wave = poppy.Wavefront(wavelength=1 * u.micron, npix=64)

        wave *= constant_opd

        assert np.allclose(
            constant_opd.opd.to_value(u.m), wave.wfe.to_value(u.m)
        ), "optic OPD and wavefront WFE should have consistent signs"
示例#9
0
def test_inwave_fraunhofer(plot=False):
    '''Verify basic functionality of the inwave kwarg for a basic OpticalSystem()'''
    npix = 128
    oversample = 2
    diam = 2.4 * u.m
    lambda_m = 0.5e-6 * u.m
    # calculate the Fraunhofer diffraction pattern
    hst = poppy.OpticalSystem(pupil_diameter=diam,
                              npix=npix,
                              oversample=oversample)
    hst.add_pupil(poppy.CircularAperture(radius=diam.value / 2))
    hst.add_pupil(
        poppy.SecondaryObscuration(secondary_radius=0.396,
                                   support_width=0.0264,
                                   support_angle_offset=45.0))
    hst.add_image(
        poppy.ScalarTransmission(planetype=poppy_core.PlaneType.image,
                                 name='focus'))

    if plot:
        plt.figure(figsize=(9, 3))
    psf1, wfs1 = hst.calc_psf(wavelength=lambda_m,
                              display_intermediates=plot,
                              return_intermediates=True)

    # now test the system by inputting a wavefront first
    wfin = poppy.Wavefront(wavelength=lambda_m,
                           npix=npix,
                           diam=diam,
                           oversample=oversample)
    if plot:
        plt.figure(figsize=(9, 3))
    psf2, wfs2 = hst.calc_psf(wavelength=lambda_m,
                              display_intermediates=plot,
                              return_intermediates=True,
                              inwave=wfin)

    wf = wfs1[-1].wavefront
    wf_no_in = wfs2[-1].wavefront

    assert np.allclose(
        wf, wf_no_in
    ), 'Results differ unexpectedly when using inwave argument in OpticalSystem().'
示例#10
0
def test_wavefront_tilt_sign_and_direction(plot=False, npix=128):
    """ Test that tilt with increasing WFE towards the +X direction moves the PSF in the -X direction
    Fraunhofer propagation version

    See also test_core.test_source_offsets_in_OpticalSystem
    """
    # Create a wavefront and apply a tilt
    wave = poppy.Wavefront(diam=1 * u.m, npix=npix)
    wave *= poppy.CircularAperture(radius=0.5 * u.m)

    tilt_angle = -0.2  # must be a negative number (for -X direction shift), and within the FOV

    wave.tilt(
        Xangle=tilt_angle
    )  # for this function, the input is the desired direction for the image to tilt.
    # A shift to -X is implemented by creating an OPD that increases toward +X
    n = wave.shape[0]
    assert wave.wfe[n // 2, n // 2 -
                    5] < wave.wfe[n // 2, n // 2 +
                                  5], "Wavefront error should increase to +X"

    if plot:
        plt.suptitle("Wavefront tilt sign test (Fraunhofer propagation)",
                     fontweight='bold')
        wave.display(what='both')

    wave.propagate_to(poppy.Detector(pixelscale=0.05, fov_pixels=128))

    if plot:
        plt.figure()
        wave.display(what='both', crosshairs=True, imagecrop=2)

    n = wave.shape[0]
    cen = poppy.measure_centroid(wave.as_fits())
    assert np.allclose(cen[0], (n - 1) /
                       2), "Tilt in X should not displace the PSF in Y"
    assert cen[1] < (
        n - 1) / 2, "WFE tilt increasing to +X should displace the PSF to -X"
    assert np.allclose(((cen[1] - (n - 1) / 2) * u.pixel *
                        wave.pixelscale).to_value(u.arcsec),
                       tilt_angle), "PSF offset did not match expected amount"
示例#11
0
def test_wavefront_conversions():
    """ Test conversions between Wavefront and FresnelWavefront
    in both directions.
    """
    import poppy

    props = lambda wf: (wf.shape, wf.ispadded, wf.oversample, wf.pixelscale)

    optic = poppy.CircularAperture()
    w = poppy.Wavefront(diam=4 * u.m)
    w *= optic

    fw = poppy.FresnelWavefront(beam_radius=2 * u.m)
    fw *= optic

    # test convert from Fraunhofer to Fresnel
    fw2 = poppy.FresnelWavefront.from_wavefront(w)
    assert props(fw) == props(fw2)
    #np.testing.assert_allclose(fw.wavefront, fw2.wavefront)

    # test convert from Fresnel to Fraunhofer
    w2 = poppy.Wavefront.from_fresnel_wavefront(fw)
    assert props(w) == props(w2)
示例#12
0
文件: optics.py 项目: mirca/webbpsf
    def get_transmission(self, wave):
        """ Make array for the pupil obscuration appropriate to the grism
        """

        if isinstance(wave, poppy.Wavefront):
            wavelength = wave.wavelength
        else:
            wavelength = float(wave)
            wave = poppy.Wavefront(wavelength=wave)
        y, x = wave.coordinates()
        ang = np.deg2rad(self.pupil_rotation_angle)
        x = np.cos(ang) * x - np.sin(ang) * y
        y = np.sin(ang) * x + np.cos(ang) * y

        _log.debug("Rotating local pupil mask axes by {0} degrees".format(
            self.cylinder_rotation_angle))

        pupil_halfsize_m = self.pupil_size_mm / 2 / 1000 * self.pupil_demagnification
        pupilmask = np.ones_like(x)
        pupilmask[np.abs(x) > pupil_halfsize_m] = 0
        pupilmask[np.abs(y) > pupil_halfsize_m] = 0

        return pupilmask
示例#13
0
文件: optics.py 项目: mirca/webbpsf
    def get_opd(self, wave):
        """ Make an OPD array corresponding to the cylindrical weak lens
        used for defocusing the spectrum in the perpendicular-to-dispersion direction.
        """

        if isinstance(wave, poppy.Wavefront):
            wavelength = wave.wavelength
        else:
            wavelength = float(wave)
            wave = poppy.Wavefront(wavelength=wave)

        # compute indices in pixels, relative to center of plane, with rotation
        # units of these are meters
        y, x = wave.coordinates()

        ang = np.deg2rad(self.cylinder_rotation_angle)
        x = np.cos(ang) * x - np.sin(ang) * y
        y = np.sin(ang) * x + np.cos(ang) * y

        _log.debug(" Rotating local grism axes by {0} degrees".format(
            self.cylinder_rotation_angle))

        # From IDL code by David Lafreniere:
        #  ;the cylindrical defocus
        #x=(dindgen(pupdim)-pupdim/2)#replicate(1,pupdim)
        #y0=(rpuppix^2+sag[s]^2)/(2*sag[s])
        #wfe1=y0-sqrt(y0^2-x^2)
        #if sag[s] lt 1.e-5 then wfe1=0.d0

        # Here I will just translate that to Python exactly, making use of the
        # variables here:

        # rpuppix = radius of pupil in pixels
        #rpuppix = self.amplitude_header['DIAM'] / self.amplitude_header['PUPLSCAL'] / 2
        # Calculate the radius of curvature of the cylinder, bsaed on
        # the chord length and height

        # In this case we're assuming the cylinder is precisely as wide as the projected
        # telescope pupil. This doesn't seem guaranteed:
        #  * actual chord length across cylinder: 27.02 mm.
        #  * projected primary scale at NIRISS = ?

        _log.debug(
            " Computing GR700XD cylinder based on RoC: {0:.3g} meters".format(
                self.cylinder_radius))
        _log.debug(
            " Computing GR700XD cylinder based on pupil demagnification: {0:.3g} primary to grism"
            .format(self.pupil_demagnification))

        # Compute the overall sag of the cylinder lens at its outer edge. This is not actually used, it's
        # just for cross-check of the values
        # the sag will depend on half the pupil size since that's the offset from center to edge
        sag0 = np.sqrt(self.cylinder_radius**2 -
                       (self.prism_size / 2)**2) - self.cylinder_radius
        _log.debug(
            " Computed GR700XD cylinder sag at lens outer edge (for cross check only): {0:.3g} meters"
            .format(sag0))

        # now compute the spatially dependent sag of the cylinder, as projected onto the primary

        # what is the pupil scale at the *reimaged pupil* of the grism?
        pupil_scale_m_per_pix = 38.0255e-6  # Based on UdeM info in wfe_cylindricallens.pro
        #sag = np.sqrt(self.cylinder_radius**2 - (x*self.amplitude_header['PUPLSCAL']/self.pupil_demagnification)**2) - self.cylinder_radius
        sag = np.sqrt(self.cylinder_radius**2 -
                      (x /
                       self.pupil_demagnification)**2) - self.cylinder_radius
        #sag = self.cylinder_radius -  np.sqrt(self.cylinder_radius**2 - (x * pupil_scale_m_per_pix )**2 )

        # what we really want to do is take the physical properties of the as-built optic, and interpolate into that
        # to compute the OPD after remapping based on the pupil scale (and distortion?)
        #y0=(rpuppix**2+self.cylinder_sag**2)/(2*self.cylinder_sag)
        #wfe1=y0-np.sqrt(y0**2-x**2)

        _log.debug(
            " Cylinder P-V: {0:.4g} meters physical sag across full array".
            format(sag.max() - sag.min()))

        #fits.writeto('py_opd.fits', sag*(self.ZnSe_index(wavelength) -1), clobber=True)
        # remove piston offset
        #sag -= sag[wnz].min()   # normalize to 0 at the minimum
        #sag -= sag[wnz].mean()    # normalize around the mean
        sag[self.amplitude ==
            0] = 0  # no OPD in opaque regions (makes no difference in propagation but improves display)
        wnz = np.where(self.amplitude !=
                       0)  # this is just for display of the log messages:
        _log.debug(
            " Cylinder P-V: {0:.4g} meters physical sag across clear aperture".
            format(sag[wnz].max() - sag[wnz].min()))

        # scale for index of refraction
        index = self.ZnS_index(wavelength)
        opd = sag * (index - 1)
        _log.debug(
            " Scaling for ZnS index of refraction {0} at {1:.3g} microns".
            format(index, wavelength * 1e6))
        _log.debug(
            " Cylinder P-V: {0:.4g} meters optical sag at {1:.3g} microns across clear aperture"
            .format(opd[wnz].max() - opd[wnz].min(), wavelength * 1e6))
        return opd
示例#14
0
文件: optics.py 项目: mirca/webbpsf
    def __init__(
        self,
        name='GR700XD',
        which='Bach',
        #cylinder_radius=22.85,  cylinder_sag_mm=4.0, rotation_angle=92.25, rotate_mask=False, transmission=None,
        shift=None):
        # Initialize the base optical element with the pupil transmission and zero OPD

        if which == 'LLNL':
            raise NotImplementedError(
                "Rotated field mask for LLNL grism not yet implemented!")
        elif which == 'Bach':
            transmission = os.path.join(utils.get_webbpsf_data_path(),
                                        "NIRISS/optics/MASKGR700XD.fits.gz")
        else:
            raise NotImplementedError("Unknown grating name:" + which)

        self.shift = shift
        poppy.AnalyticOpticalElement.__init__(
            self, name=name, planetype=poppy.poppy_core._PUPIL)

        # UPDATED NUMBERS 2013-07:
        # See Document FGS_TFI_UdM_035_RevD

        _log.debug("Computing properties for {0} grism".format(which))
        if which == 'Bach':
            #---- Phase properties ---------------
            # 3.994 microns P-V over 27.02 mm measured (Loic's email)
            # This is **surface sag**, corresponding to P-V of 6.311 waves at lambda=632.8 nm.
            # should correspond to 3.698 microns over 26 mm clear aperture.
            self.prism_size = 0.02702  # 27.02 millimeters for the physical prism
            self.prism_clear_aperture = 0.0260  # 26 mm clear aperture for the prism + mount
            self.cylinder_rotation_angle = 2  # was 2.25

            #self.cylinder_radius = 22.85 # radius of curvature  ; Nominal
            # but they discarded that and used 25.3 instead
            # From Lafreniere's wfe_cylindricallens.pro:
            #  "OVERRIDE PREVIOUS CASES AFTER CV1RR RESULTS:"
            self.cylinder_radius = 25.3  # radius of curvature

            #---- Amplitude Transmission / Pupil shape ---------------
            self.pupil_size_mm = 26.0
            # Note that the IDL code says 26 mm is 683.75 pixels using the assumed demagnification
            self.pupil_rotation_angle = 2.0

        else:
            # 5.8 microns P-V over 32.15 mm (Loic's email)
            # should correspond to 4.38 microns over 28 mm clear aperture
            self.cylinder_radius = 22.39  # radius of curvature
            self.prism_size = 0.03215  # millimeters for the physical prism
            self.prism_clear_aperture = 0.0280  # clear aperture for the prism + mount
            self.cylinder_rotation_angle = 2.25

        # We need to know the magnification scale of the NIRISS reimaged pupil
        # in order to compute the curvature in the full-pupil units that POPPY uses
        # internally

        # pupil magnification computed from 22 mm clear aperture reported =
        # 857-169 pixels = 699 pixels in the 2D array which has scale =.00645604
        # = 4.44175 meters projected on the primary

        # 2014-05-21 but wait, that's actually 26 mm!
        # so the 699 pixels at 0.00645604 m/pixel = 4.512 meters implies the magnificationa 173 not 170
        # but, double wait, it's actually more like 687 pixels across rather than 699 so that makes it 170 again.

        # therefore the magnification is 0.1708 meters projected on the primary / mm in the NIRISS pupil
        #self.pupil_demagnification =  170.8367 # meters on the primary / meters in the NIRISS pupil
        #self.pupil_demagnification =  173.56 # meters on the primary / meters in the NIRISS pupil

        # Anand says:
        #  nominally the circumscribing circle at the PW of NIRISS is ~40mm.  I use 39mm for the nrm, but it's slightly field-dependent.  Compare that to the 6.6... PM circle?
        self.pupil_demagnification = 6.6 / 0.040  # about 165

        # perform an initial population of the OPD array for display etc.
        tmp = self.get_phasor(poppy.Wavefront(2e-6))