Coverage for python/lsst/sims/featureScheduler/surveys/base_survey.py : 9%

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
1import numpy as np
2from lsst.sims.featureScheduler.utils import (empty_observation, set_default_nside,
3 hp_in_lsst_fov, read_fields, hp_in_comcam_fov,
4 comcamTessellate)
5import healpy as hp
6from lsst.sims.featureScheduler.thomson import xyz2thetaphi, thetaphi2xyz
7from lsst.sims.featureScheduler.detailers import Zero_rot_detailer
9__all__ = ['BaseSurvey', 'BaseMarkovDF_survey']
12class BaseSurvey(object):
13 """A baseclass for survey objects.
15 Parameters
16 ----------
17 basis_functions : list
18 List of basis_function objects
19 extra_features : list XXX--should this be a dict for clarity?
20 List of any additional features the survey may want to use
21 e.g., for computing final dither positions.
22 extra_basis_functions : dict of lsst.sims.featureScheduler.basis_function objects
23 Extra basis function objects. Typically not psased in, but et in the __init__.
24 ignore_obs : list of str (None)
25 If an incoming observation has this string in the note, ignore it. Handy if
26 one wants to ignore DD fields or observations requested by self. Take note,
27 if a survey is called 'mysurvey23', setting ignore_obs to 'mysurvey2' will
28 ignore it because 'mysurvey2' is a substring of 'mysurvey23'.
29 detailers : list of lsst.sims.featureScheduler.detailers objects
30 The detailers to apply to the list of observations.
31 scheduled_obs : np.array
32 An array of MJD values for when observations should execute.
33 """
34 def __init__(self, basis_functions, extra_features=None, extra_basis_functions=None,
35 ignore_obs=None, survey_name='', nside=None, detailers=None,
36 scheduled_obs=None):
37 if nside is None:
38 nside = set_default_nside()
39 if ignore_obs is None:
40 ignore_obs = []
42 if isinstance(ignore_obs, str):
43 ignore_obs = [ignore_obs]
45 self.nside = nside
46 self.survey_name = survey_name
47 self.ignore_obs = ignore_obs
49 self.reward = None
50 self.survey_index = None
52 self.basis_functions = basis_functions
54 if extra_features is None:
55 self.extra_features = {}
56 else:
57 self.extra_features = extra_features
58 if extra_basis_functions is None:
59 self.extra_basis_functions = {}
60 else:
61 self.extra_basis_functions = extra_basis_functions
63 self.reward_checked = False
65 # Attribute to track if the reward function is up-to-date.
66 self.reward_checked = False
68 # If there's no detailers, add one to set rotation to near zero
69 if detailers is None:
70 self.detailers = [Zero_rot_detailer(nside=nside)]
71 else:
72 self.detailers = detailers
74 # Scheduled observations
75 self.scheduled_obs = scheduled_obs
77 def get_scheduled_obs(self):
78 return self.scheduled_obs
80 def add_observation(self, observation, **kwargs):
81 # Check each posible ignore string
82 checks = [io not in str(observation['note']) for io in self.ignore_obs]
83 # ugh, I think here I have to assume observation is an array and not a dict.
84 if all(checks):
85 for feature in self.extra_features:
86 self.extra_features[feature].add_observation(observation, **kwargs)
87 for bf in self.extra_basis_functions:
88 self.extra_basis_functions[bf].add_observation(observation, **kwargs)
89 for bf in self.basis_functions:
90 bf.add_observation(observation, **kwargs)
91 for detailer in self.detailers:
92 detailer.add_observation(observation, **kwargs)
93 self.reward_checked = False
95 def _check_feasibility(self, conditions):
96 """
97 Check if the survey is feasable in the current conditions
98 """
99 for bf in self.basis_functions:
100 result = bf.check_feasibility(conditions)
101 if not result:
102 return result
103 return result
105 def calc_reward_function(self, conditions):
106 """
107 Parameters
108 ----------
109 conditions : lsst.sims.featureScheduler.features.Conditions object
111 Returns
112 -------
113 reward : float (or array)
115 """
116 if self._check_feasability():
117 self.reward = 0
118 else:
119 # If we don't pass feasability
120 self.reward = -np.inf
122 self.reward_checked = True
123 return self.reward
125 def generate_observations_rough(self, conditions):
126 """
127 Returns
128 -------
129 one of:
130 1) None
131 2) A list of observations
132 """
133 # If the reward function hasn't been updated with the
134 # latest info, calculate it
135 if not self.reward_checked:
136 self.reward = self.calc_reward_function(conditions)
137 obs = empty_observation()
138 return [obs]
140 def generate_observations(self, conditions):
141 observations = self.generate_observations_rough(conditions)
142 for detailer in self.detailers:
143 observations = detailer(observations, conditions)
144 return observations
146 def viz_config(self):
147 # XXX--zomg, we should have a method that goes through all the objects and
148 # makes plots/prints info so there can be a little notebook showing the config!
149 pass
152def rotx(theta, x, y, z):
153 """rotate the x,y,z points theta radians about x axis"""
154 sin_t = np.sin(theta)
155 cos_t = np.cos(theta)
156 xp = x
157 yp = y*cos_t+z*sin_t
158 zp = -y*sin_t+z*cos_t
159 return xp, yp, zp
162class BaseMarkovDF_survey(BaseSurvey):
163 """ A Markov Decision Function survey object. Uses Basis functions to compute a
164 final reward function and decide what to observe based on the reward. Includes
165 methods for dithering and defaults to dithering nightly.
167 Parameters
168 ----------
169 basis_function : list of lsst.sims.featureSchuler.basis_function objects
171 basis_weights : list of float
172 Must be same length as basis_function
173 seed : hashable
174 Random number seed, used for randomly orienting sky tessellation.
175 camera : str ('LSST')
176 Should be 'LSST' or 'comcam'
177 area_required : float (None)
178 The valid area that should be present in the reward function (square degrees).
179 """
180 def __init__(self, basis_functions, basis_weights, extra_features=None,
181 smoothing_kernel=None,
182 ignore_obs=None, survey_name='', nside=None, seed=42,
183 dither=True, detailers=None, camera='LSST', area_required=None):
185 super(BaseMarkovDF_survey, self).__init__(basis_functions=basis_functions,
186 extra_features=extra_features,
187 ignore_obs=ignore_obs, survey_name=survey_name,
188 nside=nside, detailers=detailers)
190 self.basis_weights = basis_weights
191 # Check that weights and basis functions are same length
192 if len(basis_functions) != np.size(basis_weights):
193 raise ValueError('basis_functions and basis_weights must be same length.')
195 self.camera = camera
196 # Load the OpSim field tesselation and map healpix to fields
197 if self.camera == 'LSST':
198 self.fields_init = read_fields()
199 elif self.camera == 'comcam':
200 self.fields_init = comcamTessellate()
201 else:
202 ValueError('camera %s unknown, should be "LSST" or "comcam"' % camera)
203 self.fields = self.fields_init.copy()
204 self.hp2fields = np.array([])
205 self._hp2fieldsetup(self.fields['RA'], self.fields['dec'])
207 if smoothing_kernel is not None:
208 self.smoothing_kernel = np.radians(smoothing_kernel)
209 else:
210 self.smoothing_kernel = None
212 if area_required is None:
213 self.area_required = area_required
214 else:
215 self.area_required = area_required * (np.pi/180.)**2 # To steradians
217 # Start tracking the night
218 self.night = -1
220 # Set the seed
221 np.random.seed(seed)
222 self.dither = dither
224 def _check_feasibility(self, conditions):
225 """
226 Check if the survey is feasable in the current conditions
227 """
228 for bf in self.basis_functions:
229 result = bf.check_feasibility(conditions)
230 if not result:
231 return result
232 if self.area_required is not None:
233 reward = self.calc_reward_function(conditions)
234 good_pix = np.where(np.isfinite(reward) == True)[0]
235 area = hp.nside2pixarea(self.nside) * np.size(good_pix)
236 if area < self.area_required:
237 return False
238 return result
240 def _hp2fieldsetup(self, ra, dec, leafsize=100):
241 """Map each healpixel to nearest field. This will only work if healpix
242 resolution is higher than field resolution.
243 """
244 if self.camera == 'LSST':
245 pointing2hpindx = hp_in_lsst_fov(nside=self.nside)
246 elif self.camera == 'comcam':
247 pointing2hpindx = hp_in_comcam_fov(nside=self.nside)
249 self.hp2fields = np.zeros(hp.nside2npix(self.nside), dtype=np.int)
250 for i in range(len(ra)):
251 hpindx = pointing2hpindx(ra[i], dec[i], rotSkyPos=0.)
252 self.hp2fields[hpindx] = i
254 def _spin_fields(self, lon=None, lat=None, lon2=None):
255 """Spin the field tessellation to generate a random orientation
257 The default field tesselation is rotated randomly in longitude, and then the
258 pole is rotated to a random point on the sphere.
260 Parameters
261 ----------
262 lon : float (None)
263 The amount to initially rotate in longitude (radians). Will use a random value
264 between 0 and 2 pi if None (default).
265 lat : float (None)
266 The amount to rotate in latitude (radians).
267 lon2 : float (None)
268 The amount to rotate the pole in longitude (radians).
269 """
270 if lon is None:
271 lon = np.random.rand()*np.pi*2
272 if lat is None:
273 # Make sure latitude points spread correctly
274 # http://mathworld.wolfram.com/SpherePointPicking.html
275 lat = np.arccos(2.*np.random.rand() - 1.)
276 if lon2 is None:
277 lon2 = np.random.rand()*np.pi*2
278 # rotate longitude
279 ra = (self.fields_init['RA'] + lon) % (2.*np.pi)
280 dec = self.fields_init['dec'] + 0
282 # Now to rotate ra and dec about the x-axis
283 x, y, z = thetaphi2xyz(ra, dec+np.pi/2.)
284 xp, yp, zp = rotx(lat, x, y, z)
285 theta, phi = xyz2thetaphi(xp, yp, zp)
286 dec = phi - np.pi/2
287 ra = theta + np.pi
289 # One more RA rotation
290 ra = (ra + lon2) % (2.*np.pi)
292 self.fields['RA'] = ra
293 self.fields['dec'] = dec
294 # Rebuild the kdtree with the new positions
295 # XXX-may be doing some ra,dec to conversions xyz more than needed.
296 self._hp2fieldsetup(ra, dec)
298 def smooth_reward(self):
299 """If we want to smooth the reward function.
300 """
301 if hp.isnpixok(self.reward.size):
302 # Need to swap NaNs to hp.UNSEEN so smoothing doesn't spread mask
303 reward_temp = self.reward + 0
304 mask = np.isnan(reward_temp)
305 reward_temp[mask] = hp.UNSEEN
306 self.reward_smooth = hp.sphtfunc.smoothing(reward_temp,
307 fwhm=self.smoothing_kernel,
308 verbose=False)
309 self.reward_smooth[mask] = np.nan
310 self.reward = self.reward_smooth
311 #good = ~np.isnan(self.reward_smooth)
312 # Round off to prevent strange behavior early on
313 #self.reward_smooth[good] = np.round(self.reward_smooth[good], decimals=4)
315 def calc_reward_function(self, conditions):
316 self.reward_checked = True
317 if self._check_feasibility(conditions):
318 self.reward = 0
319 indx = np.arange(hp.nside2npix(self.nside))
320 for bf, weight in zip(self.basis_functions, self.basis_weights):
321 basis_value = bf(conditions, indx=indx)
322 self.reward += basis_value*weight
324 if np.any(np.isinf(self.reward)):
325 self.reward = np.inf
326 else:
327 # If not feasable, negative infinity reward
328 self.reward = -np.inf
329 return self.reward
330 if self.smoothing_kernel is not None:
331 self.smooth_reward()
333 if self.area_required is not None:
334 good_area = np.where(np.abs(self.reward) >= 0)[0].size * hp.nside2pixarea(self.nside)
335 if good_area < self.area_required:
336 self.reward = -np.inf
338 return self.reward
340 def generate_observations_rough(self, conditions):
342 self.reward = self.calc_reward_function(conditions)
344 # Check if we need to spin the tesselation
345 if self.dither & (conditions.night != self.night):
346 self._spin_fields()
347 self.night = conditions.night.copy()
349 # XXX Use self.reward to decide what to observe.
350 return None