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