Package gavo :: Package base :: Module coords
[frames] | no frames]

Source Code for Module gavo.base.coords

  1  """ 
  2  (Mostly deprecated) code to handle coordinate systems and transform  
  3  between them.   
  4   
  5  Basically all of this should be taken over by stc and astropysics. 
  6  """ 
  7   
  8  #c Copyright 2008-2019, the GAVO project 
  9  #c 
 10  #c This program is free software, covered by the GNU GPL.  See the 
 11  #c COPYING file in the source distribution. 
 12   
 13   
 14  import math 
 15  import new 
 16  from math import sin, cos, pi #noflake: exported names 
 17  import re 
 18   
 19  import numpy 
 20   
 21  from gavo import utils 
 22  from gavo.utils import DEG 
 23  from gavo.utils import pgsphere 
 24  from gavo.utils import pyfits 
 25   
 26  # temporary measure to shut up astropy's configuration parser. 
 27  import __builtin__ 
 28  __builtin__._ASTROPY_SETUP_ = True 
 29  wcs = utils.DeferredImport("wcs", "from astropy import wcs") 
 30   
 31   
 32  fitsKwPat = re.compile("[A-Z0-9_-]{1,8}$") 
 33   
34 -def makePyfitsFromDict(d):
35 """returns a pyfits header with the cards of d.items(). 36 37 Only keys "looking like" FITS header keywords are used, i.e. all-uppercase 38 and shorter than 9 characters. 39 """ 40 res = pyfits.Header() 41 res.update(dict((str(key), val) 42 for key, val in d.iteritems() 43 if fitsKwPat.match(key) and val is not None)) 44 return res
45 46 47 _wcsTestDict = { 48 "CRVAL1": 0, "CRVAL2": 0, "CRPIX1": 50, "CRPIX2": 50, 49 "CD1_1": 0.01, "CD1_2": 0, "CD2_1": 0, "CD2_2": 0.01, 50 "NAXIS1": 100, "NAXIS2": 100, "CUNIT1": "deg", "CUNIT2": "deg", 51 "CTYPE1": 'RA---TAN-SIP', "CTYPE2": 'DEC--TAN-SIP', "LONPOLE": 180., 52 } 53 54
55 -class Box(object):
56 """is a 2D box. 57 58 The can be constructed either with two tuples, giving two points 59 delimiting the box, or with four arguments x0, x1, y0, y1. 60 61 To access the thing, you can either access the x[01], y[01] attributes 62 or use getitem to retrieve the upper right and lower left corner. 63 64 The slightly silly ordering of the bounding points (larger values 65 first) is for consistency with Postgresql. 66 """
67 - def __init__(self, x0, x1, y0=None, y1=None):
68 if y0 is None: 69 x0, y0 = x0 70 x1, y1 = x1 71 lowerLeft = (min(x0, x1), min(y0, y1)) 72 upperRight = (max(x0, x1), max(y0, y1)) 73 self.x0, self.y0 = upperRight 74 self.x1, self.y1 = lowerLeft
75
76 - def __getitem__(self, index):
77 if index==0 or index==-2: 78 return (self.x0, self.y0) 79 elif index==1 or index==-1: 80 return (self.x1, self.y1) 81 else: 82 raise IndexError("len(box) is always 2")
83
84 - def __str__(self):
85 return "((%.4g,%.4g), (%.4g,%.4g))"%(self.x0, self.y0, self.x1, self.y1)
86
87 - def __repr__(self):
88 return "Box((%g,%g), (%g,%g))"%(self.x0, self.y0, self.x1, self.y1)
89 90
91 -def getBbox(points):
92 """returns a bounding box for the sequence of 2-sequences points. 93 94 The thing returned is a coords.Box. 95 96 >>> getBbox([(0.25, 1), (-3.75, 1), (-2, 4)]) 97 Box((0.25,4), (-3.75,1)) 98 """ 99 xCoos, yCoos = [[p[i] for p in points] for i in range(2)] 100 return Box(min(xCoos), max(xCoos), min(yCoos), max(yCoos))
101 102
103 -def clampAlpha(alpha):
104 while alpha>360: 105 alpha -= 360 106 while alpha<0: 107 alpha += 360 108 return alpha
109 110
111 -def clampDelta(delta):
112 return max(-90, min(90, delta))
113 114
115 -def straddlesStitchingLine(minRA, maxRA):
116 """returns true if something bordered by minRA and maxRA presumably straddles 117 the stitching line. 118 119 This assumes minRA<maxRA, and that "something" is less than 180 degrees in 120 longitude. 121 122 Angles are in degrees here. 123 """ 124 return maxRA>270 and minRA<90
125 126
127 -def _calcFootprintMonkeypatch(self, hdr=None, undistort=True):
128 """returns the coordinates of the four corners of an image. 129 130 This is for monkeypatching astropy.wcs. pywcs, at least up to 1.11 did 131 really badly when non-spatial coordinates are present. TODO: see 132 if there's astropy code that does this better. 133 134 This method relies on the _monkey_naxis_lengths attribute left by getWCS to 135 figure out the axis lengths. 136 137 pywcs' hdr argument is always ignored here. 138 """ 139 naxis1, naxis2 = self._monkey_naxis_lengths 140 corners = [[1,1],[1,naxis2], [naxis1,naxis2], [naxis1, 1]] 141 if undistort: 142 return self.all_pix2world(corners, 1) 143 else: 144 return self.wcs_pix2world(corners,1)
145 146
147 -def _monkeypatchWCS(wcsObj, naxis, wcsFields):
148 """monkeypatches wcs.WCS instances for DaCHS' purposes. 149 """ 150 wcsObj._dachs_header = wcsFields 151 wcsObj.longAxis = naxis[0] 152 if len(naxis)>1: 153 wcsObj.latAxis = naxis[1] 154 wcsObj._monkey_naxis_lengths = [wcsFields.get("NAXIS%d"%i) 155 for i in naxis] 156 wcsObj.calcFootprint = new.instancemethod(_calcFootprintMonkeypatch, 157 wcsObj, wcsObj.__class__)
158 159
160 -def getWCS(wcsFields, naxis=(1,2), relax=True):
161 """returns a WCS instance from wcsFields 162 163 wcsFields can be either a dictionary or a pyfits header giving 164 some kind of WCS information, or an wcs.WCS instance that is 165 returned verbatim. 166 167 This will return None if no (usable) WCS information is found in the header. 168 169 We monkeypatch the resulting WCS structure quite a bit. Among 170 others: 171 172 * there's longAxis and latAxis attributes taken from naxis 173 * there's _dachs_header, containing the incoming k-v pairs 174 * there's _monkey_naxis_length, the lengths along the WCS axes. 175 """ 176 if isinstance(wcsFields, wcs.WCS): 177 return wcsFields 178 if isinstance(wcsFields, dict): 179 wcsFields = makePyfitsFromDict(wcsFields) 180 181 # pywcs used to invent identity transforms if no WCS keys are present. 182 # Hence. we do some sanity checking up front to weed those out. 183 if ("CD1_1" not in wcsFields 184 and "CDELT1" not in wcsFields 185 and "PC1_1" not in wcsFields 186 and "CNPIX1" not in wcsFields): 187 return None 188 189 # workaround for a bug in pywcs 1.11: .*_ORDER=0 must not happen 190 for key in ["AP_ORDER", "BP_ORDER", "A_ORDER", "B_ORDER"]: 191 if wcsFields.get(key)==0: 192 del wcsFields[key] 193 194 # wcslib (at least in 5.16) wants to do DSS corrections if they see 195 # either XPIXELSZ and YPIXELSZ; hide them, because that almost 196 # never works. 197 for key in ["XPIXELSZ", "YPIXELSZ"]: 198 if key in wcsFields: 199 del wcsFields[key] 200 201 wcsObj = wcs.WCS(wcsFields, relax=relax, naxis=naxis) 202 _monkeypatchWCS(wcsObj, naxis, wcsFields) 203 204 return wcsObj
205 206
207 -def pix2foc(wcsFields, pixels):
208 """returns the focal plane coordindates for the 2-sequence pixels. 209 210 (this is a thin wrapper intended to abstract for pix2world's funky 211 calling convention; also, we fix on the silly "0 pixel is 1 convention") 212 """ 213 wcsObj = getWCS(wcsFields) 214 val = wcsObj.pix2foc((pixels[0],), (pixels[1],), 1) 215 return val[0][0], val[1][0]
216 217
218 -def pix2sky(wcsFields, pixels):
219 """returns the sky coordindates for the 2-sequence pixels. 220 221 (this is a thin wrapper intended to abstract for pix2world's funky 222 calling convention; also, we fix on the silly "0 pixel is 1 convention") 223 """ 224 wcsObj = getWCS(wcsFields) 225 val = wcsObj.all_pix2world((pixels[0],), (pixels[1],), 1) 226 return val[0][0], val[1][0]
227 228
229 -def sky2pix(wcsFields, longLat):
230 """returns the pixel coordindates for the 2-sequence longLad. 231 232 (this is a thin wrapper intended to abstract for world2pix's funky 233 calling convention; also, we fix on the silly "0 pixel is 1 convention") 234 """ 235 val = getWCS(wcsFields).wcs_world2pix((longLat[0],), (longLat[1],), 1) 236 return val[0][0], val[1][0]
237 238
239 -def getPixelSizeDeg(wcsFields):
240 """returns the sizes of a pixel at roughly the center of the image for 241 wcsFields. 242 243 Near the pole, this gets a bit weird; we do some limitation of the width 244 of RA pixels there. 245 """ 246 wcs = getWCS(wcsFields) 247 width, height = wcs._dachs_header["NAXIS1"], wcs._dachs_header["NAXIS2"] 248 cosDelta = max(0.01, math.cos(pix2sky(wcs, (width/2, height/2))[1]*DEG)) 249 250 p0 = pix2sky(wcs, (width/2, height/2)) 251 p1 = pix2sky(wcs, (width/2+1, height/2)) 252 p2 = pix2sky(wcs, (width/2, height/2+1)) 253 return abs(p1[0]-p0[0])*cosDelta, abs(p2[1]-p0[1])
254 255
256 -def getWCSTrafo(wcsFields):
257 """returns a callable transforming pixel to physical coordinates. 258 259 wcsFields is passed to getWCS, see there for legal types. 260 """ 261 wcs = getWCS(wcsFields) 262 return lambda x, y: pix2sky(wcs, (x, y))
263 264
265 -def getInvWCSTrafo(wcsFields):
266 """returns a callable transforming physical to pixel coordinates. 267 268 wcsFields is passed to getWCS, see there for legal types. 269 """ 270 wcs = getWCS(wcsFields) 271 return lambda ra, dec: sky2pix(wcs, (ra,dec))
272 273
274 -def getBboxFromWCSFields(wcsFields):
275 """returns a bbox and a field center for WCS FITS header fields. 276 277 wcsFields is passed to getWCS, see there for legal types. 278 279 Warning: this is different from wcs.calcFootprint in that 280 we keep negative angles if the stitching line is crossed; also, 281 no distortions or anything like that are taken into account. 282 283 This code is only used for bboxSIAP, and you must not use it 284 for anything else; it's going to disappear with it. 285 """ 286 wcs = getWCS(wcsFields) 287 width, height = float(wcs._dachs_header["NAXIS1"] 288 ), float(wcs._dachs_header["NAXIS2"]) 289 cA, cD = pix2sky(wcs, (width/2., height/2.)) 290 wA, wD = getPixelSizeDeg(wcs) 291 wA *= width/2. 292 wD *= height/2. 293 # Compute all "corners" to ease handling of corner cases 294 bounds = [(cA+wA, cD+wD), (cA-wA, cD-wD), (cA+wA, cD-wD), 295 (cA-wA, cD+wD)] 296 bbox = getBbox(bounds) 297 if bbox[0][1]>89: 298 bbox = Box((0, clampDelta(bbox[0][1])), (360, clampDelta(bbox[1][1]))) 299 if bbox[1][1]<-89: 300 bbox = Box((0, clampDelta(bbox[0][1])), (360, clampDelta(bbox[1][1]))) 301 return bbox
302 303
304 -def getSpolyFromWCSFields(wcsFields):
305 """returns a pgsphere spoly corresponding to wcsFields 306 307 wcsFields is passed to getWCS, see there for legal types. 308 309 The polygon returned is computed by using the four corner points 310 assuming a rectangular image. This typically is only loosely related 311 to a proper spherical polygon describing the shape, as image boundaries 312 in the usual projects are not great circles. 313 314 Also, the spoly will be in the coordinate system of the WCS. If that 315 is not ICRS, you'll probably get something incompatible with most of the 316 VO. 317 """ 318 wcs = getWCS(wcsFields) 319 return pgsphere.SPoly([pgsphere.SPoint.fromDegrees(*p) 320 for p in wcs.calcFootprint(wcs._dachs_header)])
321 322
323 -def getCenterFromWCSFields(wcsFields, spatialAxes=(1,2)):
324 """returns RA and Dec of the center of an image described by wcsFields. 325 326 This will use the 1-based axes given by spatialAxes to figure out 327 the pixel lengths of the axes. 328 """ 329 wcs = getWCS(wcsFields) 330 center1 = wcs._dachs_header["NAXIS%s"%spatialAxes[0]]/2. 331 center2 = wcs._dachs_header["NAXIS%s"%spatialAxes[1]]/2. 332 return pix2sky(wcs, (center1, center2))
333 334
335 -def getCoveringCircle(wcsFields, spatialAxes=(1,2)):
336 """returns a pgsphere.scircle large enough to cover the image 337 described by wcsFields. 338 """ 339 wcs = getWCS(wcsFields) 340 center = getCenterFromWCSFields(wcs) 341 342 height, width = (wcs._dachs_header["NAXIS%s"%spatialAxes[0]], 343 wcs._dachs_header["NAXIS%s"%spatialAxes[1]]) 344 radius = max( 345 getGCDist(center, pix2sky(wcs, corner)) 346 for corner in [(0, 0), (height, 0), (0, width), (height, width)]) 347 348 return pgsphere.SCircle(pgsphere.SPoint.fromDegrees(*center), 349 radius*DEG)
350 351
352 -def getSkyWCS(hdr):
353 """returns a pair of a wcs.WCS instance and a sequence of 354 the spatial axes. 355 356 This will be None, () if no WCS could be discerned. There's some 357 heuristics involved in the identification of the spatial coordinates 358 that will probably fail for unconventional datasets. 359 """ 360 wcsAxes = [] 361 # heuristics: iterate through CTYPEn, anything that's got 362 # a - is supposed to be a position (needs some refinement :-) 363 for ind in range(1, hdr["NAXIS"]+1): 364 if "-" in hdr.get("CTYPE%s"%ind, ""): 365 wcsAxes.append(ind) 366 367 if not wcsAxes: 368 # more heuristics to be inserted here 369 return None, () 370 371 if len(wcsAxes)!=2: 372 raise utils.ValidationError("This FITS has !=2" 373 " spatial WCS axes. Please contact the DaCHS authors and" 374 " make them support it.", "PUBDID") 375 376 return getWCS(hdr, naxis=wcsAxes), wcsAxes
377 378
379 -def getPixelLimits(cooPairs, wcsFields):
380 """returns pixel cutout slices for covering cooPairs in an image with 381 wcsFields. 382 383 cooPairs is a sequence of (ra, dec) tuples. wcsFields is a DaCHS-enhanced 384 wcs.WCS instance. 385 386 Behaviour if cooPairs use a different coordinate system from wcsFields 387 is undefined at this point. 388 389 Each cutout slice is a tuple of (FITS axis number, lower limit, upper limit). 390 391 If cooPairs is off the wcsFields coverage, a null cutout on the longAxis 392 is returned. 393 """ 394 latAxis = wcsFields.latAxis 395 longAxis = wcsFields.longAxis 396 latPixels = wcsFields._dachs_header["NAXIS%d"%latAxis] 397 longPixels = wcsFields._dachs_header["NAXIS%d"%longAxis] 398 399 # pywcs used to do really funny things when we "wrap around". Therefore, we 400 # clamp values to be within the pywcs-safe region. Note that 401 # due to spherical magic this is not enough to ensure +/-Inf behaviour 402 # for SODA; TODO: re-evaluate this for astropy.wcs at some point. 403 cooPairs = [( 404 min(359.99999, max(-89.9999, ra)), 405 min(89.9999, max(-89.9999, dec))) 406 for ra, dec in cooPairs] 407 408 slices = [] 409 pixelFootprint = numpy.asarray( 410 numpy.round(wcsFields.wcs_world2pix(cooPairs, 1)), numpy.int32) 411 pixelLimits = [ 412 [min(pixelFootprint[:,0]), max(pixelFootprint[:,0])], 413 [min(pixelFootprint[:,1]), max(pixelFootprint[:,1])]] 414 415 # see if we're completely off coverage 416 if pixelLimits[0][1]<0 or pixelLimits[1][1]<0: 417 return [[longAxis, 0, 0]] 418 if pixelLimits[0][0]>longPixels or pixelLimits[1][0]>latPixels: 419 return [[longAxis, 0, 0]] 420 421 # now crop to the actual pixel values 422 pixelLimits = [ 423 [max(pixelLimits[0][0], 1), min(pixelLimits[0][1], longPixels)], 424 [max(pixelLimits[1][0], 1), min(pixelLimits[1][1], latPixels)]] 425 426 if pixelLimits[0]!=[1, longPixels]: 427 slices.append([longAxis]+pixelLimits[0]) 428 if pixelLimits[1]!=[1, latPixels]: 429 slices.append([latAxis]+pixelLimits[1]) 430 return slices
431 432 433 # let's do a tiny vector type. It's really not worth getting some dependency 434 # for this.
435 -class Vector3(object):
436 """is a 3d vector that responds to both .x... and [0]... 437 438 >>> x, y = Vector3(1,2,3), Vector3(2,3,4) 439 >>> x+y 440 Vector3(3.000000,5.000000,7.000000) 441 >>> 4*x 442 Vector3(4.000000,8.000000,12.000000) 443 >>> x*4 444 Vector3(4.000000,8.000000,12.000000) 445 >>> x*y 446 20 447 >>> "%.6f"%abs(x) 448 '3.741657' 449 >>> print(abs((x+y).normalized())) 450 1.0 451 """
452 - def __init__(self, x, y=None, z=None):
453 if isinstance(x, tuple): 454 self.coos = x 455 else: 456 self.coos = (x, y, z)
457
458 - def __repr__(self):
459 return "Vector3(%f,%f,%f)"%tuple(self.coos)
460
461 - def __str__(self):
462 def cutoff(c): 463 if abs(c)<1e-10: 464 return 0 465 else: 466 return c
467 rounded = [cutoff(c) for c in self.coos] 468 return "[%.2g,%.2g,%.2g]"%tuple(rounded)
469
470 - def __getitem__(self, index):
471 return self.coos[index]
472
473 - def __mul__(self, other):
474 """does either scalar multiplication if other is not a Vector3, or 475 a scalar product. 476 """ 477 if isinstance(other, Vector3): 478 return self.x*other.x+self.y*other.y+self.z*other.z 479 else: 480 return Vector3(self.x*other, self.y*other, self.z*other)
481 482 __rmul__ = __mul__ 483
484 - def __div__(self, scalar):
485 return Vector3(self.x/scalar, self.y/scalar, self.z/scalar)
486
487 - def __add__(self, other):
488 return Vector3(self.x+other.x, self.y+other.y, self.z+other.z)
489
490 - def __sub__(self, other):
491 return Vector3(self.x-other.x, self.y-other.y, self.z-other.z)
492
493 - def __abs__(self):
494 return math.sqrt(self.x**2+self.y**2+self.z**2)
495
496 - def cross(self, other):
497 return Vector3(self.y*other.z-self.z*other.y, 498 self.z*other.x-self.x*other.z, 499 self.x*other.y-self.y*other.x)
500
501 - def normalized(self):
502 return self/abs(self)
503
504 - def getx(self): return self.coos[0]
505 - def setx(self, x): self.coos[0] = x
506 x = property(getx, setx)
507 - def gety(self): return self.coos[1]
508 - def sety(self, y): self.coos[1] = y
509 y = property(gety, sety)
510 - def getz(self): return self.coos[2]
511 - def setz(self, z): self.coos[2] = z
512 z = property(getz, setz) 513 514
515 -def sgn(a):
516 if a<0: 517 return -1 518 elif a>0: 519 return 1 520 else: 521 return 0
522 523
524 -def computeUnitSphereCoords(alpha, delta):
525 # TODO: replaced by mathtricks.spherToCart 526 """returns the 3d coordinates of the intersection of the direction 527 vector given by the spherical coordinates alpha and delta with the 528 unit sphere. 529 530 alpha and delta are given in degrees. 531 532 >>> print(computeUnitSphereCoords(0,0)) 533 [1,0,0] 534 >>> print(computeUnitSphereCoords(0, 90)) 535 [0,0,1] 536 >>> print(computeUnitSphereCoords(90, 90)) 537 [0,0,1] 538 >>> print(computeUnitSphereCoords(90, 0)) 539 [0,1,0] 540 >>> print(computeUnitSphereCoords(180, -45)) 541 [-0.71,0,-0.71] 542 """ 543 return Vector3(*utils.spherToCart(alpha*DEG, delta*DEG))
544 545
546 -def dirVecToCelCoos(dirVec):
547 """returns alpha, delta in degrees for the direction vector dirVec. 548 549 >>> dirVecToCelCoos(computeUnitSphereCoords(25.25, 12.125)) 550 (25.25, 12.125) 551 >>> dirVecToCelCoos(computeUnitSphereCoords(25.25, 12.125)*16) 552 (25.25, 12.125) 553 >>> "%g,%g"%dirVecToCelCoos(computeUnitSphereCoords(25.25, 12.125)+ 554 ... computeUnitSphereCoords(30.75, 20.0)) 555 '27.9455,16.0801' 556 """ 557 dirVec = dirVec.normalized() 558 alpha = math.atan2(dirVec.y, dirVec.x) 559 if alpha<0: 560 alpha += 2*math.pi 561 return alpha*180./math.pi, math.asin(dirVec.z)*180./math.pi
562 563
564 -def getTangentialUnits(cPos):
565 """returns the unit vectors for RA and Dec at the unit circle position cPos. 566 567 We compute them by solving u_1*p_1+u_2*p_2=0 (we already know that 568 u_3=0) simultaneously with u_1^2+u_2^2=1 for RA, and by computing the 569 cross product of the RA unit and the radius vector for dec. 570 571 This becomes degenerate at the poles. If we're exactly on a pole, 572 we *define* the unit vectors as (1,0,0) and (0,1,0). 573 574 Orientation is a pain -- the convention used here is that unit delta 575 always points to the pole. 576 577 >>> cPos = computeUnitSphereCoords(45, -45) 578 >>> ua, ud = getTangentialUnits(cPos) 579 >>> print abs(ua), abs(ud), cPos*ua, cPos*ud 580 1.0 1.0 0.0 0.0 581 >>> print ua, ud 582 [-0.71,0.71,0] [-0.5,-0.5,-0.71] 583 >>> ua, ud = getTangentialUnits(computeUnitSphereCoords(180, 60)) 584 >>> print ua, ud 585 [0,-1,0] [0.87,0,0.5] 586 >>> ua, ud = getTangentialUnits(computeUnitSphereCoords(0, 60)) 587 >>> print ua, ud 588 [0,1,0] [-0.87,0,0.5] 589 >>> ua, ud = getTangentialUnits(computeUnitSphereCoords(0, -60)) 590 >>> print ua, ud 591 [0,1,0] [-0.87,0,-0.5] 592 """ 593 try: 594 normalizer = 1/math.sqrt(cPos.x**2+cPos.y**2) 595 except ZeroDivisionError: 596 return Vector3(1,0,0), Vector3(0,1,0) 597 alphaUnit = normalizer*Vector3(cPos.y, -cPos.x, 0) 598 deltaUnit = normalizer*Vector3(cPos.x*cPos.z, cPos.y*cPos.z, 599 -cPos.x**2-cPos.y**2) 600 # now orient the vectors: in delta, we always look towards the pole 601 if sgn(cPos.z)!=sgn(deltaUnit.z): 602 deltaUnit = -1*deltaUnit # XXX this breaks on the equator 603 # The orientation of alphaUnit depends on the hemisphere 604 if cPos.z<0: # south 605 if deltaUnit.cross(alphaUnit)*cPos<0: 606 alphaUnit = -1*alphaUnit 607 else: # north 608 if deltaUnit.cross(alphaUnit)*cPos>0: 609 alphaUnit = -1*alphaUnit 610 return alphaUnit, deltaUnit
611 612
613 -def movePm(alphaDeg, deltaDeg, pmAlpha, pmDelta, timeDiff, foreshort=0):
614 """returns alpha and delta for an object with pm pmAlpha after timeDiff. 615 616 pmAlpha has to have cos(delta) applied, everything is supposed to be 617 in degrees, the time unit is yours to choose. 618 """ 619 alpha, delta = alphaDeg/180.*math.pi, deltaDeg/180.*math.pi 620 pmAlpha, pmDelta = pmAlpha/180.*math.pi, pmDelta/180.*math.pi 621 sd, cd = math.sin(delta), math.cos(delta) 622 sa, ca = math.sin(alpha), math.cos(alpha) 623 muAbs = math.sqrt(pmAlpha**2+pmDelta**2); 624 muTot = muAbs+0.5*foreshort*timeDiff; 625 626 if muAbs<1e-20: 627 return alphaDeg, deltaDeg 628 # this is according to Mueller, 115 (4.94) 629 dirA = pmAlpha/muAbs; 630 dirD = pmDelta/muAbs; 631 sinMot = sin(muTot*timeDiff); 632 cosMot = cos(muTot*timeDiff); 633 634 dirVec = Vector3(-sd*ca*dirD*sinMot - sa*dirA*sinMot + cd*ca*cosMot, 635 -sd*sa*dirD*sinMot + ca*dirA*sinMot + cd*sa*cosMot, 636 +cd*dirD*sinMot + sd*cosMot) 637 return dirVecToCelCoos(dirVec)
638 639
640 -def getGCDist(pos1, pos2):
641 """returns the distance along a great circle between two points. 642 643 The distance is in degrees, the input positions are in degrees. 644 """ 645 scalarprod = computeUnitSphereCoords(*pos1)*computeUnitSphereCoords(*pos2) 646 # cope with numerical trouble 647 if scalarprod>=1: 648 return 0 649 return math.acos(scalarprod)/DEG
650 651
652 -def _test():
653 import doctest, coords 654 doctest.testmod(coords)
655 656 657 if __name__=="__main__": 658 _test() 659