Source code for coordio.conv

import warnings

import numpy
from skimage.transform import (AffineTransform, EuclideanTransform,
                               SimilarityTransform)

from . import defaults, libcoordio
from .exceptions import CoordIOUserWarning, CoordIOError


# low level, functional conversions

[docs]def fieldAngle2Cart(xField, yField): """Convert (ZEMAX-style) field angles in degrees to a cartesian point on the unit sphere Zemax defines field angles like this: Positive field angles imply positive slope for the ray in that direction, and thus refer to **negative** coordinates on distant objects. ZEMAX converts x field angles ( αx ) and y field angles ( αy ) to ray direction cosines using the following formulas: tanαx = l/n tanαy = m/n l^2 + m^2 + n^2 = 1 where l, m, and n are the x, y, and z direction cosines. Parameters ----------- xField: scalar or 1D array zemax style x field yField: scalar or 1D array zemax style y field Returns --------- result: list [x,y,z] coordinates on unit sphere """ # xField, yField = fieldXY # invert field degrees, fields represent slope # of ray, so a positivly sloped ray is coming from # below its respective plane, the resulting vector # from this transform should thus point downward, not up tanx = numpy.tan(numpy.radians(-1*xField)) tany = numpy.tan(numpy.radians(-1*yField)) # calculate direction cosines z = numpy.sqrt(1/(tanx**2 + tany**2 + 1)) x = tanx * z y = tany * z # if numpy.isnan(n) or numpy.isnan(l) or numpy.isnan(m): # raise RuntimeError("NaN output [%.2f, %.2f, %.2f] for input [%.2f, %.2f]"%(n, l, m, xField, yField)) return [x,y,z]
[docs]def cart2FieldAngle(x, y, z): """Convert cartesian point on unit sphere of sky to (ZEMAX-style) field angles in degrees. Zemax defines field angles like this: Positive field angles imply positive slope for the ray in that direction, and thus refer to **negative** coordinates on distant objects. ZEMAX converts x field angles ( αx ) and y field angles ( αy ) to ray direction cosines using the following formulas: tanαx = l/n tanαy = m/n l^2 + m^2 + n^2 = 1 where l, m, and n are the x, y, and z direction cosines. Parameters ----------- x: scalar or 1D array y: scalar or 1D array z: scalar or 1D array Returns --------- result: list [xField, yField] ZEMAX style field coordinates (degrees) """ # if numpy.abs(numpy.linalg.norm(cartXYZ) - 1) > SMALL_NUM: # raise RuntimeError("cartXYZ must be a vector on unit sphere with L2 norm = 1") # l, m, n = x, y, z # invert field degrees, fields represent slope # of ray, so a positivly sloped ray is coming from # below the optical axis, the resulting vector # from this transform should thus point downward, not up xField = -1 * numpy.degrees(numpy.arctan2(x, z)) yField = -1 * numpy.degrees(numpy.arctan2(y, z)) # if numpy.isnan(xField) or numpy.isnan(yField): # raise RuntimeError("NaN output [%.2f, %.2f] for input [%.2f, %.2f, %.2f]"%(xField, yField, cartXYZ[0], cartXYZ[1], cartXYZ[2])) return [xField, yField]
[docs]def cart2Sph(x, y, z): """Convert cartesian coordinates to spherical coordinates theta, phi in degrees phi is polar angle measure from z axis theta is azimuthal angle measured from x axis Parameters ----------- x: scalar or 1D array y: scalar or 1D array z: scalar or 1D array Returns --------- result: list [theta, phi] degrees """ # if numpy.abs(numpy.linalg.norm(cartXYZ) - 1) > SMALL_NUM: # raise RuntimeError("cartXYZ must be a vector on unit sphere with L2 norm = 1") # if cartXYZ[2] < 0: # raise RuntimeError("z direction must be positive for cartesian field coord") # x, y, z = cartXYZ theta = numpy.degrees(numpy.arctan2(y, x)) # wrap theta to be between 0 and 360 degrees theta = theta % 360 phi = numpy.degrees(numpy.arccos(z)) # if numpy.isnan(theta) or numpy.isnan(phi): # raise RuntimeError("NaN output [%.2f, %.2f] from input [%.2f, %.2f, %.2f]"%(theta, phi, cartXYZ[0], cartXYZ[1], cartXYZ[2])) return [theta, phi]
[docs]def sph2Cart(theta, phi, r=1): """Convert spherical coordinates theta, phi in degrees to cartesian coordinates on unit sphere. phi is polar angle measure from z axis theta is azimuthal angle measured from x axis Parameters ----------- theta: scalar or 1D array degrees, azimuthal angle phi: scalar or 1D array degrees, polar angle r: scalar radius of curvature. Default to 1 for unit sphere Returns --------- result: list [x,y,z] coordinates on sphere """ theta, phi = numpy.radians(theta), numpy.radians(phi) x = r*numpy.cos(theta) * numpy.sin(phi) y = r*numpy.sin(theta) * numpy.sin(phi) z = r*numpy.cos(phi) # if numpy.isnan(x) or numpy.isnan(y) or numpy.isnan(z): # raise RuntimeError("NaN output [%.2f, %.2f, %.2f] for input [%.2f, %.2f]"%(x, y, z, numpy.degrees(theta), numpy.degrees(phi))) return [x, y, z]
[docs]def fieldToObserved(x, y, z, altCenter, azCenter, pa): """Convert xyz unit-spherical field coordinates to Alt/Az the inverse of observetToField Az=0 is north, Az=90 is east +x points along +RA, +y points along +Dec, +z points along boresight from telescope toward sky Parameters ------------ x: scalar or 1D array unit-spherical x field coord (aligned with +RA) y: scalar or 1D array unit-spherical y field coord (aligned with +Dec) z: scalar or 1D array unit-spherical z coord altCenter: float altitude in degrees, field center azCenter: float azimuth in degrees, field center pa: float position angle of field center Returns -------- alt : float or 1D array altitude of coordinate in degrees az : float or 1D array """ cosQ = numpy.cos(numpy.radians(-1*pa)) sinQ = numpy.sin(numpy.radians(-1*pa)) rotQ = numpy.array([ [ cosQ, sinQ, 0], [-sinQ, cosQ, 0], [ 0, 0, 1] ]) coords = numpy.array( [x, y, z] ).T coords = rotQ.dot(coords.T).T sinPhi = numpy.sin(-1*numpy.radians(90-altCenter)) cosPhi = numpy.cos(-1*numpy.radians(90-altCenter)) rotPhi = numpy.array([ [1, 0, 0], [0, cosPhi, sinPhi], [0, -sinPhi, cosPhi] ]) coords = rotPhi.dot(coords.T).T sinTheta = numpy.sin(-1*numpy.radians(90-azCenter)) cosTheta = numpy.cos(-1*numpy.radians(90-azCenter)) rotTheta = numpy.array([ [ cosTheta, sinTheta, 0], [-sinTheta, cosTheta, 0], [ 0, 0, 1] ]) coords = rotTheta.dot(coords.T).T x, y, z = coords[:, 0], coords[:, 1], coords[:, 2] theta, phi = cart2Sph(x, y, z) # convert sph theta, phi to az, alt az = -1 * theta alt = 90 - phi az = az % 360 # wrap to 0,360 return alt, az
[docs]def observedToField(alt, az, altCenter, azCenter, pa): """Convert Az/Alt coordinates to cartesian coords on the unit sphere with the z axis aligned with field center (boresight) Az=0 is north, Az=90 is east Resulting Cartesian coordinates have the following convention +x points along +RA, +y points along +Dec Parameters ------------ alt: float or 1D array altitude in degrees, positive above horizon, negative below az: float or 1D array azimuth in degrees az=0 is north az=90 is east altCenter: float altitude in degrees, field center azCenter: float azimuth in degrees, field center pa: float position angle of field center Returns -------- theta : float or 1D array azimuthal angle of field coordinate (deg) phi : float or 1D array polar angle of field coordinat (deg off axis) """ # convert from alt/az to spherical phis = 90 - alt # alt thetas = -1 * az # az # work in cartesian frame coords = sph2Cart(thetas, phis) coords = numpy.array(coords).T # rotate the xyz coordinate system about z axis # such that -y axis is aligned with the azimuthal angle # of the field center sinTheta = numpy.sin(numpy.radians(90 - azCenter)) cosTheta = numpy.cos(numpy.radians(90 - azCenter)) rotTheta = numpy.array([ [ cosTheta, sinTheta, 0], [-sinTheta, cosTheta, 0], [ 0, 0, 1] ]) coords = rotTheta.dot(coords.T).T # rotate the xyz coordinate system about the x axis # such that +z points to the field center. sinPhi = numpy.sin(numpy.radians(90 - altCenter)) cosPhi = numpy.cos(numpy.radians(90 - altCenter)) rotPhi = numpy.array([ [1, 0, 0], [0, cosPhi, sinPhi], [0, -sinPhi, cosPhi] ]) coords = rotPhi.dot(coords.T).T # return coords # finally rotate about z by the parallactic angle # this puts +RA along +X and +DEC along +Y cosQ = numpy.cos(numpy.radians(pa)) sinQ = numpy.sin(numpy.radians(pa)) rotQ = numpy.array([ [ cosQ, sinQ, 0], [-sinQ, cosQ, 0], [ 0, 0, 1] ]) coords = rotQ.dot(coords.T).T x = coords[:, 0] y = coords[:, 1] z = coords[:, 2] # finally convert back from cartesian to spherical (Field) thetaPhi = cart2Sph(x, y, z) thetaPhi = numpy.array(thetaPhi).T theta, phi = thetaPhi[:,0], thetaPhi[:,1] return theta, phi
[docs]def fieldToFocal(thetaField, phiField, site, waveCat): """Convert spherical field coordinates to the modeled spherical position on the focal plane. Origin is the M1 vertex. Raises warnings for coordinates at large field angles. Parameters ----------- thetaField: float or numpy.ndarray azimuthal angle of field coordinate (deg) phiField: float or numpy.ndarray polar angle of field coordinate (deg) site: str "APO" or "LCO" waveCat: str "Boss", "Apogee", or "GFA" Result ------- xFocal: float or numpy.ndarray spherical x focal coord mm yFocal: float or numpy.ndarray spherical y focal coord mm zFocal: float or numpy.ndarray spherical z focal coord mm R: float radius of curvature for sphere b: float z location of center of sphere fieldWarn: bool or boolean array True if angle off-axis is large enough to be suspicious """ if site == "APO": fieldWarn = numpy.array(phiField) > defaults.APO_MAX_FIELD_R if site == "LCO": fieldWarn = numpy.array(phiField) > defaults.LCO_MAX_FIELD_R if hasattr(fieldWarn, "__len__"): if True in fieldWarn: warnings.warn( "Warning! Coordinate far off telescope optical axis " \ "conversion may be bogus", CoordIOUserWarning ) elif fieldWarn is True: warnings.warn( "Warning! Coordinate far off telescope optical axis, " \ "conversion may be bogus", CoordIOUserWarning ) direction = "focal" R, b, c0, c1, c2, c3, c4 = defaults.getFPModelParams(site, direction, waveCat) phiFocal = c0 * phiField + c1 * phiField**3 + c2 * phiField**5 \ + c3 * phiField**7 + c4 * phiField**9 # thetaField = thetaFocal by definition # convert back to xyz coords rTheta = numpy.radians(thetaField) rPhi = numpy.radians(180 - phiFocal) xFocal = R * numpy.cos(rTheta) * numpy.sin(rPhi) yFocal = R * numpy.sin(rTheta) * numpy.sin(rPhi) zFocal = R * numpy.cos(rPhi) + b return xFocal, yFocal, zFocal, R, b, fieldWarn
[docs]def focalToField(xFocal, yFocal, zFocal, site, waveCat): """Convert xyz focal position to unit-spherical field position. xyzFocal do not need to lie on the modeled sphere, and the spherical radius of curvature is not needed, only the origin of the sphere is needed (b). Raises warnings for coordinates at large field angles. Parameters ----------- xFocal: float or 1D array x focal coord mm yFocal: float or 1D array y focal coord mm zFocal: float or 1D array z focal coord mm, +z points along boresight toward sky. site: str "APO" or "LCO" waveCat: str "Boss", "Apogee", or "GFA" Result ------- thetaField: float or 1D array azimuthal field coordinate (deg) phiField: float or 1D array polar field coordinate (deg) fieldWarn: bool or boolean array True if angle off-axis is large enough to be suspicious """ direction = "field" R, b, c0, c1, c2, c3, c4 = defaults.getFPModelParams(site, direction, waveCat) # note by definition thetaField==thetaFocal thetaField = numpy.degrees(numpy.arctan2(yFocal, xFocal)) # generate focal phis (degree off boresight) # angle off-axis from optical axis rFocal = numpy.sqrt(xFocal**2 + yFocal**2) v = numpy.array([rFocal, zFocal]).T v[:, 1] = v[:, 1] - b # unit vector pointing toward object on focal plane from circle center # arc from vertex to off axis v = v / numpy.vstack([numpy.linalg.norm(v, axis=1)] * 2).T # FP lands at -Z so, Angle from sphere center towards ground downwardZaxis = numpy.array([0, -1]) # phi angle between ray and optical axis measured from sphere center phiFocal = numpy.degrees(numpy.arccos(v.dot(downwardZaxis))) phiField = c0 * phiFocal + c1 * phiFocal**3 + c2 * phiFocal**5 \ + c3 * phiFocal**7 + c4 * phiFocal**9 if site == "APO": fieldWarn = numpy.array(phiField) > defaults.APO_MAX_FIELD_R if site == "LCO": fieldWarn = numpy.array(phiField) > defaults.LCO_MAX_FIELD_R if hasattr(fieldWarn, "__len__"): if True in fieldWarn: warnings.warn( "Warning! Coordinate far off telescope optical axis, conversion may be bogus", CoordIOUserWarning ) elif fieldWarn is True: warnings.warn( "Warning! Coordinate far off telescope optical axis, conversion may be bogus", CoordIOUserWarning ) return thetaField, phiField, fieldWarn
[docs]def focalToWok( xFocal, yFocal, zFocal, positionAngle=0, xOffset=0, yOffset=0, zOffset=0, tiltX=0, tiltY=0, fpScale=1, projectFlat=True ): """Convert xyz focal coordinates in mm to xyz wok coordinates in mm. The origin of the focal coordinate system is the M1 vertex. focal +y points toward North, +x points toward E. The origin of the wok coordinate system is the wok vertex. -x points toward the boss slithead. +z points from telescope to sky. Tilt is applied about x axis then y axis. Note: currently zFocal irrelevant using a flat wok model. Parameters ------------- xFocal: scalar or 1D array x position of object on focal plane mm (+x aligned with +RA on image) yFocal: scalar or 1D array y position of object on focal plane mm (+y aligned with +Dec on image) zFocal: scalar or 1D array z position of object on focal plane mm (+z aligned boresight and increases from the telescope to the sky) positionAngle: scalar position angle deg. Angle measured from (image) North through East to wok +y. So position angle of 45 deg, wok +y points NE site : str either "APO" or "LCO" xOffset: scalar or None x position (mm) of wok origin (vertex) in focal coords calibrated yOffset: scalar y position (mm) of wok origin (vertex) in focal coords calibratated zOffset: scalar z position (mm) of wok origin (vertex) in focal coords calibratated tiltX: scalar tilt (deg) of wok about focal x axis at PA=0 calibrated tiltY: scalar tilt (deg) of wok about focal y axis at PA=0 calibrated fpScale: scalar radial scale multiplier projectFlat: bool if true, assume flat wok model Returns --------- xWok: scalar or 1D array x position of object in wok space mm (+x aligned with +RA on image) yWok: scalar or 1D array y position of object in wok space mm (+y aligned with +Dec on image) zWok: scalar or 1D array z position of object in wok space mm (+z aligned boresight and increases from the telescope to the sky) """ # apply calibrated tilts and translation (at PA=0) # where they should be fit # tilts are defined https://mathworld.wolfram.com/RotationMatrix.html # as coordinate system rotations counter clockwise when looking # down the positive axis toward the origin coords = numpy.array([xFocal, yFocal, zFocal]) transXYZ = numpy.array([xOffset, yOffset, zOffset]) rotX = numpy.radians(tiltX) rotY = numpy.radians(tiltY) # rotation about z axis is position angle # position angle is clockwise positive for rotation measured from # north to wok +y (when looking from above the wok) # however rotation matrices are positinve for counter-clockwise rotation # hence the sign flip that's coming rotZ = -1*numpy.radians(positionAngle) rotMatX = numpy.array([ [1, 0, 0], [0, numpy.cos(rotX), numpy.sin(rotX)], [0, -1*numpy.sin(rotX), numpy.cos(rotX)] ]) rotMatY = numpy.array([ [numpy.cos(rotY), 0, -1*numpy.sin(rotY)], [0, 1, 0], [numpy.sin(rotY), 0, numpy.cos(rotY)] ]) # rotates coordinate system rotMatZ = numpy.array([ [numpy.cos(rotZ), numpy.sin(rotZ), 0], [-numpy.sin(rotZ), numpy.cos(rotZ), 0], [0, 0, 1] ]) # first apply rotation about x axis (x tilt) coords = rotMatX.dot(coords) # next apply rotation about y axis (y tilt) coords = rotMatY.dot(coords) # apply rotation about z axis (angle of observation) coords = rotMatZ.dot(coords) # apply translation if hasattr(xFocal, "__len__"): # list of coords fed in transXYZ = numpy.array([transXYZ]*len(xFocal)).T xWok, yWok, zWok = coords - transXYZ else: # single set of xyz coords fed in xWok, yWok, zWok = coords - transXYZ # scale xyWok from dither analysis results xWok = xWok / fpScale # from gfa: 0.9998899 yWok = yWok / fpScale # from gfa: 0.9998899 if projectFlat: # force all z coords to be positioner height # necessary for a flat wok model # and this should be removed if we go # to curved wok # !!!!!!!!!!!!!!!!! if hasattr(xFocal, "__len__"): zWok = numpy.array([defaults.POSITIONER_HEIGHT]*len(xFocal)).T else: zWok = defaults.POSITIONER_HEIGHT return xWok, yWok, zWok
[docs]def wokToFocal( xWok, yWok, zWok, positionAngle=0, xOffset=0, yOffset=0, zOffset=0, tiltX=0, tiltY=0, fpScale=1, b=None, R=None ): """Convert xyz wok coordinates in mm to xyz focal coordinates in mm. The origin of the focal coordinate system is the M1 vertex. focal +y points toward North, +x points toward E. The origin of the wok coordinate system is the wok vertex. -x points toward the boss slithead. +z points from telescope to sky. Tilt is applied first about x axis then y axis. Note: currently zWok is irrelevant using a flat wok model. Parameters ------------- xWok: scalar or 1D array x position of object in wok space mm (+x aligned with +RA on image at PA=0) yWok: scalar or 1D array y position of object in wok space mm (+y aligned with +Dec on image at PA=0) zWok: scalar or 1D array z position of object in wok space mm (+z aligned boresight and increases from the telescope to the sky) positionAngle: scalar position angle deg. Angle measured from (image) North through East to wok +y. So position angle of 45 deg, wok +y points NE xOffset: scalar or None x position (mm) of wok origin (vertex) in focal coords calibrated yOffset: scalar y position (mm) of wok origin (vertex) in focal coords calibratated zOffset: scalar z position (mm) of wok origin (vertex) in focal coords calibratated tiltX: scalar tilt (deg) of wok about focal x axis at PA=0 calibrated tiltY: scalar tilt (deg) of wok about focal y axis at PA=0 calibrated fpScale: scalar radial scale multiplier b: float z location of the center of curvature, dist from M1 vertex (mm) R: float radius of curvature (mm) Returns --------- xFocal: scalar or 1D array x position of object in focal coord sys mm (+x aligned with +RA on image) yFocal: scalar or 1D array y position of object in focal coord sys mm (+y aligned with +Dec on image) zFocal: scalar or 1D array z position of object in focal coord sys mm (+z aligned boresight and increases from the telescope to the sky) """ # scale xyWok from dither analysis results xWok = numpy.array(xWok) * fpScale # from gfa: 0.9998899 yWok = numpy.array(yWok) * fpScale # from gfa: 0.9998899 # this routine is a reversal of the steps # in the function focalToWok, with rotational # angles inverted and translations applied in reverse coords = numpy.array([xWok, yWok, zWok]) rotX = numpy.radians(-1*tiltX) rotY = numpy.radians(-1*tiltY) rotZ = numpy.radians(positionAngle) rotMatX = numpy.array([ [1, 0, 0], [0, numpy.cos(rotX), numpy.sin(rotX)], [0, -numpy.sin(rotX), numpy.cos(rotX)] ]) rotMatY = numpy.array([ [numpy.cos(rotY), 0, -numpy.sin(rotY)], [0, 1, 0], [numpy.sin(rotY), 0, numpy.cos(rotY)] ]) rotMatZ = numpy.array([ [numpy.cos(rotZ), numpy.sin(rotZ), 0], [-numpy.sin(rotZ), numpy.cos(rotZ), 0], [0, 0, 1] ]) transXYZ = numpy.array([xOffset, yOffset, zOffset]) # add offsets for reverse transform if hasattr(xWok, "__len__"): # list of coords fed in transXYZ = numpy.array([transXYZ]*len(xWok)).T coords = coords + transXYZ else: # single set of xyz coords fed in coords = coords + transXYZ coords = rotMatZ.dot(coords) coords = rotMatY.dot(coords) xFocal, yFocal, zFocal = rotMatX.dot(coords) if b is not None or R is not None: # calculate z coords based on xy # basically force the point to land on the focal # surface for a given wavelength # necessary for a flat wok model # and this should be removed if we go # to curved wok # !!!!!!!!!!!!!!!!! if R is None or b is None: raise CoordIOError("Must specify both b and R if they are not None") rFocal = numpy.sqrt(xFocal**2+yFocal**2) zFocal = -1*numpy.sqrt(R**2-rFocal**2) + b return xFocal, yFocal, zFocal
def _verify3Vector(checkMe, label): if not hasattr(checkMe, "__len__"): raise RuntimeError("%s must be a 3-vector"%label) checkMe = numpy.array(checkMe).squeeze() if len(checkMe) != 3: raise RuntimeError("%s must be a 3-vector"%label) if checkMe.ndim != 1: raise RuntimeError("%s must be a 3-vector"%label) return checkMe
[docs]def repeat_scalar(input, n): """Returns an array with an scalar repeated ``n`` times. Does not affect arrays.""" if numpy.isscalar(input): return numpy.repeat(input, n) return input
[docs]def wokToTangent(xWok, yWok, zWok, b, iHat, jHat, kHat, elementHeight=defaults.POSITIONER_HEIGHT, scaleFac=1, dx=0, dy=0, dz=0): """ Convert an array of wok coordinates to tangent coordinates. xyz Wok coords are mm with orgin at wok vertex. +z points from wok toward M2. -x points toward the boss slithead In the tangent coordinate frame the xy plane is tangent to the wok surface at xyz position b. The origin is set to be elementHeight above the wok surface. Parameters ------------- xWok: 1D array x position of object in wok space mm (+x aligned with +RA on image at PA = 0) yWok: 1D array y position of object in wok space mm (+y aligned with +Dec on image at PA = 0) zWok: 1D array z position of object in wok space mm (+z aligned boresight and increases from the telescope to the sky) b: 3-element 1D vector x,y,z position (mm) of each hole element on wok surface measured in wok coords iHat: 3-element 1D vector x,y,z unit vector in wok coords that indicate the direction of the tangent coordinate x axis for each hole. jHat: 3-element 1D vector x,y,z unit vector in wok coords that indicate the direction of the tangent coordinate y axis for each hole. kHat: 3-element 1D vector x,y,z unit vector in wok coords that indicate the direction of the tangent coordinate z axis for each hole. elementHeight: 1D array height (mm) of positioner/GFA chip above wok surface scaleFac: 1D array scale factor to apply to b to account for thermal expansion of wok. scale is applied to b radially dx: 1D array x offset (mm), calibration to capture small displacements of tangent x dy: 1D array y offset (mm), calibration to capture small displacements of tangent y dz: 1D array z offset (mm), calibration to capture small displacements of tangent x Returns --------- xTangent: 1D array x position (mm) in tangent coordinates yTangent: 1D array y position (mm) in tangent coordinates zTangent: 1D array z position (mm) in tangent coordinates """ b = _verify3Vector(b, 'b').tolist() iHat = _verify3Vector(iHat, 'iHat').tolist() jHat = _verify3Vector(jHat, 'jHat').tolist() kHat = _verify3Vector(kHat, 'kHat').tolist() xyzWok = numpy.atleast_2d(numpy.array([xWok, yWok, zWok]).T).tolist() xyzTangent = libcoordio.wokToTangentArr( xyzWok, b, iHat, jHat, kHat, elementHeight, scaleFac, dx, dy, dz ) xyzTangent = numpy.array(xyzTangent) xTangent = xyzTangent[:, 0] yTangent = xyzTangent[:, 1] zTangent = xyzTangent[:, 2] return xTangent, yTangent, zTangent
def _wokToTangent(xWok, yWok, zWok, b, iHat, jHat, kHat, elementHeight=defaults.POSITIONER_HEIGHT, scaleFac=1, dx=0, dy=0, dz=0): """ Convert from wok coordinates to tangent coordinates. xyz Wok coords are mm with orgin at wok vertex. +z points from wok toward M2. -x points toward the boss slithead In the tangent coordinate frame the xy plane is tangent to the wok surface at xyz position b. The origin is set to be elementHeight above the wok surface. Parameters ------------- xWok: scalar or 1D array x position of object in wok space mm (+x aligned with +RA on image at PA = 0) yWok: scalar or 1D array y position of object in wok space mm (+y aligned with +Dec on image at PA = 0) zWok: scalar or 1D array z position of object in wok space mm (+z aligned boresight and increases from the telescope to the sky) b: 3-vector x,y,z position (mm) of element hole on wok surface measured in wok coords iHat: 3-vector x,y,z unit vector in wok coords that indicate the direction of the tangent coordinate x axis jHat: 3-vector x,y,z unit vector in wok coords that indicate the direction of the tangent coordinate y axis kHat: 3-vector x,y,z unit vector in wok coords that indicate the direction of the tangent coordinate z axis elementHeight: scalar height (mm) of positioner/GFA chip above wok surface scaleFac: scalar scale factor to apply to b to account for thermal expansion of wok. scale is applied to b radially dx: scalar x offset (mm), calibration to capture small displacements of tangent x dy: scalar y offset (mm), calibration to capture small displacements of tangent y dz: scalar z offset (mm), calibration to capture small displacements of tangent x Returns --------- xTangent: scalar or 1D array x position (mm) in tangent coordinates yTangent: scalar or 1D array y position (mm) in tangent coordinates zTangent: scalar or 1D array z position (mm) in tangent coordinates """ # check for problems b = _verify3Vector(b, "b") iHat = _verify3Vector(iHat, "iHat") jHat = _verify3Vector(jHat, "jHat") kHat = _verify3Vector(kHat, "kHat") coords = numpy.array([xWok,yWok,zWok]) # apply radial scale factor to b # assume that z is unaffected by # thermal expansion (probably reasonable) if scaleFac != 1: r = numpy.sqrt(b[0]**2+b[1]**2) theta = numpy.arctan2(b[1], b[0]) r = r*scaleFac b[0] = r*numpy.cos(theta) b[1] = r*numpy.sin(theta) isArr = hasattr(xWok, "__len__") calibOff = numpy.array([dx,dy,dz]) if isArr: b = numpy.array([b]*len(xWok)).T calibOff = numpy.array([calibOff]*len(xWok)).T # offset to wok base position coords = coords - b # rotate normal to wok surface at point b rotNorm = numpy.array([iHat, jHat, kHat], dtype="float64") coords = rotNorm.dot(coords) # offset xy plane to focal surface if isArr: coords[2,:] = coords[2,:] - elementHeight else: coords[2] = coords[2] - elementHeight # apply rotational calibration # if dRot != 0: # rotZ = numpy.radians(dRot) # rotMatZ = numpy.array([ # [numpy.cos(rotZ), numpy.sin(rotZ), 0], # [-numpy.sin(rotZ), numpy.cos(rotZ), 0], # [0, 0, 1] # ]) # coords = rotMatZ.dot(coords) coords = coords - calibOff return coords[0], coords[1], coords[2]
[docs]def tangentToWok(xTangent, yTangent, zTangent, b, iHat, jHat, kHat, elementHeight=defaults.POSITIONER_HEIGHT, scaleFac=1, dx=0, dy=0, dz=0): """ Convert from tangent coordinates at b to wok coordinates. xyz Wok coords are mm with origin at wok vertex. +z points from wok toward M2. -x points toward the boss slithead In the tangent coordinate frame the xy plane is tangent to the wok surface at xyz position b. The origin is set to be elementHeight above the wok surface. Parameters ------------- xTangent: scalar or 1D array x position (mm) in tangent coordinates yTangent: scalar or 1D array y position (mm) in tangent coordinates zTangent: scalar or 1D array z position (mm) in tangent coordinates b: 3-vector x,y,z position (mm) of element hole on wok surface measured in wok coords iHat: 3-vector x,y,z unit vector in wok coords that indicate the direction of the tangent coordinate x axis jHat: 3-vector x,y,z unit vector in wok coords that indicate the direction of the tangent coordinate y axis kHat: 3-vector x,y,z unit vector in wok coords that indicate the direction of the tangent coordinate z axis elementHeight: scalar height (mm) of positioner/GFA chip above wok surface scaleFac: scalar scale factor to apply to b to account for thermal expansion of wok. scale is applied to b radially dx: scalar x offset (mm), calibration to capture small displacements of tangent x dy: scalar y offset (mm), calibration to capture small displacements of tangent y dz: scalar z offset (mm), calibration to capture small displacements of tangent x Returns --------- xWok: scalar or 1D array x position of object in wok space mm (+x aligned with +RA on image at PA = 0) yWok: scalar or 1D array y position of object in wok space mm (+y aligned with +Dec on image at PA = 0) zWok: scalar or 1D array z position of object in wok space mm (+z aligned boresight and increases from the telescope to the sky) """ b = _verify3Vector(b, "b") iHat = _verify3Vector(iHat, "iHat") jHat = _verify3Vector(jHat, "jHat") kHat = _verify3Vector(kHat, "kHat") # format into list of lists if hasattr(xTangent, "__len__"): xyzTangent = numpy.array([xTangent,yTangent,zTangent]).T.tolist() xyzWok = libcoordio.tangentToWokArr( xyzTangent, list(b), list(iHat), list(jHat), list(kHat), elementHeight, scaleFac, dx, dy, dz) xyzWok = numpy.array(xyzWok) xWok = xyzWok[:,0] yWok = xyzWok[:,1] zWok = xyzWok[:,2] else: xWok, yWok, zWok = libcoordio.tangentToWok( [xTangent, yTangent, zTangent], list(b), list(iHat), list(jHat), list(kHat), elementHeight, scaleFac, dx, dy, dz ) return xWok, yWok, zWok
def _tangentToWok(xTangent, yTangent, zTangent, b, iHat, jHat, kHat, elementHeight=defaults.POSITIONER_HEIGHT, scaleFac=1, dx=0, dy=0, dz=0): """ Convert from tangent coordinates at b to wok coordinates. xyz Wok coords are mm with orgin at wok vertex. +z points from wok toward M2. -x points toward the boss slithead In the tangent coordinate frame the xy plane is tangent to the wok surface at xyz position b. The origin is set to be elementHeight above the wok surface. Parameters ------------- xTangent: scalar or 1D array x position (mm) in tangent coordinates yTangent: scalar or 1D array y position (mm) in tangent coordinates zTangent: scalar or 1D array z position (mm) in tangent coordinates b: 3-vector x,y,z position (mm) of element hole on wok surface measured in wok coords iHat: 3-vector x,y,z unit vector in wok coords that indicate the direction of the tangent coordinate x axis jHat: 3-vector x,y,z unit vector in wok coords that indicate the direction of the tangent coordinate y axis kHat: 3-vector x,y,z unit vector in wok coords that indicate the direction of the tangent coordinate z axis elementHeight: scalar height (mm) of positioner/GFA chip above wok surface scaleFac: scalar scale factor to apply to b to account for thermal expansion of wok. scale is applied to b radially dx: scalar x offset (mm), calibration to capture small displacements of tangent x dy: scalar y offset (mm), calibration to capture small displacements of tangent y dz: scalar z offset (mm), calibration to capture small displacements of tangent x Returns --------- xWok: scalar or 1D array x position of object in wok space mm (+x aligned with +RA on image at PA = 0) yWok: scalar or 1D array y position of object in wok space mm (+y aligned with +Dec on image at PA = 0) zWok: scalar or 1D array z position of object in wok space mm (+z aligned boresight and increases from the telescope to the sky) """ # check for problems b = _verify3Vector(b, "b") iHat = _verify3Vector(iHat, "iHat") jHat = _verify3Vector(jHat, "jHat") kHat = _verify3Vector(kHat, "kHat") calibOff = numpy.array([dx,dy,dz]) isArr = hasattr(xTangent, "__len__") if isArr: calibOff = numpy.array([calibOff]*len(xTangent)).T coords = numpy.array([xTangent,yTangent,zTangent]) # apply calibration offsets coords = coords + calibOff # apply rotational calibration # if dRot != 0: # rotZ = -1*numpy.radians(dRot) # rotMatZ = numpy.array([ # [numpy.cos(rotZ), numpy.sin(rotZ), 0], # [-numpy.sin(rotZ), numpy.cos(rotZ), 0], # [0, 0, 1] # ]) # coords = rotMatZ.dot(coords) # offset xy plane by element height if isArr: coords[2, :] = coords[2, :] + elementHeight else: coords[2] = coords[2] + elementHeight # rotate normal to wok surface at point b # invRotNorm = numpy.linalg.inv(numpy.array([iHat, jHat, kHat], dtype="float64")) # transpose is inverse! ortho-normal rows or unit vectors! invRotNorm = numpy.array([iHat, jHat, kHat], dtype="float64").T coords = invRotNorm.dot(coords) # offset to wok base position # apply radial scale factor to b # assume that z is unaffected by # thermal expansion (probably reasonable) if scaleFac != 1: r = numpy.sqrt(b[0]**2+b[1]**2) theta = numpy.arctan2(b[1], b[0]) r = r * scaleFac b[0] = r * numpy.cos(theta) b[1] = r * numpy.sin(theta) if isArr: b = numpy.array([b]*len(xTangent)).T coords = coords + b return coords[0], coords[1], coords[2]
[docs]def proj2XYplane(x, y, z, rayOrigin): """Given a point x, y, z orginating from rayOrigin, project this point onto the xy plane. Parameters ------------ x: scalar or 1D array x position of point to be projected y: scalar or 1D array y position of point to be projected z: scalar or 1D array z position of point to be projected rayOrigin: 3-vector [x,y,z] origin of ray(s) Returns -------- xProj: scalar or 1D array projected x value yProj: scalar or 1D array projected y value zProj: scalar or 1D array projected z value (should be zero always!) projDist: scalar or 1D array projected distance (a proxy for something like focus offset) """ # intersections of lines to planes... # http://geomalgorithms.com/a05-_intersect-1.html # intersect3D_SegmentPlane rayOrigin = _verify3Vector(rayOrigin, "rayOrigin") if hasattr(x, "__len__"): rayOrigin = numpy.array([rayOrigin] * len(z), dtype="float64").T x = numpy.array(x, dtype="float64") y = numpy.array(y, dtype="float64") z = numpy.array(z, dtype="float64") xyz = numpy.array([x, y, z], dtype="float64") u = xyz - rayOrigin w = rayOrigin zHat = numpy.array([0, 0, 1]) # normal to xy plane D = zHat.dot(u) N = -1 * zHat.dot(w) sI = N / D xyzProj = rayOrigin + sI * u projDist = numpy.linalg.norm(xyz - xyzProj, axis=0) # negative projDist for points below xy plane projDist *= numpy.sign(z) return xyzProj[0], xyzProj[1], xyzProj[2], projDist
# def tangentToPositioner2( # xTangent, yTangent, xBeta, yBeta, la=7.4, alphaOffDeg=0, betaOffDeg=0 # ): # """ # Determine alpha/beta positioner angles that place xBeta, yBeta coords in mm # at xTangent, yTangent. New CPP Version. Should never return NaN values # for alpha beta, and instead. # note: vectorized option not here yet. # todo: include hooks for positioner non-linearity # Parameters # ------------- # xTangent: float # x position (mm) in tangent coordinates # yTangent: float # y position (mm) in tangent coordinates # xBeta: float # x position (mm) in beta arm frame # yBeta: float # y position (mm) in beta arm frame # la: float # length (mm) of alpha arm # alphaOffDeg: float # alpha zeropoint offset (deg) # betaOffDeg: float # beta zeropoint offset (deg) # Returns # --------- # alphaDegRH: scalar or 1D array # alpha angle in degrees for a right-handed robot # betaDegRH: scalar or 1D array # beta angle in degrees for a right-handed robot # alphaDegLH: scalar or 1D array # alpha angle in degrees for a right-lefthanded robot # betaDegLH: scalar or 1D array # beta angle in degrees for a left-handed robot # err: scalar or 1D array # distance beween fiber and xyWok. This is non-zero when # xyWok is outside the positioners workspace # """ # # C++ wrapped stuff wants lists, not numpy arrays # alphaRH, betaRH, alphaLH, betaLH, dist = libcoordio.tangentToPositioner2( # [xTangent, yTangent], [xBeta, yBeta], # la, alphaOffDeg, betaOffDeg # ) # return alphaRH, betaRH, alphaLH, betaLH, dist
[docs]def tangentToPositioner( xTangent, yTangent, xBeta, yBeta, la=7.4, alphaOffDeg=0, betaOffDeg=0, lefthand=False ): """ Determine alpha/beta positioner angles that place xBeta, yBeta coords in mm at xTangent, yTangent. CPP Version. todo: include hooks for positioner non-linearity Parameters ------------- xTangent: scalar or 1D array x position (mm) in tangent coordinates yTangent: scalar or 1D array y position (mm) in tangent coordinates xBeta: scalar or 1D array x position (mm) in beta arm frame yBeta: scalar or 1D array y position (mm) in beta arm frame la: scalar or 1D array length (mm) of alpha arm alphaOffDeg: scalar alpha zeropoint offset (deg) betaOffDeg: scalar beta zeropoint offset (deg) lefthand: boolean whether to return a lefthand or righthand parity robot Returns --------- alphaDeg: scalar or 1D array alpha angle in degrees betaDeg: scalar or 1D array beta angle in degrees isOK: boolean or 1D boolean array True if point physically accessible, False otherwise """ # C++ wrapped stuff wants lists, not numpy arrays isArr = hasattr(xTangent, "__len__") if isArr: xyTangent = numpy.array([xTangent, yTangent]).T.tolist() if hasattr(xBeta, "__len__"): xyBeta = numpy.array([xBeta, yBeta]).T.tolist() else: xyBeta = [[xBeta, yBeta]]*len(xyTangent) alphaBeta = libcoordio.tangentToPositionerArr( xyTangent, xyBeta, la, alphaOffDeg, betaOffDeg, lefthand ) alphaBeta = numpy.array(alphaBeta) alpha = alphaBeta[:, 0] beta = alphaBeta[:, 1] else: alpha, beta = libcoordio.tangentToPositioner( [xTangent, yTangent], [xBeta, yBeta], la, alphaOffDeg, betaOffDeg, lefthand ) isOKAlpha = numpy.isfinite(alpha) isOKBeta = numpy.isfinite(beta) isOK = isOKAlpha & isOKBeta if not isArr: isOK = bool(isOK) return alpha, beta, isOK
def _tangentToPositioner( xTangent, yTangent, xBeta, yBeta, la=7.4, alphaOffDeg=0, betaOffDeg=0 ): """ Determine alpha/beta positioner angles that place xBeta, yBeta coords in mm at xTangent, yTangent. Python Version. todo: include hooks for positioner non-linearity note, didn't add left hand option here...use C++ Parameters ------------- xTangent: scalar or 1D array x position (mm) in tangent coordinates yTangent: scalar or 1D array y position (mm) in tangent coordinates xBeta: scalar or 1D array x position (mm) in beta arm frame yBeta: scalar or 1D array y position (mm) in beta arm frame la: scalar or 1D array length (mm) of alpha arm alphaOffDeg: scalar alpha zeropoint offset (deg) betaOffDeg: scalar beta zeropoint offset (deg) Returns --------- alphaDeg: scalar or 1D array alpha angle in degrees betaDeg: scalar or 1D array beta angle in degrees isOK: boolean or 1D boolean array True if point physically accessible, False otherwise """ isArr = hasattr(xTangent, "__len__") if isArr: xTangent = numpy.array(xTangent, dtype="float64") yTangent = numpy.array(yTangent, dtype="float64") if hasattr(xBeta, "__len__"): xBeta = numpy.array(xBeta, dtype="float64") yBeta = numpy.array(yBeta, dtype="float64") # polar coords jive better for this calculation thetaTangent = numpy.arctan2(yTangent, xTangent) rTangentSq = xTangent**2 + yTangent**2 # convert xy Beta to radial coords # the origin of the beta coord system is the # beta axis of rotation thetaBAC = numpy.arctan2(yBeta, xBeta) # radians! rBacSq = xBeta**2+yBeta**2 gamma = numpy.arccos( (la**2 + rBacSq - rTangentSq) / (2 * la * numpy.sqrt(rBacSq)) ) xi = numpy.arccos( (la**2 + rTangentSq - rBacSq) / (2 * la * numpy.sqrt(rTangentSq)) ) thetaTangent = numpy.degrees(thetaTangent) thetaBAC = numpy.degrees(thetaBAC) gamma = numpy.degrees(gamma) xi = numpy.degrees(xi) alphaDeg = thetaTangent - xi - alphaOffDeg betaDeg = 180 - gamma - thetaBAC - betaOffDeg # look for nans isOKAlpha = numpy.isfinite(alphaDeg) isOKBeta = numpy.isfinite(betaDeg) isOK = isOKAlpha & isOKBeta if not isArr: isOK = bool(isOK) # handle wrapping? not sure it's necessary alphaDeg = alphaDeg % 360 betaDeg = betaDeg % 360 # should already be there, but... # if hasattr(betaDeg, "__len__"): # if True in betaDeg > 180: # raise RuntimeError("problem in alpha/beta conversion! beta > 180") # elif betaDeg > 180: # raise RuntimeError("problem in alpha/beta conversion! beta > 180") return alphaDeg, betaDeg, isOK
[docs]def positionerToTangent( alphaDeg, betaDeg, xBeta, yBeta, la=7.4, alphaOffDeg=0, betaOffDeg=0 ): """ Determine tangent coordinates (mm) of xBeta, yBeta coords in mm from alpha/beta angle. CPP version. todo: include hooks for positioner non-linearity Parameters ------------- alphaDeg: scalar or 1D array alpha angle in degrees betaDeg: scalar or 1D array beta angle in degrees xBeta: scalar or 1D array x position (mm) in beta arm frame yBeta: scalar or 1D array y position (mm) in beta arm frame la: scalar or 1D array length (mm) of alpha arm alphaOffDeg: scalar alpha zeropoint offset (deg) betaOffDeg: scalar beta zeropoint offset (deg) Returns --------- xTangent: scalar or 1D array x position (mm) in tangent coordinates yTangent: scalar or 1D array y position (mm) in tangent coordinates """ if hasattr(alphaDeg, "__len__"): alphaBeta = numpy.array([alphaDeg, betaDeg]).T.tolist() if hasattr(xBeta, "__len__"): xyBeta = numpy.array([xBeta, yBeta]).T.tolist() else: xyBeta = [[xBeta, yBeta]] * len(alphaBeta) xyTangent = libcoordio.positionerToTangentArr( alphaBeta, xyBeta, la, alphaOffDeg, betaOffDeg ) xyTangent = numpy.array(xyTangent) xTangent = xyTangent[:, 0] yTangent = xyTangent[:, 1] else: xTangent, yTangent = libcoordio.positionerToTangent( [alphaDeg, betaDeg], [xBeta, yBeta], la, alphaOffDeg, betaOffDeg ) return xTangent, yTangent
def _positionerToTangent( alphaDeg, betaDeg, xBeta, yBeta, la=7.4, alphaOffDeg=0, betaOffDeg=0 ): """ Determine tangent coordinates (mm) of xBeta, yBeta coords in mm from alpha/beta angle. Python version. todo: include hooks for positioner non-linearity Parameters ------------- alphaDeg: scalar or 1D array alpha angle in degrees betaDeg: scalar or 1D array beta angle in degrees xBeta: scalar or 1D array x position (mm) in beta arm frame yBeta: scalar or 1D array y position (mm) in beta arm frame la: scalar or 1D array length (mm) of alpha arm alphaOffDeg: scalar alpha zeropoint offset (deg) betaOffDeg: scalar beta zeropoint offset (deg) Returns --------- xTangent: scalar or 1D array x position (mm) in tangent coordinates yTangent: scalar or 1D array y position (mm) in tangent coordinates """ # convert xy Beta to radial coords # the origin of the beta coord system is the # beta axis of rotation if hasattr(xBeta, "__len__"): xBeta = numpy.array(xBeta, dtype="float64") yBeta = numpy.array(yBeta, dtype="float64") thetaBAC = numpy.arctan2(yBeta, xBeta) # radians! rBAC = numpy.sqrt(xBeta**2 + yBeta**2) alphaRad = numpy.radians(alphaDeg+alphaOffDeg) betaRad = numpy.radians(betaDeg+betaOffDeg) cosAlpha = numpy.cos(alphaRad) sinAlpha = numpy.sin(alphaRad) cosAlphaBeta = numpy.cos(alphaRad + betaRad + thetaBAC) sinAlphaBeta = numpy.sin(alphaRad + betaRad + thetaBAC) xTangent = la * cosAlpha + rBAC * cosAlphaBeta yTangent = la * sinAlpha + rBAC * sinAlphaBeta return xTangent, yTangent
[docs]def tangentToGuide(xTangent, yTangent, xBin=1, yBin=1): xPix = (1 / xBin) * ( defaults.MICRONS_PER_MM / defaults.GFA_PIXEL_SIZE * xTangent + \ defaults.GFA_CHIP_CENTER ) yPix = (1 / yBin) * ( defaults.MICRONS_PER_MM / defaults.GFA_PIXEL_SIZE * yTangent + \ defaults.GFA_CHIP_CENTER ) return xPix, yPix
[docs]def guideToTangent(xPix, yPix, xBin=1, yBin=1): xTangent = ( (xPix * xBin - defaults.GFA_CHIP_CENTER) * defaults.GFA_PIXEL_SIZE / defaults.MICRONS_PER_MM ) yTangent = ( (yPix * yBin - defaults.GFA_CHIP_CENTER) * defaults.GFA_PIXEL_SIZE / defaults.MICRONS_PER_MM ) return xTangent, yTangent
# class FVCUW(object): # # right now this is for UWs test bench, fix image scale and use # # euclidean transforms # defaultScale = 0.026926930620282834 # defaultRotation = -0.005353542811111436 # defaultTranslation = numpy.array([-75.69606047, -48.68561274]) # # transform from CCD pixels to mm # def __init__(self): # self.tform = SimilarityTransform( # scale=self.defaultScale, # rotation=self.defaultRotation, # translation=self.defaultTranslation # ) # def fit(self, xpix, ypix, xWok, yWok): # xypixels = numpy.array([xpix, ypix]).T # xyWok = numpy.array([xWok,yWok]).T # self.tform.estimate(xypixels, xyWok) # def fvcToWok(self, xpix, ypix): # xypixels = numpy.array([xpix, ypix]).T # xyWok = self.tform(xypixels) # return xyWok[:,0], xyWok[:,1] # def wokToFVC(self, xWok, yWok): # xyWok = numpy.array([xWok, yWok]).T # xypix = self.tform.inverse(xyWok) # return xypix[:,0], xypix[:,1]