"""
Definition of the structure of the internal representation (the AST).
For now, we want to be able to capture what STC-S can do (and a bit more).
This means that we do not support generic coordinates (yet), elements,
xlink and all the other stuff.
"""
#c Copyright 2008-2023, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL. See the
#c COPYING file in the source distribution.
import itertools
import operator
import re
from gavo import utils
from gavo.stc import times
from gavo.stc import units
from gavo.stc import common
from gavo.utils import pgsphere
from functools import reduce
################ Coordinate Systems
[docs]class RefPos(common.ASTNode):
"""is a reference position.
Right now, this is just a wrapper for a RefPos id, as defined by STC-S,
or None for Unknown.
"""
# If we ever support non-standard origins, they should go into a different
# class, I guess. Or, we'd need a sentinel for standardOrigin (like,
# NONSTANDARD). None, anyway, is for Unknown, and we shouldn't change that.
_a_standardOrigin = None
_a_planetaryEphemeris = None
NullRefPos = RefPos()
class _CoordFrame(common.ASTNode):
"""is an astronomical coordinate frame.
"""
_a_name = None
_a_refPos = None
def _setupNode(self):
if self.refPos is None:
self.refPos = NullRefPos
def isSpherical(self):
"""returns True if this is a frame deemed suitable for space
frame transformations.
This is really a property of stc.sphermath rather than of the
data model, but it's more convenient to have this as a frame
method.
"""
return (isinstance(self, SpaceFrame)
and self.nDim>1
and self.flavor=="SPHERICAL")
[docs]class TimeFrame(_CoordFrame):
nDim = 1
_a_timeScale = None
[docs]class SpaceFrame(_CoordFrame):
_a_flavor = "SPHERICAL"
_a_nDim = None
_a_refFrame = None
_a_equinox = None # if non-null, it has to match [BJ][0-9]+[.][0-9]+
def _setupNode(self):
if self.refFrame=="J2000":
self.refFrame = "FK5"
self.equinox = "J2000.0"
elif self.refFrame=="B1950":
self.refFrame = "FK4"
self.equinox = "B1950.0"
if self.nDim is None:
self.nDim = 2
_CoordFrame._setupNode(self)
[docs] def getEquinox(self):
"""returns a datetime.datetime instance for the frame's equinox.
It will return None if no equinox is given, and it may raise an
STCValueError if an invalid equinox string has been set.
"""
if self.equinox is None:
return None
mat = re.match("([B|J])([0-9.]+)", self.equinox)
if not mat:
raise common.STCValueError("Equinoxes must be [BJ]<float>, but %s isn't"%(
self.equinox))
if mat.group(1)=='B':
return times.bYearToDateTime(float(mat.group(2)))
else:
return times.jYearToDateTime(float(mat.group(2)))
[docs] def asTriple(self):
"""returns a triple defining the space frame for spherc's purposes.
This is for the computation of coordinate transforms. Since we only
do coordinate transforms for spherical coordinate systems, this
will, for now, raise STCValueErrors if everything but 2 or 3D SPHERICAL
flavours. The other cases need more thought anyway.
"""
if self.flavor!="SPHERICAL" or (self.nDim!=2 and self.nDim!=3):
raise common.STCValueError("Can only conform 2/3-spherical coordinates")
return (self.refFrame, self.getEquinox(), self.refPos.standardOrigin)
[docs]class SpectralFrame(_CoordFrame):
nDim = 1
[docs]class RedshiftFrame(_CoordFrame):
nDim = 1
_a_dopplerDef = None
_a_type = None
[docs]class CoordSys(common.ASTNode):
"""is an astronomical coordinate system.
"""
_a_timeFrame = None
_a_spaceFrame = None
_a_spectralFrame = None
_a_redshiftFrame = None
_a_name = None
_a_libraryId = None # for standard coordinate systems, the ivo://whatever.
class _CooTypeSentinel(object):
"""is a base for type indicators.
Never instantiate any of these.
"""
[docs]class SpectralType(_CooTypeSentinel):
posAttr = "freq"
[docs]class TimeType(_CooTypeSentinel):
posAttr = "time"
[docs]class SpaceType(_CooTypeSentinel):
posAttr = "place"
[docs]class RedshiftType(_CooTypeSentinel):
posAttr = "redshift"
[docs]class VelocityType(_CooTypeSentinel):
posAttr = "velocity"
############### Coordinates and their intervals
class _WiggleSpec(common.ASTNode):
"""A base for "wiggle" specifications.
These are Errors, Resolutions, Sizes, and PixSizes. They may come
as simple coordinates (i.e., scalars or vectors) or, in 2 and 3D,
as radii or matrices (see below). In all cases, two values may
be given to indicate ranges.
These need an adaptValuesWith(converter) method that will return a wiggle of
the same type but with every value replaced with the result of the
application of converter to that value.
"""
[docs]class CooWiggle(_WiggleSpec):
"""A wiggle given in coordinates.
The values attributes stores them just like coordinates are stored.
"""
_a_values = ()
_a_origUnit = None
inexactAttrs = set(["values"])
[docs] def adaptValuesWith(self, unitConverter):
if unitConverter is None:
return self
return self.change(values=tuple(unitConverter(v) for v in self.values))
[docs] def getValues(self):
return self.values
[docs]class RadiusWiggle(_WiggleSpec):
"""An wiggle given as a radius.
If unit adaption is necessary and the base value is a vector, the radii
are assumed to be of the dimension of the first vector component.
"""
_a_radii = ()
_a_origUnit = None
inexactAttrs = set(["radii"])
[docs] def adaptValuesWith(self, unitConverter):
if unitConverter is None:
return self
return self.change(radii=tuple(unitConverter(itertools.repeat(r))[0]
for r in self.radii))
[docs] def getValues(self):
return self.radii
[docs]class MatrixWiggle(_WiggleSpec):
"""A matrix for specifying wiggle.
The matrix/matrices are stored as sequences of sequences; see
stcxgen._wrapMatrix for details.
"""
_a_matrices = ()
_a_origUnit = None
[docs] def adaptValuesWith(self, unitConverter):
raise common.STCValueError("Matrix wiggles cannot be transformed.")
class _CoordinateLike(common.ASTNode):
"""An abstract base for everything that has a frame.
They can return a position object of the proper type and with the
same unit as self.
When deriving from _CoordinateLike, you have at some point to define
a cType class attribute that has values in the _CooTypeSentinels above.
"""
_a_frame = None
_a_name = None
def getPosition(self, initArgs=None):
"""returns a position appropriate for this class.
This is a shallow copy of the xCoo object itself for xCoos,
xCoo for xInterval, and SpaceCoo for Geometries. Common attributes
are copied to the new object.
"""
posClass = _positionClassMap[self.cType]
if initArgs is None:
initArgs = {}
for name, default in posClass._nodeAttrs:
if name!="id" and name not in initArgs:
initArgs[name] = getattr(self, name, default)
return posClass(**initArgs)
class _Coordinate(_CoordinateLike):
"""An abstract base for coordinates.
They have an iterTransformed(convFunc) method iterating over
constructor keys that have to be changed when some convFunc is
applied to the coordinate. These may be multiple values when,
e.g., errors are given or for geometries.
Since these only make sense together with units, some elementary
unit handling is required. Since we keep the basic unit model
of STC, this is a bit over-complicated.
First, for the benefit of STC-S, a method getUnitString() ->
string or None is required. It should return an STC-S-legal
unit string.
Second, a method getUnitArgs() -> dict or None is required.
It has to return a dictionary with all unit-related constructor
arguments (that's unit and velTimeUnit for the standard coordinate
types). No None values are allowed; if self's units are not
defined, return None.
Third, a method getUnitConverter(otherUnits) -> function or None is required.
OtherUnits can be a tuple or a result of getUnitArgs. The tuple is
interpreted as (baseUnit, timeUnit). The function returned must
accept self's coordinate values in otherUnit and return them in self's
unit(s). This is the function that iterTransformed requires.
"""
_a_error = None
_a_resolution = None
_a_pixSize = None
_a_value = None
_a_size = None
_dimensionedAttrs = ["error", "resolution", "pixSize", "size"]
inexactAttrs = set(["value"])
def _setupNode(self):
for name in self._dimensionedAttrs:
wiggle = getattr(self, name)
if wiggle and wiggle.origUnit is not None:
setattr(self, name, wiggle.adaptValuesWith(
self.getUnitConverter(wiggle.origUnit)))
self._setupNodeNext(_Coordinate)
def iterTransformed(self, converter):
if self.value is not None:
yield "value", converter(self.value)
for attName in self._dimensionedAttrs:
wiggle = getattr(self, attName)
if wiggle:
yield attName, wiggle.adaptValuesWith(converter)
class _OneDMixin(object):
"""provides attributes for 1D-Coordinates (Time, Spectral, Redshift)
"""
_a_unit = None
def getUnitString(self):
return self.unit
def getUnitConverter(self, otherUnits):
if self.unit is None or not otherUnits:
return None
if isinstance(otherUnits, dict):
otherUnits = (otherUnits["unit"],)
return units.getBasicConverter(self.unit, otherUnits[0], True)
def getUnitArgs(self):
if self.unit:
return {"unit": self.unit}
def getValues(self):
return [self.value]
class _SpatialMixin(object):
"""provides attributes for positional coordinates.
In addition to unit management, this is also carries an epoch in years.
You can, in addition, set yearDef. If None, Julian years are implied,
but you can have B for Bessel years.
"""
_a_unit = ()
_a_epoch = None
_a_yearDef = None
cType = SpaceType
def getUnitString(self):
if self.unit:
if len(set(self.unit))==1:
return self.unit[0]
else:
return " ".join(self.unit)
def getUnitConverter(self, otherUnits):
if self.unit is None or not otherUnits:
return None
if isinstance(otherUnits, dict):
otherUnits = (otherUnits["unit"],)
f = units.getVectorConverter(self.unit, otherUnits[0], True)
return f
def getUnitArgs(self):
if self.unit==():
# horrendous default: an empty spatial coordinate as (deg, deg),
# because that's what most likely works with geometries.
# Oh boy, STC1 is *so* broken it's not funny any more.
return {"unit": ("deg", "deg")}
return {"unit": self.unit}
def getValues(self):
if self.value is None:
return []
return self.value
class _VelocityMixin(object):
"""provides attributes for velocities.
"""
_a_unit = ()
_a_velTimeUnit = ()
_a_epoch = None
_a_yearDef = None
cType = VelocityType
def _setupNode(self):
if self.unit:
if not self.velTimeUnit or len(self.unit)!=len(self.velTimeUnit):
raise common.STCValueError("Invalid units for Velocity: %s/%s."%(
repr(self.unit), repr(self.velTimeUnit)))
self._setupNodeNext(_VelocityMixin)
def getUnitString(self):
if self.unit:
strs = ["%s/%s"%(u, tu)
for u, tu in zip(self.unit, self.velTimeUnit)]
if len(set(strs))==1:
return strs[0]
else:
return " ".join(strs)
def getUnitConverter(self, otherUnits):
if self.unit is None or not otherUnits:
return None
if isinstance(otherUnits, dict):
otherUnits = (otherUnits["unit"], otherUnits["velTimeUnit"])
return units.getVelocityConverter(self.unit, self.velTimeUnit,
otherUnits[0], otherUnits[1], True)
def getUnitArgs(self):
return {"unit": self.unit, "velTimeUnit": self.velTimeUnit}
class _RedshiftMixin(object):
"""provides attributes for redshifts.
"""
_a_velTimeUnit = None
_a_unit = None
cType = RedshiftType
def _setupNode(self):
if self.unit and not self.velTimeUnit:
raise common.STCValueError("Invalid units for Redshift: %s/%s."%(
repr(self.unit), repr(self.velTimeUnit)))
self._setupNodeNext(_RedshiftMixin)
def getUnitString(self):
if self.unit:
return "%s/%s"%(self.unit, self.velTimeUnit)
def getUnitConverter(self, otherUnits):
if self.unit is None or not otherUnits:
return None
if isinstance(otherUnits, dict):
otherUnits = (otherUnits["unit"], otherUnits["velTimeUnit"])
return units.getRedshiftConverter(self.unit, self.velTimeUnit,
otherUnits[0], otherUnits[1], True)
def getUnitArgs(self):
return {"unit": self.unit, "velTimeUnit": self.velTimeUnit}
[docs]class SpaceCoo(_Coordinate, _SpatialMixin):
pgClass = pgsphere.SPoint
[docs]class VelocityCoo(_Coordinate, _VelocityMixin): pass
[docs]class RedshiftCoo(_Coordinate, _RedshiftMixin): pass
[docs]class TimeCoo(_Coordinate, _OneDMixin):
cType = TimeType
[docs]class SpectralCoo(_Coordinate, _OneDMixin):
cType = SpectralType
_positionClassMap = {
SpectralType: SpectralCoo,
TimeType: TimeCoo,
SpaceType: SpaceCoo,
RedshiftType: RedshiftCoo,
VelocityType: VelocityCoo,
}
class _CoordinateInterval(_CoordinateLike):
_a_lowerLimit = None
_a_upperLimit = None
_a_fillFactor = None
_a_origUnit = None
inexactAttrs = set(["lowerLimit", "upperLimit"])
def adaptValuesWith(self, converter):
changes = {"origUnit": None}
if self.lowerLimit is not None:
changes["lowerLimit"] = converter(self.lowerLimit)
if self.upperLimit is not None:
changes["upperLimit"] = converter(self.upperLimit)
return self.change(**changes)
def getTransformed(self, sTrafo, destFrame):
ll, ul = self.lowerLimit, self.upperLimit
if ll is None:
return self.change(upperLimit=sTrafo(ul), frame=destFrame)
elif ul is None:
return self.change(lowerLimit=sTrafo(ll), frame=destFrame)
else:
return self.change(upperLimit=sTrafo(ul), lowerLimit=sTrafo(ll),
frame=destFrame)
def getValues(self):
return [l for l in (self.lowerLimit, self.upperLimit) if l is not None]
[docs]class SpaceInterval(_CoordinateInterval):
cType = SpaceType
# See fromPgSphere docstring on this
pgClass = pgsphere.SBox
[docs] def getValues(self):
return reduce(lambda a,b: a+b, _CoordinateInterval.getValues(self))
[docs] @classmethod
def fromPg(cls, frame, pgBox):
return cls(frame=frame,
lowerLimit=(pgBox.corner1.x/utils.DEG, pgBox.corner1.y/utils.DEG),
upperLimit=(pgBox.corner2.x/utils.DEG, pgBox.corner2.y/utils.DEG))
# Service for stcsast -- this may go away again
PositionInterval = SpaceInterval
[docs]class VelocityInterval(_CoordinateInterval):
cType = VelocityType
[docs]class RedshiftInterval(_CoordinateInterval):
cType = RedshiftType
[docs]class TimeInterval(_CoordinateInterval):
cType = TimeType
[docs] def adaptValuesWith(self, converter):
# timeIntervals are unitless; units only refer to errors, etc,
# which we don't have here.
return self
[docs]class SpectralInterval(_CoordinateInterval):
cType = SpectralType
################ Geometries
class _Geometry(_CoordinateLike):
"""A base class for all kinds of geometries.
Geometries may have "dependent" quantities like radii, sizes, etc. For
those, the convention is that if they are 1D, they must be expressed in the
unit of the first component of the position units, otherwise (in particular,
for box size) in the full unit of the position. This has to be made sure by
the client.
To make this work, Geometries are unit adapted on STC adoption.
Since their dependents need to be adapted as well, they have to
define adaptDependents(...) methods. They take the units for
all dependent quantities (which may all be None). This is used
in stxast.
Also getTransformed usually needs to be overridden for these.
Geometries may contain two sorts of column references; ordinary ones
are just stand-ins of actual values, while GeometryColRefs describe the
whole thing in a database column.
For the spatial registry prototype, geometries can define an
asSMoc(order=6) method returning a pgpshere.SMoc coverage for them.
"""
_a_size = None
_a_fillFactor = None
_a_origUnit = None
_a_geoColRef = None
cType = SpaceType
def getValues(self):
if self.geoColRef:
return [self.geoColRef]
else:
return self._getValuesSplit()
[docs]class AllSky(_Geometry):
[docs] def adaptValuesWith(self, converter):
return self
[docs] def adaptDepUnits(self):
pass
def _getValuesSplit(self):
return []
[docs] def asSMoc(self, order=6):
return pgsphere.SMoc.fromCells(0, list(range(12)))
[docs]class Circle(_Geometry):
_a_center = None
_a_radius = None
pgClass = pgsphere.SCircle
[docs] def adaptValuesWith(self, converter):
sTrafo = units.getBasicConverter(converter.fromUnit[0],
converter.toUnit[0])
return self.change(center=converter(self.center),
radius=sTrafo(self.radius))
def _getValuesSplit(self):
return [self.center[0], self.center[1], self.radius]
[docs] @classmethod
def fromPg(cls, frame, sCircle):
return cls(frame=frame,
center=(sCircle.center.x/utils.DEG, sCircle.center.y/utils.DEG),
radius=(sCircle.radius/utils.DEG))
[docs] def asSMoc(self, order):
return pgsphere.SCircle(
pgsphere.SPoint.fromDegrees(*self.center),
self.radius*utils.DEG
).asPoly(
).asSMoc()
[docs]class Ellipse(_Geometry):
_a_center = None
_a_smajAxis = _a_sminAxis = None
_a_posAngle = None
[docs] def adaptValuesWith(self, converter):
sTrafo = units.getBasicConverter(converter.fromUnit[0],
converter.toUnit[0])
return self.change(center=converter(self.center),
smajAxis=sTrafo(self.smajAxis), sminAxis=sTrafo(self.sminAxis))
def _getValuesSplit(self):
return list(self.center)+[self.smajAxis]+[self.sminAxis]+[
self.posAngle]
[docs]class Box(_Geometry):
_a_center = None
_a_boxsize = None
[docs] def adaptValuesWith(self, converter):
return self.change(center=converter(self.center),
boxsize=converter(self.boxsize))
def _getValuesSplit(self):
return list(self.center)+list(self.boxsize)
[docs]class Polygon(_Geometry):
_a_vertices = ()
pgClass = pgsphere.SPoly
def __init__(self, *args, **kwargs):
_Geometry.__init__(self, *args, **kwargs)
[docs] def adaptValuesWith(self, converter):
return self.change(vertices=tuple(converter(v) for v in self.vertices))
def _getValuesSplit(self):
return reduce(operator.add, self.vertices)
[docs] @classmethod
def fromPg(cls, frame, sPoly):
return cls(frame=frame,
vertices=[(p.x/utils.DEG, p.y/utils.DEG)
for p in sPoly.points])
[docs] def asSMoc(self, order=6):
# let's assume points are in degrees here; this is hopefully only
# temporary, anyway
return pgsphere.SPoly(
[pgsphere.SPoint.fromDegrees(*p) for p in self.vertices]).asSMoc(order)
[docs]class Convex(_Geometry):
_a_vectors = ()
[docs] def adaptValuesWith(self, converter):
raise common.STCNotImplementedError("Cannot adapt units for convexes yet.")
def _getValuesSplit(self):
return reduce(operator.add, self.vectors)
class _Compound(_Geometry):
"""A set-like operator on geometries.
"""
_a_children = ()
def polish(self):
for node in self.children:
if node.frame is None:
node.frame = self.frame
getattr(node, "polish", lambda: None)()
def adaptValuesWith(self, converter):
return self.change(children=[child.adaptValuesWith(converter)
for child in self.children])
def _applyToChildren(self, function):
newChildren, changes = [], False
for c in self.children:
nc = function(c)
newChildren.append(nc)
changes = changes or (c is not nc)
if changes:
return self.change(children=newChildren)
else:
return self
def binarizeOperands(self):
"""returns self with binarized operands.
If no operand needed binarizing, self is returned.
"""
res = self._applyToChildren(binarizeCompound)
return res
def debinarizeOperands(self):
"""returns self with debinarized operands.
If no operand needed debinarizing, self is returned.
"""
return self._applyToChildren(debinarizeCompound)
def getTransformed(self, sTrafo, destFrame):
return self.change(children=tuple(
child.getTransformed(sTrafo, destFrame) for child in self.children),
frame=destFrame)
class _MultiOpCompound(_Compound):
"""is a compound that has a variable number of operands.
"""
[docs]class Union(_MultiOpCompound): pass
[docs]class Intersection(_MultiOpCompound): pass
[docs]class Difference(_Compound): pass
[docs]class Not(_Compound): pass
def _buildBinaryTree(compound, items):
"""returns a binary tree of nested instances compound instances.
items has to have at least length 2.
"""
items = list(items)
root = compound.change(children=items[-2:])
items[-2:] = []
while items:
root = compound.change(children=[items.pop(), root])
return root
[docs]def binarizeCompound(compound):
"""returns compound as a binary tree.
For unions and intersections, compounds consisting of more than two
operands will be split up into parts of two arguments each.
"""
if not isinstance(compound, _Compound):
return compound
compound = compound.binarizeOperands()
if len(compound.children)==1:
return compound.children[0]
elif len(compound.children)==2:
return compound
else:
newChildren = [compound.children[0],
_buildBinaryTree(compound, compound.children[1:])]
return compound.change(children=newChildren)
[docs]def debinarizeCompound(compound):
"""returns compound with flattened operators.
"""
if not isinstance(compound, _Compound):
return compound
compound = compound.debinarizeOperands()
newChildren, changes = [], False
for c in compound.children:
if c.__class__==compound.__class__:
newChildren.extend(c.children)
changes = True
else:
newChildren.append(c)
if changes:
return compound.change(children=newChildren)
else:
return compound
################ Toplevel
[docs]class STCSpec(common.ASTNode):
"""is an STC specification, i.e., the root of an STC tree.
"""
_a_astroSystem = None
_a_systems = ()
_a_time = None
_a_place = None
_a_freq = None
_a_redshift = None
_a_velocity = None
_a_timeAs = ()
_a_areas = ()
_a_freqAs = ()
_a_redshiftAs = ()
_a_velocityAs = ()
@property
def sys(self):
return self.astroSystem
[docs] def buildIdMap(self):
if hasattr(self, "idMap"):
return
self.idMap = {}
for node in self.iterNodes():
if node.id:
self.idMap[node.id] = node
[docs] def polish(self):
"""does global fixups when parsing is finished.
This method has to be called after the element is complete. The
standard parsers do this.
For convenience, it returns the instance itself.
"""
# Fix local frames if not given (e.g., manual construction)
if self.place is not None and self.place.frame is None:
self.place.frame = self.astroSystem.spaceFrame
for area in self.areas:
if area.frame is None:
area.frame = self.astroSystem.spaceFrame
getattr(area, "polish", lambda: None)()
# Operations here cannot be in a _setupNode since when parsing from
# XML, there may be IdProxies instead of real objects.
# Equinox for ecliptic defaults to observation time
if self.place:
frame = self.place.frame
if frame and frame.equinox is None and frame.refFrame=="ECLIPTIC":
if self.time and self.time.value:
frame.equinox = "J%.8f"%(times.dateTimeToJYear(self.time.value))
return self
def _applyToAreas(self, function):
newAreas, changes = [], False
for a in self.areas:
na = function(a)
changes = changes or na is not a
newAreas.append(na)
if changes:
return self.change(areas=newAreas)
else:
return self
[docs] def binarize(self):
"""returns self with any compound present brought to a binary tree.
This will return self if nothing needs to change.
"""
return self._applyToAreas(binarizeCompound)
[docs] def debinarize(self):
"""returns self with any compound present brought to a binary tree.
This will return self if nothing needs to change.
"""
return self._applyToAreas(debinarizeCompound)
[docs] def getColRefs(self):
"""returns a list of column references embedded in this AST.
"""
if not hasattr(self, "_colRefs"):
self._colRefs = []
for n in self.iterNodes():
if hasattr(n, "getValues"):
self._colRefs.extend(
v.dest for v in n.getValues() if isinstance(v, common.ColRef))
return self._colRefs
[docs] def stripUnits(self):
"""removes all unit specifications from this AST.
This is intended for non-standalone STC, e.g., in VOTables, where
external unit specifications are present. Removing the units
prevents "bleeding out" of conflicting in-STC specifications
(that mostly enter through defaulting).
This ignores the immutability of nodes and is in general a major pain.
"""
for node in self.iterNodes():
if hasattr(node, "unit"):
node.unit = node.__class__._a_unit
if hasattr(node, "velTimeUnit"):
node.velTimeUnit = node.__class__._a_unit
[docs]def fromPgSphere(refFrame, pgGeom):
"""Returns an AST for a pgsphere object as defined in utils.pgsphere.
This interprets the pgSphere box as a coordinate interval, which is wrong
but probably what most VO protocols expect.
"""
frame = SpaceFrame(refFrame=refFrame)
if isinstance(pgGeom, SpaceCoo.pgClass):
return STCSpec(place=SpaceCoo.fromPg(frame, pgGeom))
for stcGeo in [Circle, SpaceInterval, Polygon]:
if isinstance(pgGeom, stcGeo.pgClass):
return STCSpec(areas=[stcGeo.fromPg(frame, pgGeom)])
raise common.STCValueError("Unknown pgSphere object %r"%pgGeom)