Example #1
0
def surface_normal_from_cartesian_derivatives(fx, fy, r, t):
    """Use Cartesian derivatives to compute polar surface normals.

    Parameters
    ----------
    fx : numpy.ndarray
        derivative of f w.r.t. x
    fy : numpy.ndarray
        derivative of f w.r.t. y
    r : numpy.ndarray
        radial coordinates
    t : numpy.ndarray
        azimuthal coordinates

    Returns
    -------
    numpy.ndarray, numpy.ndarray
        r, t derivatives; will contain a singularity where r=0,
        see fix_zero_singularity

    """
    cost = np.cos(t)
    sint = np.sin(t)
    onebyr = 1 / r
    r = fx * cost + fy * sint
    t = fx * -sint / onebyr + fy * cost / onebyr
    return r, t
Example #2
0
def surface_normal_from_cylindrical_derivatives(fp, ft, r, t):
    """Use polar derivatives to compute Cartesian surface normals.

    Parameters
    ----------
    fp : numpy.ndarray
        derivative of f w.r.t. r
    ft : numpy.ndarray
        derivative of f w.r.t. t
    r : numpy.ndarray
        radial coordinates
    t : numpy.ndarray
        azimuthal coordinates

    Returns
    -------
    numpy.ndarray, numpy.ndarray
        x, y derivatives; will contain a singularity where r=0,
        see fix_zero_singularity

    """
    cost = np.cos(t)
    sint = np.sin(t)
    x = fp * cost - 1 / r * ft * sint
    y = fp * sint + 1 / r * ft * cost
    return x, y
Example #3
0
def off_axis_conic_sigma(c, kappa, r, t, dx, dy=0):
    """Lowercase sigma (direction cosine projection term) for an off-axis conic.

    See Eq. (5.2) of oe-20-3-2483.

    Parameters
    ----------
    c : float
        axial curvature of the conic
    kappa : float
        conic constant
    r : numpy.ndarray
        radial coordinate, where r=0 is centered on the off-axis section
    t : numpy.ndarray
        azimuthal coordinate
    dx : float
        shift of the surface in x with respect to the base conic vertex,
        mutually exclusive to dy (only one may be nonzero)
        use dx=0 when dy != 0
    dy : float
        shift of the surface in y with respect to the base conic vertex

    Returns
    -------
    sigma(r,t)

    """
    if dy != 0 and dx != 0:
        raise ValueError('only one of dx/dy may be nonzero')

    if dx != 0:
        s = dx
        oblique_term = 2 * s * r * np.cos(t)
    else:
        s = dy
        oblique_term = 2 * s * r * np.sin(t)

    aggregate_term = r * r + oblique_term + s * s
    csq = c * c
    num = np.sqrt(1 - (1 + kappa) * csq * aggregate_term)
    den = np.sqrt(1 - kappa * csq * aggregate_term)  # flipped sign, 1-kappa
    return num / den
Example #4
0
def off_axis_conic_sag(c, kappa, r, t, dx, dy=0):
    """Sag of an off-axis conicoid.

    Parameters
    ----------
    c : float
        axial curvature of the conic
    kappa : float
        conic constant
    r : numpy.ndarray
        radial coordinate, where r=0 is centered on the off-axis section
    t : numpy.ndarray
        azimuthal coordinate
    dx : float
        shift of the surface in x with respect to the base conic vertex,
        mutually exclusive to dy (only one may be nonzero)
        use dx=0 when dy != 0
    dy : float
        shift of the surface in y with respect to the base conic vertex

    Returns
    -------
    numpy.ndarray
        surface sag, z(x,y)

    """
    if dy != 0 and dx != 0:
        raise ValueError('only one of dx/dy may be nonzero')

    if dx != 0:
        s = dx
        oblique_term = 2 * s * r * np.cos(t)
    else:
        s = dy
        oblique_term = 2 * s * r * np.sin(t)

    aggregate_term = r * r + oblique_term + s * s
    num = c * aggregate_term
    csq = c * c
    den = 1 + np.sqrt(1 - (1 + kappa) * csq * aggregate_term)
    return num / den
Example #5
0
def hopkins(a, b, c, r, t, H):
    """Hopkins' aberration expansion.

    This function uses the "W020" or "W131" like notation, with Wabc separating
    into the a, b, c arguments.  To produce a sine term instead of cosine,
    make a the negative of the order.  In other words, for W222S you would use
    hopkins(2, 2, 2, ...) and for W222T you would use
    hopkins(-2, 2, 2, ...).

    Parameters
    ----------
    a : int
        azimuthal order
    b : int
        radial order
    c : int
        order in field ("H-order")
    r : numpy.ndarray
        radial pupil coordinate
    t : numpy.ndarray
        azimuthal pupil coordinate
    H : numpy.ndarray
        field coordinate

    Returns
    -------
    numpy.ndarray
        polynomial evaluated at this point

    """
    # c = "component"
    if a < 0:
        c1 = np.sin(abs(a)*t)
    else:
        c1 = np.cos(a*t)

    c2 = r ** b

    c3 = H ** c

    return c1 * c2 * c3
Example #6
0
def zernike_nm(n, m, r, t, norm=True):
    """Zernike polynomial of radial order n, azimuthal order m at point r, t.

    Parameters
    ----------
    n : int
        radial order
    m : int
        azimuthal order
    r : numpy.ndarray
        radial coordinates
    t : numpy.ndarray
        azimuthal coordinates
    norm : bool, optional
        if True, orthonormalize the result (unit RMS)
        else leave orthogonal (zero-to-peak = 1)

    Returns
    -------
    numpy.ndarray
        zernike mode of order n,m at points r,t

    """
    x = 2 * r**2 - 1
    am = abs(m)
    n_j = (n - am) // 2
    out = jacobi(n_j, 0, am, x)
    if m != 0:
        if m < 0:
            out *= (r**am * np.sin(am * t))
        else:
            out *= (r**am * np.cos(m * t))

    if norm:
        out *= zernike_norm(n, m)

    return out
Example #7
0
def Q2d_sequence(nms, r, t):
    """Sequence of 2D-Q polynomials.

    Parameters
    ----------
    nms : iterable of tuple
        (n,m) for each desired term
    r : numpy.ndarray
        radial coordinates
    t : numpy.ndarray
        azimuthal coordinates

    Returns
    -------
    generator
        yields one term for each element of nms

    """
    # see Q2d for general sense of this algorithm.
    # the way this one works is to compute the maximum N for each |m|, and then
    # compute the recurrence for each of those sequences and storing it.  A loop
    # is then iterated over the input nms, and selected value with appropriate
    # prefixes / other terms yielded.

    u = r
    x = u**2

    def factory():
        return 0

    # maps |m| => N
    m_has_pos = set()
    m_has_neg = set()
    max_ns = defaultdict(factory)
    for n, m in nms:
        m_ = abs(m)
        if max_ns[m_] < n:
            max_ns[m_] = n
        if m > 0:
            m_has_pos.add(m_)
        else:
            m_has_neg.add(m_)

    # precompute these reusable pieces of data
    u_scales = {}
    sin_scales = {}
    cos_scales = {}

    for absm in max_ns.keys():
        u_scales[absm] = u**absm
        if absm in m_has_neg:
            sin_scales[absm] = np.sin(absm * t)
        if absm in m_has_pos:
            cos_scales[absm] = np.cos(absm * t)

    sequences = {}
    for m, N in max_ns.items():
        if m == 0:
            sequences[m] = list(Qbfs_sequence(range(N + 1), r))
        else:
            sequences[m] = []
            P0 = 1 / 2
            if m == 1:
                P1 = 1 - x / 2
            else:
                P1 = (m - .5) + (1 - m) * x

            f0 = f_q2d(0, m)
            Q0 = 1 / (2 * f0)
            sequences[m].append(Q0)
            if N == 0:
                continue

            g0 = g_q2d(0, m)
            f1 = f_q2d(1, m)
            Q1 = (P1 - g0 * Q0) * (1 / f1)
            sequences[m].append(Q1)
            if N == 1:
                continue
            # everything above here works, or at least everything in the returns works
            if m == 1:
                P2 = (3 - x * (12 - 8 * x)) / 6
                P3 = (5 - x * (60 - x * (120 - 64 * x))) / 10

                g1 = g_q2d(1, m)
                f2 = f_q2d(2, m)
                Q2 = (P2 - g1 * Q1) * (1 / f2)

                g2 = g_q2d(2, m)
                f3 = f_q2d(3, m)
                Q3 = (P3 - g2 * Q2) * (1 / f3)
                sequences[m].append(Q2)
                sequences[m].append(Q3)
                # Q2, Q3 correct
                if N <= 3:
                    continue
                Pnm2, Pnm1 = P2, P3
                Qnm1 = Q3
                min_n = 4
            else:
                Pnm2, Pnm1 = P0, P1
                Qnm1 = Q1
                min_n = 2

            for nn in range(min_n, N + 1):
                A, B, C = abc_q2d(nn - 1, m)
                Pn = (A + B * x) * Pnm1 - C * Pnm2

                gnm1 = g_q2d(nn - 1, m)
                fn = f_q2d(nn, m)
                Qn = (Pn - gnm1 * Qnm1) * (1 / fn)
                sequences[m].append(Qn)

                Pnm2, Pnm1 = Pnm1, Pn
                Qnm1 = Qn

    for n, m in nms:
        if m != 0:
            if m < 0:
                # m < 0, double neg = pos
                prefix = sin_scales[-m] * u_scales[-m]
            else:
                prefix = cos_scales[m] * u_scales[m]

            yield sequences[abs(m)][n] * prefix
        else:
            yield sequences[0][n]
Example #8
0
def Q2d(n, m, r, t):
    """2D Q polynomial, aka the Forbes polynomials.

    Parameters
    ----------
    n : int
        radial polynomial order
    m : int
        azimuthal polynomial order
    r : numpy.ndarray
        radial coordinate, slope orthogonal in [0,1]
    t : numpy.ndarray
        azimuthal coordinate, radians

    Returns
    -------
    numpy.ndarray
        array containing Q2d_n^m(r,t)
        the leading coefficient u^m or u^2 (1 - u^2) and sines/cosines
        are included in the return

    """
    # Q polynomials have auxiliary polynomials "P"
    # which are scaled jacobi polynomials under the change of variables
    # x => 2x - 1 with alpha = -3/2, beta = m-3/2
    # the scaling prefix may be found in A.4 of oe-20-3-2483

    # impl notes:
    # Pn is computed using a recurrence over order n.  The recurrence is for
    # a single value of m, and the 'seed' depends on both m and n.
    #
    # in general, Q_n^m = [P_n^m(x) - g_n-1^m Q_n-1^m] / f_n^m

    # for the sake of consistency, this function takes args of (r,t)
    # but the papers define an argument of u (really, u^2...)
    # which is what I call rho (or r).
    # for the sake of consistency of impl, I alias r=>u
    # and compute x = u**2 to match the papers
    u = r
    x = u**2
    if m == 0:
        return Qbfs(n, r)

    # m == 0 already was short circuited, so we only
    # need to consider the m =/= 0 case for azimuthal terms
    if sign(m) == -1:
        m = abs(m)
        prefix = u**m * np.sin(m * t)
    else:
        prefix = u**m * np.cos(m * t)
        m = abs(m)

    P0 = 1 / 2
    if m == 1:
        P1 = 1 - x / 2
    else:
        P1 = (m - .5) + (1 - m) * x

    f0 = f_q2d(0, m)
    Q0 = 1 / (2 * f0)
    if n == 0:
        return Q0 * prefix

    g0 = g_q2d(0, m)
    f1 = f_q2d(1, m)
    Q1 = (P1 - g0 * Q0) * (1 / f1)
    if n == 1:
        return Q1 * prefix
    # everything above here works, or at least everything in the returns works
    if m == 1:
        P2 = (3 - x * (12 - 8 * x)) / 6
        P3 = (5 - x * (60 - x * (120 - 64 * x))) / 10

        g1 = g_q2d(1, m)
        f2 = f_q2d(2, m)
        Q2 = (P2 - g1 * Q1) * (1 / f2)

        g2 = g_q2d(2, m)
        f3 = f_q2d(3, m)
        Q3 = (P3 - g2 * Q2) * (1 / f3)
        # Q2, Q3 correct
        if n == 2:
            return Q2 * prefix
        elif n == 3:
            return Q3 * prefix

        Pnm2, Pnm1 = P2, P3
        Qnm1 = Q3
        min_n = 4
    else:
        Pnm2, Pnm1 = P0, P1
        Qnm1 = Q1
        min_n = 2

    for nn in range(min_n, n + 1):
        A, B, C = abc_q2d(nn - 1, m)
        Pn = (A + B * x) * Pnm1 - C * Pnm2

        gnm1 = g_q2d(nn - 1, m)
        fn = f_q2d(nn, m)
        Qn = (Pn - gnm1 * Qnm1) * (1 / fn)

        Pnm2, Pnm1 = Pnm1, Pn
        Qnm1 = Qn

    # flake8 can't prove that the branches above the loop guarantee that we
    # enter the loop and Qn is defined
    return Qn * prefix  # NOQA
Example #9
0
def compute_z_zprime_Q2d(cm0, ams, bms, u, t):
    """Compute the surface sag and first radial and azimuthal derivative of a Q2D surface.

    Excludes base sphere.

    from Eq. 2.2 and Appendix B of oe-20-3-2483.

    Parameters
    ----------
    cm0 : iterable
        surface coefficients when m=0 (inside curly brace, top line, Eq. B.1)
        span n=0 .. len(cms)-1 and mus tbe fully dense
    ams : iterable of iterables
        ams[0] are the coefficients for the m=1 cosine terms,
        ams[1] for the m=2 cosines, and so on.  Same order n rules as cm0
    bms : iterable of iterables
        same as ams, but for the sine terms
        ams and bms must be the same length - that is, if an azimuthal order m
        is presnet in ams, it must be present in bms.  The azimuthal orders
        need not have equal radial expansions.

        For example, if ams extends to m=3, then bms must reach m=3
        but, if the ams for m=3 span n=0..5, it is OK for the bms to span n=0..3,
        or any other value, even just [0].
    u : numpy.ndarray
        normalized radial coordinates (rho/rho_max)
    t : numpy.ndarray
        azimuthal coordinate, in the range [0, 2pi]

    Returns
    -------
    numpy.ndarray, numpy.ndarray, numpy.ndarray
        surface sag, radial derivative of sag, azimuthal derivative of sag

    """
    usq = u * u
    z = np.zeros_like(u)
    dr = np.zeros_like(u)
    dt = np.zeros_like(u)

    # this is terrible, need to re-think this
    if cm0 is not None and len(cm0) > 0:
        zm0, zprimem0 = compute_z_zprime_Qbfs(cm0, u, usq)
        z += zm0
        dr += zprimem0

    # B.1
    # cos(mt)[sum a^m Q^m(u^2)] + sin(mt)[sum b^m Q^m(u^2)]
    #        ~~~~~~~~~~~~~~~~~~          ~~~~~~~~~~~~~~~~~~
    # variables:     Sa                           Sb
    # => because of am/bm going into Clenshaw's method, cannot
    # simplify, need to do the recurrence twice
    # u^m is outside the entire expression, think about that later
    m = 0
    # initialize to zero and incr at the front of the loop
    # to avoid putting an m += 1 at the bottom (too far from init)
    for a_coef, b_coef in zip(ams, bms):
        m += 1
        # TODO: consider zeroing alphas and re-using it to reduce
        # alloc pressure inside this func; need care since len of any coef vector
        # may be unequal

        if len(a_coef) == 0:
            continue

        # can't use "as" => as keyword
        Na = len(a_coef) - 1
        Nb = len(b_coef) - 1
        alphas_a = clenshaw_q2d_der(a_coef, m, usq)
        alphas_b = clenshaw_q2d_der(b_coef, m, usq)
        Sa = 0.5 * alphas_a[0][0]
        Sb = 0.5 * alphas_b[0][0]
        Sprimea = 0.5 * alphas_a[1][0]
        Sprimeb = 0.5 * alphas_b[1][0]
        if m == 1 and Na > 2:
            Sa -= 2 / 5 * alphas_a[0][3]
            # derivative is same, but instead of 0 index, index=j==1
            Sprimea -= 2 / 5 * alphas_a[1][3]
        if m == 1 and Nb > 2:
            Sb -= 2 / 5 * alphas_b[0][3]
            Sprimeb -= 2 / 5 * alphas_b[1][3]

        um = u**m
        cost = np.cos(m * t)
        sint = np.sin(m * t)

        kernel = cost * Sa + sint * Sb
        total_sum = um * kernel

        z += total_sum

        # for the derivatives, we have two cases of the product rule:
        # between "cost" and Sa, and between "sint" and "Sb"
        # within each of those is a chain rule, just as for Zernike
        # then there is a final product rule for the outer term
        # differentiating in this way is just like for the classical asphere
        # equation; differentiate each power separately
        # if F(x) = S(x^2), then
        # d/dx(cos(m * t) * Fx) = 2x F'(x^2) cos(mt)
        # with u^m in front, taken to its conclusion
        # F = Sa, G = Sb
        # d/dx(x^m (cos(m y) F(x^2) + sin(m y) G(x^2))) =
        # x^(m - 1) (2 x^2 (F'(x^2) cos(m y) + G'(x^2) sin(m y)) + m F(x^2) cos(m y) + m G(x^2) sin(m y))
        #                                                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        #                                                                         m x "kernel" above
        # d/dy(x^m (cos(m y) F(x^2) + sin(m y) G(x^2))) = m x^m (G(x^2) cos(m y) - F(x^2) sin(m y))
        umm1 = u**(m - 1)
        twousq = 2 * usq
        aterm = cost * (twousq * Sprimea + m * Sa)
        bterm = sint * (twousq * Sprimeb + m * Sb)
        dr += umm1 * (aterm + bterm)
        dt += m * um * (-Sa * sint + Sb * cost)

    return z, dr, dt
Example #10
0
def off_axis_conic_sigma_der(c, kappa, r, t, dx, dy=0):
    """Derivatives of 1/off_axis_conic_sigma.

    See Eq. (5.2) of oe-20-3-2483.

    Parameters
    ----------
    c : float
        axial curvature of the conic
    kappa : float
        conic constant
    r : numpy.ndarray
        radial coordinate, where r=0 is centered on the off-axis section
    t : numpy.ndarray
        azimuthal coordinate
    dx : float
        shift of the surface in x with respect to the base conic vertex,
        mutually exclusive to dy (only one may be nonzero)
        use dx=0 when dy != 0
    dy : float
        shift of the surface in y with respect to the base conic vertex

    Returns
    -------
    numpy.ndarray, numpy.ndarray
        d/dr(z), d/dt(z)

    """
    if dy != 0 and dx != 0:
        raise ValueError('only one of dx/dy may be nonzero')

    cost = np.cos(t)
    sint = np.sin(t)
    if dx != 0:
        s = dx
        oblique_term = 2 * s * r * cost
        ddr_oblique = 2 * r + 2 * s * cost
        # I accept the evil in writing this the way I have
        # to deduplicate the computation
        ddt_oblique_ = r * (-s) * sint
    else:
        s = dy
        oblique_term = 2 * s * r * sint
        ddr_oblique = 2 * r + 2 * s * sint
        ddt_oblique_ = r * s * cost

    aggregate_term = r * r + oblique_term + s * s
    csq = c * c
    # d/dr first
    phi_kernel = (1 + kappa) * csq * aggregate_term
    phi = np.sqrt(1 - phi_kernel)
    notquitephi_kernel = kappa * csq * aggregate_term
    notquitephi = np.sqrt(1 + notquitephi_kernel)

    num = csq * (1 + kappa) * ddr_oblique * notquitephi
    den = 2 * (1 - phi_kernel)**(3 / 2)
    term1 = num / den

    num = csq * kappa * ddr_oblique
    den = 2 * phi * notquitephi
    term2 = num / den
    dr = term1 + term2

    # d/dt
    num = csq * (1 + kappa) * ddt_oblique_ * notquitephi
    den = (1 - phi_kernel)**(3 / 2)  # phi^3?
    term1 = num / den

    num = csq * kappa * ddt_oblique_
    den = phi * notquitephi
    term2 = num / den
    dt = term1 + term2  # minus in writing, but sine/cosine
    return dr, dt
Example #11
0
def off_axis_conic_der(c, kappa, r, t, dx, dy=0):
    """Radial and azimuthal derivatives of an off-axis conic.

    Parameters
    ----------
    c : float
        axial curvature of the conic
    kappa : float
        conic constant
    r : numpy.ndarray
        radial coordinate, where r=0 is centered on the off-axis section
    t : numpy.ndarray
        azimuthal coordinate
    dx : float
        shift of the surface in x with respect to the base conic vertex,
        mutually exclusive to dy (only one may be nonzero)
        use dx=0 when dy != 0
    dy : float
        shift of the surface in y with respect to the base conic vertex

    Returns
    -------
    numpy.ndarray, numpy.ndarray
        d/dr(z), d/dt(z)

    """
    if dy != 0 and dx != 0:
        raise ValueError('only one of dx/dy may be nonzero')

    cost = np.cos(t)
    sint = np.sin(t)
    if dx != 0:
        s = dx
        oblique_term = 2 * s * r * cost
        ddr_oblique = 2 * r + 2 * s * cost
        # I accept the evil in writing this the way I have
        # to deduplicate the computation
        ddt_oblique_ = r * (-s) * sint
        ddt_oblique = 2 * ddt_oblique_
    else:
        s = dy
        oblique_term = 2 * s * r * sint
        ddr_oblique = 2 * r + 2 * s * sint
        ddt_oblique_ = r * s * cost
        ddt_oblique = 2 * ddt_oblique_

    aggregate_term = r * r + oblique_term + s * s
    csq = c * c
    c3 = csq * c
    # d/dr first
    num = c * ddr_oblique
    phi_kernel = (1 + kappa) * csq * aggregate_term
    phi = np.sqrt(1 - phi_kernel)
    phip1 = 1 + phi
    phip1sq = phip1 * phip1
    den = phip1
    term1 = num / den

    num = c3 * (1 + kappa) * ddr_oblique * aggregate_term
    den = (2 * phi) * phip1sq
    term2 = num / den
    dr = term1 + term2

    # d/dt
    num = c * ddt_oblique
    den = phip1
    term1 = num / den

    num = c3 * (1 + kappa) * ddt_oblique_ * aggregate_term
    den = phi * phip1sq
    term2 = num / den
    dt = term1 + term2

    return dr, dt
Example #12
0
def zernike_nm_sequence(nms, r, t, norm=True):
    """Zernike polynomial of radial order n, azimuthal order m at point r, t.

    Parameters
    ----------
    nms : iterable of tuple of int,
        sequence of (n, m); looks like [(1,1), (3,1), ...]
    r : numpy.ndarray
        radial coordinates
    t : numpy.ndarray
        azimuthal coordinates
    norm : bool, optional
        if True, orthonormalize the result (unit RMS)
        else leave orthogonal (zero-to-peak = 1)

    Returns
    -------
    generator
        yields one mode at a time of nms

    """
    # this function deduplicates all possible work.  It uses a connection
    # to the jacobi polynomials to efficiently compute a series of zernike
    # polynomials
    # it follows this basic algorithm:
    # for each (n, m) compute the appropriate Jacobi polynomial order
    # collate the unique values of that for each |m|
    # compute a set of jacobi polynomials for each |m|
    # compute r^|m| , sin(|m|*t), and cos(|m|*t for each |m|
    #
    # benchmarked at 12.26 ns/element (256x256), 4.6GHz CPU = 56 clocks per element
    # ~36% faster than previous impl (12ms => 8.84 ms)
    x = 2 * r**2 - 1
    ms = [e[1] for e in nms]
    am = truenp.abs(ms)
    amu = truenp.unique(am)

    def factory():
        return 0

    jacobi_sequences_mjn = defaultdict(factory)
    # jacobi_sequences_mjn is a lookup table from |m| to all orders < max(n_j)
    # for each |m|, i.e. 0 .. n_j_max
    for nm, am_ in zip(nms, am):
        n = nm[0]
        nj = (n - am_) // 2
        if nj > jacobi_sequences_mjn[am_]:
            jacobi_sequences_mjn[am_] = nj

    for k in jacobi_sequences_mjn:
        nj = jacobi_sequences_mjn[k]
        jacobi_sequences_mjn[k] = truenp.arange(nj + 1)

    jacobi_sequences = {}

    jacobi_sequences_mjn = dict(jacobi_sequences_mjn)
    for k in jacobi_sequences_mjn:
        n_jac = jacobi_sequences_mjn[k]
        jacobi_sequences[k] = list(jacobi_sequence(n_jac, 0, k, x))

    powers_of_m = {}
    sines = {}
    cosines = {}
    for m in amu:
        powers_of_m[m] = r**m
        sines[m] = np.sin(m * t)
        cosines[m] = np.cos(m * t)

    for n, m in nms:
        absm = abs(m)
        nj = (n - absm) // 2
        jac = jacobi_sequences[absm][nj]
        if norm:
            jac = jac * zernike_norm(n, m)

        if m == 0:
            # rotationally symmetric Zernikes are jacobi
            yield jac
        else:
            if m < 0:
                azpiece = sines[absm]
            else:
                azpiece = cosines[absm]

            radialpiece = powers_of_m[absm]
            out = jac * azpiece * radialpiece  # jac already contains the norm
            yield out
Example #13
0
def zernike_nm_der(n, m, r, t, norm=True):
    """Derivatives of Zernike polynomial of radial order n, azimuthal order m, w.r.t r and t.

    Parameters
    ----------
    n : int
        radial order
    m : int
        azimuthal order
    r : numpy.ndarray
        radial coordinates
    t : numpy.ndarray
        azimuthal coordinates
    norm : bool, optional
        if True, orthonormalize the result (unit RMS)
        else leave orthogonal (zero-to-peak = 1)

    Returns
    -------
    numpy.ndarray, numpy.ndarray
        dZ/dr, dZ/dt

    """
    # x = 2 * r ** 2 - 1
    # R = radial polynomial R_n^m, not dZ/dr
    # R = P_(n-m)//2^(0,|m|) (x)
    # = modified jacobi polynomial
    # dR = 4r R'(x) (chain rule)
    # => use jacobi_der
    # if m == 0, dZ = dR
    # for m != 0, Z = r^|m| * R * cos(mt)
    # the cosine term has no impact on the radial derivative,
    # for which we need the product rule:
    # d/dr(u v) = v(du/dr) + u(dv/dr)
    #      u    = R, which we already have the derivative of
    #        v  = r^|m| = r^k
    #     dv/dr = k r^(k-1)
    # d/dr(Z)   = r^k * (4r * R'(x)) + R * k r^(k-1)
    #             ------------------   -------------
    #                    v du              u dv
    #
    # all of that is multiplied by d/dr( cost ) or sint, which is just a "pass-through"
    # since cost does not depend on r
    #
    # in azimuth it's the other way around: regular old Zernike computation,
    # multiplied by d/dt ( cost )
    x = 2 * r**2 - 1
    am = abs(m)
    n_j = (n - am) // 2
    # dv from above == d/dr(R(2r^2-1))
    dv = (4 * r) * jacobi_der(n_j, 0, am, x)
    if norm:
        znorm = zernike_norm(n, m)
    if m == 0:
        dr = dv
        dt = np.zeros_like(dv)
    else:
        v = jacobi(n_j, 0, am, x)
        u = r**am
        du = am * r**(am - 1)
        dr = v * du + u * dv
        if m < 0:
            dt = am * np.cos(am * t)
            dr *= np.sin(am * t)
        else:
            dt = -m * np.sin(m * t)
            dr *= np.cos(m * t)

        # dt = dt * (u * v)
        # = cost * r^|m| * R
        # faster to write it as two in-place ops here
        # (no allocations)
        dt *= u
        dt *= v

        # ugly as this is, we skip one multiply
        # by doing these extra ifs
        if norm:
            dt *= znorm

    if norm:
        dr *= znorm

    return dr, dt
Example #14
0
def generate_finite_ray_fan(nrays,
                            na,
                            P=0,
                            min_na=None,
                            azimuth=90,
                            yangle=0,
                            xangle=0,
                            n=1,
                            distribution='uniform'):
    """Generate a 1D fan of rays.

    Parameters
    ----------
    nrays : int
        the number of rays in the fan
    na : float
        object-space numerical aperture
    P : numpy.ndarray
        length 3 vector containing the position from which the rays emanate
    min_na : float, optional
        minimum NA for the beam, -na if None
    azimuth: float
        angle in the XY plane, degrees.  0=X ray fan, 90=Y ray fan
    yangle : float
        propagation angle of the chief/gut ray with respect to the Y axis, clockwise
    xangle : float
        propagation angle of the gut ray with respect to the X axis, clockwise
    n : float
        refractive index at P (1=vacuum)
    distribution : str, {'uniform', 'random', 'cheby'}
        The distribution to use when placing the rays
        a uniform distribution has rays which are equally spaced from minr to maxr,
        random has rays randomly distributed in minr and maxr, while cheby has the
        Cheby-Gauss-Lobatto roots as its locations from minr to maxr

    Returns
    -------
    numpy.ndarray, numpy.ndarray
        "P" and "S" variables, positions and direction cosines of the rays

    """
    # TODO: revisit this; tracing a parabola from the focus, the output
    # ray spacing is not uniform as it should be.  Or is this some manifestation
    # of the sine condition?
    # more likely it's the square root since it hides unless the na is big
    P = _ensure_P_vec(P)
    distribution = distribution.lower()
    if min_na is None:
        min_na = -na

    max_t = np.arcsin(na / n)
    min_t = np.arcsin(min_na / n)
    if distribution == 'uniform':
        t = np.linspace(min_t, max_t, nrays, dtype=config.precision)
    elif distribution == 'random':
        t = np.random.uniform(low=min_t, high=max_t,
                              size=nrays).astype(config.precision)

    # use the even function for the y direction cosine,
    # use trig identity to compute the z direction cosine
    l = np.sin(t)  # NOQA
    m = np.sqrt(1 - l * l)  # NOQA
    k = np.array([0.], dtype=t.dtype)
    k = np.broadcast_to(k, (nrays, ))
    if azimuth == 0:
        k, l = l, k  # NOQA  swap Y and X axes

    S = np.stack([k, l, m], axis=1)
    if yangle != 0 and xangle != 0:
        R = make_rotation_matrix((0, yangle, -xangle))
        # newaxis for batch matmul, squeeze needed for size 1 dim after
        S = np.matmul(R, S[..., np.newaxis]).squeeze()

    # need to see a copy of P for each ray, -> add empty dim and broadcast
    P = P[np.newaxis, :]
    P = np.broadcast_to(P, (nrays, 3))
    return P, S