text: import numpy as np
  import astropy.units as u
  import poppy
  
  
  ####################################
  # Test sign conventions
  ####################################
  
  # Test: positive OPD in an optic produces positive WFE in a wave after interacting with that optic
  #       (in the absence of phase wrapping)
  
  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"
  
  
  # Test: Tilt with increasing WFE towards the +X direction moves the PSF in the -X direction
  
  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"
  
  def test_wavefront_tilt_sign_and_direction_fresnel(plot=False, npix=128):
      """ Test that tilt with increasing WFE towards the +X direction moves the PSF in the -X direction
      Fresnel propagation version
  
      See also test_core.test_source_offsets_in_OpticalSystem
      """
      # Create a wavefront and apply a tilt
      wave = poppy.FresnelWavefront(beam_radius=0.5 * u.m, npix=npix, oversample=8)
      wave *= poppy.CircularAperture(radius=0.5 * u.m)
  
      # tilt in arcseconds
      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 (Fresnel propagation)", fontweight='bold')
          wave.display(what='both')
  
      focal_length = 1 * u.m
      wave *= poppy.QuadraticLens(f_lens=focal_length)
  
      wave.propagate_fresnel(focal_length)
  
      if plot:
          plt.figure()
          wave.display(what='both', crosshairs=True, imagecrop=0.00001, scale='log')
  
      n = wave.shape[0]
      nominal_cen = n // 2  # In Fresnel mode, PSFs are centered on a pixel by default
      # (different from in Fraunhofer mode by half a pixel)
  
      cen = poppy.measure_centroid(wave.as_fits())
      assert np.allclose(cen[0], nominal_cen), "Tilt in X should not displace the PSF in Y"
      assert cen[1] < nominal_cen, "WFE tilt increasing to +X should displace the PSF to -X"
      assert np.allclose(((cen[1] - nominal_cen) * u.pixel * wave.pixelscale).to_value(u.m),
                         ((tilt_angle * u.arcsec).to_value( u.radian) * focal_length).to_value(u.m)), "PSF offset distance did not match expected amount"
  
  
  # Test: A positive lens has positive WFE
  def test_lens_wfe_sign(plot=False):
      """ Test that a positive lens has a positive WFE, and that
      the WFE profile matches the expectations for the parabola increasing toward the edges of the lens
      """
  
      # Setup lens
      nwaves = 1
      wavelength = 1*u.micron
      lens = poppy.ThinLens(name = "Positive lens, converging", nwaves=nwaves, radius=1)
  
      #Sample
      npix =33  # use odd number so it passes exactly through the origin
      lens_opd = lens.sample(what='opd', npix=npix, grid_size=2, wavelength=wavelength)
  
      # Compute expected WFE for comparison
      cen = (npix-1)//2
      x = (np.arange(npix)-cen) / (npix/2)
      y = nwaves*wavelength.to_value(u.m) * x**2
      y += lens_opd.min()
  
      assert np.allclose(lens_opd[lens_opd.shape[0]//2], y), "Lens WFE did not match expectations for a positive lens"
  
      if plot:
          plt.figure(figsize=(8,6))
          lens.display(what='both')
  
          plt.figure()
          plt.imshow(lens_opd, cmap=poppy.conf.cmap_diverging)
          plt.colorbar()
          plt.title("Lens WFE, sampled coarsely")
          plt.figure()
          plt.plot(lens_opd[lens_opd.shape[0]//2])
  
          plt.plot(y, ls='--')
          plt.axvline(cen, ls=":")
          plt.xlabel("Pixels")
          plt.ylabel("WFE [m]")
          plt.title("Cut along X axis")
  
  
  
  # Test: a negative weak lens produces images (before focus) that have consistent orientation with the exit pupil
  #       a positive weak lens produces images (after focus) that have the opposite orientation as the exit pupil
  
  # First, define some utility functions for checking which way each of the letter F's is oriented.
  def brighter_top_half(image):
      s = image.shape
      top = image[s[0]//2:].sum()
      bot = image[:s[0]//2].sum()
      return top > bot
  
  def brighter_left_half(image):
      return not brighter_top_half(image.transpose())
  
  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"
  
  def test_pupil_orientations_before_and_after_focus_fresnel(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.FresnelWavefront(beam_radius=1.5*u.m, oversample=2)
      wave0 *= poppy.LetterFAperture(radius = 1/np.sqrt(2)*u.m)
  
      focal_length = 1.0*u.m
      lens = poppy.QuadraticLens(f_lens=focal_length, name="Converging lens")
      wave0 *= lens
  
      wave1 = wave0.copy()
      wave2 = wave0.copy()
  
      fov = 0.00003
  
      wave1.propagate_fresnel(0.99999*focal_length)
  
      wave2.propagate_fresnel(1.00001*focal_length)
  
      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])
  
      assert brighter_top_half(wave0.intensity) and brighter_left_half(wave0.intensity), "Letter F should be brighter at top and left at pupil"
      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 "
      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 "
  
  # Test: Tilts of segments in a segmented DM have the expected effects
  def test_segment_tilt_sign_and_direction(display=False):
      hexdm = poppy.HexSegmentedDeformableMirror(flattoflat=0.5 * u.m, rings=1)
  
      osys2 = poppy.OpticalSystem(pupil_diameter=2 * u.m, npix=128, oversample=1)
      osys2.add_pupil(poppy.MultiHexagonAperture(flattoflat=0.5 * u.m, rings=1, center=True))
      osys2.add_pupil(hexdm)
      osys2.add_detector(0.10, fov_arcsec=10)
  
      psf_ref = osys2.calc_psf()  # reference, with no tilts
  
      hexdm.set_actuator(0, 0.2 * u.micron, 0, 0)  # piston
      hexdm.set_actuator(1, 0, 2 * u.arcsec, 0)  # tip
      hexdm.set_actuator(2, 0, 0, 1 * u.arcsec)  # tilt
  
      if display:
          import matplotlib.pyplot as plt
          hexdm.display(what='opd', colorbar_orientation='vertical', opd_vmax=2e-6)
          plt.figure(figsize=(14, 5))
          plt.suptitle("Segment tilt sign test (Fraunhofer propagation)", fontweight='bold')
  
      psf2 = osys2.calc_psf(display_intermediates=display)
  
      diff_psf = psf2[0].data - psf_ref[0].data
  
      if display:
          plt.figure()
          poppy.display_psf_difference(psf2, psf_ref)
  
      assert brighter_left_half(diff_psf), 'Tilting a segment with +X tilt WFE should move its PSF to -X'
      assert not brighter_top_half(diff_psf), 'Tilting a segment with +Y tilt WFE should move its PSF to -Y'
  