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

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 ignore_obs : list of str (None)
23 If an incoming observation has this string in the note, ignore it. Handy if
24 one wants to ignore DD fields or observations requested by self. Take note,
25 if a survey is called 'mysurvey23', setting ignore_obs to 'mysurvey2' will
26 ignore it because 'mysurvey2' is a substring of 'mysurvey23'.
27 detailers : list of lsst.sims.featureScheduler.detailers objects
28 The detailers to apply to the list of observations.
29 scheduled_obs : np.array
30 An array of MJD values for when observations should execute.
31 """
32 def __init__(self, basis_functions, extra_features=None,
33 ignore_obs=None, survey_name='', nside=None, detailers=None,
34 scheduled_obs=None):
35 if nside is None:
36 nside = set_default_nside()
37 if ignore_obs is None:
38 ignore_obs = []
40 if isinstance(ignore_obs, str):
41 ignore_obs = [ignore_obs]
43 self.nside = nside
44 self.survey_name = survey_name
45 self.ignore_obs = ignore_obs
47 self.reward = None
48 self.survey_index = None
50 self.basis_functions = basis_functions
52 if extra_features is None:
53 self.extra_features = {}
54 else:
55 self.extra_features = extra_features
56 self.reward_checked = False
58 # Attribute to track if the reward function is up-to-date.
59 self.reward_checked = False
61 # If there's no detailers, add one to set rotation to near zero
62 if detailers is None:
63 self.detailers = [Zero_rot_detailer(nside=nside)]
64 else:
65 self.detailers = detailers
67 # Scheduled observations
68 self.scheduled_obs = scheduled_obs
70 def get_scheduled_obs(self):
71 return self.scheduled_obs
73 def add_observation(self, observation, **kwargs):
74 # Check each posible ignore string
75 checks = [io not in str(observation['note']) for io in self.ignore_obs]
76 # ugh, I think here I have to assume observation is an array and not a dict.
77 if all(checks):
78 for feature in self.extra_features:
79 self.extra_features[feature].add_observation(observation, **kwargs)
80 for bf in self.basis_functions:
81 bf.add_observation(observation, **kwargs)
82 for detailer in self.detailers:
83 detailer.add_observation(observation, **kwargs)
84 self.reward_checked = False
86 def _check_feasibility(self, conditions):
87 """
88 Check if the survey is feasable in the current conditions
89 """
90 for bf in self.basis_functions:
91 result = bf.check_feasibility(conditions)
92 if not result:
93 return result
94 return result
96 def calc_reward_function(self, conditions):
97 """
98 Parameters
99 ----------
100 conditions : lsst.sims.featureScheduler.features.Conditions object
102 Returns
103 -------
104 reward : float (or array)
106 """
107 if self._check_feasability():
108 self.reward = 0
109 else:
110 # If we don't pass feasability
111 self.reward = -np.inf
113 self.reward_checked = True
114 return self.reward
116 def generate_observations_rough(self, conditions):
117 """
118 Returns
119 -------
120 one of:
121 1) None
122 2) A list of observations
123 """
124 # If the reward function hasn't been updated with the
125 # latest info, calculate it
126 if not self.reward_checked:
127 self.reward = self.calc_reward_function(conditions)
128 obs = empty_observation()
129 return [obs]
131 def generate_observations(self, conditions):
132 observations = self.generate_observations_rough(conditions)
133 for detailer in self.detailers:
134 observations = detailer(observations, conditions)
135 return observations
137 def viz_config(self):
138 # XXX--zomg, we should have a method that goes through all the objects and
139 # makes plots/prints info so there can be a little notebook showing the config!
140 pass
143def rotx(theta, x, y, z):
144 """rotate the x,y,z points theta radians about x axis"""
145 sin_t = np.sin(theta)
146 cos_t = np.cos(theta)
147 xp = x
148 yp = y*cos_t+z*sin_t
149 zp = -y*sin_t+z*cos_t
150 return xp, yp, zp
153class BaseMarkovDF_survey(BaseSurvey):
154 """ A Markov Decision Function survey object. Uses Basis functions to compute a
155 final reward function and decide what to observe based on the reward. Includes
156 methods for dithering and defaults to dithering nightly.
158 Parameters
159 ----------
160 basis_function : list of lsst.sims.featureSchuler.basis_function objects
162 basis_weights : list of float
163 Must be same length as basis_function
164 seed : hashable
165 Random number seed, used for randomly orienting sky tessellation.
166 camera : str ('LSST')
167 Should be 'LSST' or 'comcam'
168 """
169 def __init__(self, basis_functions, basis_weights, extra_features=None,
170 smoothing_kernel=None,
171 ignore_obs=None, survey_name='', nside=None, seed=42,
172 dither=True, detailers=None, camera='LSST'):
174 super(BaseMarkovDF_survey, self).__init__(basis_functions=basis_functions,
175 extra_features=extra_features,
176 ignore_obs=ignore_obs, survey_name=survey_name,
177 nside=nside, detailers=detailers)
179 self.basis_weights = basis_weights
180 # Check that weights and basis functions are same length
181 if len(basis_functions) != np.size(basis_weights):
182 raise ValueError('basis_functions and basis_weights must be same length.')
184 self.camera = camera
185 # Load the OpSim field tesselation and map healpix to fields
186 if self.camera == 'LSST':
187 self.fields_init = read_fields()
188 elif self.camera == 'comcam':
189 self.fields_init = comcamTessellate()
190 else:
191 ValueError('camera %s unknown, should be "LSST" or "comcam"' %camera)
192 self.fields = self.fields_init.copy()
193 self.hp2fields = np.array([])
194 self._hp2fieldsetup(self.fields['RA'], self.fields['dec'])
196 if smoothing_kernel is not None:
197 self.smoothing_kernel = np.radians(smoothing_kernel)
198 else:
199 self.smoothing_kernel = None
201 # Start tracking the night
202 self.night = -1
204 # Set the seed
205 np.random.seed(seed)
206 self.dither = dither
208 def _hp2fieldsetup(self, ra, dec, leafsize=100):
209 """Map each healpixel to nearest field. This will only work if healpix
210 resolution is higher than field resolution.
211 """
212 if self.camera == 'LSST':
213 pointing2hpindx = hp_in_lsst_fov(nside=self.nside)
214 elif self.camera == 'comcam':
215 pointing2hpindx = hp_in_comcam_fov(nside=self.nside)
217 self.hp2fields = np.zeros(hp.nside2npix(self.nside), dtype=np.int)
218 for i in range(len(ra)):
219 hpindx = pointing2hpindx(ra[i], dec[i], rotSkyPos=0.)
220 self.hp2fields[hpindx] = i
222 def _spin_fields(self, lon=None, lat=None, lon2=None):
223 """Spin the field tessellation to generate a random orientation
225 The default field tesselation is rotated randomly in longitude, and then the
226 pole is rotated to a random point on the sphere.
228 Parameters
229 ----------
230 lon : float (None)
231 The amount to initially rotate in longitude (radians). Will use a random value
232 between 0 and 2 pi if None (default).
233 lat : float (None)
234 The amount to rotate in latitude (radians).
235 lon2 : float (None)
236 The amount to rotate the pole in longitude (radians).
237 """
238 if lon is None:
239 lon = np.random.rand()*np.pi*2
240 if lat is None:
241 # Make sure latitude points spread correctly
242 # http://mathworld.wolfram.com/SpherePointPicking.html
243 lat = np.arccos(2.*np.random.rand() - 1.)
244 if lon2 is None:
245 lon2 = np.random.rand()*np.pi*2
246 # rotate longitude
247 ra = (self.fields_init['RA'] + lon) % (2.*np.pi)
248 dec = self.fields_init['dec'] + 0
250 # Now to rotate ra and dec about the x-axis
251 x, y, z = thetaphi2xyz(ra, dec+np.pi/2.)
252 xp, yp, zp = rotx(lat, x, y, z)
253 theta, phi = xyz2thetaphi(xp, yp, zp)
254 dec = phi - np.pi/2
255 ra = theta + np.pi
257 # One more RA rotation
258 ra = (ra + lon2) % (2.*np.pi)
260 self.fields['RA'] = ra
261 self.fields['dec'] = dec
262 # Rebuild the kdtree with the new positions
263 # XXX-may be doing some ra,dec to conversions xyz more than needed.
264 self._hp2fieldsetup(ra, dec)
266 def smooth_reward(self):
267 """If we want to smooth the reward function.
268 """
269 if hp.isnpixok(self.reward.size):
270 # Need to swap NaNs to hp.UNSEEN so smoothing doesn't spread mask
271 reward_temp = self.reward + 0
272 mask = np.isnan(reward_temp)
273 reward_temp[mask] = hp.UNSEEN
274 self.reward_smooth = hp.sphtfunc.smoothing(reward_temp,
275 fwhm=self.smoothing_kernel,
276 verbose=False)
277 self.reward_smooth[mask] = np.nan
278 self.reward = self.reward_smooth
279 #good = ~np.isnan(self.reward_smooth)
280 # Round off to prevent strange behavior early on
281 #self.reward_smooth[good] = np.round(self.reward_smooth[good], decimals=4)
283 def calc_reward_function(self, conditions):
284 self.reward_checked = True
285 if self._check_feasibility(conditions):
286 self.reward = 0
287 indx = np.arange(hp.nside2npix(self.nside))
288 for bf, weight in zip(self.basis_functions, self.basis_weights):
289 basis_value = bf(conditions, indx=indx)
290 self.reward += basis_value*weight
292 if np.any(np.isinf(self.reward)):
293 self.reward = np.inf
294 else:
295 # If not feasable, negative infinity reward
296 self.reward = -np.inf
297 return self.reward
298 if self.smoothing_kernel is not None:
299 self.smooth_reward()
301 return self.reward
303 def generate_observations_rough(self, conditions):
305 self.reward = self.calc_reward_function(conditions)
307 # Check if we need to spin the tesselation
308 if self.dither & (conditions.night != self.night):
309 self._spin_fields()
310 self.night = conditions.night.copy()
312 # XXX Use self.reward to decide what to observe.
313 return None