Hide keyboard shortcuts

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 

19 

20__all__ = ['Model_observatory'] 

21 

22 

23class Model_observatory(object): 

24 """A class to generate a realistic telemetry stream for the scheduler 

25 """ 

26 

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 """ 

50 

51 if nside is None: 

52 nside = set_default_nside() 

53 self.nside = nside 

54 

55 self.cloud_limit = cloud_limit 

56 

57 self.alt_min = np.radians(alt_min) 

58 self.lax_dome = lax_dome 

59 

60 self.mjd_start = mjd_start 

61 

62 self.sim_ToO = sim_ToO 

63 

64 self.park_after = park_after/60./24. # To days 

65 

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) 

70 

71 # Load up all the models we need 

72 

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) 

78 

79 sched_downtimes = self.sched_downtime_data() 

80 unsched_downtimes = self.unsched_downtime_data() 

81 

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) 

90 

91 self.downtimes = np.array(list(zip(down_starts, down_ends)), dtype=list(zip(['start', 'end'], [float, float]))) 

92 self.downtimes.sort(order='start') 

93 

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 

103 

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] 

107 

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 

113 

114 self.cloud_data = CloudData(mjd_start_time, offset_year=0) 

115 

116 self.sky_model = sb.SkyModelPre(speedLoad=quickTest) 

117 

118 self.observatory = Kinem_model(mjd0=mjd_start) 

119 

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) 

124 

125 self.almanac = Almanac(mjd_start=mjd_start) 

126 

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 

133 

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) 

140 

141 self.obsID_counter = 0 

142 

143 def get_info(self): 

144 """ 

145 Returns 

146 ------- 

147 Array with model versions that were instantiated 

148 """ 

149 

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} 

156 

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']) 

177 

178 return result 

179 

180 def return_conditions(self): 

181 """ 

182 

183 Returns 

184 ------- 

185 lsst.sims.featureScheduler.features.conditions object 

186 """ 

187 

188 self.conditions.mjd = self.mjd 

189 

190 self.conditions.night = self.night 

191 # Current time as astropy time 

192 current_time = Time(self.mjd, format='mjd') 

193 

194 # Clouds. XXX--just the raw value 

195 self.conditions.bulk_cloud = self.cloud_data(current_time) 

196 

197 # use conditions object itself to get aprox altitude of each healpx 

198 alts = self.conditions.alt 

199 azs = self.conditions.az 

200 

201 good = np.where(alts > self.alt_min) 

202 

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 

208 

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 

219 

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) 

224 

225 self.conditions.mounted_filters = self.observatory.mounted_filters 

226 self.conditions.current_filter = self.observatory.current_filter[0] 

227 

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 

239 

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'] 

246 

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'] 

254 

255 self.conditions.lmst, last = calcLmstLast(self.mjd, self.site.longitude_rad) 

256 

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 

261 

262 self.conditions.rotTelPos = self.observatory.last_rot_tel_pos_rad 

263 

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] 

274 

275 # Planet positions from almanac 

276 self.conditions.planet_positions = self.almanac.get_planet_positions(self.mjd) 

277 

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 

283 

284 return self.conditions 

285 

286 @property 

287 def mjd(self): 

288 return self._mjd 

289 

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] 

295 

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') 

301 

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 

310 

311 observation['night'] = self.night 

312 observation['mjd'] = self.mjd 

313 

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]] 

318 

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']) 

323 

324 lmst, last = calcLmstLast(self.mjd, self.site.longitude_rad) 

325 observation['lmst'] = lmst 

326 

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'] 

341 

342 observation['ID'] = self.obsID_counter 

343 self.obsID_counter += 1 

344 

345 return observation 

346 

347 def check_up(self, mjd): 

348 """See if we are in downtime 

349 

350 True if telescope is up 

351 False if in downtime 

352 """ 

353 

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 

359 

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) 

366 

367 

368 Returns 

369 ------- 

370 bool 

371 

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 

377 

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')) 

381 

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 

406 

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. 

415 

416 return observation 

417 

418 def observe(self, observation): 

419 """Try to make an observation 

420 

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 """ 

428 

429 start_night = self.night.copy() 

430 

431 if np.isnan(observation['rotSkyPos']): 

432 observation = self._update_rotSkyPos(observation) 

433 

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() 

438 

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']) 

444 

445 # inf slewtime means the observation failed (probably outsire alt limits) 

446 if ~np.all(np.isfinite(slewtime)): 

447 return None, False 

448 

449 observation_worked, new_mjd = self.check_mjd(self.mjd + (slewtime + visittime)/24./3600.) 

450 

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 

465 

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 

480 

481 return result, new_night