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 self.conditions.cumulative_azimuth_rad = self.observatory.cumulative_azimuth_rad 

264 

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] 

275 

276 # Planet positions from almanac 

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

278 

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 

284 

285 return self.conditions 

286 

287 @property 

288 def mjd(self): 

289 return self._mjd 

290 

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] 

296 

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

302 

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 

311 

312 observation['night'] = self.night 

313 observation['mjd'] = self.mjd 

314 

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

319 

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

324 

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

326 observation['lmst'] = lmst 

327 

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

342 

343 observation['ID'] = self.obsID_counter 

344 self.obsID_counter += 1 

345 

346 return observation 

347 

348 def check_up(self, mjd): 

349 """See if we are in downtime 

350 

351 True if telescope is up 

352 False if in downtime 

353 """ 

354 

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 

360 

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) 

367 

368 

369 Returns 

370 ------- 

371 bool 

372 

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 

378 

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

382 

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 

407 

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. 

416 

417 return observation 

418 

419 def observe(self, observation): 

420 """Try to make an observation 

421 

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

429 

430 start_night = self.night.copy() 

431 

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

433 observation = self._update_rotSkyPos(observation) 

434 

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

439 

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

445 

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

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

448 return None, False 

449 

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

451 

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 

467 

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 

482 

483 return result, new_night