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
10
11
12
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
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
36
37
38 _a_standardOrigin = None
39 _a_planetaryEphemeris = None
40
41
42 NullRefPos = RefPos()
45 """is an astronomical coordinate frame.
46 """
47 _a_name = None
48 _a_refPos = None
49
51 if self.refPos is None:
52 self.refPos = NullRefPos
53
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
70
73 _a_flavor = "SPHERICAL"
74 _a_nDim = None
75 _a_refFrame = None
76 _a_equinox = None
77
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
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
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
121
127
138
141 """is a base for type indicators.
142
143 Never instantiate any of these.
144 """
145
148
151
154
157
160
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
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
190 if unitConverter is None:
191 return self
192 return self.change(values=tuple(unitConverter(v) for v in self.values))
193
196
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
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
217
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
230
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
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
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
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
313
316 """provides attributes for 1D-Coordinates (Time, Spectral, Redshift)
317 """
318 _a_unit = None
319
322
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
331 if self.unit:
332 return {"unit": self.unit}
333
336
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
352 if self.unit:
353 if len(set(self.unit))==1:
354 return self.unit[0]
355 else:
356 return " ".join(self.unit)
357
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
367 if self.unit==():
368
369
370
371 return {"unit": ("deg", "deg")}
372 return {"unit": self.unit}
373
375 if self.value is None:
376 return []
377 return self.value
378
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
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
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
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
415 return {"unit": self.unit, "velTimeUnit": self.velTimeUnit}
416
419 """provides attributes for redshifts.
420 """
421 _a_velTimeUnit = None
422 _a_unit = None
423
424 cType = RedshiftType
425
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
433 if self.unit:
434 return "%s/%s"%(self.unit, self.velTimeUnit)
435
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
445 return {"unit": self.unit, "velTimeUnit": self.velTimeUnit}
446
447
448 -class SpaceCoo(_Coordinate, _SpatialMixin):
450
453
454 -class TimeCoo(_Coordinate, _OneDMixin):
464
467
468
469 _positionClassMap = {
470 SpectralType: SpectralCoo,
471 TimeType: TimeCoo,
472 SpaceType: SpaceCoo,
473 RedshiftType: RedshiftCoo,
474 VelocityType: VelocityCoo,
475 }
479 _a_lowerLimit = None
480 _a_upperLimit = None
481 _a_fillFactor = None
482 _a_origUnit = None
483
484 inexactAttrs = set(["lowerLimit", "upperLimit"])
485
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
503
504
506 return [l for l in (self.lowerLimit, self.upperLimit) if l is not None]
507
509 cType = SpaceType
510
511
512 pgClass = pgsphere.SBox
513
515 return reduce(lambda a,b: a+b, _CoordinateInterval.getValues(self))
516
535
536 @classmethod
537 - def fromPg(cls, frame, pgBox):
541
542
543
544 PositionInterval = SpaceInterval
549
552
560
564
565
566
567
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
601 if self.geoColRef:
602 return [self.geoColRef]
603 else:
604 return self._getValuesSplit()
605
622
625 _a_center = None
626 _a_radius = None
627
628 pgClass = pgsphere.SCircle
629
632
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
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
654
657 _a_center = None
658 _a_smajAxis = _a_sminAxis = None
659 _a_posAngle = None
660
664
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
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
689
691 return self.change(center=converter(self.center),
692 boxsize=converter(self.boxsize))
693
695 return list(self.center)+list(self.boxsize)
696
699 _a_vertices = ()
700
701 pgClass = pgsphere.SPoly
702
706
708 return self.change(vertices=tuple(converter(v) for v in self.vertices))
709
711 return reduce(operator.add, self.vertices)
712
713 @classmethod
714 - def fromPg(cls, frame, sPoly):
718
724
727 _a_vectors = ()
728
731
734
736 return reduce(operator.add, self.vectors)
737
740 """A set-like operator on geometries.
741 """
742 _a_children = ()
743
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
754
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
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
775 """returns self with debinarized operands.
776
777 If no operand needed debinarizing, self is returned.
778 """
779 return self._applyToChildren(debinarizeCompound)
780
785
788 """is a compound that has a variable number of operands.
789 """
790
791
792 -class Union(_MultiOpCompound): pass
795 -class Not(_Compound): pass
796
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
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
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
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
869 return self.astroSystem
870
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
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
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
896
897
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
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
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
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
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
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
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