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
264 # Add in the almanac information
265 self.conditions.night = self.night
266 self.conditions.sunset = self.almanac.sunsets['sunset'][self.almanac_indx]
267 self.conditions.sun_n12_setting = self.almanac.sunsets['sun_n12_setting'][self.almanac_indx]
268 self.conditions.sun_n18_setting = self.almanac.sunsets['sun_n18_setting'][self.almanac_indx]
269 self.conditions.sun_n18_rising = self.almanac.sunsets['sun_n18_rising'][self.almanac_indx]
270 self.conditions.sun_n12_rising = self.almanac.sunsets['sun_n12_rising'][self.almanac_indx]
271 self.conditions.sunrise = self.almanac.sunsets['sunrise'][self.almanac_indx]
272 self.conditions.moonrise = self.almanac.sunsets['moonrise'][self.almanac_indx]
273 self.conditions.moonset = self.almanac.sunsets['moonset'][self.almanac_indx]
275 # Planet positions from almanac
276 self.conditions.planet_positions = self.almanac.get_planet_positions(self.mjd)
278 # See if there are any ToOs to include
279 if self.sim_ToO is not None:
280 toos = self.sim_ToO(self.mjd)
281 if toos is not None:
282 self.conditions.targets_of_opportunity = toos
284 return self.conditions
286 @property
287 def mjd(self):
288 return self._mjd
290 @mjd.setter
291 def mjd(self, value):
292 self._mjd = value
293 self.almanac_indx = self.almanac.mjd_indx(value)
294 self.night = self.almanac.sunsets['night'][self.almanac_indx]
296 def observation_add_data(self, observation):
297 """
298 Fill in the metadata for a completed observation
299 """
300 current_time = Time(self.mjd, format='mjd')
302 observation['clouds'] = self.cloud_data(current_time)
303 observation['airmass'] = 1./np.cos(np.pi/2. - observation['alt'])
304 # Seeing
305 fwhm_500 = self.seeing_data(current_time)
306 seeing_dict = self.seeing_model(fwhm_500, observation['airmass'])
307 observation['FWHMeff'] = seeing_dict['fwhmEff'][self.seeing_indx_dict[observation['filter'][0]]]
308 observation['FWHM_geometric'] = seeing_dict['fwhmGeom'][self.seeing_indx_dict[observation['filter'][0]]]
309 observation['FWHM_500'] = fwhm_500
311 observation['night'] = self.night
312 observation['mjd'] = self.mjd
314 hpid = _raDec2Hpid(self.sky_model.nside, observation['RA'], observation['dec'])
315 observation['skybrightness'] = self.sky_model.returnMags(self.mjd,
316 indx=[hpid],
317 extrapolate=True)[observation['filter'][0]]
319 observation['fivesigmadepth'] = m5_flat_sed(observation['filter'][0], observation['skybrightness'],
320 observation['FWHMeff'],
321 observation['exptime']/observation['nexp'],
322 observation['airmass'], nexp=observation['nexp'])
324 lmst, last = calcLmstLast(self.mjd, self.site.longitude_rad)
325 observation['lmst'] = lmst
327 sun_moon_info = self.almanac.get_sun_moon_positions(self.mjd)
328 observation['sunAlt'] = sun_moon_info['sun_alt']
329 observation['sunAz'] = sun_moon_info['sun_az']
330 observation['sunRA'] = sun_moon_info['sun_RA']
331 observation['sunDec'] = sun_moon_info['sun_dec']
332 observation['moonAlt'] = sun_moon_info['moon_alt']
333 observation['moonAz'] = sun_moon_info['moon_az']
334 observation['moonRA'] = sun_moon_info['moon_RA']
335 observation['moonDec'] = sun_moon_info['moon_dec']
336 observation['moonDist'] = _angularSeparation(observation['RA'], observation['dec'],
337 observation['moonRA'], observation['moonDec'])
338 observation['solarElong'] = _angularSeparation(observation['RA'], observation['dec'],
339 observation['sunRA'], observation['sunDec'])
340 observation['moonPhase'] = sun_moon_info['moon_phase']
342 observation['ID'] = self.obsID_counter
343 self.obsID_counter += 1
345 return observation
347 def check_up(self, mjd):
348 """See if we are in downtime
350 True if telescope is up
351 False if in downtime
352 """
354 result = True
355 indx = np.searchsorted(self.downtimes['start'], mjd, side='right')-1
356 if mjd < self.downtimes['end'][indx]:
357 result = False
358 return result
360 def check_mjd(self, mjd, cloud_skip=20.):
361 """See if an mjd is ok to observe
362 Parameters
363 ----------
364 cloud_skip : float (20)
365 How much time to skip ahead if it's cloudy (minutes)
368 Returns
369 -------
370 bool
372 mdj : float
373 If True, the input mjd. If false, a good mjd to skip forward to.
374 """
375 passed = True
376 new_mjd = mjd + 0
378 # Maybe set this to a while loop to make sure we don't land on another cloudy time?
379 # or just make this an entire recursive call?
380 clouds = self.cloud_data(Time(mjd, format='mjd'))
382 if clouds > self.cloud_limit:
383 passed = False
384 while clouds > self.cloud_limit:
385 new_mjd = new_mjd + cloud_skip/60./24.
386 clouds = self.cloud_data(Time(new_mjd, format='mjd'))
387 alm_indx = np.searchsorted(self.almanac.sunsets['sunset'], mjd) - 1
388 # at the end of the night, advance to the next setting twilight
389 if mjd > self.almanac.sunsets['sun_n12_rising'][alm_indx]:
390 passed = False
391 new_mjd = self.almanac.sunsets['sun_n12_setting'][alm_indx+1]
392 if mjd < self.almanac.sunsets['sun_n12_setting'][alm_indx]:
393 passed = False
394 new_mjd = self.almanac.sunsets['sun_n12_setting'][alm_indx+1]
395 # We're in a down night, advance to next night
396 if not self.check_up(mjd):
397 passed = False
398 new_mjd = self.almanac.sunsets['sun_n12_setting'][alm_indx+1]
399 # recursive call to make sure we skip far enough ahead
400 if not passed:
401 while not passed:
402 passed, new_mjd = self.check_mjd(new_mjd)
403 return False, new_mjd
404 else:
405 return True, mjd
407 def _update_rotSkyPos(self, observation):
408 """If we have an undefined rotSkyPos, try to fill it out.
409 """
410 alt, az = _approx_RaDec2AltAz(observation['RA'], observation['dec'], self.site.latitude_rad,
411 self.site.longitude_rad, self.mjd)
412 obs_pa = _approx_altaz2pa(alt, az, self.site.latitude_rad)
413 observation['rotSkyPos'] = (obs_pa + observation['rotTelPos']) % (2*np.pi)
414 observation['rotTelPos'] = 0.
416 return observation
418 def observe(self, observation):
419 """Try to make an observation
421 Returns
422 -------
423 observation : observation object
424 None if there was no observation taken. Completed observation with meta data filled in.
425 new_night : bool
426 Have we started a new night.
427 """
429 start_night = self.night.copy()
431 if np.isnan(observation['rotSkyPos']):
432 observation = self._update_rotSkyPos(observation)
434 # If there has been a long gap, assume telescope stopped tracking and parked
435 gap = self.mjd - self.observatory.last_mjd
436 if gap > self.park_after:
437 self.observatory.park()
439 # Compute what alt,az we have tracked to (or are parked at)
440 start_alt, start_az, start_rotTelPos = self.observatory.current_alt_az(self.mjd)
441 # Slew to new position and execute observation. Use the requested rotTelPos position,
442 # obsevation['rotSkyPos'] will be ignored.
443 slewtime, visittime = self.observatory.observe(observation, self.mjd, rotTelPos=observation['rotTelPos'])
445 # inf slewtime means the observation failed (probably outsire alt limits)
446 if ~np.all(np.isfinite(slewtime)):
447 return None, False
449 observation_worked, new_mjd = self.check_mjd(self.mjd + (slewtime + visittime)/24./3600.)
451 if observation_worked:
452 observation['visittime'] = visittime
453 observation['slewtime'] = slewtime
454 observation['slewdist'] = _angularSeparation(start_az, start_alt,
455 self.observatory.last_az_rad,
456 self.observatory.last_alt_rad)
457 self.mjd = self.mjd + slewtime/24./3600.
458 # Reach into the observatory model to pull out the relevant data it has calculated
459 # Note, this might be after the observation has been completed.
460 observation['alt'] = self.observatory.last_alt_rad
461 observation['az'] = self.observatory.last_az_rad
462 observation['pa'] = self.observatory.last_pa_rad
463 observation['rotTelPos'] = self.observatory.last_rot_tel_pos_rad
464 observation['rotSkyPos'] = self.observatory.current_rotSkyPos_rad
466 # Metadata on observation is after slew and settle, so at start of exposure.
467 result = self.observation_add_data(observation)
468 self.mjd = self.mjd + visittime/24./3600.
469 new_night = False
470 else:
471 result = None
472 self.observatory.park()
473 # Skip to next legitimate mjd
474 self.mjd = new_mjd
475 now_night = self.night
476 if now_night == start_night:
477 new_night = False
478 else:
479 new_night = True
481 return result, new_night