1 """
2 Transformation of STC-S CSTs to STC ASTs.
3
4 The central function here is buildTree; the rest of the module basically
5 provides the handler functions. All this is tied together in parseSTCS.
6 """
7
8
9
10
11
12
13
14 from __future__ import print_function
15
16 from gavo.stc import common
17 from gavo.stc import dm
18 from gavo.stc import stcs
19 from gavo.stc import syslib
20
21
22 -def buildTree(tree, context, pathFunctions={}, nameFunctions={},
23 typeFunctions={}):
24 """traverses tree, calling functions on nodes.
25
26 pathFunctions is a dictionary mapping complete paths (i.e., tuples
27 of node labels) to handler functions, nameFunctions name a single
28 label and are called for nodes that don't match a pathFunction if
29 the last item of their paths is the label. Both of these currently
30 are not used. Instead, everything hinges on the fallback, which is
31 a node's type value (generated from the key words), matched against
32 typeFunctions.
33
34 The handler functions must be iterators. If they yield anything,
35 it must be key-value pairs.
36
37 All key-value pairs are collected in a dictionary that is then
38 returned. If value is a tuple, it is appended to the current value
39 for the key.
40
41 Context is an arbitrary object containing ancillary information for
42 building nodes. What's in there and what's not is up to the functions
43 and their callers.
44 """
45 resDict = {}
46 for path, node in stcs.iterNodes(tree):
47 if path in pathFunctions:
48 handler = pathFunctions[path]
49 elif path and path[-1] in nameFunctions:
50 handler = nameFunctions[path[-1]]
51 elif node.get("type") in typeFunctions:
52 handler = typeFunctions[node["type"]]
53 else:
54 continue
55 for res in handler(node, context):
56 k, v = res
57 if isinstance(v, tuple):
58 resDict.setdefault(k, []).extend(v)
59 else:
60 if k in resDict:
61 raise common.STCInternalError("Attempt to overwrite key '%s', old"
62 " value %s, new value %s (this should probably have been"
63 " a tuple)"%(k, resDict[k], v))
64 resDict[k] = v
65 return resDict
66
67
68 -class GenericContext(object):
69 """is an object that can be used for context.
70
71 It simply exposes all its constructor arguments as attributes.
72 """
73 - def __init__(self, **kwargs):
74 for k, v in kwargs.iteritems():
75 setattr(self, k, v)
76
77
78
79
81 refposName = node.get("refpos")
82 if refposName=="UNKNOWNRefPos":
83 refposName = None
84 return dm.RefPos(standardOrigin=refposName,
85 planetaryEphemeris=node.get("plEphemeris"))
86
88 yield "redshiftFrame", dm.RedshiftFrame(dopplerDef=node["dopplerdef"],
89 type=node["redshiftType"], refPos=_makeRefpos(node))
90
91
92
93 _frameTrans = {
94 "GALACTIC": "GALACTIC_II",
95 "UNKNOWNFrame": None,}
96
98 nDim, flavor = stcs.stcsFlavors[node["flavor"]]
99 frame = node["frame"]
100 frame = _frameTrans.get(frame, frame)
101 equinox = None
102 if node.get("equinox"):
103 if "." in node["equinox"]:
104 equinox = node["equinox"]
105 else:
106 equinox = node["equinox"]+".0"
107 yield "spaceFrame", dm.SpaceFrame(refPos=_makeRefpos(node),
108 flavor=flavor, nDim=nDim, refFrame=frame, equinox=equinox)
109
113
115 ts = node.get("timescale")
116 if ts=="nil":
117 ts = None
118 yield "timeFrame", dm.TimeFrame(refPos=_makeRefpos(node),
119 timeScale=ts)
120
122 """returns constructor arguments for a CoordSys from an STC-S CST.
123 """
124 args = buildTree(cst, None, nameFunctions={
125 'redshift': _buildRedshiftFrame,
126 'space': _buildSpaceFrame,
127 'spectral': _buildSpectralFrame,
128 'time': _buildTimeFrame,
129 })
130 return "system", dm.CoordSys(**args)
131
132
133
134
135
137 """iterates over dim-dimensional vectors made of values.
138
139 The function does not check if the last vector is actually complete.
140 """
141 if isinstance(values, common.ColRef):
142 yield values
143 return
144 if dim==1 and not spatial:
145 for v in values:
146 yield v
147 else:
148 for index in range(0, len(values), dim):
149 yield tuple(values[index:index+dim])
150
151
153 """iterates over pairs dim-dimensional vectors.
154
155 It will always return at least one empty (i.e., None, None) pair.
156 The last pair returned may be incomplete (specifying a start
157 value only, supposedly) but not empty.
158 """
159 first, startValue = True, None
160 for item in iterVectors(coos, dim, spatial):
161 if startValue is None:
162 if first:
163 first = False
164 startValue = item
165 else:
166 yield (startValue, item)
167 startValue = None
168 if startValue is None:
169 if first:
170 yield (None, None)
171 else:
172 yield (startValue, None)
173
174
176 if val is None:
177 return
178 values = _makeCooValues(nDim, val, minItems, maxItems, spatial)
179 if not values:
180 return
181 if nDim>1:
182 if set([1])==set(len(set(v)) for v in values):
183 return dm.RadiusWiggle(radii=tuple(v[0] for v in values))
184 return dm.CooWiggle(values=values)
185
186
188 """makes sure values is valid a source of between minItems and maxItems
189 nDim-dimensional tuples.
190
191 minItems and maxItems may both be None to signify no limit.
192 """
193 if isinstance(values, common.GeometryColRef):
194 values.expectedLength = nDim
195 numItems = len(values)/nDim
196 if numItems*nDim!=len(values):
197
198
199 if len(values)==1 and isinstance(values[0], common.ColRef):
200 return
201 raise common.STCSParseError("%s is not valid input to create %d-dimensional"
202 " coordinates"%(values, nDim))
203 if minItems is not None and numItems<minItems:
204 raise common.STCSParseError("Expected at least %d coordinates in %s."%(
205 minItems, values))
206 if maxItems is not None and numItems>maxItems:
207 raise common.STCValueError(
208 "Expected not more than %d coordinates in %s."%(maxItems, values))
209
210
211 -def _makeCooValues(nDim, values, minItems=None, maxItems=None, spatial=False):
212 """returns a list of nDim-Tuples made up of values.
213
214 If values does not contain an integral multiple of nDim items,
215 the function will raise an STCSParseError. You can also optionally
216 give a minimally or maximally expected number of tuples. If the
217 constraints are violated, again an STCSParseError is raised.
218
219 If spatial is true, tuples will be returned even for 1D data.
220 """
221 if values is None:
222 if minItems:
223 raise common.STCSParseError("Expected at least %s coordinate items but"
224 " found none."%minItems)
225 else:
226 return
227 _validateCoos(values, nDim, minItems, maxItems)
228 return tuple(v for v in iterVectors(values, nDim, spatial))
229
230
232 args["unit"] = node.get("unit")
233
234
236 unit = node.get("unit")
237 if unit=="nil":
238 args["unit"] = ""
239 args["velTimeUnit"] = None
240 elif unit:
241 parts = unit.split("/")
242 if len(parts)!=2:
243 raise common.STCSParseError("'%s' is not a valid unit for redshifts"%unit)
244 args["unit"] = parts[0]
245 args["velTimeUnit"] = parts[1]
246
247
249 if unit:
250 parts = unit.split()
251 if len(parts)==nDim:
252 return tuple(parts)
253 elif len(parts)==1:
254 return (unit,)*nDim
255 else:
256 raise common.STCSParseError("'%s' is not a valid for unit %d-dimensional"
257 " spatial coordinates"%(unit, nDim))
258
259
263
264
266 unit, nDim = node.get("unit"), frame.nDim
267 if unit:
268 su, vu = [], []
269 parts = unit.split()
270 for uS in parts:
271 up = uS.split("/")
272 if len(up)!=2:
273 raise common.STCSParseError(
274 "'%s' is not a valid unit for velocities."%uS)
275 su.append(up[0])
276 vu.append(up[1])
277 args["unit"] = _mogrifySpaceUnit(" ".join(su), nDim)
278 args["velTimeUnit"] = _mogrifySpaceUnit(" ".join(vu), nDim)
279
280
281 _unitMakers = {
282 dm.SpectralType: _addUnitPlain,
283 dm.TimeType: _addUnitPlain,
284 dm.SpaceType: _addUnitSpatial,
285 dm.RedshiftType: _addUnitRedshift,
286 dm.VelocityType: _addUnitVelocity,
287 }
288
289
291 """returns a dictionary containing constructor arguments common to
292 all items dealing with coordinates.
293 """
294 nDim = frame.nDim
295 args = {
296 "error": _makeWiggleValues(nDim, node.get("error"), maxItems=2,
297 spatial=spatial),
298 "resolution": _makeWiggleValues(nDim, node.get("resolution"), maxItems=2,
299 spatial=spatial),
300 "pixSize": _makeWiggleValues(nDim, node.get("pixSize"), maxItems=2,
301 spatial=spatial),
302 "size": _makeWiggleValues(nDim, node.get("size"), maxItems=2,
303 spatial=spatial),
304 "frame": frame,
305 }
306 if spatial and node.get("epoch"):
307 if isinstance(node["epoch"], common.ColRef):
308 args["epoch"] = node["epoch"]
309 args["yearDef"] = "J"
310 else:
311 args["epoch"] = float(node["epoch"][1:])
312 args["yearDef"] = node["epoch"][0]
313 _unitMakers[posClass.cType](args, node, frame)
314 return args
315
316
317 -def _makeCooBuilder(frameName, intervalClass, intervalKey,
318 posClass, posKey, iterIntervKeys, spatial=False):
319 """returns a function(node, context) -> ASTNode for building a
320 coordinate-like AST node.
321
322 frameName is the name of the coordinate frame within
323 context.system,
324
325 (interval|pos)(Class|Key) are the class (key) to be used
326 (returned) for the interval/geometry and simple coordinate found
327 in the phrase. If intervalClass is None, no interval/geometry
328 will be built.
329
330 iterIntervKeys is an iterator that yields key/value pairs for intervals
331 or geometries embedded.
332
333 Single positions are always expected under the coo key.
334 """
335 positionExclusiveKeys = ["error", "resolution", "pixSize", "value",
336 "size", "unit", "velTimeUnit", "epoch", "yearDef"]
337 def builder(node, context):
338 frame = getattr(context.system, frameName)
339 nDim = frame.nDim
340 args = _makeBasicCooArgs(node, frame, posClass, spatial)
341
342
343 if "pos" in node:
344 args["value"] = _makeCooValues(nDim, node["pos"],
345 minItems=1, maxItems=1, spatial=spatial)[0]
346 else:
347 args["value"] = None
348 yield posKey, posClass(**args)
349
350
351 if intervalClass is None:
352 return
353 for key in positionExclusiveKeys:
354 if key in args:
355 del args[key]
356 for k, v in iterIntervKeys(node, nDim, spatial=spatial):
357 args[k] = v
358 if "fillfactor" in node:
359 args["fillFactor"] = node["fillfactor"]
360 if len(set(args))>1:
361 yield intervalKey, (intervalClass(**args),)
362
363 return builder
364
365
367 """returns a function yielding ASTNode constructor keys for intervals.
368 """
369 def iterKeys(node, nDim, spatial=False):
370 res, coos = {}, node.get("coos", ())
371 _validateCoos(coos, nDim, None, None)
372 for interval in _iterIntervals(coos, nDim, spatial):
373 if preferUpper:
374 res["upperLimit"], res["lowerLimit"] = interval
375 else:
376 res["lowerLimit"], res["upperLimit"] = interval
377 if res["upperLimit"]:
378 yield "upperLimit", res["upperLimit"]
379 if res["lowerLimit"]:
380 yield "lowerLimit", res["lowerLimit"]
381 return iterKeys
382
383
384
385
386
387
389 """returns a key iterator for use with _makeCooBuilder that yields
390 the keys particular to certain geometries.
391
392 ArgDesc describes what keys should be parsed from the node's coos key.
393 It consists for tuples of name and type code, where type code is one of:
394
395 - r -- a single real value.
396 - v -- a vector of dimensionality given by the system (i.e., nDim).
397 - rv -- a sequence of v items of arbitrary length.
398 - cv -- a sequence of "Convex" vectors (dim 4) of arbitrary length.
399
400 rv may only occur at the end of argDesc since it will consume all
401 remaining coordinates.
402 """
403 parseLines = [
404 "def iterKeys(node, nDim, spatial=True):",
405 ' if False: yield',
406 ' coos = node.get("coos", ())',
407 ' yield "origUnit",'
408 ' _mogrifySpaceUnit(node.get("unit"), nDim)',
409 " try:",
410 " pass"]
411
412 parseLines.extend([
413 ' if isinstance(coos, common.GeometryColRef):',
414 ' yield "geoColRef", coos',
415 ' return'])
416 for name, code in argDesc:
417 if code=="r":
418 parseLines.append(' yield "%s", coos.pop(0)'%name)
419 elif code=="v":
420 parseLines.append(' vec = coos[:nDim]')
421 parseLines.append(' coos = coos[nDim:]')
422 parseLines.append(' _validateCoos(vec, nDim, 1, 1)')
423 parseLines.append(' yield "%s", tuple(vec)'%name)
424 elif code=="rv":
425 parseLines.append(' yield "%s", _makeCooValues(nDim, coos)'%name)
426 parseLines.append(' coos = []')
427 elif code=="cv":
428 parseLines.append(' yield "%s", _makeCooValues(4, coos)'%name)
429 parseLines.append(' coos = []')
430 parseLines.append(' except IndexError:')
431 parseLines.append(' raise common.STCSParseError("Not enough coordinates'
432 ' while parsing %s")'%clsName)
433 parseLines.append(
434 ' if coos: raise common.STCSParseError("Too many coordinates'
435 ' while building %s, remaining: %%s"%%coos)'%clsName)
436 exec "\n".join(parseLines)
437 return iterKeys
438
439
441 return dict(
442 (clsName, _makeGeometryKeyIterator(argDesc, clsName))
443 for clsName, argDesc in [
444 ("AllSky", []),
445 ("Circle", [('center', 'v'), ('radius', 'r')]),
446 ("Ellipse", [('center', 'v'), ('smajAxis', 'r'), ('sminAxis', 'r'),
447 ('posAngle', 'r')]),
448 ("Box", [('center', 'v'), ('boxsize', 'v')]),
449 ("Polygon", [("vertices", "rv")]),
450 ("Convex", [("vectors", "cv")]),
451 ("PositionInterval", [("lowerLimit", "v"), ("upperLimit", "v")]),
452 ])
453
454 _geometryKeyIterators = _makeGeometryKeyIterators()
455
456
458 """returns a builder for Geometries.
459
460 See _makeGeometryKeyIterator for the meaning of the arguments.
461 """
462 return _makeCooBuilder("spaceFrame", cls, "areas", dm.SpaceCoo,
463 "place", _geometryKeyIterators[cls.__name__], spatial=True)
464
465
467 """yields keys to configure compound geometries.
468 """
469 children = []
470 for c in node["children"]:
471 childType = c["subtype"]
472 destCls = getattr(dm, childType)
473 if childType in _geometryKeyIterators:
474 children.append(destCls(**dict(
475 _geometryKeyIterators[childType](c, nDim, True))))
476 else:
477 children.append(destCls(**dict(
478 _compoundGeometryKeyIterator(c, nDim, True))))
479 yield "children", children
480
481
483 """returns a builder for compound geometries.
484 """
485 return _makeCooBuilder("spaceFrame", cls, "areas", dm.SpaceCoo,
486 "place", _compoundGeometryKeyIterator, spatial=True)
487
488
489
490
491
493 """returns an argument dict for constructing STCSpecs for plain coordinates.
494 """
495 context = GenericContext(system=system)
496
497 return buildTree(cst, context, typeFunctions = {
498 "Time": _makeCooBuilder("timeFrame", None, None,
499 dm.TimeCoo, "time", None),
500 "StartTime": _makeCooBuilder("timeFrame", dm.TimeInterval, "timeAs",
501 dm.TimeCoo, "time", _makeIntervalKeyIterator()),
502 "StopTime": _makeCooBuilder("timeFrame", dm.TimeInterval, "timeAs",
503 dm.TimeCoo, "time", _makeIntervalKeyIterator(preferUpper=True)),
504 "TimeInterval": _makeCooBuilder("timeFrame", dm.TimeInterval, "timeAs",
505 dm.TimeCoo, "time", _makeIntervalKeyIterator()),
506
507 "Position": _makeCooBuilder("spaceFrame", None, None, dm.SpaceCoo,
508 "place", None, spatial=True),
509 "PositionInterval": _makeCooBuilder("spaceFrame",
510 dm.SpaceInterval, "areas", dm.SpaceCoo, "place",
511 _makeIntervalKeyIterator(), spatial=True),
512 "Velocity": _makeCooBuilder("spaceFrame",
513 dm.VelocityInterval, "velocityAs", dm.VelocityCoo, "velocity",
514 _makeIntervalKeyIterator(), spatial=True),
515 "VelocityInterval": _makeCooBuilder("spaceFrame",
516 dm.VelocityInterval, "velocityAs", dm.VelocityCoo, "velocity",
517 _makeIntervalKeyIterator(), spatial=True),
518 "AllSky": _makeGeometryBuilder(dm.AllSky),
519 "Circle": _makeGeometryBuilder(dm.Circle),
520 "Ellipse": _makeGeometryBuilder(dm.Ellipse),
521 "Box": _makeGeometryBuilder(dm.Box),
522 "Polygon": _makeGeometryBuilder(dm.Polygon),
523 "Convex": _makeGeometryBuilder(dm.Convex),
524
525 "Union": _makeCompoundGeometryBuilder(dm.Union),
526 "Intersection": _makeCompoundGeometryBuilder(dm.Intersection),
527 "Difference": _makeCompoundGeometryBuilder(dm.Difference),
528 "Not": _makeCompoundGeometryBuilder(dm.Not),
529
530 "Spectral": _makeCooBuilder("spectralFrame", None, None,
531 dm.SpectralCoo, "freq", None),
532 "SpectralInterval": _makeCooBuilder("spectralFrame",
533 dm.SpectralInterval, "freqAs", dm.SpectralCoo, "freq",
534 _makeIntervalKeyIterator()),
535
536 "Redshift": _makeCooBuilder("redshiftFrame", None, None,
537 dm.RedshiftCoo, "redshift", None),
538 "RedshiftInterval": _makeCooBuilder("redshiftFrame",
539 dm.RedshiftInterval, "redshiftAs", dm.RedshiftCoo, "redshift",
540 _makeIntervalKeyIterator()),
541
542 })
543
544
556
557
559 """returns an STC AST for an STC-S expression with identifiers instead of
560 values.
561
562 The identifiers are denoted in double-quoted strings. Legal identifiers
563 follow the python syntax (i.e., these are *not* SQL quoted identifiers).
564 """
565 return parseSTCS(literal, grammarFactory=stcs.getColrefGrammar)
566
567
568 if __name__=="__main__":
569 print(parseSTCS("Union FK5 (Box 12 -13 2 2 Not (Circle 14 -13.5 3))"))
570