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

Source Code for Module gavo.stc.stcsast

  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  #c Copyright 2008-2019, the GAVO project 
  9  #c 
 10  #c This program is free software, covered by the GNU GPL.  See the 
 11  #c COPYING file in the source distribution. 
 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: # No handler, ignore this node 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 ############## Coordinate systems 79
80 -def _makeRefpos(node):
81 refposName = node.get("refpos") 82 if refposName=="UNKNOWNRefPos": 83 refposName = None 84 return dm.RefPos(standardOrigin=refposName, 85 planetaryEphemeris=node.get("plEphemeris"))
86
87 -def _buildRedshiftFrame(node, context):
88 yield "redshiftFrame", dm.RedshiftFrame(dopplerDef=node["dopplerdef"], 89 type=node["redshiftType"], refPos=_makeRefpos(node))
90 91 # Simple translations of reference frame names from STC-S to STC-X 92 93 _frameTrans = { 94 "GALACTIC": "GALACTIC_II", 95 "UNKNOWNFrame": None,} 96
97 -def _buildSpaceFrame(node, context):
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: # allow J2000 and expand it to J2000.0 106 equinox = node["equinox"]+".0" 107 yield "spaceFrame", dm.SpaceFrame(refPos=_makeRefpos(node), 108 flavor=flavor, nDim=nDim, refFrame=frame, equinox=equinox)
109
110 -def _buildSpectralFrame(node, context):
111 yield "spectralFrame", dm.SpectralFrame( 112 refPos=_makeRefpos(node))
113
114 -def _buildTimeFrame(node, context):
115 ts = node.get("timescale") 116 if ts=="nil": 117 ts = None 118 yield "timeFrame", dm.TimeFrame(refPos=_makeRefpos(node), 119 timeScale=ts)
120
121 -def getCoordSys(cst):
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 ############## Coordinates and their intervals 134 135
136 -def iterVectors(values, dim, spatial):
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
152 -def _iterIntervals(coos, dim, spatial=False):
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
175 -def _makeWiggleValues(nDim, val, minItems=None, maxItems=None, spatial=False):
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: # might be error radii if all values are equal 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
187 -def _validateCoos(values, nDim, minItems, maxItems):
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 # special case: a *single* ColRef is good for anything (could be 198 # an array or something) 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
231 -def _addUnitPlain(args, node, frame):
232 args["unit"] = node.get("unit")
233 234
235 -def _addUnitRedshift(args, node, frame):
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
248 -def _mogrifySpaceUnit(unit, nDim):
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
260 -def _addUnitSpatial(args, node, frame):
261 unit, nDim = node.get("unit"), frame.nDim 262 args["unit"] = _mogrifySpaceUnit(unit, nDim)
263 264
265 -def _addUnitVelocity(args, node, frame):
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
290 -def _makeBasicCooArgs(node, frame, posClass, spatial=False):
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 # Yield a coordinate 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 # Yield an area if defined in this phrase and non-empty 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: # don't yield intervals that just define a frame 361 yield intervalKey, (intervalClass(**args),)
362 363 return builder 364 365
366 -def _makeIntervalKeyIterator(preferUpper=False):
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 ###################### Geometries 386 387
388 -def _makeGeometryKeyIterator(argDesc, clsName):
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', # make sure the thing is an iterator 406 ' coos = node.get("coos", ())', 407 ' yield "origUnit",' 408 ' _mogrifySpaceUnit(node.get("unit"), nDim)', 409 " try:", 410 " pass"] 411 # Everthing below here just coordinates 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 #noflake: name created via exec
438 439
440 -def _makeGeometryKeyIterators():
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
457 -def _makeGeometryBuilder(cls):
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
466 -def _compoundGeometryKeyIterator(node, nDim, spatial):
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: # child is another compound geometry 477 children.append(destCls(**dict( 478 _compoundGeometryKeyIterator(c, nDim, True)))) 479 yield "children", children
480 481
482 -def _makeCompoundGeometryBuilder(cls):
483 """returns a builder for compound geometries. 484 """ 485 return _makeCooBuilder("spaceFrame", cls, "areas", dm.SpaceCoo, 486 "place", _compoundGeometryKeyIterator, spatial=True)
487 488 489 ###################### Top level 490 491
492 -def getCoords(cst, system):
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
545 -def parseSTCS(literal, grammarFactory=None):
546 """returns an STC AST for an STC-S expression. 547 """ 548 cst = stcs.getCST(literal, grammarFactory) 549 if "libSystem" in cst: 550 system = syslib.getLibrarySystem(cst["libSystem"]) 551 else: 552 system = getCoordSys(cst)[1] 553 args = {"astroSystem": system} 554 args.update(getCoords(cst, system)) 555 return dm.STCSpec(**args).polish()
556 557
558 -def parseQSTCS(literal):
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