Coverage for python/lsst/sims/utils/SpatialBounds.py : 19%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2This file defines classes that control who ObservationMetaData describes
3its field of view (i.e. is it a box in RA, Dec, is it a circle in RA, Dec....?)
4"""
5from __future__ import division
6from builtins import str
7from builtins import object
9# Hopefully it will be extensible so that we can add different shapes in the
10# future
12import numpy as np
13from future.utils import with_metaclass
15__all__ = ["SpatialBounds", "CircleBounds", "BoxBounds"]
18class SpatialBoundsMetaClass(type):
19 """
20 Meta class for fieldOfView. This class builds a registry of all
21 valid fields of view so that fields can be instantiated from just a
22 dictionary key.
23 """
25 # Largely this is being copied from the DBObjectMeta class in
26 # dbConnection.py
28 def __init__(cls, name, bases, dct):
29 if not hasattr(cls, 'SBregistry'):
30 cls.SBregistry = {}
31 else:
32 cls.SBregistry[cls.boundType] = cls
34 return super(SpatialBoundsMetaClass, cls).__init__(name, bases, dct)
37class SpatialBounds(with_metaclass(SpatialBoundsMetaClass, object)):
38 """
39 Classes inheriting from this class define spatial bounds on the objects
40 contained within a catalog. They also translate those bounds into
41 constraints on SQL queries made by the query_columns method in
42 CatalogDBobject (see dbConnnection.py)
44 Daughter classes of this class need the following:
46 self.boundType = a string by which the class is identified in the
47 registry of FieldOfView classes
49 __init__() that accepts (in this order) RA, Dec, and characteristic
50 length. Init should then construct the parameters defining the bound
51 however is appropriate (e.g. setting self.RAmax and self.RAmin for a box)
53 to_SQL() = a method that accepts RAcolname and DECcolname (strings denoting
54 the names of the database columns containing RA and DEc) and which returns
55 a string that characterizes the bound as an SQL 'WHERE' statement.
56 """
58 def __init__(self, *args):
59 """
60 Accepts a center point and a characteristic length defining the extent of
61 the bounds
63 @param[in] ra is the center RA in radians
65 @param[in] dec is the center Dec in radians
67 @param[in] length is either a single characteristic length (in radians)
68 or a list of characteristic lengths defining the shape of the bound
69 """
71 raise NotImplementedError()
73 def to_SQL(self, *args):
74 """
75 Accepts the names of the columns referring to RA and Dec in the database.
76 Uses the stored RA, Dec, and length for this object to return an SQL
77 query that only selects the region of RA and Dec desired
79 @param[in] RAname a string; the name of the RA column in the database
81 @param[in] DECname a string; the name of the Dec column in the database
83 @returns a string; an SQL query that only selects the desired region in RA, Dec
84 """
86 raise NotImplementedError()
88 def __eq__(self, other):
89 for param in self.__dict__:
90 if param not in other.__dict__:
91 return False
93 if self.__dict__[param] != other.__dict__[param]:
94 return False
96 for param in other.__dict__:
97 if param not in self.__dict__:
98 return False
100 return True
102 def __ne__(self, other):
103 return not self.__eq__(other)
105 @classmethod
106 def getSpatialBounds(self, name, *args, **kwargs):
107 if name in self.SBregistry:
108 return self.SBregistry[name](*args, **kwargs)
109 else:
110 raise RuntimeError("There is no SpatialBounds class keyed to %s" % name)
113class CircleBounds(SpatialBounds):
115 boundType = 'circle'
117 def __init__(self, ra, dec, radius):
118 """
119 Accepts a center point and a characteristic length defining the extent of
120 the bounds
122 @param[in] ra is the center RA in radians
124 @param[in] dec is the center Dec in radians
126 @param[in] length is the radius of the field of view in radians
127 """
129 if not (isinstance(ra, float) or isinstance(ra, np.float)):
130 try:
131 ra = np.float(ra)
132 except:
133 raise RuntimeError('In CircleBounds, ra must be a float; you have %s' % type(ra))
135 if not (isinstance(dec, float) or isinstance(dec, np.float)):
136 try:
137 dec = np.float(dec)
138 except:
139 raise RuntimeError('In CircleBounds, dec must be a float; you have %s' % type(dec))
141 if not (isinstance(radius, float) or isinstance(radius, np.float)):
142 try:
143 radius = np.float(radius)
144 except:
145 raise RuntimeError('In CircleBounds, radius must be a float; you have %s' % type(radius))
147 self.RA = ra
148 self.DEC = dec
149 self.radius = radius
151 self.RAdeg = np.degrees(ra)
152 self.DECdeg = np.degrees(dec)
153 self.radiusdeg = np.degrees(radius)
155 def __eq__(self, other):
156 return (type(self) == type(other)) and \
157 (self.RA == other.RA) and \
158 (self.DEC == other.DEC) and \
159 (self.radius == other.radius) and \
160 (self.RAdeg == other.RAdeg) and \
161 (self.DECdeg == other.DECdeg) and \
162 (self.radiusdeg == other.radiusdeg)
164 def to_SQL(self, RAname, DECname):
166 cosDec = np.cos(self.DEC)
167 adjusted_radius = np.abs(np.degrees(np.arcsin(np.sin(self.radius)/cosDec)))
169 if np.abs(cosDec) > 1.0e-20:
170 RAmax = self.RAdeg + 2.0*adjusted_radius
171 RAmin = self.RAdeg - 2.0*adjusted_radius
172 else:
173 # just in case, for some reason, we are looking at the poles
174 RAmax = 361.0
175 RAmin = -361.0
177 if (np.isnan(RAmax) or np.isnan(RAmin) or
178 RAmin<adjusted_radius or
179 RAmax>360.0-adjusted_radius):
181 RAmax = 361.0
182 RAmin = -361.0
184 DECmax = self.DECdeg + self.radiusdeg
185 DECmin = self.DECdeg - self.radiusdeg
187 # initially demand that all objects are within a box containing the circle
188 # set from the DEC1=DEC2 and RA1=RA2 limits of the haversine function
189 if RAmax-RAmin<361.0:
190 bound = ("%s between %f and %f and %s between %f and %f "
191 % (RAname, RAmin, RAmax, DECname, DECmin, DECmax))
192 else:
193 bound = ('%s between %f and %f ' % (DECname, DECmin, DECmax))
195 # then use the Haversine function to constrain the angular distance form boresite to be within
196 # the desired radius. See
197 # http://en.wikipedia.org/wiki/Haversine_formula
198 bound = bound + \
199 ("and 2 * ASIN(SQRT( POWER(SIN(0.5*(%s - %s) * PI() / 180.0),2)" %
200 (DECname, self.DECdeg))
201 bound = bound + \
202 ("+ COS(%s * PI() / 180.0) * COS(%s * PI() / 180.0) " %
203 (DECname, self.DECdeg))
204 bound = bound + \
205 ("* POWER(SIN(0.5 * (%s - %s) * PI() / 180.0),2)))" %
206 (RAname, self.RAdeg))
207 bound = bound + (" < %s " % self.radius)
209 return bound
212class BoxBounds(SpatialBounds):
214 boundType = 'box'
216 def __init__(self, ra, dec, length):
217 """
218 Accepts a center point and a characteristic length defining the extent of
219 the bounds
221 @param[in] ra is the center RA in radians
223 @param[in] dec is the center Dec in radians
225 @param[in] length is either a single characteristic length (in radians)
226 or a list of characteristic lengths defining the shape of the bound.
227 If a single value, the field of view will be a square with side of 2 x length.
228 If it is a list/tuple/array, the field of view will be a rectangle with side lengths
229 RA = 2 x length[0] and Dec = 2 x length[1]
230 """
232 if not (isinstance(ra, float) or isinstance(ra, np.float)):
233 try:
234 ra = np.float(ra)
235 except:
236 raise RuntimeError('In BoxBounds ra must be a float; you have %s' % type(ra))
238 if not (isinstance(dec, float) or isinstance(dec, np.float)):
239 try:
240 dec = np.float(dec)
241 except:
242 raise RuntimeError('In BoxBounds dec must be a float; you have %s' % type(dec))
244 self.RA = ra
245 self.DEC = dec
247 self.RAdeg = np.degrees(ra)
248 self.DECdeg = np.degrees(dec)
250 try:
251 if hasattr(length, '__len__'):
252 if len(length) == 1:
253 lengthRAdeg = np.degrees(length[0])
254 lengthDECdeg = np.degrees(length[0])
255 else:
256 lengthRAdeg = np.degrees(length[0])
257 lengthDECdeg = np.degrees(length[1])
258 else:
259 length = np.float(length)
260 lengthRAdeg = np.degrees(length)
261 lengthDECdeg = np.degrees(length)
263 except:
264 raise RuntimeError("BoxBounds is unsure how to handle length %s type: %s" % (
265 str(length), type(length)))
267 self.RAminDeg = self.RAdeg - lengthRAdeg
268 self.RAmaxDeg = self.RAdeg + lengthRAdeg
269 self.DECminDeg = self.DECdeg - lengthDECdeg
270 self.DECmaxDeg = self.DECdeg + lengthDECdeg
272 self.RAminDeg %= 360.0
273 self.RAmaxDeg %= 360.0
275 def __eq__(self, other):
276 return (type(self) == type(other)) and \
277 (self.RA == other.RA) and \
278 (self.RAdeg == other.RAdeg) and \
279 (self.DEC == other.DEC) and \
280 (self.DECdeg == other.DECdeg) and \
281 (self.RAminDeg == other.RAminDeg) and \
282 (self.RAmaxDeg == other.RAmaxDeg) and \
283 (self.DECminDeg == other.DECminDeg) and \
284 (self.DECmaxDeg == other.DECmaxDeg)
286 def to_SQL(self, RAname, DECname):
287 # KSK: I don't know exactly what we do here. This is in code, but operating
288 # on a database is it less confusing to work in degrees or radians?
289 # (RAmin, RAmax, DECmin, DECmax) = map(math.radians,
290 # (RAmin, RAmax, DECmin, DECmax))
292 # Special case where the whole region is selected
293 if self.RAminDeg < 0 and self.RAmaxDeg > 360.:
294 bound = "%s between %f and %f" % (
295 DECname, self.DECminDeg, self.DECmaxDeg)
296 return bound
298 if self.RAminDeg > self.RAmaxDeg:
299 bound = ("%s not between %f and %f and %s between %f and %f"
300 % (RAname, self.RAmaxDeg, self.RAminDeg,
301 DECname, self.DECminDeg, self.DECmaxDeg))
302 bound += (" and %s+360.0 not between %f and %f" %
303 (RAname, self.RAmaxDeg, self.RAminDeg))
304 else:
305 bound = ("%s between %f and %f and %s between %f and %f"
306 % (RAname, self.RAminDeg, self.RAmaxDeg, DECname, self.DECminDeg, self.DECmaxDeg))
308 return bound