# Path: batoid/utils.py
# def bilinear_fit(ux, uy, kx, ky):
#     a = np.empty((len(ux), 3), dtype=float)
#     a[:,0] = 1
#     a[:,1] = ux
#     a[:,2] = uy
#     b = np.empty((len(ux), 2), dtype=float)
#     b[:,0] = kx
#     b[:,1] = ky
#     x, _, _, _ = np.linalg.lstsq(a, b, rcond=-1)
#     return x
# 
# def fieldToDirCos(u, v, projection='postel'):
#     """Convert field angle to direction cosines using specified projection.
# 
#     Parameters
#     ----------
#     u, v : float
#         Tangent plane coordinates in radians.
#     projection : {'postel', 'zemax', 'gnomonic', 'stereographic', 'lambert', 'orthographic'}
#         Projection used to convert field angle to direction cosines.
# 
#     Returns
#     -------
#     alpha, beta, gamma : float
#         Direction cosines (unit vector projected onto x, y, z in order)
# 
#     Notes
#     -----
#     The tangent plane reference is at (u,v) = (0,0), which corresponds to
#     (alpha, beta, gamma) = (0, 0, -1) (a ray coming directly from above).  The
#     orientation is such that vx (vy) is positive when u (v) is positive.
#     """
#     if projection == 'postel':
#         return postelToDirCos(u, v)
#     elif projection == 'zemax':
#         return zemaxToDirCos(u, v)
#     elif projection == 'gnomonic':
#         return gnomonicToDirCos(u, v)
#     elif projection == 'stereographic':
#         return stereographicToDirCos(u, v)
#     elif projection == 'lambert':
#         return lambertToDirCos(u, v)
#     elif projection == 'orthographic':
#         return orthographicToDirCos(u, v)
#     else:
#         raise ValueError("Bad projection: {}".format(projection))


import numpy as np
import batoid
    import galsim
    import galsim
    import galsim
    import galsim
from .utils import bilinear_fit, fieldToDirCos
    from numbers import Real
    optic : batoid.Optic
        Optical system
    theta_x, theta_y : float
        Field angle in radians
    wavelength : float
        Wavelength in meters
    nrad : int, optional
        Number of ray radii to use.  (see RayVector.asPolar())
    naz : int, optional
        Approximate number of azimuthal angles in outermost ring.  (see
        RayVector.asPolar())
    projection : {'postel', 'zemax', 'gnomonic', 'stereographic', 'lambert', 'orthographic'}
        Projection used to convert field angle to direction cosines.

    Returns
    -------
    dkdu : (2, 2), ndarray
        Jacobian transformation matrix for converting between (kx, ky) of rays
        impacting the focal plane and initial pupil plane coordinate.
    """

    rays = batoid.RayVector.asPolar(
        optic=optic, theta_x=theta_x, theta_y=theta_y, wavelength=wavelength,
        projection=projection, nrad=nrad, naz=naz
    )
    ux = np.array(rays.x)
    uy = np.array(rays.y)

    optic.trace(rays)
    w = ~rays.vignetted

# Path: batoid/utils.py
# def bilinear_fit(ux, uy, kx, ky):
#     a = np.empty((len(ux), 3), dtype=float)
#     a[:,0] = 1
#     a[:,1] = ux
#     a[:,2] = uy
#     b = np.empty((len(ux), 2), dtype=float)
#     b[:,0] = kx
#     b[:,1] = ky
#     x, _, _, _ = np.linalg.lstsq(a, b, rcond=-1)
#     return x
# 
# def fieldToDirCos(u, v, projection='postel'):
#     """Convert field angle to direction cosines using specified projection.
# 
#     Parameters
#     ----------
#     u, v : float
#         Tangent plane coordinates in radians.
#     projection : {'postel', 'zemax', 'gnomonic', 'stereographic', 'lambert', 'orthographic'}
#         Projection used to convert field angle to direction cosines.
# 
#     Returns
#     -------
#     alpha, beta, gamma : float
#         Direction cosines (unit vector projected onto x, y, z in order)
# 
#     Notes
#     -----
#     The tangent plane reference is at (u,v) = (0,0), which corresponds to
#     (alpha, beta, gamma) = (0, 0, -1) (a ray coming directly from above).  The
#     orientation is such that vx (vy) is positive when u (v) is positive.
#     """
#     if projection == 'postel':
#         return postelToDirCos(u, v)
#     elif projection == 'zemax':
#         return zemaxToDirCos(u, v)
#     elif projection == 'gnomonic':
#         return gnomonicToDirCos(u, v)
#     elif projection == 'stereographic':
#         return stereographicToDirCos(u, v)
#     elif projection == 'lambert':
#         return lambertToDirCos(u, v)
#     elif projection == 'orthographic':
#         return orthographicToDirCos(u, v)
#     else:
#         raise ValueError("Bad projection: {}".format(projection))


# Path: batoid/analysis.py
import numpy as np
import batoid
    import galsim
    import galsim
    import galsim
    import galsim
from .utils import bilinear_fit, fieldToDirCos
    from numbers import Real

    optic : batoid.Optic
        Optical system
    theta_x, theta_y : float
        Field angle in radians
    wavelength : float
        Wavelength in meters
    nrad : int, optional
        Number of ray radii to use.  (see RayVector.asPolar())
    naz : int, optional
        Approximate number of azimuthal angles in outermost ring.  (see
        RayVector.asPolar())
    projection : {'postel', 'zemax', 'gnomonic', 'stereographic', 'lambert', 'orthographic'}
        Projection used to convert field angle to direction cosines.

    Returns
    -------
    dkdu : (2, 2), ndarray
        Jacobian transformation matrix for converting between (kx, ky) of rays
        impacting the focal plane and initial pupil plane coordinate.
    """

    rays = batoid.RayVector.asPolar(
        optic=optic, theta_x=theta_x, theta_y=theta_y, wavelength=wavelength,
        projection=projection, nrad=nrad, naz=naz
    )
    ux = np.array(rays.x)
    uy = np.array(rays.y)

    optic.trace(rays)
    w = ~rays.vignetted

    soln = bilinear_fit(ux[w], uy[w], rays.kx[w], rays.ky[w])