Coverage for python/lsst/sims/featureScheduler/modelObservatory/model_observatory.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.utils import (_hpid2RaDec, _raDec2Hpid, Site, calcLmstLast,
3 m5_flat_sed, _approx_RaDec2AltAz, _angularSeparation, _approx_altaz2pa)
4import lsst.sims.skybrightness_pre as sb
5import healpy as hp
6from lsst.sims.downtimeModel import ScheduledDowntimeData, UnscheduledDowntimeData
7import lsst.sims.downtimeModel as downtimeModel
8from lsst.sims.seeingModel import SeeingData, SeeingModel
9from lsst.sims.cloudModel import CloudData
10from lsst.sims.featureScheduler.features import Conditions
11from lsst.sims.featureScheduler.utils import set_default_nside, create_season_offset
12from astropy.coordinates import EarthLocation
13from astropy.time import Time
14from lsst.sims.almanac import Almanac
15import warnings
16import matplotlib.pylab as plt
17from importlib import import_module
18from lsst.sims.featureScheduler.modelObservatory import Kinem_model
20__all__ = ['Model_observatory']
23class Model_observatory(object):
24 """A class to generate a realistic telemetry stream for the scheduler
25 """
27 def __init__(self, nside=None, mjd_start=59853.5, seed=42, quickTest=True,
28 alt_min=5., lax_dome=True, cloud_limit=0.3, sim_ToO=None,
29 seeing_db=None, park_after=10.):
30 """
31 Parameters
32 ----------
33 nside : int (None)
34 The healpix nside resolution
35 mjd_start : float (59853.5)
36 The MJD to start the observatory up at
37 alt_min : float (5.)
38 The minimum altitude to compute models at (degrees).
39 lax_dome : bool (True)
40 Passed to observatory model. If true, allows dome creep.
41 cloud_limit : float (0.3)
42 The limit to stop taking observations if the cloud model returns something equal or higher
43 sim_ToO : sim_targetoO object (None)
44 If one would like to inject simulated ToOs into the telemetry stream.
45 seeing_db : filename of the seeing data database (None)
46 If one would like to use an alternate seeing database
47 park_after : float (10)
48 Park the telescope after a gap longer than park_after (minutes)
49 """
51 if nside is None:
52 nside = set_default_nside()
53 self.nside = nside
55 self.cloud_limit = cloud_limit
57 self.alt_min = np.radians(alt_min)
58 self.lax_dome = lax_dome
60 self.mjd_start = mjd_start
62 self.sim_ToO = sim_ToO
64 self.park_after = park_after/60./24. # To days
66 # Create an astropy location
67 self.site = Site('LSST')
68 self.location = EarthLocation(lat=self.site.latitude, lon=self.site.longitude,
69 height=self.site.height)
71 # Load up all the models we need
73 mjd_start_time = Time(self.mjd_start, format='mjd')
74 # Downtime
75 self.down_nights = []
76 self.sched_downtime_data = ScheduledDowntimeData(mjd_start_time)
77 self.unsched_downtime_data = UnscheduledDowntimeData(mjd_start_time)
79 sched_downtimes = self.sched_downtime_data()
80 unsched_downtimes = self.unsched_downtime_data()
82 down_starts = []
83 down_ends = []
84 for dt in sched_downtimes:
85 down_starts.append(dt['start'].mjd)
86 down_ends.append(dt['end'].mjd)
87 for dt in unsched_downtimes:
88 down_starts.append(dt['start'].mjd)
89 down_ends.append(dt['end'].mjd)
91 self.downtimes = np.array(list(zip(down_starts, down_ends)), dtype=list(zip(['start', 'end'], [float, float])))
92 self.downtimes.sort(order='start')
94 # Make sure there aren't any overlapping downtimes
95 diff = self.downtimes['start'][1:] - self.downtimes['end'][0:-1]
96 while np.min(diff) < 0:
97 # Should be able to do this wihtout a loop, but this works
98 for i, dt in enumerate(self.downtimes[0:-1]):
99 if self.downtimes['start'][i+1] < dt['end']:
100 new_end = np.max([dt['end'], self.downtimes['end'][i+1]])
101 self.downtimes[i]['end'] = new_end
102 self.downtimes[i+1]['end'] = new_end
104 good = np.where(self.downtimes['end'] - np.roll(self.downtimes['end'], 1) != 0)
105 self.downtimes = self.downtimes[good]
106 diff = self.downtimes['start'][1:] - self.downtimes['end'][0:-1]
108 self.seeing_data = SeeingData(mjd_start_time, seeing_db=seeing_db)
109 self.seeing_model = SeeingModel()
110 self.seeing_indx_dict = {}
111 for i, filtername in enumerate(self.seeing_model.filter_list):
112 self.seeing_indx_dict[filtername] = i
114 self.cloud_data = CloudData(mjd_start_time, offset_year=0)
116 self.sky_model = sb.SkyModelPre(speedLoad=quickTest)
118 self.observatory = Kinem_model(mjd0=mjd_start)
120 self.filterlist = ['u', 'g', 'r', 'i', 'z', 'y']
121 self.seeing_FWHMeff = {}
122 for key in self.filterlist:
123 self.seeing_FWHMeff[key] = np.zeros(hp.nside2npix(self.nside), dtype=float)
125 self.almanac = Almanac(mjd_start=mjd_start)
127 # Let's make sure we're at an openable MJD
128 good_mjd = False
129 to_set_mjd = mjd_start
130 while not good_mjd:
131 good_mjd, to_set_mjd = self.check_mjd(to_set_mjd)
132 self.mjd = to_set_mjd
134 sun_moon_info = self.almanac.get_sun_moon_positions(self.mjd)
135 season_offset = create_season_offset(self.nside, sun_moon_info['sun_RA'])
136 self.sun_RA_start = sun_moon_info['sun_RA'] + 0
137 # Conditions object to update and return on request
138 self.conditions = Conditions(nside=self.nside, mjd_start=mjd_start,
139 season_offset=season_offset, sun_RA_start=self.sun_RA_start)
141 self.obsID_counter = 0
143 def get_info(self):
144 """
145 Returns
146 -------
147 Array with model versions that were instantiated
148 """
150 # The things we want to get info on
151 models = {'cloud data': self.cloud_data, 'sky model': self.sky_model,
152 'seeing data': self.seeing_data, 'seeing model': self.seeing_model,
153 'observatory model': self.observatory,
154 'sched downtime data': self.sched_downtime_data,
155 'unched downtime data': self.unsched_downtime_data}
157 result = []
158 for model_name in models:
159 try:
160 module_name = models[model_name].__module__
161 module = import_module(module_name)
162 ver = import_module(module.__package__+'.version')
163 version = ver.__version__
164 fingerprint = ver.__fingerprint__
165 except:
166 version = 'NA'
167 fingerprint = 'NA'
168 result.append([model_name+' version', version])
169 result.append([model_name+' fingerprint', fingerprint])
170 result.append([model_name+' module', models[model_name].__module__])
171 try:
172 info = models[model_name].config_info()
173 for key in info:
174 result.append([key, str(info[key])])
175 except:
176 result.append([model_name, 'no config_info'])
178 return result
180 def return_conditions(self):
181 """
183 Returns
184 -------
185 lsst.sims.featureScheduler.features.conditions object
186 """
188 self.conditions.mjd = self.mjd
190 self.conditions.night = self.night
191 # Current time as astropy time
192 current_time = Time(self.mjd, format='mjd')
194 # Clouds. XXX--just the raw value
195 self.conditions.bulk_cloud = self.cloud_data(current_time)
197 # use conditions object itself to get aprox altitude of each healpx
198 alts = self.conditions.alt
199 azs = self.conditions.az
201 good = np.where(alts > self.alt_min)
203 # Compute the airmass at each heapix
204 airmass = np.zeros(alts.size, dtype=float)
205 airmass.fill(np.nan)
206 airmass[good] = 1./np.cos(np.pi/2. - alts[good])
207 self.conditions.airmass = airmass
209 # reset the seeing
210 for key in self.seeing_FWHMeff:
211 self.seeing_FWHMeff[key].fill(np.nan)
212 # Use the model to get the seeing at this time and airmasses.
213 FWHM_500 = self.seeing_data(current_time)
214 seeing_dict = self.seeing_model(FWHM_500, airmass[good])
215 fwhm_eff = seeing_dict['fwhmEff']
216 for i, key in enumerate(self.seeing_model.filter_list):
217 self.seeing_FWHMeff[key][good] = fwhm_eff[i, :]
218 self.conditions.FWHMeff = self.seeing_FWHMeff
220 # sky brightness
221 self.conditions.skybrightness = self.sky_model.returnMags(self.mjd, airmass_mask=False,
222 planet_mask=False,
223 moon_mask=False, zenith_mask=False)
225 self.conditions.mounted_filters = self.observatory.mounted_filters
226 self.conditions.current_filter = self.observatory.current_filter[0]
228 # Compute the slewtimes
229 slewtimes = np.empty(alts.size, dtype=float)
230 slewtimes.fill(np.nan)
231 # If there has been a gap, park the telescope
232 gap = self.mjd - self.observatory.last_mjd
233 if gap > self.park_after:
234 self.observatory.park()
235 slewtimes[good] = self.observatory.slew_times(0., 0., self.mjd, alt_rad=alts[good], az_rad=azs[good],
236 filtername=self.observatory.current_filter,
237 lax_dome=self.lax_dome, update_tracking=False)
238 self.conditions.slewtime = slewtimes
240 # Let's get the sun and moon
241 sun_moon_info = self.almanac.get_sun_moon_positions(self.mjd)
242 # convert these to scalars
243 for key in sun_moon_info:
244 sun_moon_info[key] = sun_moon_info[key].max()
245 self.conditions.moonPhase = sun_moon_info['moon_phase']
247 self.conditions.moonAlt = sun_moon_info['moon_alt']
248 self.conditions.moonAz = sun_moon_info['moon_az']
249 self.conditions.moonRA = sun_moon_info['moon_RA']
250 self.conditions.moonDec = sun_moon_info['moon_dec']
251 self.conditions.sunAlt = sun_moon_info['sun_alt']
252 self.conditions.sunRA = sun_moon_info['sun_RA']
253 self.conditions.sunDec = sun_moon_info['sun_dec']
255 self.conditions.lmst, last = calcLmstLast(self.mjd, self.site.longitude_rad)
257 self.conditions.telRA = self.observatory.current_RA_rad
258 self.conditions.telDec = self.observatory.current_dec_rad
259 self.conditions.telAlt = self.observatory.last_alt_rad
260 self.conditions.telAz = self.observatory.last_az_rad
262 self.conditions.rotTelPos = self.observatory.last_rot_tel_pos_rad
263 self.conditions.cumulative_azimuth_rad = self.observatory.cumulative_azimuth_rad
265 # Add in the almanac information
266 self.conditions.night = self.night
267 self.conditions.sunset = self.almanac.sunsets['sunset'][self.almanac_indx]
268 self.conditions.sun_n12_setting = self.almanac.sunsets['sun_n12_setting'][self.almanac_indx]
269 self.conditions.sun_n18_setting = self.almanac.sunsets['sun_n18_setting'][self.almanac_indx]
270 self.conditions.sun_n18_rising = self.almanac.sunsets['sun_n18_rising'][self.almanac_indx]
271 self.conditions.sun_n12_rising = self.almanac.sunsets['sun_n12_rising'][self.almanac_indx]
272 self.conditions.sunrise = self.almanac.sunsets['sunrise'][self.almanac_indx]
273 self.conditions.moonrise = self.almanac.sunsets['moonrise'][self.almanac_indx]
274 self.conditions.moonset = self.almanac.sunsets['moonset'][self.almanac_indx]
276 # Planet positions from almanac
277 self.conditions.planet_positions = self.almanac.get_planet_positions(self.mjd)
279 # See if there are any ToOs to include
280 if self.sim_ToO is not None:
281 toos = self.sim_ToO(self.mjd)
282 if toos is not None:
283 self.conditions.targets_of_opportunity = toos
285 return self.conditions
287 @property
288 def mjd(self):
289 return self._mjd
291 @mjd.setter
292 def mjd(self, value):
293 self._mjd = value
294 self.almanac_indx = self.almanac.mjd_indx(value)
295 self.night = self.almanac.sunsets['night'][self.almanac_indx]
297 def observation_add_data(self, observation):
298 """
299 Fill in the metadata for a completed observation
300 """
301 current_time = Time(self.mjd, format='mjd')
303 observation['clouds'] = self.cloud_data(current_time)
304 observation['airmass'] = 1./np.cos(np.pi/2. - observation['alt'])
305 # Seeing
306 fwhm_500 = self.seeing_data(current_time)
307 seeing_dict = self.seeing_model(fwhm_500, observation['airmass'])
308 observation['FWHMeff'] = seeing_dict['fwhmEff'][self.seeing_indx_dict[observation['filter'][0]]]
309 observation['FWHM_geometric'] = seeing_dict['fwhmGeom'][self.seeing_indx_dict[observation['filter'][0]]]
310 observation['FWHM_500'] = fwhm_500
312 observation['night'] = self.night
313 observation['mjd'] = self.mjd
315 hpid = _raDec2Hpid(self.sky_model.nside, observation['RA'], observation['dec'])
316 observation['skybrightness'] = self.sky_model.returnMags(self.mjd,
317 indx=[hpid],
318 extrapolate=True)[observation['filter'][0]]
320 observation['fivesigmadepth'] = m5_flat_sed(observation['filter'][0], observation['skybrightness'],
321 observation['FWHMeff'],
322 observation['exptime']/observation['nexp'],
323 observation['airmass'], nexp=observation['nexp'])
325 lmst, last = calcLmstLast(self.mjd, self.site.longitude_rad)
326 observation['lmst'] = lmst
328 sun_moon_info = self.almanac.get_sun_moon_positions(self.mjd)
329 observation['sunAlt'] = sun_moon_info['sun_alt']
330 observation['sunAz'] = sun_moon_info['sun_az']
331 observation['sunRA'] = sun_moon_info['sun_RA']
332 observation['sunDec'] = sun_moon_info['sun_dec']
333 observation['moonAlt'] = sun_moon_info['moon_alt']
334 observation['moonAz'] = sun_moon_info['moon_az']
335 observation['moonRA'] = sun_moon_info['moon_RA']
336 observation['moonDec'] = sun_moon_info['moon_dec']
337 observation['moonDist'] = _angularSeparation(observation['RA'], observation['dec'],
338 observation['moonRA'], observation['moonDec'])
339 observation['solarElong'] = _angularSeparation(observation['RA'], observation['dec'],
340 observation['sunRA'], observation['sunDec'])
341 observation['moonPhase'] = sun_moon_info['moon_phase']
343 observation['ID'] = self.obsID_counter
344 self.obsID_counter += 1
346 return observation
348 def check_up(self, mjd):
349 """See if we are in downtime
351 True if telescope is up
352 False if in downtime
353 """
355 result = True
356 indx = np.searchsorted(self.downtimes['start'], mjd, side='right')-1
357 if mjd < self.downtimes['end'][indx]:
358 result = False
359 return result
361 def check_mjd(self, mjd, cloud_skip=20.):
362 """See if an mjd is ok to observe
363 Parameters
364 ----------
365 cloud_skip : float (20)
366 How much time to skip ahead if it's cloudy (minutes)
369 Returns
370 -------
371 bool
373 mdj : float
374 If True, the input mjd. If false, a good mjd to skip forward to.
375 """
376 passed = True
377 new_mjd = mjd + 0
379 # Maybe set this to a while loop to make sure we don't land on another cloudy time?
380 # or just make this an entire recursive call?
381 clouds = self.cloud_data(Time(mjd, format='mjd'))
383 if clouds > self.cloud_limit:
384 passed = False
385 while clouds > self.cloud_limit:
386 new_mjd = new_mjd + cloud_skip/60./24.
387 clouds = self.cloud_data(Time(new_mjd, format='mjd'))
388 alm_indx = np.searchsorted(self.almanac.sunsets['sunset'], mjd) - 1
389 # at the end of the night, advance to the next setting twilight
390 if mjd > self.almanac.sunsets['sun_n12_rising'][alm_indx]:
391 passed = False
392 new_mjd = self.almanac.sunsets['sun_n12_setting'][alm_indx+1]
393 if mjd < self.almanac.sunsets['sun_n12_setting'][alm_indx]:
394 passed = False
395 new_mjd = self.almanac.sunsets['sun_n12_setting'][alm_indx+1]
396 # We're in a down night, advance to next night
397 if not self.check_up(mjd):
398 passed = False
399 new_mjd = self.almanac.sunsets['sun_n12_setting'][alm_indx+1]
400 # recursive call to make sure we skip far enough ahead
401 if not passed:
402 while not passed:
403 passed, new_mjd = self.check_mjd(new_mjd)
404 return False, new_mjd
405 else:
406 return True, mjd
408 def _update_rotSkyPos(self, observation):
409 """If we have an undefined rotSkyPos, try to fill it out.
410 """
411 alt, az = _approx_RaDec2AltAz(observation['RA'], observation['dec'], self.site.latitude_rad,
412 self.site.longitude_rad, self.mjd)
413 obs_pa = _approx_altaz2pa(alt, az, self.site.latitude_rad)
414 observation['rotSkyPos'] = (obs_pa + observation['rotTelPos']) % (2*np.pi)
415 observation['rotTelPos'] = 0.
417 return observation
419 def observe(self, observation):
420 """Try to make an observation
422 Returns
423 -------
424 observation : observation object
425 None if there was no observation taken. Completed observation with meta data filled in.
426 new_night : bool
427 Have we started a new night.
428 """
430 start_night = self.night.copy()
432 if np.isnan(observation['rotSkyPos']):
433 observation = self._update_rotSkyPos(observation)
435 # If there has been a long gap, assume telescope stopped tracking and parked
436 gap = self.mjd - self.observatory.last_mjd
437 if gap > self.park_after:
438 self.observatory.park()
440 # Compute what alt,az we have tracked to (or are parked at)
441 start_alt, start_az, start_rotTelPos = self.observatory.current_alt_az(self.mjd)
442 # Slew to new position and execute observation. Use the requested rotTelPos position,
443 # obsevation['rotSkyPos'] will be ignored.
444 slewtime, visittime = self.observatory.observe(observation, self.mjd, rotTelPos=observation['rotTelPos'])
446 # inf slewtime means the observation failed (probably outsire alt limits)
447 if ~np.all(np.isfinite(slewtime)):
448 return None, False
450 observation_worked, new_mjd = self.check_mjd(self.mjd + (slewtime + visittime)/24./3600.)
452 if observation_worked:
453 observation['visittime'] = visittime
454 observation['slewtime'] = slewtime
455 observation['slewdist'] = _angularSeparation(start_az, start_alt,
456 self.observatory.last_az_rad,
457 self.observatory.last_alt_rad)
458 self.mjd = self.mjd + slewtime/24./3600.
459 # Reach into the observatory model to pull out the relevant data it has calculated
460 # Note, this might be after the observation has been completed.
461 observation['alt'] = self.observatory.last_alt_rad
462 observation['az'] = self.observatory.last_az_rad
463 observation['pa'] = self.observatory.last_pa_rad
464 observation['rotTelPos'] = self.observatory.last_rot_tel_pos_rad
465 observation['rotSkyPos'] = self.observatory.current_rotSkyPos_rad
466 observation['cummTelAz'] = self.observatory.cumulative_azimuth_rad
468 # Metadata on observation is after slew and settle, so at start of exposure.
469 result = self.observation_add_data(observation)
470 self.mjd = self.mjd + visittime/24./3600.
471 new_night = False
472 else:
473 result = None
474 self.observatory.park()
475 # Skip to next legitimate mjd
476 self.mjd = new_mjd
477 now_night = self.night
478 if now_night == start_night:
479 new_night = False
480 else:
481 new_night = True
483 return result, new_night