Пример #1
0
    def __init__(self, width, sample_spacing=None, samples=0):
        """Create a Pinhole instance.

        Parameters
        ----------
        width : `float`
            the width of the pinhole
        sample_spacing : `float`
            spacing of samples in the synthetic image
        samples : `int`
            number of samples per dimension in the synthetic image

        Notes
        -----
        Default of 0 samples allows quick creation for convolutions without
        generating the image; use samples > 0 for an actual image.

        """
        self.width = width

        # produce coordinate arrays
        if samples > 0:
            ext = samples / 2 * sample_spacing
            x, y = m.linspace(-ext, ext,
                              samples), m.linspace(-ext, ext, samples)
            xv, yv = m.meshgrid(x, y)
            w = width / 2
            # paint a circle on a black background
            arr = m.zeros((samples, samples))
            arr[m.sqrt(xv**2 + yv**2) < w] = 1
        else:
            arr, x, y = None, None, None

        super().__init__(data=arr, unit_x=x, unit_y=y, has_analytic_ft=True)
Пример #2
0
def rotated_ellipse(width_major, width_minor, major_axis_angle=0, samples=128):
    """Generate a binary mask for an ellipse, centered at the origin.

    The major axis will notionally extend to the limits of the array, but this
    will not be the case for rotated cases.

    Parameters
    ----------
    width_major : `float`
        width of the ellipse in its major axis
    width_minor : `float`
        width of the ellipse in its minor axis
    major_axis_angle : `float`
        angle of the major axis w.r.t. the x axis, degrees
    samples : `int`
        number of samples

    Returns
    -------
    `numpy.ndarray`
        An ndarray of shape (samples,samples) of value 0 outside the ellipse,
        and value 1 inside the ellipse

    Notes
    -----
    The formula applied is:
         ((x-h)cos(A)+(y-k)sin(A))^2      ((x-h)sin(A)+(y-k)cos(A))^2
        ______________________________ + ______________________________ 1
                     a^2                               b^2
    where x and y are the x and y dimensions, A is the rotation angle of the
    major axis, h and k are the centers of the the ellipse, and a and b are
    the major and minor axis widths.  In this implementation, h=k=0 and the
    formula simplifies to:
            (x*cos(A)+y*sin(A))^2             (x*sin(A)+y*cos(A))^2
        ______________________________ + ______________________________ 1
                     a^2                               b^2

    see SO:
    https://math.stackexchange.com/questions/426150/what-is-the-general-equation-of-the-ellipse-that-is-not-in-the-origin-and-rotate

    Raises
    ------
    ValueError
        Description

    """
    if width_minor > width_major:
        raise ValueError(
            'By definition, major axis must be larger than minor.')

    arr = m.ones((samples, samples))
    lim = width_major
    x, y = m.linspace(-lim, lim, samples), m.linspace(-lim, lim, samples)
    xv, yv = m.meshgrid(x, y)
    A = m.radians(-major_axis_angle)
    a, b = width_major, width_minor
    major_axis_term = ((xv * m.cos(A) + yv * m.sin(A))**2) / a**2
    minor_axis_term = ((xv * m.sin(A) - yv * m.cos(A))**2) / b**2
    arr[major_axis_term + minor_axis_term > 1] = 0
    return arr
Пример #3
0
    def __init__(self, fno, wavelength, extent=None, samples=None):
        """Create a new AiryDisk.

        Parameters
        ----------
        fno : `float`
            F/# associated with the PSF
        wavelength : `float`
            wavelength of light, in microns
        extent : `float`
            cartesian window half-width, e.g. 10 will make an RoI 20x20 microns wide
        samples : `int`
            number of samples across full width

        """
        if samples is not None:
            x = m.linspace(-extent, extent, samples)
            y = m.linspace(-extent, extent, samples)
            xx, yy = m.meshgrid(x, y)
            rho, phi = cart_to_polar(xx, yy)
            data = airydisk(rho, fno, wavelength)
        else:
            x, y, data = None, None, None
        self.fno = fno
        self.wavelength = wavelength
        super().__init__(data, x, y)
        self.has_analytic_ft = True
Пример #4
0
    def __init__(self,
                 spokes,
                 sinusoidal=True,
                 background='black',
                 sample_spacing=2,
                 samples=256):
        """Produces a Siemen's Star.

        Parameters
        ----------
        spokes : `int`
            number of spokes in the star.
        sinusoidal : `bool`
            if True, generates a sinusoidal Siemen' star, else, generates a bar/block siemen's star
        background : 'string', {'black', 'white'}
            background color
        sample_spacing : `float`
            spacing of samples, in microns
        samples : `int`
            number of samples per dimension in the synthetic image

        Raises
        ------
        ValueError
            background other than black or white

        """
        relative_width = 0.9
        self.spokes = spokes

        # generate a coordinate grid
        x = m.linspace(-1, 1, samples)
        y = m.linspace(-1, 1, samples)
        xx, yy = m.meshgrid(x, y)
        rv, pv = cart_to_polar(xx, yy)
        ext = sample_spacing * samples / 2
        ux, uy = m.arange(-ext, ext,
                          sample_spacing), m.arange(-ext, ext, sample_spacing)

        # generate the siemen's star as a (rho,phi) polynomial
        arr = m.cos(spokes / 2 * pv)

        if not sinusoidal:  # make binary
            arr[arr < 0] = -1
            arr[arr > 0] = 1

        # scale to (0,1) and clip into a disk
        arr = (arr + 1) / 2
        if background.lower() in ('b', 'black'):
            arr[rv > relative_width] = 0
        elif background.lower() in ('w', 'white'):
            arr[rv > relative_width] = 1
        else:
            raise ValueError('invalid background color')

        super().__init__(data=arr, unit_x=ux, unit_y=uy, has_analytic_ft=False)
Пример #5
0
    def __init__(self,
                 width,
                 orientation='Vertical',
                 sample_spacing=None,
                 samples=0):
        """Create a new Slit instance.

        Parameters
        ----------
        width : `float`
            the width of the slit in microns
        orientation : `string`, {'Horizontal', 'Vertical', 'Crossed', 'Both'}
            the orientation of the slit; Crossed and Both produce the same results
        sample_spacing : `float`
            spacing of samples in the synthetic image
        samples : `int`
            number of samples per dimension in the synthetic image

        Notes
        -----
        Default of 0 samples allows quick creation for convolutions without
        generating the image; use samples > 0 for an actual image.

        """
        w = width / 2

        if samples > 0:
            ext = samples / 2 * sample_spacing
            x, y = m.linspace(-ext, ext,
                              samples), m.linspace(-ext, ext, samples)
            arr = m.zeros((samples, samples))
        else:
            arr, x, y = None, None, None

        # paint in the slit
        if orientation.lower() in ('v', 'vert', 'vertical'):
            if samples > 0:
                arr[:, abs(x) < w] = 1
            self.orientation = 'Vertical'
            self.width_x = width
            self.width_y = 0
        elif orientation.lower() in ('h', 'horiz', 'horizontal'):
            if samples > 0:
                arr[abs(y) < w, :] = 1
            self.width_x = 0
            self.width_y = width
            self.orientation = 'Horizontal'
        elif orientation.lower() in ('b', 'both', 'c', 'crossed'):
            if samples > 0:
                arr[abs(y) < w, :] = 1
                arr[:, abs(x) < w] = 1
            self.orientation = 'Crossed'
            self.width_x, self.width_y = width, width

        super().__init__(arr, x, y, has_analytic_ft=True)
Пример #6
0
def fit_sphere(z):
    x, y = m.linspace(-1, 1, z.shape[1]), m.linspace(-1, 1, z.shape[0])
    xx, yy = m.meshgrid(x, y)
    pts = m.isfinite(z)
    xx_, yy_ = xx[pts].flatten(), yy[pts].flatten()
    rho, phi = cart_to_polar(xx_, yy_)
    focus = defocus(rho, phi)

    coefs = m.lstsq(m.stack([focus, m.ones(focus.shape)]).T, z[pts].flatten(), rcond=None)[0]
    rho, phi = cart_to_polar(xx, yy)
    sphere = defocus(rho, phi) * coefs[0]
    return sphere
Пример #7
0
def _compute_output_grid(convolvable1, convolvable2):
    """Calculate the output grid to be used when two convolvables are on different grids."""
    # determine output spacing
    errorstr = 'when convolving two analytic objects, one must have a grid.'
    if m.isnan(convolvable1.sample_spacing):
        if m.isnan(convolvable2.sample_spacing):
            raise ValueError(errorstr)
        else:
            output_spacing = convolvable2.sample_spacing
    elif m.isnan(convolvable2.sample_spacing):
        if m.isnan(convolvable1.sample_spacing):
            raise ValueError(errorstr)
        else:
            output_spacing = convolvable1.sample_spacing
    else:
        output_spacing = min(convolvable1.sample_spacing,
                             convolvable2.sample_spacing)

    # determine region of output
    if convolvable1.unit_x is None:
        c1ux, c1uy = (0, 0), (0, 0)
    else:
        c1ux, c1uy = convolvable1.unit_x, convolvable1.unit_y

    if convolvable2.unit_x is None:  # this isn't totally DRY
        c2ux, c2uy = (0, 0), (0, 0)
    else:
        c2ux, c2uy = convolvable2.unit_x, convolvable2.unit_y

    output_x_left = min(c1ux[0], c2ux[0])
    output_x_right = max(c1ux[-1], c2ux[-1])
    output_y_left = min(c1uy[0], c2uy[0])
    output_y_right = max(c1uy[-1], c2uy[-1])

    # if region is not an integer multiple of sample spacings, enlarge to make this true
    x_rem = (output_x_right - output_x_left) % output_spacing
    y_rem = (output_y_right - output_y_left) % output_spacing
    if x_rem > 1e-3:
        adj = x_rem / 2
        output_x_left -= adj
        output_x_right += adj
    if y_rem > 1e-3:
        adj = y_rem / 2
        output_y_left -= adj
        output_y_right += adj

    # finally, compute the output window
    samples_x = int((output_x_right - output_x_left) // output_spacing)
    samples_y = int((output_y_right - output_y_left) // output_spacing)
    unit_out_x = m.linspace(output_x_left, output_x_right, samples_x)
    unit_out_y = m.linspace(output_y_left, output_y_right, samples_y)
    return unit_out_x, unit_out_y
Пример #8
0
    def __init__(self,
                 width_x,
                 width_y=None,
                 sample_spacing=0.1,
                 samples_x=384,
                 samples_y=None):
        """Create a new OLPF object.

        Parameters
        ----------
        width_x : `float`
            blur width in the x direction, microns
        width_y : `float`
            blur width in the y direction, microns
        sample_spacing : `float`, optional
            center to center spacing of samples
        samples_x : `int`, optional
            number of samples along x axis
        samples_y : `int`, optional
            number of samples along y axis; duplicates x if None

        """
        # compute relevant spacings
        if width_y is None:
            width_y = width_x
        if samples_y is None:
            samples_y = samples_x

        self.width_x = width_x
        self.width_y = width_y

        if samples_x is None:  # do no math
            data, ux, uy = None, m.zeros(2), m.zeros(2)
        else:
            space_x = width_x / 2
            space_y = width_y / 2
            shift_x = int(space_x // sample_spacing)
            shift_y = int(space_y // sample_spacing)
            center_x = samples_x // 2
            center_y = samples_y // 2

            data = m.zeros((samples_x, samples_y))

            data[center_y - shift_y, center_x - shift_x] = 1
            data[center_y - shift_y, center_x + shift_x] = 1
            data[center_y + shift_y, center_x - shift_x] = 1
            data[center_y + shift_y, center_x + shift_x] = 1
            ux = m.linspace(-space_x, space_x, samples_x)
            uy = m.linspace(-space_y, space_y, samples_y)
        super().__init__(data=data, unit_x=ux, unit_y=uy, has_analytic_ft=True)
Пример #9
0
    def __init__(self,
                 angle=4,
                 contrast=0.9,
                 crossed=False,
                 sample_spacing=2,
                 samples=256):
        """Create a new TitledSquare instance.

        Parameters
        ----------
        angle : `float`
            angle in degrees to tilt w.r.t. the y axis
        contrast : `float`
            difference between minimum and maximum values in the image
        crossed : `bool`, optional
            whether to make a single edge (crossed=False) or pair of crossed edges (crossed=True)
            aka a "BMW target"
        sample_spacing : `float`
            spacing of samples
        samples : `int`
            number of samples

        """
        diff = (1 - contrast) / 2
        arr = m.full((samples, samples), 1 - diff)
        x = m.linspace(-0.5, 0.5, samples)
        y = m.linspace(-0.5, 0.5, samples)
        xx, yy = m.meshgrid(x, y)
        sf = samples * sample_spacing

        angle = m.radians(angle)
        xp = xx * m.cos(angle) - yy * m.sin(angle)
        # yp = xx * m.sin(angle) + yy * m.cos(angle)  # do not need this
        mask = xp > 0  # single edge
        if crossed:
            mask = xp > 0  # set of 4 edges
            upperright = mask & m.rot90(mask)
            lowerleft = m.rot90(upperright, 2)
            mask = upperright | lowerleft

        arr[mask] = diff
        self.contrast = contrast
        self.black = diff
        self.white = 1 - diff
        super().__init__(data=arr,
                         unit_x=x * sf,
                         unit_y=y * sf,
                         has_analytic_ft=False)
Пример #10
0
    def _make_mtf_vs_field_vs_focus(self, num_fields, focus_range, num_focus,
                                    freqs):
        ''' TODO: docstring
        '''
        self._uniformly_spaced_fields(num_fields)
        net_mtfs = [None] * num_fields
        for idx in range(num_fields):
            focus, net_mtfs[idx] = self._make_mtf_thrufocus(
                idx, focus_range, num_focus)

        fields = (self.fields[-1] * self.fov_y) * m.linspace(0, 1, num_fields)
        t_cube = m.empty((num_focus, num_fields, len(freqs)))
        s_cube = m.empty((num_focus, num_fields, len(freqs)))
        for idx, mtfs in enumerate(net_mtfs):
            for idx2, submtf in enumerate(mtfs):
                t = submtf.exact_polar(freqs, 0)
                s = submtf.exact_polar(freqs, 90)
                t_cube[idx2, idx, :] = t
                s_cube[idx2, idx, :] = s

        TCube = MTFvFvF(data=t_cube,
                        focus=focus,
                        field=fields,
                        freq=freqs,
                        azimuth='Tan')
        SCube = MTFvFvF(data=s_cube,
                        focus=focus,
                        field=fields,
                        freq=freqs,
                        azimuth='Sag')
        return TCube, SCube
Пример #11
0
def generate_mtf(pixel_aperture=1, azimuth=0, num_samples=128):
    """Generate the 1D diffraction-limited MTF for a given pixel width and azimuth.

    Parameters
    ----------
    pixel_aperture : `float`
        aperture of the pixel, microns.  Pixel is assumed to be square
    azimuth : `float`
        azimuth to retrieve the MTF at, in degrees
    num_samples : `int`
        number of samples in the output array

    Returns
    -------
    frequencies : `numpy.ndarray`
        unit axis, cy/mm
    mtf : `numpy.ndarray`
        MTF values (rel. 1.0).

    Notes
    -----
    Azimuth is not actually implemented yet.

    """
    pitch_unit = pixel_aperture / 1e3
    normalized_frequencies = m.linspace(0, 2, num_samples)
    otf = m.sinc(normalized_frequencies)
    mtf = abs(otf)
    return normalized_frequencies / pitch_unit, mtf
Пример #12
0
    def _make_mtf_thrufocus(self, field_index, focus_range, num_pts):
        ''' Makes a list of MTF objects corresponding to different focus shifts
            for the lens.  Focusrange will be applied symmetrically.

        Args:
            field_index: (`int`): index of the desired field in the self.fields
                iterable.

            focus_range: (`float`): focus range, in microns.

            num_pts (`int`): number of points to compute MTF at.  Note that for
                and even number of points, the zero defocus point will not be
                sampled.

        Returns:
            list of `MTF` objects.

        '''
        # todo: parallelize
        focus_shifts = m.linspace(-focus_range, focus_range, num_pts)
        defocus_wvs = image_displacement_to_defocus(focus_shifts, self.fno,
                                                    self.wavelength)

        mtfs = []
        pupil = self._make_pupil(field_index)
        for defocus in defocus_wvs:
            defocus_p = Seidel(W020=defocus,
                               epd=self.epd,
                               samples=self.samples,
                               wavelength=self.wavelength)
            psf = PSF.from_pupil(pupil.merge(defocus_p), self.efl)
            mtfs.append(MTF.from_psf(psf))
        return focus_shifts, mtfs
Пример #13
0
    def __init__(self,
                 width_x,
                 width_y=None,
                 sample_spacing=0,
                 samples_x=None,
                 samples_y=None):
        """Create a new `PixelAperture` object.

        Parameters
        ----------
        width_x : `float`
            width of the aperture in the x dimension, in microns.
        width_y : `float`, optional
            siez of the aperture in the y dimension, in microns
        sample_spacing : `float`, optional
            spacing of samples, in microns
        samples_x : `int`, optional
            number of samples in the x dimension
        samples_y : `int`, optional
            number of samples in the y dimension

        """
        if width_y is None:
            width_y = width_x
        if samples_y is None:
            samples_y = samples_x

        self.width_x = width_x
        self.width_y = width_y

        if samples_x is None:  # do no math
            data, ux, uy = None, m.zeros(2), m.zeros(2)
        else:  # build PixelAperture model
            center_x = samples_x // 2
            center_y = samples_y // 2
            half_width = width_x / 2
            half_height = width_y / 2
            steps_x = int(half_width // sample_spacing)
            steps_y = int(half_height // sample_spacing)

            data = m.zeros((samples_x, samples_y))
            data[center_y - steps_y:center_y + steps_y,
                 center_x - steps_x:center_x + steps_x] = 1
            extx, exty = samples_x // 2 * sample_spacing, samples_y // 2 * sample_spacing
            ux, uy = m.linspace(-extx, extx,
                                samples_x), m.linspace(-exty, exty, samples_y)
        super().__init__(data=data, unit_x=ux, unit_y=uy, has_analytic_ft=True)
Пример #14
0
def _compute_output_grid(convolvable1, convolvable2):
    # determine output spacing
    if convolvable1.sample_spacing < convolvable2.sample_spacing:
        output_spacing = convolvable1.sample_spacing
    else:
        output_spacing = convolvable2.sample_spacing

    # determine region of output
    if convolvable1.unit_x[0] < convolvable2.unit_x[0]:
        output_x_left = convolvable1.unit_x[0]
    else:
        output_x_left = convolvable2.unit_x[0]

    if convolvable1.unit_x[-1] > convolvable2.unit_x[-1]:
        output_x_right = convolvable1.unit_x[-1]
    else:
        output_x_right = convolvable2.unit_x[-1]

    if convolvable1.unit_y[0] < convolvable2.unit_y[0]:
        output_y_left = convolvable1.unit_y[0]
    else:
        output_y_left = convolvable2.unit_y[0]

    if convolvable1.unit_y[-1] > convolvable2.unit_y[-1]:
        output_y_right = convolvable1.unit_y[-1]
    else:
        output_y_right = convolvable2.unit_y[-1]

    # if region is not an integer multiple of sample spacings, enlarge to make this true
    x_rem = (output_x_right - output_x_left) % output_spacing
    y_rem = (output_y_right - output_y_left) % output_spacing
    if x_rem > 1e-3:
        adj = x_rem / 2
        output_x_left -= adj
        output_x_right += adj
    if y_rem > 1e-3:
        adj = y_rem / 2
        output_y_left -= adj
        output_y_right += adj

    # finally, compute the output window
    samples_x = (output_x_right - output_x_left) // output_spacing
    samples_y = (output_y_right - output_y_left) // output_spacing
    unit_out_x = m.linspace(output_x_left, output_x_right, samples_x)
    unit_out_y = m.linspace(output_y_left, output_y_right, samples_y)
    return unit_out_x, unit_out_y
Пример #15
0
    def __init__(self,
                 angle=4,
                 background='white',
                 sample_spacing=2,
                 samples=256):
        """Create a new TitledSquare instance.

        Parameters
        ----------
        angle : `float`
            angle in degrees to tilt w.r.t. the x axis
        background : `string`
            white or black; the square will be the opposite color of the background
        sample_spacing : `float`
            spacing of samples
        samples : `int`
            number of samples

        """
        radius = 0.3
        if background.lower() == 'white':
            arr = m.ones((samples, samples))
            fill_with = 0
        else:
            arr = m.zeros((samples, samples))
            fill_with = 1

        # TODO: optimize by working with index numbers directly and avoid
        # creation of X,Y arrays for performance.
        x = m.linspace(-0.5, 0.5, samples)
        y = m.linspace(-0.5, 0.5, samples)
        xx, yy = m.meshgrid(x, y)
        sf = samples * sample_spacing

        # TODO: convert inline operation to use of rotation matrix
        angle = m.radians(angle)
        xp = xx * m.cos(angle) - yy * m.sin(angle)
        yp = xx * m.sin(angle) + yy * m.cos(angle)
        mask = (abs(xp) < radius) * (abs(yp) < radius)
        arr[mask] = fill_with
        super().__init__(data=arr,
                         unit_x=x * sf,
                         unit_y=y * sf,
                         has_analytic_ft=False)
Пример #16
0
def fit(data, num_terms=16, rms_norm=False, round_at=6):
    ''' Fits a number of Zernike coefficients to provided data by minimizing
        the root sum square between each coefficient and the given data.  The
        data should be uniformly sampled in an x,y grid.

    Args:

        data (`numpy.ndarray`): data to fit to.

        num_terms (`int`): number of terms to fit, fits terms 0~num_terms.

        rms_norm (`bool`): if true, normalize coefficients to unit RMS value.

        round_at (`int`): decimal place to round values at.

    Returns:
        numpy.ndarray: an array of coefficients matching the input data.

    '''
    if num_terms > len(zernmap):
        raise ValueError(f'number of terms must be less than {len(zernmap)}')

    # precompute the valid indexes in the original data
    pts = m.isfinite(data)

    # set up an x/y rho/phi grid to evaluate Zernikes on
    x, y = m.linspace(-1, 1, data.shape[1]), m.linspace(-1, 1, data.shape[0])
    xx, yy = m.meshgrid(x, y)
    rho = m.sqrt(xx**2 + yy**2)[pts].flatten()
    phi = m.arctan2(xx, yy)[pts].flatten()

    # compute each Zernike term
    zernikes = []
    for i in range(num_terms):
        func = z.zernikes[zernmap[i]]
        base_zern = func(rho, phi)
        if rms_norm:
            base_zern *= func.norm
        zernikes.append(base_zern)
    zerns = m.asarray(zernikes).T

    # use least squares to compute the coefficients
    coefs = m.lstsq(zerns, data[pts].flatten(), rcond=None)[0]
    return coefs.round(round_at)
Пример #17
0
def uniform_cart_to_polar(x, y, data):
    """Interpolate data uniformly sampled in cartesian coordinates to polar coordinates.

    Parameters
    ----------
    x : `numpy.ndarray`
        sorted 1D array of x sample pts
    y : `numpy.ndarray`
        sorted 1D array of y sample pts
    data : `numpy.ndarray`
        data sampled over the (x,y) coordinates

    Returns
    -------
    rho : `numpy.ndarray`
        samples for interpolated values
    phi : `numpy.ndarray`
        samples for interpolated values
    f(rho,phi) : `numpy.ndarray`
        data uniformly sampled in (rho,phi)

    """
    # create a set of polar coordinates to interpolate onto
    xmin, xmax = min(x), max(x)
    ymin, ymax = min(y), max(y)

    _max = max(abs(m.asarray([xmin, xmax, ymin, ymax])))

    rho = m.linspace(0, _max, len(x))
    phi = m.linspace(0, 2 * m.pi, len(y))
    rv, pv = m.meshgrid(rho, phi)

    # map points to x, y and make a grid for the original samples
    xv, yv = polar_to_cart(rv, pv)

    # interpolate the function onto the new points
    f = interpolate.RegularGridInterpolator((y, x),
                                            data,
                                            bounds_error=False,
                                            fill_value=0)
    return rho, phi, f((yv, xv), method='linear')
Пример #18
0
    def plot_mtf_vs_field(self,
                          num_pts,
                          freqs=[10, 20, 30, 40, 50],
                          title='MTF vs Field',
                          minorgrid=True,
                          fig=None,
                          ax=None):
        ''' Generates a plot of the MTF vs Field for the lens.

        Args:
            num_pts (`int`): number of field points to evaluate.

            freqs (`iterable`): frequencies to evaluate the MTF at.

            fig (`matplotlib.pyplot.figure`): figure to plot inside.

            ax (`matplotlib.pyplot.axis`): axis to plot ini.

        Return:
            `tuple` containing:

                `matplotlib.pyplot.figure` figure containing the plot.

                `matplotlib.pyplot.axis` axis containing the plot.

        '''
        data_s, data_t = self.mtf_vs_field(num_pts, freqs)
        flds_abs = m.linspace(0, self.fov_y, num_pts)
        fig, ax = share_fig_ax(fig, ax)
        for i in range(len(freqs)):
            ln, = ax.plot(flds_abs, data_s[:, i], lw=3, ls='--')
            ax.plot(flds_abs,
                    data_t[:, i],
                    lw=3,
                    color=ln.get_color(),
                    label=f'{freqs[i]}lp/mm')

        ax.plot(0, 0, color='k', ls='--', label='Sag')
        ax.plot(0, 0, color='k', label='Tan')
        # todo: respect units of `self`

        if minorgrid is True:
            ax.set_yticks([0.1, 0.3, 0.5, 0.7, 0.9], minor=True)
            ax.grid(True, which='minor')

        ax.set(xlim=(0, self.fov_y),
               xlabel='Image Height [mm]',
               ylim=(0, 1),
               ylabel='MTF [Rel. 1.0]',
               title=title)
        ax.legend()

        return fig, ax
Пример #19
0
    def plot_encircled_energy(self,
                              axlim=None,
                              npts=50,
                              lw=config.lw,
                              zorder=config.zorder,
                              fig=None,
                              ax=None):
        """Make a 1D plot of the encircled energy at the given azimuth.

        Parameters
        ----------
        azimuth : `float`
            azimuth to plot at, in degrees
        axlim : `float`
            limits of axis, will plot [0, axlim]
        npts : `int`, optional
            number of points to use from [0, axlim]
        lw : `float`, optional
            line width
        zorder : `int` optional
            zorder
        fig : `matplotlib.figure.Figure`, optional
            Figure containing the plot
        ax : `matplotlib.axes.Axis`, optional:
            Axis containing the plot

        Returns
        -------
        fig : `matplotlib.figure.Figure`, optional
            Figure containing the plot
        ax : `matplotlib.axes.Axis`, optional:
            Axis containing the plot

        """
        if axlim is None:
            if len(self._ee) is not 0:
                xx, yy = sort_xy(self._ee.keys(), self._ee.values())
            else:
                raise ValueError(
                    'if no values for encircled energy have been computed, axlim must be provided'
                )
        elif axlim is 0:
            raise ValueError('computing from 0 to 0 is stupid')
        else:
            xx = m.linspace(1e-5, axlim, npts)
            yy = self.encircled_energy(xx)

        fig, ax = share_fig_ax(fig, ax)
        ax.plot(xx, yy, lw=lw, zorder=zorder)
        ax.set(xlabel='Image Plane Distance [μm]',
               ylabel='Encircled Energy [Rel 1.0]',
               xlim=(0, axlim))
        return fig, ax
Пример #20
0
def make_xy_grid(samples_x, samples_y=None):
    """Create an x, y grid from -1, 1 with n number of samples.

    Parameters
    ----------
    samples_x : `int`
        number of samples in x direction
    samples_y : `int`
        number of samples in y direction, if None, copied from sample_x

    Returns
    -------
    xx : `numpy.ndarray`
        x meshgrid
    yy : `numpy.ndarray`
        y meshgrid

    """
    if samples_y is None:
        samples_y = samples_x
    x = m.linspace(-1, 1, samples_x, dtype=config.precision)
    y = m.linspace(-1, 1, samples_y, dtype=config.precision)
    xx, yy = m.meshgrid(x, y)
    return xx, yy
Пример #21
0
def diffraction_limited_mtf(fno, wavelength, frequencies=None, samples=128):
    """Give the diffraction limited MTF for a circular pupil and the given parameters.

    Parameters
    ----------
    fno : `float`
        f/# of the lens.
    wavelength : `float`
        wavelength of light, in microns.
    frequencies : `numpy.ndarray`
        spatial frequencies of interest, in cy/mm if frequencies are given, samples is ignored.
    samples : `int`
        number of points in the output array, if frequencies not given.

    Returns
    -------
    if frequencies not given:
        frequencies : `numpy.ndarray`
            array of ordinate data
        mtf : `numpy.ndarray`
            array of coordinate data
    else:
        mtf : `numpy.ndarray`
            array of MTF data

    Notes
    -----
    If frequencies are given, just returns the MTF.  If frequencies are not
    given, returns both the frequencies and the MTF.

    """
    extinction = 1 / (wavelength / 1000 * fno)
    if frequencies is None:
        normalized_frequency = m.linspace(0, 1, samples)
    else:
        normalized_frequency = m.asarray(frequencies) / extinction
        try:
            normalized_frequency[normalized_frequency > 1] = 1  # clamp values
        except TypeError:  # single freq
            if normalized_frequency > 1:
                normalized_frequency = 1

    mtf = _difflim_mtf_core(normalized_frequency)

    if frequencies is None:
        return normalized_frequency * extinction, mtf
    else:
        return mtf
Пример #22
0
def thrufocus_mtf_from_wavefront(focused_wavefront, sim_params):
    """Create a thru-focus T/S MTF curve at each frequency requested from a focused wavefront.

    Parameters
    ----------
    focused_wavefront : `Pupil`
        a pupil object

    sim_params : `SimulationConfig`
        a SimulationConfig namedtuple

    Returns
    -------
    `pandas.DataFrame`
        dataframe of data

    Notes
    -----
    see macros.DEFAULT_SIM_PARAMS for an example config

    """
    import pandas as pd
    s = sim_params
    focusdiv_wvs = m.linspace(-1*s.focus_range_waves, s.focus_range_waves, s.focus_planes)
    focusdiv_um = defocus_to_image_displacement(focusdiv_wvs, s.fno, s.wvl, s.focus_zernike, s.focus_normed)
    dfs = []
    for focus, displacement in zip(focusdiv_wvs, focusdiv_um):
        if s.focus_zernike:
            defocus = FringeZernike(base=1, Z4=focus, rms_norm=s.focus_normed, samples=s.samples,
                                    epd=s.efl / s.fno,
                                    wavelength=s.wvl,
                                    mask=s.mask,
                                    mask_target=s.mask_target)
        else:
            defocus = Seidel(W020=focus,
                             epd=s.efl / s.fno,
                             samples=s.samples,
                             wavelength=s.wvl,
                             mask=s.mask,
                             mask_target=s.mask_target)
        mtf = MTF.from_pupil(focused_wavefront + defocus, efl=s.efl)
        tan, sag = mtf_ts_extractor(mtf, s.freqs)
        dfs.append(mtf_ts_to_dataframe(tan, sag, s.freqs, focus=displacement))
    return pd.concat(dfs)
Пример #23
0
def thrufocus_mtf_from_wavefront_array(focused_wavefront, sim_params):
    """Create a thru-focus T/S MTF curve at each frequency requested from a focused wavefront.

    TODO: refactor

    Parameters
    ----------
    focused_wavefront : `Pupil`
        a pupil object

    sim_params : `SimulationConfig`
        a SimulationConfig namedtuple

    Returns
    -------
    `pandas.DataFrame`
        dataframe of data

    Notes
    -----
    see marcros.DEFAULT_SIM_PARAMS for an example config.

    """
    s = sim_params
    focusdiv_wvs = m.linspace(-s.focus_range_waves, s.focus_range_waves, s.focus_planes)
    tt, ss = m.empty((s.focus_planes, len(s.freqs))), m.empty((s.focus_planes, len(s.freqs)))
    for idx, focus in enumerate(focusdiv_wvs):
        if s.focus_zernike:
            defocus = FringeZernike(base=1, Z4=focus, rms_norm=s.focus_normed, samples=s.samples,
                                    epd=s.efl / s.fno,
                                    wavelength=s.wvl)
        else:
            defocus = Seidel(W020=focus,
                             epd=s.efl / s.fno,
                             samples=s.samples,
                             wavelength=s.wvl)
        mtf = MTF.from_pupil(focused_wavefront + defocus, efl=s.efl)
        tan, sag = mtf_ts_extractor(mtf, s.freqs)
        tt[idx, :] = tan
        ss[idx, :] = sag

    return tt, ss
Пример #24
0
def inverted_circle(samples=128, radius=1):
    """ Create an inverted circular mask (obscuration).

    Parameters
    ----------
    samples : `int`, optional
        number of samples in the square output array

    Returns
    ------
    `numpy.ndarray`
        binary ndarray representation of the mask

    """
    if radius is 0:
        return m.ones((samples, samples))
    else:
        x = m.linspace(-1, 1, samples)
        y = x
        xx, yy = m.meshgrid(x, y)
        rho, phi = cart_to_polar(xx, yy)
        mask = m.ones(rho.shape)
        mask[rho < radius] = 0
        return mask
Пример #25
0
def radial_mtf_to_mtfffd_data(tan, sag, imagehts, azimuths, upsample):
    """Take radial MTF data and map it to inputs to the MTFFFD constructor.

    Performs upsampling/interpolation in cartesian coordinates

    Parameters
    ----------
    tan : `np.ndarray`
        tangential data
    sag : `np.ndarray`
        sagittal data
    imagehts : `np.ndarray`
        array of image heights
    azimuths : iterable
        azimuths corresponding to the first dimension of the tan/sag arrays
    upsample : `float`
        upsampling factor

    Returns
    -------
    out_x : `np.ndarray`
        x coordinates of the output data
    out_y : `np.ndarray`
        y coordinates of the output data
    tan : `np.ndarray`
        tangential data
    sag : `np.ndarray`
        sagittal data

    """
    azimuths = m.asarray(azimuths)
    imagehts = m.asarray(imagehts)

    if imagehts[0] > imagehts[-1]:
        # distortion profiled, values "reversed"
        # just flip imagehts, since spacing matters and not exact values
        imagehts = imagehts[::-1]
    amin, amax = min(azimuths), max(azimuths)
    imin, imax = min(imagehts), max(imagehts)
    aq = m.linspace(amin, amax, int(len(azimuths) * upsample))
    iq = m.linspace(imin, imax, int(
        len(imagehts) * 4))  # hard-code 4x linear upsample, change later
    aa, ii = m.meshgrid(aq, iq, indexing='ij')

    # for each frequency, build an interpolating function and upsample
    up_t = m.empty((len(aq), tan.shape[1], len(iq)))
    up_s = m.empty((len(aq), sag.shape[1], len(iq)))
    for idx in range(tan.shape[1]):
        t, s = tan[:, idx, :], sag[:, idx, :]
        interpft = RGI((azimuths, imagehts), t, method='linear')
        interpfs = RGI((azimuths, imagehts), s, method='linear')
        up_t[:, idx, :] = interpft((aa, ii))
        up_s[:, idx, :] = interpfs((aa, ii))

    # compute the locations of the samples on a cartesian grid
    xd, yd = m.outer(m.cos(m.radians(aq)),
                     iq), m.outer(m.sin(m.radians(aq)), iq)
    samples = m.stack([xd.ravel(), yd.ravel()], axis=1)

    # for the output cartesian grid, figure out the x-y coverage and build a regular grid
    absamin = min(abs(azimuths))
    closest_to_90 = azimuths[m.argmin(azimuths - 90)]
    xfctr = m.cos(m.radians(absamin))
    yfctr = m.cos(m.radians(closest_to_90))
    xmin, xmax = imin * xfctr, imax * xfctr
    ymin, ymax = imin * yfctr, imax * yfctr
    xq, yq = m.linspace(xmin, xmax, len(iq)), m.linspace(ymin, ymax, len(iq))
    xx, yy = m.meshgrid(xq, yq)

    outt, outs = [], []
    # for each frequency, interpolate onto the cartesian grid
    for idx in range(up_t.shape[1]):
        datt = griddata(samples,
                        up_t[:, idx, :].ravel(), (xx, yy),
                        method='linear')
        dats = griddata(samples,
                        up_s[:, idx, :].ravel(), (xx, yy),
                        method='linear')
        outt.append(datt.reshape(xx.shape))
        outs.append(dats.reshape(xx.shape))

    outt, outs = m.rollaxis(m.asarray(outt), 0,
                            3), m.rollaxis(m.asarray(outs), 0, 3)
    return xq, yq, outt, outs
Пример #26
0
    def __init__(self,
                 samples=128,
                 dia=1.0,
                 wavelength=0.55,
                 opd_unit='waves',
                 mask='circle',
                 mask_target='both',
                 ux=None,
                 uy=None,
                 phase=None):
        """Create a new `Pupil` instance.

        Parameters
        ----------
        samples : `int`, optional
            number of samples across the pupil interior
        dia : `float`, optional
            diameter of the pupil, mm
        wavelength : `float`, optional
            wavelength of light, um
        opd_unit : `str`, optional, {'waves', 'um', 'nm'}
            unit used to m.express the OPD.  Equivalent strings may be used to the
            valid options, e.g. 'microns', or 'nanometers'
        mask : `str` or `numpy.ndarray`
            mask used to define the amplitude and boundary of the pupil; any
            regular polygon from `prysm.geometry` as a string, e.g. 'circle' is
            valid.  A user-provided ndarray can also be used.
        mask_target : `str`, {'phase', 'fcn', 'both', None}
            which array to mask during pupil creation; only masking fcn is
            faster for numerical propagations but will make plot2d() and the
            phase array not be truncated properly.  Note that if the mask is not
            binary and `phase` or `both` is used, phase plots will also not be
            correct, as they will be attenuated by the mask.
        ux : `np.ndarray`
            x axis units
        uy : `np.ndarray`
            y axis units
        phase : `np.ndarray`
            phase data

        Notes
        -----
        If ux give, assume uy and phase also given; skip much of the pupil building process
        and simply copy values.

        Raises
        ------
        ValueError
            if the OPD unit given is invalid

        """
        if ux is None:
            # must build a pupil
            self.dia = dia
            ux = m.linspace(-dia / 2, dia / 2, samples)
            uy = m.linspace(-dia / 2, dia / 2, samples)
            self.samples = samples
            need_to_build = True
        else:
            # data already known
            need_to_build = False
        super().__init__(unit_x=ux,
                         unit_y=uy,
                         phase=phase,
                         wavelength=wavelength,
                         phase_unit=opd_unit,
                         spatial_unit='mm')
        self.xaxis_label = 'Pupil ξ'
        self.yaxis_label = 'Pupil η'
        self.zaxis_label = 'OPD'
        self.rho = self.phi = None

        if need_to_build:
            if type(mask) is not m.ndarray:
                mask = mcache(mask, self.samples)

            self._mask = mask
            self.mask_target = mask_target
            self.build()
            self.mask(self._mask, self.mask_target)
        else:
            protomask = m.isnan(phase)
            mask = m.ones(protomask.shape)
            mask[protomask] = 0
            self._mask = mask
            self.mask_target = 'fcn'