Exemple #1
0
def f_qbfs(n):
    """f(m) from oe-18-19-19700 eq. (A.16)."""
    if n == 0:
        return 2
    elif n == 1:
        return np.sqrt(19) / 2
    else:
        term1 = n * (n + 1) + 3
        term2 = g_qbfs(n - 1)**2
        term3 = h_qbfs(n - 2)**2
        return np.sqrt(term1 - term2 - term3)
Exemple #2
0
def sphere_sag(c, rhosq, phi=None):
    """Sag of a spherical surface.

    Parameters
    ----------
    c : float
        surface curvature
    rhosq : numpy.ndarray
        radial coordinate squared
        e.g. for a 15 mm half-diameter optic,
        rho = 0 .. 15
        rhosq = 0 .. 225
        there is no requirement on rectilinear sampling or array
        dimensionality
    phi : numpy.ndarray, optional
        (1 - c^2 r^2)^.5
        computed if not provided
        many surface types utilize phi; its computation can be
        de-duplicated by passing the optional argument

    Returns
    -------
    numpy.ndarray
        surface sag

    """
    if phi is None:
        csq = c * c
        phi = np.sqrt(1 - csq * rhosq)

    return (c * rhosq) / (1 + phi)
def refract(n, nprime, S, r):
    """Use Newton-Raphson iteration to solve Snell's law for the exitant direction cosines.

    Parameters
    ----------
    n : float
        preceeding index of refraction
    nprime : float
        following index of refraction
    S : numpy.ndarray
        shape (3,) or (N,3), any float dtype
        (k,l,m) incident direction cosines
    r : numpy.ndarray
        shape (3,) or (N,3), any float dtype
        surface normals (Fx, Fy, 1)

    Returns
    -------
    numpy.ndarray
        Sprime, a length 3 vector containing the exitant direction cosines

    """
    mu = n/nprime
    musq = mu * mu
    cosI = _multi_dot(r, S)
    cosIsq = cosI * cosI
    # the inline newaxis-es are terrible for readability, but serve a performance purpose
    # broadcast the square root to 2D, so that fewer very expensive sqrt ops are done
    # then, in the second term, broadcast cosI for compatability with S and r
    # since it is needed there
    first_term = np.sqrt(1 - musq * (1 - cosIsq))[:, np.newaxis] * r
    second_term = mu * (S - cosI[:, np.newaxis] * r)
    return first_term + second_term
Exemple #4
0
def noll_to_nm(idx):
    """Convert Noll Z to (n, m) two-term index."""
    # I don't really understand this code, the math is inspired by POPPY
    # azimuthal order
    n = int(np.ceil((-1 + np.sqrt(1 + 8 * idx)) / 2) - 1)
    if n == 0:
        m = 0
    else:
        # this is sort of a rising factorial to use that term incorrectly
        nseries = int((n + 1) * (n + 2) / 2)
        res = idx - nseries - 1

        if is_odd(idx):
            sign = -1
        else:
            sign = 1

        if is_odd(n):
            ms = [1, 1]
        else:
            ms = [0]

        for i in range(n // 2):
            ms.append(ms[-1] + 2)
            ms.append(ms[-1])

        m = ms[res] * sign

    return n, m
Exemple #5
0
def conic_sag_der(c, kappa, rho, phi=None):
    """Sag of a spherical surface.

    Parameters
    ----------
    c : float
        surface curvature
    kappa : float
        conic constant, 0=sphere, 1=parabola, etc
    rho : numpy.ndarray
        radial coordinate
        e.g. for a 15 mm half-diameter optic,
        rho = 0 .. 15
        there is no requirement on rectilinear sampling or array
        dimensionality
    phi : numpy.ndarray, optional
        (1 - (1+kappa) c^2 r^2)^.5
        computed if not provided
        many surface types utilize phi; its computation can be
        de-duplicated by passing the optional argument

    Returns
    -------
    numpy.ndarray
        surface sag

    """
    if phi is None:
        csq = c**2
        rhosq = rho * rho
        phi = np.sqrt(1 - (1 + kappa) * csq * rhosq)

    return (c * rho) / phi
Exemple #6
0
def fringe_to_nm(idx):
    """Convert Fringe Z to (n, m) two-term index."""
    m_n = 2 * (np.ceil(np.sqrt(idx)) - 1)  # sum of n+m
    g_s = (m_n /
           2)**2 + 1  # start of each group of equal n+m given as idx index
    n = m_n / 2 + np.floor((idx - g_s) / 2)
    m = (m_n - n) * (1 - np.mod(idx - g_s, 2) * 2)
    return int(n), int(m)
Exemple #7
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
Exemple #8
0
def f_q2d(n, m):
    """Lowercase f term for 2D-Q polynomials.  oe-20-3-2483 Eq. (A.18b).

    Parameters
    ----------
    n : int
        radial order
    m : int
        azimuthal order

    Returns
    -------
    float
        f

    """
    if n == 0:
        return np.sqrt(F_q2d(n=0, m=m))
    else:
        return np.sqrt(F_q2d(n, m) - g_q2d(n - 1, m)**2)
Exemple #9
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
Exemple #10
0
def phi_spheroid(c, k, rhosq):
    """'phi' for a spheroid.

    phi = sqrt(1 - c^2 rho^2)

    Parameters
    ----------
    c : float
        curvature, reciprocal radius of curvature
    k : float
        kappa, conic constant
    rhosq : numpy.ndarray
        squared radial coordinate (non-normalized)

    Returns
    -------
    numpy.ndarray
        phi term

    """
    csq = c * c
    return np.sqrt(1 - (1 + k) * csq * rhosq)
Exemple #11
0
def _establish_axis(P1, P2):
    """Given two points, establish an axis between them.

    Parameters
    ----------
    P1 : numpy.ndarray
        shape (3,), any float dtype
        first point
    P2 : numpy.ndarray
        shape (3,), any float dtype
        second point

    Returns
    -------
    numpy.ndarray, numpy.ndarray
        P1 (same exact PyObject) and direction cosine from P1 -> P2

    """
    diff = P2 - P1
    euclidean_distance = np.sqrt(diff**2).sum()
    num = diff
    den = euclidean_distance
    return num / den
Exemple #12
0
def zernikes_to_magnitude_angle_nmkey(coefs):
    """Convert Zernike polynomial set to a magnitude and phase representation.

    Parameters
    ----------
    coefs : list of tuples
        a list looking like[(1,2,3),] where (1,2) are the n, m indices and 3 the coefficient

    Returns
    -------
    dict
        dict keyed by tuples of (n, |m|) with values of (rho, phi) where rho is the magnitudes, and phi the phase

    """
    def mkary():  # default for defaultdict
        return list()

    combinations = defaultdict(mkary)

    # for each name and coefficient, make a len 2 array.  Put the Y or 0 degree values in the first slot
    for n, m, coef in coefs:
        m2 = abs(m)
        key = (n, m2)
        combinations[key].append(coef)

    for key, value in combinations.items():
        if len(value) == 1:
            magnitude = value[0]
            angle = 0
        else:
            magnitude = np.sqrt(sum([v**2 for v in value]))
            angle = np.degrees(np.arctan2(*value))

        combinations[key] = (magnitude, angle)

    return dict(combinations)
Exemple #13
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
Exemple #14
0
def Qbfs(n, x):
    """Qbfs polynomial of order n at point(s) x.

    Parameters
    ----------
    n : int
        polynomial order
    x : numpy.array
        point(s) at which to evaluate

    Returns
    -------
    numpy.ndarray
        Qbfs_n(x)

    """
    # to compute the Qbfs polynomials, compute the auxiliary polynomial P_n
    # recursively.  Simultaneously use the recurrence relation for Q_n
    # to compute the intermediary Q polynomials.
    # for input x, transform r = x ^ 2
    # then compute P(r) and consequently Q(r)
    # and scale outputs by Qbfs = r*(1-r) * Q
    # the auxiliary polynomials are the jacobi polynomials with
    # alpha,beta = (-1/2,+1/2),
    # also known as the chebyshev polynomials of the third kind, V(x)

    # the first two Qbfs polynomials are
    # Q_bfs0 = x^2 - x^4
    # Q_bfs1 = 1/19^.5 * (13 - 16 * x^2) * (x^2 - x^4)
    rho = x**2
    # c_Q is the leading term used to convert Qm to Qbfs
    c_Q = rho * (1 - rho)
    if n == 0:
        return c_Q  # == x^2 - x^4

    if n == 1:
        return 1 / np.sqrt(19) * (13 - 16 * rho) * c_Q

    # c is the leading term of the recurrence relation for P
    c = 2 - 4 * rho
    # P0, P1 are the first two terms of the recurrence relation for auxiliary
    # polynomial P_n
    P0 = np.ones_like(x) * 2
    P1 = 6 - 8 * rho
    Pnm2 = P0
    Pnm1 = P1

    # Q0, Q1 are the first two terms of the recurrence relation for Qm
    Q0 = np.ones_like(x)
    Q1 = 1 / np.sqrt(19) * (13 - 16 * rho)
    Qnm2 = Q0
    Qnm1 = Q1
    for nn in range(2, n + 1):
        Pn = c * Pnm1 - Pnm2
        Pnm2 = Pnm1
        Pnm1 = Pn
        g = g_qbfs(nn - 1)
        h = h_qbfs(nn - 2)
        f = f_qbfs(nn)
        Qn = (Pn - g * Qnm1 - h * Qnm2) * (
            1 / f)  # small optimization; mul by 1/f instead of div by f
        Qnm2 = Qnm1
        Qnm1 = Qn

    # Qn is certainly defined (flake8 can't tell the previous ifs bound the loop
    # to always happen once)
    return Qn * c_Q  # NOQA
Exemple #15
0
def Qbfs_sequence(ns, x):
    """Qbfs polynomials of orders ns at point(s) x.

    Parameters
    ----------
    ns : Iterable of int
        polynomial orders
    x : numpy.array
        point(s) at which to evaluate

    Returns
    -------
    generator of numpy.ndarray
        yielding one order of ns at a time

    """
    # see the leading comment of Qbfs for some explanation of this code
    # and prysm:jacobi.py#jacobi_sequence the "_sequence" portion

    ns = list(ns)
    min_i = 0

    rho = x**2
    # c_Q is the leading term used to convert Qm to Qbfs
    c_Q = rho * (1 - rho)
    if ns[min_i] == 0:
        yield np.ones_like(x) * c_Q
        min_i += 1

    if min_i == len(ns):
        return

    if ns[min_i] == 1:
        yield 1 / np.sqrt(19) * (13 - 16 * rho) * c_Q
        min_i += 1

    if min_i == len(ns):
        return

    # c is the leading term of the recurrence relation for P
    c = 2 - 4 * rho
    # P0, P1 are the first two terms of the recurrence relation for auxiliary
    # polynomial P_n
    P0 = np.ones_like(x) * 2
    P1 = 6 - 8 * rho
    Pnm2 = P0
    Pnm1 = P1

    # Q0, Q1 are the first two terms of the recurrence relation for Qbfs_n
    Q0 = np.ones_like(x)
    Q1 = 1 / np.sqrt(19) * (13 - 16 * rho)
    Qnm2 = Q0
    Qnm1 = Q1
    for nn in range(2, ns[-1] + 1):
        Pn = c * Pnm1 - Pnm2
        Pnm2 = Pnm1
        Pnm1 = Pn
        g = g_qbfs(nn - 1)
        h = h_qbfs(nn - 2)
        f = f_qbfs(nn)
        Qn = (Pn - g * Qnm1 - h * Qnm2) * (
            1 / f)  # small optimization; mul by 1/f instead of div by f
        Qnm2 = Qnm1
        Qnm1 = Qn
        if ns[min_i] == nn:
            yield Qn * c_Q
            min_i += 1

        if min_i == len(ns):
            return
Exemple #16
0
def ansi_j_to_nm(idx):
    """Convert ANSI single term to (n,m) two-term index."""
    n = int(np.ceil((-3 + np.sqrt(9 + 8 * idx)) / 2))
    m = 2 * idx - n * (n + 2)
    return n, m
Exemple #17
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
Exemple #18
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