Package gavo :: Package stc :: Module dm
[frames] | no frames]

Source Code for Module gavo.stc.dm

  1  """ 
  2  Definition of the structure of the internal representation (the AST). 
  3   
  4  For now, we want to be able to capture what STC-S can do (and a bit more).   
  5  This means that we do not support generic coordinates (yet), elements, 
  6  xlink and all the other stuff. 
  7  """ 
  8   
  9  #c Copyright 2008-2019, the GAVO project 
 10  #c 
 11  #c This program is free software, covered by the GNU GPL.  See the 
 12  #c COPYING file in the source distribution. 
 13   
 14   
 15  import itertools 
 16  import operator 
 17  import re 
 18   
 19  from gavo import utils 
 20  from gavo.stc import times 
 21  from gavo.stc import units 
 22  from gavo.stc import common 
 23  from gavo.utils import pgsphere 
24 25 26 ################ Coordinate Systems 27 28 29 -class RefPos(common.ASTNode):
30 """is a reference position. 31 32 Right now, this is just a wrapper for a RefPos id, as defined by STC-S, 33 or None for Unknown. 34 """ 35 # If we ever support non-standard origins, they should go into a different 36 # class, I guess. Or, we'd need a sentinel for standardOrigin (like, 37 # NONSTANDARD). None, anyway, is for Unknown, and we shouldn't change that. 38 _a_standardOrigin = None 39 _a_planetaryEphemeris = None
40 41 42 NullRefPos = RefPos()
43 44 -class _CoordFrame(common.ASTNode):
45 """is an astronomical coordinate frame. 46 """ 47 _a_name = None 48 _a_refPos = None 49
50 - def _setupNode(self):
51 if self.refPos is None: 52 self.refPos = NullRefPos
53
54 - def isSpherical(self):
55 """returns True if this is a frame deemed suitable for space 56 frame transformations. 57 58 This is really a property of stc.sphermath rather than of the 59 data model, but it's more convenient to have this as a frame 60 method. 61 """ 62 return (isinstance(self, SpaceFrame) 63 and self.nDim>1 64 and self.flavor=="SPHERICAL")
65
66 67 -class TimeFrame(_CoordFrame):
68 nDim = 1 69 _a_timeScale = None
70
71 72 -class SpaceFrame(_CoordFrame):
73 _a_flavor = "SPHERICAL" 74 _a_nDim = None 75 _a_refFrame = None 76 _a_equinox = None # if non-null, it has to match [BJ][0-9]+[.][0-9]+ 77
78 - def _setupNode(self):
79 if self.refFrame=="J2000": 80 self.refFrame = "FK5" 81 self.equinox = "J2000.0" 82 elif self.refFrame=="B1950": 83 self.refFrame = "FK4" 84 self.equinox = "B1950.0" 85 if self.nDim is None: 86 self.nDim = 2 87 _CoordFrame._setupNode(self)
88
89 - def getEquinox(self):
90 """returns a datetime.datetime instance for the frame's equinox. 91 92 It will return None if no equinox is given, and it may raise an 93 STCValueError if an invalid equinox string has been set. 94 """ 95 if self.equinox is None: 96 return None 97 mat = re.match("([B|J])([0-9.]+)", self.equinox) 98 if not mat: 99 raise common.STCValueError("Equinoxes must be [BJ]<float>, but %s isn't"%( 100 self.equinox)) 101 if mat.group(1)=='B': 102 return times.bYearToDateTime(float(mat.group(2))) 103 else: 104 return times.jYearToDateTime(float(mat.group(2)))
105
106 - def asTriple(self):
107 """returns a triple defining the space frame for spherc's purposes. 108 109 This is for the computation of coordinate transforms. Since we only 110 do coordinate transforms for spherical coordinate systems, this 111 will, for now, raise STCValueErrors if everything but 2 or 3D SPHERICAL 112 flavours. The other cases need more thought anyway. 113 """ 114 if self.flavor!="SPHERICAL" or (self.nDim!=2 and self.nDim!=3): 115 raise common.STCValueError("Can only conform 2/3-spherical coordinates") 116 return (self.refFrame, self.getEquinox(), self.refPos.standardOrigin)
117
118 119 -class SpectralFrame(_CoordFrame):
120 nDim = 1
121
122 123 -class RedshiftFrame(_CoordFrame):
124 nDim = 1 125 _a_dopplerDef = None 126 _a_type = None
127
128 129 -class CoordSys(common.ASTNode):
130 """is an astronomical coordinate system. 131 """ 132 _a_timeFrame = None 133 _a_spaceFrame = None 134 _a_spectralFrame = None 135 _a_redshiftFrame = None 136 _a_name = None 137 _a_libraryId = None # for standard coordinate systems, the ivo://whatever.
138
139 140 -class _CooTypeSentinel(object):
141 """is a base for type indicators. 142 143 Never instantiate any of these. 144 """
145
146 -class SpectralType(_CooTypeSentinel):
147 posAttr = "freq"
148
149 -class TimeType(_CooTypeSentinel):
150 posAttr = "time"
151
152 -class SpaceType(_CooTypeSentinel):
153 posAttr = "place"
154
155 -class RedshiftType(_CooTypeSentinel):
156 posAttr = "redshift"
157
158 -class VelocityType(_CooTypeSentinel):
159 posAttr = "velocity"
160
161 162 ############### Coordinates and their intervals 163 164 165 -class _WiggleSpec(common.ASTNode):
166 """A base for "wiggle" specifications. 167 168 These are Errors, Resolutions, Sizes, and PixSizes. They may come 169 as simple coordinates (i.e., scalars or vectors) or, in 2 and 3D, 170 as radii or matrices (see below). In all cases, two values may 171 be given to indicate ranges. 172 173 These need an adaptValuesWith(converter) method that will return a wiggle of 174 the same type but with every value replaced with the result of the 175 application of converter to that value. 176 """
177
178 179 -class CooWiggle(_WiggleSpec):
180 """A wiggle given in coordinates. 181 182 The values attributes stores them just like coordinates are stored. 183 """ 184 _a_values = () 185 _a_origUnit = None 186 187 inexactAttrs = set(["values"]) 188
189 - def adaptValuesWith(self, unitConverter):
190 if unitConverter is None: 191 return self 192 return self.change(values=tuple(unitConverter(v) for v in self.values))
193
194 - def getValues(self):
195 return self.values
196
197 198 -class RadiusWiggle(_WiggleSpec):
199 """An wiggle given as a radius. 200 201 If unit adaption is necessary and the base value is a vector, the radii 202 are assumed to be of the dimension of the first vector component. 203 """ 204 _a_radii = () 205 _a_origUnit = None 206 207 inexactAttrs = set(["radii"]) 208
209 - def adaptValuesWith(self, unitConverter):
210 if unitConverter is None: 211 return self 212 return self.change(radii=tuple(unitConverter(itertools.repeat(r))[0] 213 for r in self.radii))
214
215 - def getValues(self):
216 return self.radii
217
218 219 -class MatrixWiggle(_WiggleSpec):
220 """A matrix for specifying wiggle. 221 222 The matrix/matrices are stored as sequences of sequences; see 223 stcxgen._wrapMatrix for details. 224 """ 225 _a_matrices = () 226 _a_origUnit = None 227
228 - def adaptValuesWith(self, unitConverter):
229 raise common.STCValueError("Matrix wiggles cannot be transformed.")
230
231 232 -class _CoordinateLike(common.ASTNode):
233 """An abstract base for everything that has a frame. 234 235 They can return a position object of the proper type and with the 236 same unit as self. 237 238 When deriving from _CoordinateLike, you have at some point to define 239 a cType class attribute that has values in the _CooTypeSentinels above. 240 """ 241 _a_frame = None 242 _a_name = None 243
244 - def getPosition(self, initArgs=None):
245 """returns a position appropriate for this class. 246 247 This is a shallow copy of the xCoo object itself for xCoos, 248 xCoo for xInterval, and SpaceCoo for Geometries. Common attributes 249 are copied to the new object. 250 """ 251 posClass = _positionClassMap[self.cType] 252 if initArgs is None: 253 initArgs = {} 254 for name, default in posClass._nodeAttrs: 255 if name!="id" and name not in initArgs: 256 initArgs[name] = getattr(self, name, default) 257 return posClass(**initArgs)
258
259 260 -class _Coordinate(_CoordinateLike):
261 """An abstract base for coordinates. 262 263 They have an iterTransformed(convFunc) method iterating over 264 constructor keys that have to be changed when some convFunc is 265 applied to the coordinate. These may be multiple values when, 266 e.g., errors are given or for geometries. 267 268 Since these only make sense together with units, some elementary 269 unit handling is required. Since we keep the basic unit model 270 of STC, this is a bit over-complicated. 271 272 First, for the benefit of STC-S, a method getUnitString() -> 273 string or None is required. It should return an STC-S-legal 274 unit string. 275 276 Second, a method getUnitArgs() -> dict or None is required. 277 It has to return a dictionary with all unit-related constructor 278 arguments (that's unit and velTimeUnit for the standard coordinate 279 types). No None values are allowed; if self's units are not 280 defined, return None. 281 282 Third, a method getUnitConverter(otherUnits) -> function or None is required. 283 OtherUnits can be a tuple or a result of getUnitArgs. The tuple is 284 interpreted as (baseUnit, timeUnit). The function returned must 285 accept self's coordinate values in otherUnit and return them in self's 286 unit(s). This is the function that iterTransformed requires. 287 """ 288 _a_error = None 289 _a_resolution = None 290 _a_pixSize = None 291 _a_value = None 292 _a_size = None 293 294 _dimensionedAttrs = ["error", "resolution", "pixSize", "size"] 295 296 inexactAttrs = set(["value"]) 297
298 - def _setupNode(self):
299 for name in self._dimensionedAttrs: 300 wiggle = getattr(self, name) 301 if wiggle and wiggle.origUnit is not None: 302 setattr(self, name, wiggle.adaptValuesWith( 303 self.getUnitConverter(wiggle.origUnit))) 304 self._setupNodeNext(_Coordinate)
305
306 - def iterTransformed(self, converter):
307 if self.value is not None: 308 yield "value", converter(self.value) 309 for attName in self._dimensionedAttrs: 310 wiggle = getattr(self, attName) 311 if wiggle: 312 yield attName, wiggle.adaptValuesWith(converter)
313
314 315 -class _OneDMixin(object):
316 """provides attributes for 1D-Coordinates (Time, Spectral, Redshift) 317 """ 318 _a_unit = None 319
320 - def getUnitString(self):
321 return self.unit
322
323 - def getUnitConverter(self, otherUnits):
324 if self.unit is None or not otherUnits: 325 return None 326 if isinstance(otherUnits, dict): 327 otherUnits = (otherUnits["unit"],) 328 return units.getBasicConverter(self.unit, otherUnits[0], True)
329
330 - def getUnitArgs(self):
331 if self.unit: 332 return {"unit": self.unit}
333
334 - def getValues(self):
335 return [self.value]
336
337 338 -class _SpatialMixin(object):
339 """provides attributes for positional coordinates. 340 341 In addition to unit management, this is also carries an epoch in years. 342 You can, in addition, set yearDef. If None, Julian years are implied, 343 but you can have B for Bessel years. 344 """ 345 _a_unit = () 346 _a_epoch = None 347 _a_yearDef = None 348 349 cType = SpaceType 350
351 - def getUnitString(self):
352 if self.unit: 353 if len(set(self.unit))==1: 354 return self.unit[0] 355 else: 356 return " ".join(self.unit)
357
358 - def getUnitConverter(self, otherUnits):
359 if self.unit is None or not otherUnits: 360 return None 361 if isinstance(otherUnits, dict): 362 otherUnits = (otherUnits["unit"],) 363 f = units.getVectorConverter(self.unit, otherUnits[0], True) 364 return f
365
366 - def getUnitArgs(self):
367 if self.unit==(): 368 # horrendous default: an empty spatial coordiate as (deg, deg), 369 # because that's what most likely works with geometries. 370 # Oh boy, STC1 is *so* broken it's not funny any more. 371 return {"unit": ("deg", "deg")} 372 return {"unit": self.unit}
373
374 - def getValues(self):
375 if self.value is None: 376 return [] 377 return self.value
378
379 380 -class _VelocityMixin(object):
381 """provides attributes for velocities. 382 """ 383 _a_unit = () 384 _a_velTimeUnit = () 385 _a_epoch = None 386 _a_yearDef = None 387 388 cType = VelocityType 389
390 - def _setupNode(self):
391 if self.unit: 392 if not self.velTimeUnit or len(self.unit)!=len(self.velTimeUnit): 393 raise common.STCValueError("Invalid units for Velocity: %s/%s."%( 394 repr(self.unit), repr(self.velTimeUnit))) 395 self._setupNodeNext(_VelocityMixin)
396
397 - def getUnitString(self):
398 if self.unit: 399 strs = ["%s/%s"%(u, tu) 400 for u, tu in zip(self.unit, self.velTimeUnit)] 401 if len(set(strs))==1: 402 return strs[0] 403 else: 404 return " ".join(strs)
405
406 - def getUnitConverter(self, otherUnits):
407 if self.unit is None or not otherUnits: 408 return None 409 if isinstance(otherUnits, dict): 410 otherUnits = (otherUnits["unit"], otherUnits["velTimeUnit"]) 411 return units.getVelocityConverter(self.unit, self.velTimeUnit, 412 otherUnits[0], otherUnits[1], True)
413
414 - def getUnitArgs(self):
415 return {"unit": self.unit, "velTimeUnit": self.velTimeUnit}
416
417 418 -class _RedshiftMixin(object):
419 """provides attributes for redshifts. 420 """ 421 _a_velTimeUnit = None 422 _a_unit = None 423 424 cType = RedshiftType 425
426 - def _setupNode(self):
427 if self.unit and not self.velTimeUnit: 428 raise common.STCValueError("Invalid units for Redshift: %s/%s."%( 429 repr(self.unit), repr(self.velTimeUnit))) 430 self._setupNodeNext(_RedshiftMixin)
431
432 - def getUnitString(self):
433 if self.unit: 434 return "%s/%s"%(self.unit, self.velTimeUnit)
435
436 - def getUnitConverter(self, otherUnits):
437 if self.unit is None or not otherUnits: 438 return None 439 if isinstance(otherUnits, dict): 440 otherUnits = (otherUnits["unit"], otherUnits["velTimeUnit"]) 441 return units.getRedshiftConverter(self.unit, self.velTimeUnit, 442 otherUnits[0], otherUnits[1], True)
443
444 - def getUnitArgs(self):
445 return {"unit": self.unit, "velTimeUnit": self.velTimeUnit}
446
447 448 -class SpaceCoo(_Coordinate, _SpatialMixin):
449 pgClass = pgsphere.SPoint
450
451 -class VelocityCoo(_Coordinate, _VelocityMixin): pass
452 -class RedshiftCoo(_Coordinate, _RedshiftMixin): pass
453
454 -class TimeCoo(_Coordinate, _OneDMixin):
455 cType = TimeType 456
457 - def iterTransformed(self, converter):
458 # here, we never transform the value (since it can be a datetime 459 # or whatever). 460 for attName in self._dimensionedAttrs: 461 wiggle = getattr(self, attName) 462 if wiggle: 463 yield attName, wiggle.adaptValuesWith(converter)
464
465 -class SpectralCoo(_Coordinate, _OneDMixin):
466 cType = SpectralType
467 468 469 _positionClassMap = { 470 SpectralType: SpectralCoo, 471 TimeType: TimeCoo, 472 SpaceType: SpaceCoo, 473 RedshiftType: RedshiftCoo, 474 VelocityType: VelocityCoo, 475 }
476 477 478 -class _CoordinateInterval(_CoordinateLike):
479 _a_lowerLimit = None 480 _a_upperLimit = None 481 _a_fillFactor = None 482 _a_origUnit = None 483 484 inexactAttrs = set(["lowerLimit", "upperLimit"]) 485
486 - def adaptValuesWith(self, converter):
487 changes = {"origUnit": None} 488 if self.lowerLimit is not None: 489 changes["lowerLimit"] = converter(self.lowerLimit) 490 if self.upperLimit is not None: 491 changes["upperLimit"] = converter(self.upperLimit) 492 return self.change(**changes)
493
494 - def getTransformed(self, sTrafo, destFrame):
495 ll, ul = self.lowerLimit, self.upperLimit 496 if ll is None: 497 return self.change(upperLimit=sTrafo(ul), frame=destFrame) 498 elif ul is None: 499 return self.change(lowerLimit=sTrafo(ll), frame=destFrame) 500 else: 501 return self.change(upperLimit=sTrafo(ul), lowerLimit=sTrafo(ll), 502 frame=destFrame)
503 504
505 - def getValues(self):
506 return [l for l in (self.lowerLimit, self.upperLimit) if l is not None]
507
508 -class SpaceInterval(_CoordinateInterval):
509 cType = SpaceType 510 511 # See fromPgSphere docstring on this 512 pgClass = pgsphere.SBox 513
514 - def getValues(self):
515 return reduce(lambda a,b: a+b, _CoordinateInterval.getValues(self))
516
517 - def getTransformed(self, sTrafo, destFrame):
518 # for n-d coordinates, upper and lower limit can be difficult 519 # under rotations. We need to look into this (>2 would need separate 520 # support) 521 ll, ul = self.lowerLimit, self.upperLimit 522 if (self.frame.nDim==1 523 or (self.lowerLimit is None or self.upperLimit is None)): 524 return _CoordinateInterval.getTransformed(self, sTrafo, destFrame) 525 elif self.frame.nDim==2: 526 vertices = [sTrafo(coo) for coo in ( 527 (ll[0], ll[1]), (ul[0], ll[1]), (ll[0], ul[1]), (ul[0], ul[1]))] 528 xVals = [coo[0] for coo in vertices] 529 yVals = [coo[1] for coo in vertices] 530 return self.change(upperLimit=(max(xVals), max(yVals)), 531 lowerLimit=(min(xVals), min(yVals)), frame=destFrame) 532 else: 533 raise NotImplemented("Cannot yet transform coordinate intervals" 534 " in n>2 dims.")
535 536 @classmethod
537 - def fromPg(cls, frame, pgBox):
538 return cls(frame=frame, 539 lowerLimit=(pgBox.corner1.x/utils.DEG, pgBox.corner1.y/utils.DEG), 540 upperLimit=(pgBox.corner2.x/utils.DEG, pgBox.corner2.y/utils.DEG))
541 542 543 # Service for stcsast -- this may go away again 544 PositionInterval = SpaceInterval
545 546 547 -class VelocityInterval(_CoordinateInterval):
548 cType = VelocityType
549
550 -class RedshiftInterval(_CoordinateInterval):
551 cType = RedshiftType
552
553 -class TimeInterval(_CoordinateInterval):
554 cType = TimeType 555
556 - def adaptValuesWith(self, converter):
557 # timeIntervals are unitless; units only refer to errors, etc, 558 # which we don't have here. 559 return self
560
561 562 -class SpectralInterval(_CoordinateInterval):
563 cType = SpectralType
564
565 566 567 ################ Geometries 568 569 -class _Geometry(_CoordinateLike):
570 """A base class for all kinds of geometries. 571 572 Geometries may have "dependent" quantities like radii, sizes, etc. For 573 those, the convention is that if they are 1D, they must be expressed in the 574 unit of the first component of the position units, otherwise (in particular, 575 for box size) in the full unit of the position. This has to be made sure by 576 the client. 577 578 To make this work, Geometries are unit adapted on STC adoption. 579 Since their dependents need to be adapted as well, they have to 580 define adaptDependents(...) methods. They take the units for 581 all dependent quantities (which may all be None). This is used 582 in stxast. 583 584 Also getTransformed usually needs to be overridden for these. 585 586 Geometries may contain two sorts of column references; ordinary ones 587 are just stand-ins of actual values, while GeometryColRefs describe the 588 whole thing in a database column. 589 590 For the spatial registry prototype, geometries can define an 591 asSMoc(order=6) method returning a pgpshere.SMoc coverage for them. 592 """ 593 _a_size = None 594 _a_fillFactor = None 595 _a_origUnit = None 596 _a_geoColRef = None 597 598 cType = SpaceType 599
600 - def getValues(self):
601 if self.geoColRef: 602 return [self.geoColRef] 603 else: 604 return self._getValuesSplit()
605
606 607 -class AllSky(_Geometry):
608 - def getTransformed(self, sTrafo, destFrame):
609 return self.change(frame=destFrame)
610
611 - def adaptValuesWith(self, converter):
612 return self
613
614 - def adaptDepUnits(self):
615 pass
616
617 - def _getValuesSplit(self):
618 return []
619
620 - def asSMoc(self, order=6):
621 return pgsphere.SMoc.fromCells(0, range(12))
622
623 624 -class Circle(_Geometry):
625 _a_center = None 626 _a_radius = None 627 628 pgClass = pgsphere.SCircle 629
630 - def getTransformed(self, sTrafo, destFrame):
631 return self.change(center=sTrafo(self.center), frame=destFrame)
632
633 - def adaptValuesWith(self, converter):
634 sTrafo = units.getBasicConverter(converter.fromUnit[0], 635 converter.toUnit[0]) 636 return self.change(center=converter(self.center), 637 radius=sTrafo(self.radius))
638
639 - def _getValuesSplit(self):
640 return [self.center[0], self.center[1], self.radius]
641 642 @classmethod
643 - def fromPg(cls, frame, sCircle):
644 return cls(frame=frame, 645 center=(sCircle.center.x/utils.DEG, sCircle.center.y/utils.DEG), 646 radius=(sCircle.radius/utils.DEG))
647
648 - def asSMoc(self, order):
649 return pgsphere.SCircle( 650 pgsphere.SPoint.fromDegrees(*self.center), 651 self.radius*utils.DEG 652 ).asPoly( 653 ).asSMoc()
654
655 656 -class Ellipse(_Geometry):
657 _a_center = None 658 _a_smajAxis = _a_sminAxis = None 659 _a_posAngle = None 660
661 - def getTransformed(self, sTrafo, destFrame):
662 # XXX TODO: actually rotate the ellipse. 663 return self.change(center=sTrafo(self.center), frame=destFrame)
664
665 - def adaptValuesWith(self, converter):
666 sTrafo = units.getBasicConverter(converter.fromUnit[0], 667 converter.toUnit[0]) 668 return self.change(center=converter(self.center), 669 smajAxis=sTrafo(self.smajAxis), sminAxis=sTrafo(self.sminAxis))
670
671 - def _getValuesSplit(self):
672 return list(self.center)+[self.smajAxis]+[self.sminAxis]+[ 673 self.posAngle]
674
675 676 -class Box(_Geometry):
677 _a_center = None 678 _a_boxsize = None 679
680 - def getTransformed(self, sTrafo, destFrame):
681 """returns a Polygon corresponding to this Box after rotation. 682 """ 683 center, boxsize = self.center, self.boxsize 684 return Polygon(vertices=tuple(sTrafo(coo) for coo in ( 685 (center[0]-boxsize[0], center[1]-boxsize[1]), 686 (center[0]-boxsize[0], center[1]+boxsize[1]), 687 (center[0]+boxsize[0], center[1]+boxsize[1]), 688 (center[0]+boxsize[0], center[1]-boxsize[1]))), frame=destFrame)
689
690 - def adaptValuesWith(self, converter):
691 return self.change(center=converter(self.center), 692 boxsize=converter(self.boxsize))
693
694 - def _getValuesSplit(self):
695 return list(self.center)+list(self.boxsize)
696
697 698 -class Polygon(_Geometry):
699 _a_vertices = () 700 701 pgClass = pgsphere.SPoly 702
703 - def getTransformed(self, sTrafo, destFrame):
704 return self.change(vertices=tuple(sTrafo(v) for v in self.vertices), 705 frame=destFrame)
706
707 - def adaptValuesWith(self, converter):
708 return self.change(vertices=tuple(converter(v) for v in self.vertices))
709
710 - def _getValuesSplit(self):
711 return reduce(operator.add, self.vertices)
712 713 @classmethod
714 - def fromPg(cls, frame, sPoly):
715 return cls(frame=frame, 716 vertices=[(p.x/utils.DEG, p.y/utils.DEG) 717 for p in sPoly.points])
718
719 - def asSMoc(self, order=6):
720 # let's assume points are in degrees here; this is hopefully only 721 # temporary, anyway 722 return pgsphere.SPoly( 723 [pgsphere.SPoint.fromDegrees(*p) for p in self.vertices]).asSMoc(order)
724
725 726 -class Convex(_Geometry):
727 _a_vectors = () 728
729 - def getTransformed(self, sTrafo, destFrame):
730 raise common.STCNotImplementedError("Cannot transform convexes yet.")
731
732 - def adaptValuesWith(self, converter):
733 raise common.STCNotImplementedError("Cannot adapt units for convexes yet.")
734
735 - def _getValuesSplit(self):
736 return reduce(operator.add, self.vectors)
737
738 739 -class _Compound(_Geometry):
740 """A set-like operator on geometries. 741 """ 742 _a_children = () 743
744 - def polish(self):
745 for node in self.children: 746 if node.frame is None: 747 node.frame = self.frame 748 getattr(node, "polish", lambda: None)()
749 750
751 - def adaptValuesWith(self, converter):
752 return self.change(children=[child.adaptValuesWith(converter) 753 for child in self.children])
754
755 - def _applyToChildren(self, function):
756 newChildren, changes = [], False 757 for c in self.children: 758 nc = function(c) 759 newChildren.append(nc) 760 changes = changes or (c is not nc) 761 if changes: 762 return self.change(children=newChildren) 763 else: 764 return self
765
766 - def binarizeOperands(self):
767 """returns self with binarized operands. 768 769 If no operand needed binarizing, self is returned. 770 """ 771 res = self._applyToChildren(binarizeCompound) 772 return res
773
774 - def debinarizeOperands(self):
775 """returns self with debinarized operands. 776 777 If no operand needed debinarizing, self is returned. 778 """ 779 return self._applyToChildren(debinarizeCompound)
780
781 - def getTransformed(self, sTrafo, destFrame):
782 return self.change(children=tuple( 783 child.getTransformed(sTrafo, destFrame) for child in self.children), 784 frame=destFrame)
785
786 787 -class _MultiOpCompound(_Compound):
788 """is a compound that has a variable number of operands. 789 """
790
791 792 -class Union(_MultiOpCompound): pass
793 -class Intersection(_MultiOpCompound): pass
794 -class Difference(_Compound): pass
795 -class Not(_Compound): pass
796
797 798 -def _buildBinaryTree(compound, items):
799 """returns a binary tree of nested instances compound instances. 800 801 items has to have at least length 2. 802 """ 803 items = list(items) 804 root = compound.change(children=items[-2:]) 805 items[-2:] = [] 806 while items: 807 root = compound.change(children=[items.pop(), root]) 808 return root
809
810 811 -def binarizeCompound(compound):
812 """returns compound as a binary tree. 813 814 For unions and intersections, compounds consisting of more than two 815 operands will be split up into parts of two arguments each. 816 """ 817 if not isinstance(compound, _Compound): 818 return compound 819 compound = compound.binarizeOperands() 820 if len(compound.children)==1: 821 return compound.children[0] 822 elif len(compound.children)==2: 823 return compound 824 else: 825 newChildren = [compound.children[0], 826 _buildBinaryTree(compound, compound.children[1:])] 827 return compound.change(children=newChildren)
828
829 830 -def debinarizeCompound(compound):
831 """returns compound with flattened operators. 832 """ 833 if not isinstance(compound, _Compound): 834 return compound 835 compound = compound.debinarizeOperands() 836 newChildren, changes = [], False 837 for c in compound.children: 838 if c.__class__==compound.__class__: 839 newChildren.extend(c.children) 840 changes = True 841 else: 842 newChildren.append(c) 843 if changes: 844 return compound.change(children=newChildren) 845 else: 846 return compound
847
848 849 ################ Toplevel 850 851 -class STCSpec(common.ASTNode):
852 """is an STC specification, i.e., the root of an STC tree. 853 """ 854 _a_astroSystem = None 855 _a_systems = () 856 _a_time = None 857 _a_place = None 858 _a_freq = None 859 _a_redshift = None 860 _a_velocity = None 861 _a_timeAs = () 862 _a_areas = () 863 _a_freqAs = () 864 _a_redshiftAs = () 865 _a_velocityAs = () 866 867 @property
868 - def sys(self):
869 return self.astroSystem
870
871 - def buildIdMap(self):
872 if hasattr(self, "idMap"): 873 return 874 self.idMap = {} 875 for node in self.iterNodes(): 876 if node.id: 877 self.idMap[node.id] = node
878
879 - def polish(self):
880 """does global fixups when parsing is finished. 881 882 This method has to be called after the element is complete. The 883 standard parsers do this. 884 885 For convenience, it returns the instance itself. 886 """ 887 # Fix local frames if not given (e.g., manual construction) 888 if self.place is not None and self.place.frame is None: 889 self.place.frame = self.astroSystem.spaceFrame 890 for area in self.areas: 891 if area.frame is None: 892 area.frame = self.astroSystem.spaceFrame 893 getattr(area, "polish", lambda: None)() 894 895 # Operations here cannot be in a _setupNode since when parsing from 896 # XML, there may be IdProxies instead of real objects. 897 # Equinox for ecliptic defaults to observation time 898 if self.place: 899 frame = self.place.frame 900 if frame and frame.equinox is None and frame.refFrame=="ECLIPTIC": 901 if self.time and self.time.value: 902 frame.equinox = "J%.8f"%(times.dateTimeToJYear(self.time.value)) 903 return self
904
905 - def _applyToAreas(self, function):
906 newAreas, changes = [], False 907 for a in self.areas: 908 na = function(a) 909 changes = changes or na is not a 910 newAreas.append(na) 911 if changes: 912 return self.change(areas=newAreas) 913 else: 914 return self
915
916 - def binarize(self):
917 """returns self with any compound present brought to a binary tree. 918 919 This will return self if nothing needs to change. 920 """ 921 return self._applyToAreas(binarizeCompound)
922
923 - def debinarize(self):
924 """returns self with any compound present brought to a binary tree. 925 926 This will return self if nothing needs to change. 927 """ 928 return self._applyToAreas(debinarizeCompound)
929
930 - def getColRefs(self):
931 """returns a list of column references embedded in this AST. 932 """ 933 if not hasattr(self, "_colRefs"): 934 self._colRefs = [] 935 for n in self.iterNodes(): 936 if hasattr(n, "getValues"): 937 self._colRefs.extend( 938 v.dest for v in n.getValues() if isinstance(v, common.ColRef)) 939 return self._colRefs
940
941 - def stripUnits(self):
942 """removes all unit specifications from this AST. 943 944 This is intended for non-standalone STC, e.g., in VOTables, where 945 external unit specifications are present. Removing the units 946 prevents "bleeding out" of conflicting in-STC specifications 947 (that mostly enter through defaulting). 948 949 This ignores the immutability of nodes and is in general a major pain. 950 """ 951 for node in self.iterNodes(): 952 if hasattr(node, "unit"): 953 node.unit = node.__class__._a_unit 954 if hasattr(node, "velTimeUnit"): 955 node.velTimeUnit = node.__class__._a_unit
956
957 958 -def fromPgSphere(refFrame, pgGeom):
959 """Returns an AST for a pgsphere object as defined in utils.pgsphere. 960 961 This interprets the pgSphere box as a coordinate interval, which is wrong 962 but probably what most VO protocols expect. 963 """ 964 frame = SpaceFrame(refFrame=refFrame) 965 966 if isinstance(pgGeom, SpaceCoo.pgClass): 967 return STCSpec(place=SpaceCoo.fromPg(frame, pgGeom)) 968 969 for stcGeo in [Circle, SpaceInterval, Polygon]: 970 if isinstance(pgGeom, stcGeo.pgClass): 971 return STCSpec(areas=[stcGeo.fromPg(frame, pgGeom)]) 972 973 raise common.STCValueError("Unknown pgSphere object %r"%pgGeom)
974