Coverage for python/lsst/sims/featureScheduler/schedulers/core_scheduler.py : 14%

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
1from __future__ import absolute_import
2from builtins import object
3import numpy as np
4import healpy as hp
5from lsst.sims.utils import _hpid2RaDec
6from lsst.sims.featureScheduler.utils import hp_in_lsst_fov, set_default_nside, hp_in_comcam_fov, int_rounded
7from lsst.sims.utils import _approx_RaDec2AltAz
8from lsst.sims.featureScheduler.utils import approx_altaz2pa
11import logging
14__all__ = ['Core_scheduler']
17class Core_scheduler(object):
18 """Core scheduler that takes completed observations and observatory status and requests observations
20 Parameters
21 ----------
22 surveys : list (or list of lists) of lsst.sims.featureScheduler.survey objects
23 A list of surveys to consider. If multiple surveys return the same highest
24 reward value, the survey at the earliest position in the list will be selected.
25 Can also be a list of lists to make heirarchical priorities.
26 nside : int
27 A HEALpix nside value.
28 camera : str ('LSST')
29 Which camera to use for computing overlapping HEALpixels for an observation.
30 Can be 'LSST' or 'comcam'
31 conditions : a lsst.sims.featureScheduler.features.Conditions object (None)
32 An object that hold the current conditions and derived values (e.g., 5-sigma depth). Will
33 generate a default if set to None.
34 """
36 def __init__(self, surveys, nside=None, camera='LSST', rotator_limits=[85., 275.]):
37 """
38 Parameters
39 ----------
40 surveys : list (or list of lists) of lsst.sims.featureScheduler.survey objects
41 A list of surveys to consider. If multiple surveys return the same highest
42 reward value, the survey at the earliest position in the list will be selected.
43 Can also be a list of lists to make heirarchical priorities.
44 nside : int
45 A HEALpix nside value.
46 camera : str ('LSST')
47 Which camera to use for computing overlapping HEALpixels for an observation.
48 Can be 'LSST' or 'comcam'
49 rotator_limits : sequence of floats
50 """
51 if nside is None:
52 nside = set_default_nside()
54 self.log = logging.getLogger("Core_scheduler")
55 # initialize a queue of observations to request
56 self.queue = []
57 # The indices of self.survey_lists that provided the last addition(s) to the queue
58 self.survey_index = [None, None]
60 # If we have a list of survey objects, convert to list-of-lists
61 if isinstance(surveys[0], list):
62 self.survey_lists = surveys
63 else:
64 self.survey_lists = [surveys]
65 self.nside = nside
66 hpid = np.arange(hp.nside2npix(nside))
67 self.ra_grid_rad, self.dec_grid_rad = _hpid2RaDec(nside, hpid)
68 # Should just make camera a class that takes a pointing and returns healpix indices
69 if camera == 'LSST':
70 self.pointing2hpindx = hp_in_lsst_fov(nside=nside)
71 elif camera == 'comcam':
72 self.pointing2hpindx = hp_in_comcam_fov(nside=nside)
73 else:
74 raise ValueError('camera %s not implamented' % camera)
76 # keep track of how many observations get flushed from the queue
77 self.flushed = 0
78 self.rotator_limits = np.sort(np.radians(rotator_limits))
80 def flush_queue(self):
81 """"
82 Like it sounds, clear any currently queued desired observations.
83 """
84 self.queue = []
85 self.survey_index = [None, None]
87 def add_observation(self, observation):
88 """
89 Record a completed observation and update features accordingly.
91 Parameters
92 ----------
93 observation : dict-like
94 An object that contains the relevant information about a
95 completed observation (e.g., mjd, ra, dec, filter, rotation angle, etc)
96 """
98 # Find the healpixel centers that are included in an observation
99 indx = self.pointing2hpindx(observation['RA'], observation['dec'],
100 rotSkyPos=observation['rotSkyPos'])
101 for surveys in self.survey_lists:
102 for survey in surveys:
103 survey.add_observation(observation, indx=indx)
105 def update_conditions(self, conditions_in):
106 """
107 Parameters
108 ----------
109 conditions : dict-like
110 The current conditions of the telescope (pointing position, loaded filters, cloud-mask, etc)
111 """
112 # Add the current queue and scheduled queue to the conditions
113 self.conditions = conditions_in
114 # put the local queue in the conditions
115 self.conditions.queue = self.queue
117 # XXX---TODO: Could potentially put more complicated info from all
118 # the surveys in the conditions object here. e.g., when a DDF plans to next request
119 # observations.
121 def _check_queue_mjd_only(self, mjd):
122 """
123 Check if there are things in the queue that can be executed using only MJD and not full conditions.
124 This is primarly used by sim_runner to reduce calls calculating updated conditions when they are not
125 needed.
126 """
127 result = False
128 if len(self.queue) > 0:
129 if (int_rounded(mjd) < int_rounded(self.queue[0]['flush_by_mjd'])) | (self.queue[0]['flush_by_mjd'] == 0):
130 result = True
131 return result
133 def request_observation(self, mjd=None):
134 """
135 Ask the scheduler what it wants to observe next
137 Paramters
138 ---------
139 mjd : float (None)
140 The Modified Julian Date. If None, it uses the MJD from the conditions from the last conditions update.
142 Returns
143 -------
144 observation object (ra,dec,filter,rotangle)
145 Returns None if the queue fails to fill
146 """
147 if mjd is None:
148 mjd = self.conditions.mjd
149 if len(self.queue) == 0:
150 self._fill_queue()
152 if len(self.queue) == 0:
153 return None
154 else:
155 # If the queue has gone stale, flush and refill. Zero means no flush_by was set.
156 if (int_rounded(mjd) > int_rounded(self.queue[0]['flush_by_mjd'])) & (self.queue[0]['flush_by_mjd'] != 0):
157 self.flushed += len(self.queue)
158 self.flush_queue()
159 self._fill_queue()
160 if len(self.queue) == 0:
161 return None
162 observation = self.queue.pop(0)
163 # If we are limiting the camera rotator
164 if self.rotator_limits is not None:
165 alt, az = _approx_RaDec2AltAz(observation['RA'], observation['dec'], self.conditions.site.latitude_rad,
166 self.conditions.site.longitude_rad, mjd)
167 obs_pa = approx_altaz2pa(alt, az, self.conditions.site.latitude_rad)
168 rotTelPos_expected = (obs_pa - observation['rotSkyPos']) % (2.*np.pi)
169 if (int_rounded(rotTelPos_expected) > int_rounded(self.rotator_limits[0])) & (int_rounded(rotTelPos_expected) < int_rounded(self.rotator_limits[1])):
170 diff = np.abs(self.rotator_limits - rotTelPos_expected)
171 limit_indx = np.min(np.where(diff == np.min(diff))[0])
172 observation['rotSkyPos'] = (obs_pa - self.rotator_limits[limit_indx]) % (2.*np.pi)
173 return observation
175 def _fill_queue(self):
176 """
177 Compute reward function for each survey and fill the observing queue with the
178 observations from the highest reward survey.
179 """
181 rewards = None
182 for ns, surveys in enumerate(self.survey_lists):
183 rewards = np.zeros(len(surveys))
184 for i, survey in enumerate(surveys):
185 rewards[i] = np.nanmax(survey.calc_reward_function(self.conditions))
186 # If we have a good reward, break out of the loop
187 if np.nanmax(rewards) > -np.inf:
188 self.survey_index[0] = ns
189 break
190 if (np.nanmax(rewards) == -np.inf) | (np.isnan(np.nanmax(rewards))):
191 self.flush_queue()
192 else:
193 to_fix = np.where(np.isnan(rewards) == True)
194 rewards[to_fix] = -np.inf
195 # Take a min here, so the surveys will be executed in the order they are
196 # entered if there is a tie.
197 self.survey_index[1] = np.min(np.where(rewards == np.nanmax(rewards)))
199 # Survey return list of observations
200 result = self.survey_lists[self.survey_index[0]][self.survey_index[1]].generate_observations(self.conditions)
201 self.queue = result
203 if len(self.queue) == 0:
204 self.log.warning('Failed to fill queue')