1 """
2 Bindings for the pgsphere libarary and psycopg2.
3
4 Basically, once per program run, you need to call preparePgSphere(connection),
5 and you're done.
6
7 All native representation is in rad.
8 """
9
10
11
12
13
14
15
16 import math
17 import re
18 import tempfile
19 from cStringIO import StringIO
20
21 from gavo.utils import codetricks
22 from gavo.utils import excs
23 from gavo.utils import mathtricks
24 from gavo.utils import misctricks
25 from gavo.utils import texttricks
26 from gavo.utils.fitstools import pyfits
27 from gavo.utils.mathtricks import DEG
28
29
30 _TRAILING_ZEROES = re.compile("0+(\s|$)")
32 """remove zeroes in front of whitespace or the string end.
33
34 This is used for cosmetics in STC-S strings.
35
36 >>> removeTrailingZeroes("23.3420 21.2 12.00000")
37 '23.342 21.2 12.'
38 """
39 return _TRAILING_ZEROES.sub(r"\1", s)
40
43 """is raised when an SBox is constructed from center and size such that
44 it overlaps the pole.
45 """
47 self.box1, self.box2 = box1, box2
48
49
50 -def _query(conn, query, pars=None):
55
58 """A base class for objects adapting pgSphere objects.
59
60 The all need a pgType attribute and two static methods
61 _adaptToPgSphere(obj) and _castFromPgSphere(value, cursor).
62
63 You must also define a sequence checkedAttributes; all attributes
64 metioned there must be equal for two adapted values to be equal (equality
65 testing here really is mainly for unit tests with hand-crafted values).
66
67 Also, all subclasses you should provide an asPoly returning a spherical
68 polygon. This is used when uploading VOTables with REGION columns.
69
70 In practice, you also want:
71
72 fromDALI(cls, daliSeq) -- a classmethod turning the DALI votable
73 representation (typically, float arrays in degrees) into a corresponding
74 object.
75 asDALI(self) -- the inverse of fromDALI
76 asSODA(self) -- returns a representation of the geometry as
77 used in SODA parameters.
78 """
79 pgType = None
80
88
90 return not self==other
91
93 return "<pgsphere %s>"%self.asSTCS("Unknown")
94
96 raise ValueError("%s objects cannot be turned into polygons."%
97 self.__class__)
98
100 """returns the "SODA-form" for this circle.
101
102 This is a string containing blank-separated float literals; the
103 floats are what asDALI returns, and hence this is built on top of
104 asDALI. We don't worry about the fact that the coordinates just
105 *might* be non-ICRS.
106 """
107 return removeTrailingZeroes(
108 " ".join("%.10f"%v for v in self.asDALI()))
109
112 """A point on a sphere from pgSphere.
113
114 >>> SPoint(1, 1).asSTCS("ICRS")
115 'Position ICRS 57.2957795131 57.2957795131'
116 >>> SPoint.fromDegrees(1, -1).asSTCS("ICRS")
117 'Position ICRS 1. -1.'
118 """
119 pgType = "spoint"
120 checkedAttributes = ["x", "y"]
121 pattern = re.compile(r"\s*\(\s*([0-9.e-]+)\s*,\s*([0-9.e-]+)\s*\)")
122
125
127 return "SPoint(%r, %r)"%(self.x, self.y)
128
129 @staticmethod
131 return AsIs("spoint '(%.10f,%.10f)'"%(spoint.x, spoint.y))
132
133 @classmethod
137
138 @classmethod
140 if x is None or y is None:
141 return None
142 return cls(x*DEG, y*DEG)
143
145 """returns this point as (long, lat) in degrees.
146 """
147 return (self.x/DEG, self.y/DEG)
148
149 - def asSTCS(self, systemString):
152
154 return "spoint '(%.10f,%.10f)'"%(self.x, self.y)
155
157 return "(%r, %r)"%(self.x, self.y)
158
160 """returns self as a triple of cx, cy, cz on the unit sphere.
161 """
162 return mathtricks.spherToCart(self.x, self.y)
163
166
167 @classmethod
170
173 """A spherical circle from pgSphere.
174
175 The constructor accepts an SPoint center and a radius in rad.
176 """
177 pgType = "scircle"
178 checkedAttributes = ["center", "radius"]
179 pattern = re.compile("<(\([^)]*\))\s*,\s*([0-9.e-]+)>")
180
182 self.center, self.radius = center, float(radius)
183
184 @staticmethod
186 return AsIs("scircle '< %s, %r >'"%(sc.center.p(), sc.radius))
187
188 @classmethod
193
195 """returns the "DALI-form" for this circle.
196
197 This is an array containing the center coordinates and the radius
198 in degrees.
199 """
200 return (self.center.x/DEG, self.center.y/DEG, self.radius/DEG)
201
202 @classmethod
204 """returns a circle from its DALI float sequence.
205
206 Any None in daliSeq makes this a None to help with VOTable null value
207 handling.
208 """
209 if None in daliSeq:
210 return None
211 ra, dec, radius = [float(s) for s in daliSeq]
212 return cls(SPoint.fromDegrees(ra, dec), radius*DEG)
213
214 - def asSTCS(self, systemString):
217
219 return "scircle '< (%.10f, %.10f), %.10f >'"%(
220 self.center.x, self.center.y, self.radius)
221
222 - def asPoly(self, nSegments=32):
223
224
225
226
227
228 r = math.sin(self.radius)
229 innerOffset = math.cos(self.radius)
230 rotationMatrix = mathtricks.getRotZ(math.pi/2-self.center.x).matMul(
231 mathtricks.getRotX(math.pi/2-self.center.y))
232
233 points = []
234 for i in range(nSegments):
235 angle = 2.*i*math.pi/nSegments
236 dx, dy = r*math.sin(angle), r*math.cos(angle)
237 points.append(SPoint(
238 *mathtricks.cartToSpher(rotationMatrix.vecMul((dx, dy, innerOffset)))))
239 return SPoly(points)
240
241
242 -class SPoly(PgSAdapter):
243 """A spherical polygon from pgSphere.
244
245 The constructor accepts a list points of SPoints.
246 """
247 pgType = "spoly"
248 checkedAttributes = ["points"]
249 pattern = re.compile("\([^)]+\)")
250
252 self.points = tuple(points)
253
254 @staticmethod
256 return AsIs("spoly '{%s}'"%(", ".join(p.p() for p in spoly.points)))
257
258 @classmethod
263
265 """returns the DALI form of this polygon.
266
267 This is a sequence of floats of the vertex coordinates in degrees.
268 """
269 res = []
270 for p in self.points:
271 res.extend([p.x/DEG, p.y/DEG])
272 return res
273
274 @classmethod
276 """returns a polygon from a DALI float-sequence
277
278 This returns None for empty daliSeqs to help with VOTable NULL value
279 handling.
280 """
281 if not daliSeq:
282 return None
283 return cls([SPoint.fromDegrees(*tuple(p))
284 for p in misctricks.grouped(2, daliSeq)])
285
287 """returns the vertices as a sequence of (long, lat) pairs in
288 degrees.
289
290 This form is required by some functions from base.coords.
291 """
292 return [p.asCooPair() for p in self.points]
293
294 - def asSTCS(self, systemString):
297
299 return "spoly '{%s}'"%(",".join("(%.10f,%.10f)"%(p.x, p.y)
300 for p in self.points))
301
304
305 - def asSMoc(self, order=6, inclusive=True):
306 """returns an SMoc instance for this polygon.
307
308 If inclusive is False, do not include healpixes that lie
309
310 order is the maximum order of the moc returned.
311 """
312 import healpy
313 pixels = healpy.query_polygon(vertices=[
314 p.asUnitSphereVec() for p in self.points],
315 nside=2**order, nest=True)
316 return SMoc.fromCells(order, pixels)
317
318
319 -class SBox(PgSAdapter):
320 """A spherical box from pgSphere.
321
322 The constructor accepts the two corner points.
323 """
324 pgType = "sbox"
325 checkedAttributes = ["corner1", "corner2"]
326 pattern = re.compile("\([^()]+\)")
327
329 self.corner1, self.corner2 = corner1, corner2
330
331 @staticmethod
333 return AsIs("sbox '(%s, %s)'"%(sbox.corner1.p(), sbox.corner2.p()))
334
335 @classmethod
340
341 @classmethod
343 """returns an SBox corresponding to what SIAP passes in.
344
345 In particular, all values are in degrees, and a cartesian projection
346 is assumed.
347
348 This is for use with SIAP and tries to partially implement that silly
349 prescription of "folding" over at the poles. If that happens,
350 a TwoSBoxes exception is raised. It contains two SBoxes that
351 should be ORed. I agree that sucks. Let's fix SIAP.
352 """
353 if 90-abs(dec)<0.1:
354 raSize = 360
355 else:
356 raSize = raSize/math.cos(dec*DEG)
357 decSize = abs(decSize)
358 minRA, maxRA = ra-raSize/2., ra+raSize/2.
359 bottom, top = dec-decSize/2., dec+decSize/2.
360
361
362
363
364
365 if bottom<-90 and top>90:
366 raise ValueError("Cannot fold over at both poles")
367 elif bottom<-90:
368 raise TwoSBoxes(
369 cls(
370 SPoint.fromDegrees(minRA, -90),
371 SPoint.fromDegrees(maxRA, top)),
372 cls(
373 SPoint.fromDegrees(180+minRA, -90),
374 SPoint.fromDegrees(180+maxRA, top)))
375 elif top>90:
376 raise TwoSBoxes(
377 cls(
378 SPoint.fromDegrees(minRA, bottom),
379 SPoint.fromDegrees(maxRA, 90)),
380 cls(
381 SPoint.fromDegrees(180+minRA, bottom),
382 SPoint.fromDegrees(180+maxRA, 90)))
383 return cls(SPoint.fromDegrees(minRA, bottom),
384 SPoint.fromDegrees(maxRA, top))
385
386 - def asSTCS(self, systemString):
387 return removeTrailingZeroes("PositionInterval %s %s %s"%(systemString,
388 "%.10f %.10f"%(self.corner1.x/DEG, self.corner1.y/DEG),
389 "%.10f %.10f"%(self.corner2.x/DEG, self.corner2.y/DEG)))
390
392 x1, y1 = self.corner1.x, self.corner1.y
393 x2, y2 = self.corner2.x, self.corner2.y
394 minX, maxX = min(x1, x2), max(x1, x2)
395 minY, maxY = min(y1, y2), max(y1, y2)
396 return SPoly((
397 SPoint(minX, minY),
398 SPoint(minX, maxY),
399 SPoint(maxX, maxY),
400 SPoint(maxX, minY)))
401
402 @classmethod
408
410
411
412 return self.corner1.asDALI()+self.corner2.asDALI()
413
414
415
416 import __builtin__
417 __builtin__._ASTROPY_SETUP_ = True
418
419
420 -class SMoc(PgSAdapter):
421 """a MOC with a pgsphere interface.
422
423 The default constructor accepts a pymoc.MOC instance, which is
424 accessible as the moc attribute. The database interface uses
425 the ASCII serialisation, for which there's the fromASCII constructor.
426 """
427 pgType = "smoc"
428 checkedAttributes = ["moc"]
429
432
433 _orderHeadRE = re.compile(r"(\d+)/")
434 _rangeRE = re.compile(r"\s*(\d+)(?:-(\d+))?\s*$")
435
436 @classmethod
438 """returns a sequence of cells from a MOC cell literal.
439
440 firstPos is the string position of the beginning of cellLiteral
441 for error messages.
442 """
443 curPos = 0
444 cells = []
445
446 for item in cellLiteral.split(","):
447 mat = cls._rangeRE.match(item)
448 if not mat:
449 raise ValueError("MOC literal syntax error at char %s"%
450 (firstPos+curPos))
451
452 if mat.group(2) is None:
453 cells.append(int(mat.group(1)))
454 else:
455 cells.extend(range(int(mat.group(1)), int(mat.group(2))+1))
456
457 curPos += len(item)+1
458
459 return cells
460
461 @classmethod
463 """returns an SMoc from a quasi-standard ASCII serialisation.
464 """
465 import pymoc
466
467
468
469 seps = list(cls._orderHeadRE.finditer(literal))+[re.search("$", literal)]
470 if len(seps)==1:
471 raise ValueError("No order separator visible in MOC literal '%s'"%
472 texttricks.makeEllipsis(literal, 40))
473 if not re.match(r"\s*$", literal[:seps[0].start()]):
474 raise ValueError("MOC literal '%s' does not start with order spec"%
475 texttricks.makeEllipsis(literal, 40))
476
477 moc = pymoc.MOC()
478
479 for openMat, closeMat in codetricks.iterRanges(seps):
480 order = int(openMat.group(1))
481 cells = cls._parseCells(literal[openMat.end():closeMat.start()],
482 openMat.end())
483 moc.add(order, cells)
484
485 return cls(moc)
486
487 @classmethod
489 """returns a SMoc instance from a collection of pixels at order.
490 """
491 import pymoc
492
493 moc = pymoc.MOC(order=order, cells=pixels)
494 moc.normalize()
495 return cls(moc)
496
497 @staticmethod
505
507 """returns an ascii serialisation of this MOC.
508 """
509
510
511 parts = []
512 for order, cells in self.moc:
513 ranges = []
514 rmin = rmax = None
515
516 for cell in sorted(cells):
517 if rmin is None:
518 rmin = rmax = cell
519 elif rmax==cell-1:
520 rmax = cell
521 else:
522 ranges.append(self._formatASCIIRange(rmin, rmax))
523 rmin = rmax = cell
524
525 ranges.append(self._formatASCIIRange(rmin, rmax))
526 parts.append("%d/%s"%(order, ",".join(ranges)))
527
528 return " ".join(parts)
529
530 @classmethod
532 """returns an SMoc from a string containing a FITS-serialised MOC.
533 """
534 from pymoc.io.fits import read_moc_fits_hdu
535 import pymoc
536
537 moc = pymoc.MOC()
538 read_moc_fits_hdu(moc,
539 pyfits.open(StringIO(literal))[1])
540 return cls(moc)
541
542 @staticmethod
544 return AsIs("smoc '%s'"%(smoc.asASCII()))
545
546 @classmethod
550
552 raise TypeError("MOCs cannot be represented as polygons")
553
555
556
557 return "MOC "+self.asASCII()
558
560 """returns a copy of self, normalised for order.
561 """
562 moc = self.moc.copy()
563 moc.normalize(order)
564 return self.__class__(moc)
565
567 """returns a png string with a plot visualising this moc.
568 """
569 from pymoc.util.plot import plot_moc
570 with tempfile.NamedTemporaryFile(suffix=".png") as f:
571 plot_moc(self.moc, filename=f.name, projection="moll", **kwargs)
572 return f.read()
573
575 """returns a standard FITS (table) representation of this MOC.
576 """
577 from pymoc.io.fits import write_moc_fits
578
579 with tempfile.NamedTemporaryFile(suffix=".fits") as f:
580 write_moc_fits(self.moc, f)
581 f.seek(0)
582 return f.read()
583
585 """returns the string representation of this MOC.
586
587 This isn't what DALI actually says at this point, but we suspect
588 it will say that at some point.
589 """
590 return self.asASCII()
591
592 @classmethod
594 """returns an SMoc from a MOC string.
595
596 (see asDALI)
597 """
598 return cls.fromASCII(literal)
599
601 """returns a list of integers usable as nuniqs.
602 """
603 res = []
604 for order, cells in self.moc:
605 shiftedOrder = 4 * (4 ** order)
606 res.extend(pix+shiftedOrder for pix in cells)
607 return res
608
609
610 try:
611 import psycopg2
612 from psycopg2.extensions import (register_adapter, AsIs, register_type,
613 new_type)
614
615 _getPgSClass = codetricks.buildClassResolver(PgSAdapter, globals().values(),
616 key=lambda obj: obj.pgType, default=PgSAdapter)
620 if hasattr(psycopg2, "_pgsphereLoaded"):
621 return
622 try:
623 oidmap = _query(conn,
624 "SELECT typname, oid"
625 " FROM pg_type"
626 " WHERE typname ~ "
627 " '^s(point|trans|circle|line|ellipse|poly|path|box|moc)'")
628 for typeName, oid in oidmap:
629 cls = _getPgSClass(typeName)
630 if cls is not PgSAdapter:
631 register_adapter(cls, cls._adaptToPgSphere)
632 register_type(
633 new_type((oid,), "spoint", cls._castFromPgSphere))
634 psycopg2._pgsphereLoaded = True
635 conn.commit()
636 except:
637 psycopg2._pgsphereLoaded = False
638
639 except ImportError:
640 raise
641
642
643
644 pass
650
651
652 if __name__=="__main__":
653 _test()
654