1 """
2 Converting ASTs to STC-S.
3
4 The strategy here is to first generate an STCS CST, remove defaults from it
5 and then flatten out the whole thing.
6
7 The AST serializers here either return a dictionary, which is then
8 updated to the current node's dictionary, or a tuple of key and value,
9 which is then added to the current dictionary.
10 """
11
12
13
14
15
16
17
18 import datetime
19 import itertools
20
21 from gavo import utils
22 from gavo.stc import common
23 from gavo.stc import dm
24 from gavo.stc import stcs
25 from gavo.stc import syslib
26
27
29 """updates the first dictionary with all further ones and returns it.
30
31 If duplicate keys exist, later arguments will overwrite values set
32 by earlier arguments.
33 """
34 res = dicts[0]
35 for d in dicts[1:]:
36 res.update(d)
37 return res
38
39
40
41
42 -def refPosToCST(node):
43 return {
44 "refpos": node.standardOrigin,
45 "planetaryEphemeris": node.planetaryEphemeris,}
46
47 stcsFlavors = {
48 (2, "SPHERICAL"): "SPHER2",
49 (3, "SPHERICAL"): "SPHER3",
50 (3, "UNITSPHERE"): "UNITSPHER",
51 (1, "CARTESIAN"): "CART1",
52 (2, "CARTESIAN"): "CART2",
53 (3, "CARTESIAN"): "CART3",
54 }
55
57 try:
58 return stcsFlavors[(node.nDim, node.flavor)]
59 except KeyError:
60 raise common.STCValueError("Coordinate Frame %s cannot"
61 " be represented it STC-S"%node)
62
63
64 _frameTrans = {
65 "GALACTIC_II": "GALACTIC",
66 None: "UNKNOWNFrame",
67 }
68
70 if node is None:
71 return {"frame": "UNKNOWNFrame"}
72
73 frame = node.refFrame
74 frame = _frameTrans.get(frame, frame)
75 return _combine({
76 "flavor": _computeFlavor(node),
77 "frame": frame,
78 "equinox": node.equinox,},
79 refPosToCST(node.refPos))
80
82 if node is None:
83 return {}
84 return _combine({
85 "timescale": node.timeScale,},
86 refPosToCST(node.refPos))
87
89 if node is None:
90 return {}
91 return refPosToCST(node.refPos)
92
94 if node is None:
95 return {}
96 return _combine({
97 "redshiftType": node.type,
98 "dopplerdef": node.dopplerDef,},
99 refPosToCST(node.refPos))
100
101
102
103
105 if node is None:
106 return
107 if isinstance(node, dm.CooWiggle):
108 return node.values
109 elif isinstance(node, dm.RadiusWiggle):
110 return tuple((r,)*nDim for r in node.radii)
111 else:
112 raise common.STCValueError("Cannot serialize %s wiggles into STC-S"%
113 node.__class__.__name__)
114
115
117 """returns a function returning a CST fragment for a coordinate.
118 """
119 def toCST(node):
120 if node.frame is None:
121 return {}
122 nDim = node.frame.nDim
123 return {
124 "error": _wiggleToCST(node.error, nDim),
125 "resolution": _wiggleToCST(node.resolution, nDim),
126 "pixSize": _wiggleToCST(node.pixSize, nDim),
127 "unit": node.getUnitString(),
128 "type": cooType,
129 "pos": node.value or None,}
130 return toCST
131
132
134 res = {}
135 if node.lowerLimit or node.upperLimit:
136 res["coos"] = [c for c in (node.lowerLimit, node.upperLimit)
137 if c is not None]
138 return res
139
140
142
143 if node.lowerLimit and node.upperLimit:
144 type, coos = "TimeInterval", (node.lowerLimit, node.upperLimit)
145 elif node.lowerLimit:
146 type, coos = "StartTime", (node.lowerLimit,)
147 elif node.upperLimit:
148 type, coos = "StopTime", (node.upperLimit,)
149 else:
150 type, coos = "TimeInterval", ()
151 return {
152 "type": type,
153 "coos": coos}
154
155
157 """returns a CST fragment for an area.
158
159 areaType this CST type of the node returned, cooMaker is a function
160 that receives the node and returns a dictionary containing at least
161 a coos key. It can set other keys as well (e.g.
162 _makeTimeIntervalCoos needs to override the type key).
163 """
164 def toCST(node):
165 return _combine({
166 "fillfactor": node.fillFactor,
167 "type": areaType},
168 cooMaker(node))
169 return toCST
170
171
174 """returns a mapper building a CST fragment for a subphrase.
175
176 cooMapper and areaMapper are functions returning CST fragments
177 for coordinates and areas of this type, respectively.
178
179 getASTItems is a function that receives the AST root and has to
180 return either None (no matching items found in AST) or a pair
181 of coordinate and area, where area may be None. Use _makeASTItemsGetter
182 to build these functions.
183
184 The function returned expects the root of the AST as argument.
185 """
186 def toCST(astRoot):
187 items = getASTItems(astRoot)
188 if items is None:
189 return {}
190 coo, area = items
191 areaKeys = {}
192 if area:
193 areaKeys = areaMapper(area)
194 cooKeys = cooMapper(coo)
195 frame = coo.frame or area.frame
196 return _combine(cooKeys,
197 areaKeys,
198 frameMapper(frame))
199 return toCST
200
201
203 """returns a function that extracts coordinates and areas of
204 a certain type from an AST.
205
206 The function does all kinds of sanity checks and raises STCValueErrors
207 if those fail.
208
209 If all goes well, it will return a pair coo, area. coo is always
210 non-None, area may be None.
211 """
212 def getASTItems(astRoot):
213 areas, coo = getattr(astRoot, areaName), getattr(astRoot, cooName)
214 if not areas and not coo:
215 return None
216 if len(areas)>1:
217 raise common.STCValueError("STC-S does not support more than one area"
218 " but %s has length %d"%(areaName, len(areas)))
219 if areas:
220 area = areas[0]
221 else:
222 area = None
223 return coo, area
224 return getASTItems
225
226
228 if node.frame is None:
229 return {}
230 cstNode = getBase(node)
231 cstNode["size"] = _wiggleToCST(node.size, node.frame.nDim)
232 if node.epoch is not None:
233 yearDef = node.yearDef
234 if yearDef is None:
235 yearDef = "J"
236 cstNode["epoch"] = yearDef+str(node.epoch)
237 return cstNode
238
239
240 _timeToCST = _makePhraseTreeMapper(
241 _makeCooTreeMapper("Time"),
242 _makeAreaTreeMapper("TimeInterval", _makeTimeIntervalCoos),
243 _timeFrameToCST,
244 _makeASTItemsGetter("time", "timeAs"))
245 _simpleSpatialToCST = _makePhraseTreeMapper(
246 _spatialCooToCST,
247 _makeAreaTreeMapper("PositionInterval"),
248 _spaceFrameToCST,
249 _makeASTItemsGetter("place", "areas"))
250 _spectralToCST = _makePhraseTreeMapper(
251 _makeCooTreeMapper("Spectral"),
252 _makeAreaTreeMapper("SpectralInterval"),
253 _spectralFrameToCST,
254 _makeASTItemsGetter("freq", "freqAs"))
255 _redshiftToCST = _makePhraseTreeMapper(
256 _makeCooTreeMapper("Redshift"),
257 _makeAreaTreeMapper("RedshiftInterval"),
258 _redshiftFrameToCST,
259 _makeASTItemsGetter("redshift", "redshiftAs"))
260 _velocityToCST = _makePhraseTreeMapper(
261 _makeCooTreeMapper("VelocityInterval"),
262 _makeAreaTreeMapper("VelocityInterval"),
263 lambda _: {},
264 _makeASTItemsGetter("velocity", "velocityAs"))
265
267 if astRoot.astroSystem.libraryId:
268 return syslib.stripIVOID(astRoot.astroSystem.libraryId)
269
270
272 return {"geoCoos": ()}
273
275 return {"geoCoos": node.center+(node.radius,)}
276
278 return {"geoCoos": node.center+(node.smajAxis, node.sminAxis, node.posAngle)}
279
281 return {"geoCoos": node.center+node.boxsize}
282
284 return {"geoCoos": tuple(itertools.chain(*node.vertices))}
285
287 return {"geoCoos": tuple(itertools.chain(*node.vectors))}
288
290 return {"geoCoos": node.lowerLimit+node.upperLimit}
291
292 _compoundGeos = ["Union", "Difference", "Intersection", "Not"]
293
295 children = []
296 for c in node.children:
297 nodeName = c.__class__.__name__
298 base = {"subtype": nodeName}
299 if c in _compoundGeos:
300 children.append(_combine(base, _makeUnionCoos(c)))
301 else:
302 children.append(_combine(base,
303 globals()["_make%sCoos"%nodeName](c)))
304 return {"children": children}
305
306 _makeDifferenceCoos = _makeIntersectionCoos = _makeNotCoos = _makeUnionCoos
307
308 _geometryMappers = dict([(n, _makePhraseTreeMapper(
309 _spatialCooToCST,
310 _makeAreaTreeMapper(n, globals()["_make%sCoos"%n]),
311 _spaceFrameToCST,
312 _makeASTItemsGetter("place", "areas")))
313 for n in ["AllSky", "Circle", "Ellipse", "Box", "Polygon", "Convex"]+
314 _compoundGeos])
315
317 args = {}
318 velocityArgs = _velocityToCST(astRoot)
319 if velocityArgs:
320 args = {"velocity": velocityArgs}
321 node = (astRoot.areas and astRoot.areas[0]) or astRoot.place
322
323 if not node:
324
325
326 if args:
327 args.update(_spaceFrameToCST(astRoot.astroSystem.spaceFrame))
328 args["type"] = "Position"
329 elif isinstance(node, (dm.SpaceCoo, dm.SpaceInterval)):
330 args.update(_simpleSpatialToCST(astRoot))
331 else:
332 args.update(_geometryMappers[node.__class__.__name__](astRoot))
333 return args
334
335
336
337
339 res = " ".join(s for s in strList if s is not None)
340 if not res:
341 return None
342 return res
343
344
346 """returns a string made up of the non-null values in kwList in node.
347
348 To make things a bit more flexible, you can give lists in kwList.
349 Their elements will be inserted into the result as-is.
350
351 Flatteners is a dictionary mapping keys from kwList to functions
352
353 flat(val, node) -> string
354
355 that turn a value for the keyword to a complete string.
356 _makeKeywordFlattener and _makeNoKeywordFlattener generate such functions.
357 """
358 res = []
359 for key in kwList:
360 if isinstance(key, list):
361 res.extend(key)
362 elif key not in node:
363 pass
364 elif key in flatteners:
365 res.append(flatteners[key](node[key], node))
366 else:
367 res.append(node[key])
368 return _joinWithNull(res)
369
370
372 """returns a sensible STC-S string representation for many sorts of
373 values, dispatched on their type.
374
375 This function can be used as a flattener.
376 """
377 if val is None:
378 return ""
379 elif isinstance(val, basestring):
380 return str(val)
381 elif isinstance(val, (int, float)):
382 return str(val)
383 elif isinstance(val, (list, tuple)):
384 return " ".join(_flattenValue(v) for v in val)
385 elif isinstance(val, datetime.datetime):
386 return val.isoformat()
387 elif isinstance(val, common.ColRef):
388 return '"%s"'%val.dest
389 else:
390 raise common.STCValueError("Cannot serialize %r to STC-S"%val)
391
392
394 """returns a flattener for position values of type key.
395
396 The trick is to suppress key when the node already represents a position
397 type. Thus, we generate "Time 2" rather than Time Time 2 analogous to
398 "TimeInterval [...] Time 2".
399 """
400 def flattenPosition(val, node):
401 if val is not None:
402 if node["type"]==key:
403 return _flattenValue(val)
404 else:
405 return "%s %s"%(key, _flattenValue(val))
406 return flattenPosition
407
408
410 """returns a function returning a flattened value with keyword in front.
411 """
412 if keyword:
413 fmtStr = "%s %%s"%keyword
414 else:
415 fmtStr = "%s"
416 def flatten(val, node):
417 if val is not None and val!=():
418 return fmtStr%(_flattenValue(val))
419 return flatten
420
421
423 return _joinWithNull([node["refpos"], node["planetaryEphemeris"]])
424
425
426
427 _commonFlatteners = {
428 "fillfactor": _makeKeywordFlattener("fillfactor"),
429 "unit": _makeKeywordFlattener("unit"),
430 "error": _makeKeywordFlattener("Error"),
431 "resolution": _makeKeywordFlattener("Resolution"),
432 "size": _makeKeywordFlattener("Size"),
433 "pixSize": _makeKeywordFlattener("PixSize"),
434 "epoch": _makeKeywordFlattener("Epoch"),
435 "coos": _flattenValue,
436 "refpos": _flattenRefPos,
437 }
438
439
441 """returns a flattener for 1-D coordinates (time, spectral, redshift).
442 """
443 flatteners = {
444 "pos": _makePosValueFlattener(posKey),
445 }
446 flatteners.update(_commonFlatteners)
447 keyList = ["type", "fillfactor"]+frameKeys+["coos", "pos",
448 "unit", "error", "resolution", "pixSize"]
449 def flatten(node):
450 return _joinKeysWithNull(node, keyList, flatteners)
451 return flatten
452
453
455 """returns a flattener for velocities.
456
457 This is only used by _makePositionFlattener, since velocity is a subclause.
458 """
459 flatteners = {
460 "pos": _makePosValueFlattener("VelocityInterval"),
461 }
462 flatteners.update(_commonFlatteners)
463
464 def flattenVelocity(val, node):
465 """custom flattener for velocities, used by _flattenPosition.
466 """
467 res = []
468 if val:
469 if val.get("coos") is not None:
470 res.extend(["VelocityInterval", _joinKeysWithNull(val,
471 ["fillfactor", "coos"], flatteners)])
472 if val.get("pos") is not None:
473 res.extend(["Velocity", _joinKeysWithNull(val, ["pos"], flatteners)])
474 if not res:
475 res.append("Velocity")
476 res.append(_joinKeysWithNull(val,
477 ["unit", "error", "resolution", "size", "pixSize"], flatteners))
478 return _joinWithNull(res)
479
480 return flattenVelocity
481
482
484 def flattenCompoundChildren(childList, node):
485 """custom flattener for compounds (i.e., and, or, not)
486 """
487 res = []
488 for c in childList:
489 if "geoCoos" in c:
490 res.append("%s %s"%(
491 c["subtype"], _flattenValue(c["geoCoos"])))
492 else:
493 res.append("%s %s"%(c["subtype"],
494 flattenCompoundChildren(c["children"], node)))
495 if "geoCoos" in node:
496 return " ".join(res)
497 else:
498 return "(%s)"%(" ".join(res))
499
500 flatteners = {
501 "pos": _makePosValueFlattener("Position"),
502 "geoCoos": _makeKeywordFlattener(""),
503 "velocity": _makeVelocityFlattener(),
504 "children": flattenCompoundChildren,
505 }
506 flatteners.update(_commonFlatteners)
507
508 def flattenPosition(node):
509 """returns an STC-S representation of position and velocity.
510 """
511 return _joinKeysWithNull(node, ["type", "fillfactor", "frame",
512 "equinox", "refpos", "flavor", "epoch", "coos", "geoCoos", "children",
513 "pos", "unit", "error", "resolution", "size", "pixSize", "velocity"],
514 flatteners)
515
516 return flattenPosition
517
518
519
520
521 _flattenTime = _make1DCooFlattener("Time", ["timescale", "refpos"])
522 _flattenSpectral = _make1DCooFlattener("Spectral", ["refpos"])
523 _flattenRedshift = _make1DCooFlattener("Redshift",
524 ["refpos", "redshiftType", "dopplerdef"])
525 _flattenSpatial = _makeSpatialFlattener()
526
528 if node:
529 return "System %s"%node
530
531
533 """returns a flattened string for an STCS CST.
534
535 Flattening destroys the tree.
536 """
537 return "\n".join([s for s in (
538 _flattenTime(cst.get("time", {})),
539 _flattenSpatial(cst.get("space", {})),
540 _flattenSpectral(cst.get("spectral", {})),
541 _flattenRedshift(cst.get("redshift", {})),
542 _flattenSystem(cst.get("libSystem")),)
543 if s])
544
545
547 """returns an STC-S string for an AST.
548 """
549 cst = stcs.removeDefaults({
550 "time": _timeToCST(astRoot),
551 "space": _spatialToCST(astRoot),
552 "spectral": _spectralToCST(astRoot),
553 "redshift": _redshiftToCST(astRoot),
554 "libSystem": _sysIdToCST(astRoot),
555 })
556 return _flattenCST(cst)
557
558
560 """returns a phrase for the spatial system (for manual STC-S generation).
561 """
562 cst = stcs.removeDefaults({"space": _spatialToCST(astRoot)})
563 return _joinKeysWithNull(cst["space"], ["frame",
564 "equinox", "refpos", "flavor", "epoch"], _commonFlatteners)
565
566
568 """A factory for Boxes.
569 """
570 if colDesc["dbtype"]!="box":
571 return
572
573 if colDesc.original.stc:
574 systemString = getSpatialSystem(colDesc.original.stc)
575 else:
576 systemString = "UNKNOWN"
577
578 def mapper(val):
579 if val is None:
580 return ""
581 else:
582 return "Box %s %s %s %s %s"%((systemString,)+val[0]+val[1])
583 colDesc["datatype"], colDesc["arraysize"] = "char", "*"
584 return mapper
585 utils.registerDefaultMF(_boxMapperFactory)
586