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 Site, calcLmstLast, _approx_altAz2RaDec, _approx_altaz2pa, _approx_RaDec2AltAz 

3import healpy as hp 

4import matplotlib.pylab as plt 

5from lsst.sims.featureScheduler.utils import smallest_signed_angle 

6 

7__all__ = ["Kinem_model"] 

8TwoPi = 2.*np.pi 

9 

10 

11class radec2altazpa(object): 

12 """Class to make it easy to swap in different alt/az conversion if wanted 

13 """ 

14 def __init__(self, location): 

15 self.location = location 

16 

17 def __call__(self, ra, dec, mjd): 

18 alt, az, pa = _approx_RaDec2AltAz(ra, dec, self.location.lat_rad, self.location.lon_rad, mjd, 

19 return_pa=True) 

20 return alt, az, pa 

21 

22 

23def _getRotSkyPos(paRad, rotTelRad): 

24 """ 

25 Paramteres 

26 ---------- 

27 paRad : float or array 

28 The parallactic angle 

29 """ 

30 return (rotTelRad - paRad) % TwoPi 

31 

32 

33def _getRotTelPos(paRad, rotSkyRad): 

34 """Make it run from -180 to 180 

35 """ 

36 result = (rotSkyRad + paRad) % TwoPi 

37 return result 

38 

39 

40class Kinem_model(object): 

41 """ 

42 A Kinematic model of the telescope. 

43 

44 Parameters 

45 ---------- 

46 location : astropy.coordinates.EarthLocation object 

47 The location of the telescope. If None, defaults to lsst.sims.utils.Site info 

48 park_alt : float (86.5) 

49 The altitude the telescope gets parked at (degrees) 

50 start_filter : str ('r') 

51 The filter that gets loaded when the telescope is parked 

52 mjd0 : float (0) 

53 The MJD to assume we are starting from 

54 

55 Note there are additional parameters in the methods setup_camera, setup_dome, setup_telescope, 

56 and setup_optics. Just breaking it up a bit to make it more readable. 

57 """ 

58 def __init__(self, location=None, park_alt=86.5, park_az=0., start_filter='r', mjd0=0): 

59 self.park_alt_rad = np.radians(park_alt) 

60 self.park_az_rad = np.radians(park_az) 

61 self.current_filter = start_filter 

62 if location is None: 

63 self.location = Site('LSST') 

64 self.location.lat_rad = np.radians(self.location.latitude) 

65 self.location.lon_rad = np.radians(self.location.longitude) 

66 # Our RA,Dec to Alt,Az converter 

67 self.radec2altaz = radec2altazpa(self.location) 

68 

69 self.setup_camera() 

70 self.setup_dome() 

71 self.setup_telescope() 

72 self.setup_optics() 

73 

74 # Park the telescope 

75 self.park() 

76 self.last_mjd = mjd0 

77 

78 def mount_filters(self, filter_list): 

79 """Change which filters are mounted 

80 """ 

81 self.mounted_filters = filter_list 

82 

83 def setup_camera(self, readtime=2., shuttertime=1., filter_changetime=120., fov=3.5, 

84 rotator_min=-90, rotator_max=90, maxspeed=3.5, accel=1.0, decel=1.0): 

85 """ 

86 Parameters 

87 ---------- 

88 readtime : float (2) 

89 The readout time of the CCDs (seconds) 

90 shuttertime : float (1.) 

91 The time it takes the shutter to go from closed to fully open (seconds) 

92 filter_changetime : float (120) 

93 The time it takes to change filters (seconds) 

94 fov : float (3.5) 

95 The camera field of view (degrees) 

96 rotator_min : float (-90) 

97 The minimum angle the camera rotator can move to (degrees) 

98 maxspeed : float (3.5) 

99 The maximum speed of the rotator (degrees/s) 

100 accel : float (1.0) 

101 The acceleration of the rotator (degrees/s^2) 

102 """ 

103 self.readtime = readtime 

104 self.shuttertime = shuttertime 

105 self.filter_changetime = filter_changetime 

106 self.camera_fov = np.radians(fov) 

107 

108 self.telrot_minpos_rad = np.radians(rotator_min) 

109 self.telrot_maxpos_rad = np.radians(rotator_max) 

110 self.telrot_maxspeed_rad = np.radians(maxspeed) 

111 self.telrot_accel_rad = np.radians(accel) 

112 self.telrot_decel_rad = np.radians(decel) 

113 self.mounted_filters = ['u', 'g', 'r', 'i', 'y'] 

114 

115 def setup_dome(self, altitude_maxspeed=1.75, altitude_accel=0.875, altitude_decel=0.875, 

116 altitude_freerange=0., azimuth_maxspeed=1.5, azimuth_accel=0.75, 

117 azimuth_decel=0.75, azimuth_freerange=4.0, settle_time=1.0): 

118 """input in degrees, degees/second, degrees/second**2, and seconds. 

119 Freerange is the range in which there is zero delay. 

120 """ 

121 self.domalt_maxspeed_rad = np.radians(altitude_maxspeed) 

122 self.domalt_accel_rad = np.radians(altitude_accel) 

123 self.domalt_decel_rad = np.radians(altitude_decel) 

124 self.domalt_free_range = np.radians(altitude_freerange) 

125 self.domaz_maxspeed_rad = np.radians(azimuth_maxspeed) 

126 self.domaz_accel_rad = np.radians(azimuth_accel) 

127 self.domaz_decel_rad = np.radians(azimuth_decel) 

128 self.domaz_free_range = np.radians(azimuth_freerange) 

129 self.domaz_settletime = settle_time 

130 

131 def setup_telescope(self, altitude_minpos=20.0, altitude_maxpos=86.5, 

132 azimuth_minpos=-270.0, azimuth_maxpos=270.0, altitude_maxspeed=3.5, 

133 altitude_accel=3.5, altitude_decel=3.5, azimuth_maxspeed=7.0, 

134 azimuth_accel=7.0, azimuth_decel=7.0, settle_time=3.0): 

135 """input in degrees, degees/second, degrees/second**2, and seconds. 

136 """ 

137 self.telalt_minpos_rad = np.radians(altitude_minpos) 

138 self.telalt_maxpos_rad = np.radians(altitude_maxpos) 

139 self.telaz_minpos_rad = np.radians(azimuth_minpos) 

140 self.telaz_maxpos_rad = np.radians(azimuth_maxpos) 

141 self.telalt_maxspeed_rad = np.radians(altitude_maxspeed) 

142 self.telalt_accel_rad = np.radians(altitude_accel) 

143 self.telalt_decel_rad = np.radians(altitude_decel) 

144 self.telaz_maxspeed_rad = np.radians(azimuth_maxspeed) 

145 self.telaz_accel_rad = np.radians(azimuth_accel) 

146 self.telaz_decel_rad = np.radians(azimuth_decel) 

147 self.mount_settletime = settle_time 

148 

149 def setup_optics(self, ol_slope=1.0/3.5, cl_delay=[0.0, 36.], cl_altlimit=[0.0, 9.0, 90.0]): 

150 """ 

151 Parameters 

152 ---------- 

153 ol_slope : float 

154 seconds/degree in altitude slew. 

155 cl_delay : list ([0.0, 36]) 

156 The delays for closed optics loops (seconds) 

157 cl_altlimit : list ([0.0, 9.0, 90.0]) 

158 The altitude limits (degrees) for performing closed optice loops. Should be one element longer than cl_delay. 

159 """ 

160 

161 self.optics_ol_slope = ol_slope/np.radians(1.) # ah, 1./np.radians(1)=np.pi/180 

162 self.optics_cl_delay = cl_delay 

163 self.optics_cl_altlimit = np.radians(cl_altlimit) 

164 

165 def park(self): 

166 """Put the telescope in the park position. 

167 """ 

168 # I'm going to ignore that the old model had the dome altitude at 90 and telescope altitude 86 for park. 

169 # We should usually be dome az limited anyway, so this should be a negligible approximation. 

170 

171 self.parked = True 

172 

173 # We have no current position we are tracking 

174 self.current_RA_rad = None 

175 self.current_dec_rad = None 

176 self.current_rotSkyPos_rad = None 

177 self.cumulative_azimuth_rad = 0 

178 

179 # The last position we were at (or the current if we are parked) 

180 self.last_az_rad = self.park_az_rad 

181 self.last_alt_rad = self.park_alt_rad 

182 self.last_rot_tel_pos_rad = 0 

183 

184 def current_alt_az(self, mjd): 

185 """return the current alt az position that we have tracked to. 

186 """ 

187 if self.parked: 

188 return self.last_alt_rad, self.last_az_rad, self.last_rot_tel_pos_rad 

189 else: 

190 alt_rad, az_rad, pa = self.radec2altaz(self.current_RA_rad, self.current_dec_rad, mjd) 

191 rotTelPos = _getRotTelPos(pa, self.last_rot_tel_pos_rad) 

192 return alt_rad, az_rad, rotTelPos 

193 

194 def _uamSlewTime(self, distance, vmax, accel): 

195 """Compute slew time delay assuming uniform acceleration (for any component). 

196 If you accelerate uniformly to vmax, then slow down uniformly to zero, distance traveled is 

197 d = vmax**2 / accel 

198 To travel distance d while accelerating/decelerating at rate a, time required is t = 2 * sqrt(d / a) 

199 If hit vmax, then time to acceleration to/from vmax is 2*vmax/a and distance in those 

200 steps is vmax**2/a. The remaining distance is (d - vmax^2/a) and time needed is (d - vmax^2/a)/vmax 

201 

202 This method accepts arrays of distance, and assumes acceleration == deceleration. 

203 

204 Parameters 

205 ---------- 

206 distance : numpy.ndarray 

207 Distances to travel. Must be positive value. 

208 vmax : float 

209 Max velocity 

210 accel : float 

211 Acceleration (and deceleration) 

212 

213 Returns 

214 ------- 

215 numpy.ndarray 

216 """ 

217 dm = vmax**2 / accel 

218 slewTime = np.where(distance < dm, 2 * np.sqrt(distance / accel), 

219 2 * vmax / accel + (distance - dm) / vmax) 

220 return slewTime 

221 

222 def slew_times(self, ra_rad, dec_rad, mjd, rotSkyPos=None, rotTelPos=None, filtername='r', 

223 lax_dome=True, alt_rad=None, az_rad=None, starting_alt_rad=None, starting_az_rad=None, 

224 starting_rotTelPos_rad=None, update_tracking=False, include_readtime=True): 

225 """Calculates ``slew'' time to a series of alt/az/filter positions from the current 

226 position (stored internally). 

227 Assumptions (currently): 

228 Assumes we have been tracking on ra,dec,rotSkyPos position. 

229 Ignores the motion of the sky while we are slewing (this approx should probably average out over time). 

230 No checks for if we have tracked beyond limits. Assumes folks put telescope in park if there's a long gap. 

231 Assumes the camera rotator never needs to (or can't) do a slew over 180 degrees. 

232 

233 Calculates the ``slew'' time necessary to get from current state 

234 to alt2/az2/filter2. The time returned is actually the time between 

235 the end of an exposure at current location and the beginning of an exposure 

236 at alt2/az2, since it includes readout time in the ``slew'' time. 

237 

238 Parameters 

239 ---------- 

240 ra_rad : np.ndarray 

241 The RA(s) of the location(s) we wish to slew to (radians) 

242 dec_rad : np.ndarray 

243 The declination(s) of the location(s) we wish to slew to (radians) 

244 mjd : float 

245 The current moodified julian date (days) 

246 rotSkyPos : np.ndarray 

247 The desired rotSkyPos(s) (radians). Angle between up on the chip and North. Note, 

248 it is possible to set a rotSkyPos outside the allowed camera rotator range, in which case 

249 the slewtime will be np.inf. If both rotSkyPos and rotTelPos are set, rotTelPos will be used. 

250 rotTelPos : np.ndarray 

251 The desired rotTelPos(s) (radians). 

252 filtername : str 

253 The filter(s) of the desired observations. Set to None to compute only telescope and dome motion times. 

254 alt_rad : np.ndarray 

255 The altitude(s) of the destination pointing(s) (radians). Will override ra_rad,dec_rad if provided. 

256 az_rad : np.ndarray 

257 The azimuth(s) of the destination pointing(s) (radians). Will override ra_rad,dec_rad if provided. 

258 lax_dome : boolean, default False 

259 If True, allow the dome to creep, model a dome slit, and don't 

260 require the dome to settle in azimuth. If False, adhere to the way 

261 SOCS calculates slew times (as of June 21 2017). 

262 starting_alt_rad : float (None) 

263 The starting altitude for the slew (radians). If None, will use internally stored last pointing. 

264 starting_az_rad : float (None) 

265 The starting azimuth for the slew (radians). If None, will use internally stored last pointing. 

266 starting_rotTelPos_rad : float (None) 

267 The starting camera rotation for the slew (radians). If None, will use internally stored last pointing. 

268 update_tracking : bool (False) 

269 If True, update the internal attributes to say we are tracking the specified RA,Dec,RotSkyPos position. 

270 include_readtime : bool (True) 

271 Assume the camera must be read before opening the shutter, and include that readtime in the returned slewtime. 

272 Readtime will never be inclded if the telescope was parked before the slew. 

273 

274 Returns 

275 ------- 

276 np.ndarray 

277 The number of seconds between the two specified exposures. Will be np.nan or np.inf if slew is not possible. 

278 """ 

279 if filtername not in self.mounted_filters: 

280 return np.nan 

281 

282 # Don't trust folks to do pa calculation correctly, if both rotations set, rotSkyPos wins 

283 if (rotTelPos is not None) & (rotSkyPos is not None): 

284 if np.isfinite(rotTelPos): 

285 rotSkyPos = None 

286 else: 

287 rotTelPos = None 

288 

289 # alt,az not provided, calculate from RA,Dec 

290 if alt_rad is None: 

291 alt_rad, az_rad, pa = self.radec2altaz(ra_rad, dec_rad, mjd) 

292 else: 

293 pa = _approx_altaz2pa(alt_rad, az_rad, self.location.lat_rad) 

294 if update_tracking: 

295 ra_rad, dec_rad = _approx_altAz2RaDec(alt_rad, az_rad, self.location.lat_rad, 

296 self.location.lon_rad, mjd) 

297 

298 if starting_alt_rad is None: 

299 if self.parked: 

300 starting_alt_rad = self.park_alt_rad 

301 starting_az_rad = self.park_az_rad 

302 else: 

303 starting_alt_rad, starting_az_rad, starting_pa = self.radec2altaz(self.current_RA_rad, 

304 self.current_dec_rad, mjd) 

305 

306 deltaAlt = np.abs(alt_rad - starting_alt_rad) 

307 delta_az_short = smallest_signed_angle(starting_az_rad, az_rad) 

308 delta_az_long = delta_az_short - TwoPi 

309 daslz = np.where(delta_az_short < 0)[0] 

310 delta_az_long[daslz] = TwoPi + delta_az_short[daslz] 

311 azlz = np.where(delta_az_short < 0)[0] 

312 delta_az_long[azlz] = TwoPi + delta_az_short[azlz] 

313 # So, for every position, we can get there by slewing long or short way 

314 cummulative_az_short = delta_az_short + self.cumulative_azimuth_rad 

315 oob = np.where((cummulative_az_short < self.telaz_minpos_rad) | (cummulative_az_short > self.telaz_maxpos_rad))[0] 

316 # Set out of bounds azimuths to infinite distance 

317 delta_az_short[oob] = np.inf 

318 cummulative_az_long = delta_az_long + self.cumulative_azimuth_rad 

319 oob = np.where((cummulative_az_long < self.telaz_minpos_rad) | (cummulative_az_long > self.telaz_maxpos_rad))[0] 

320 delta_az_long[oob] = np.inf 

321 

322 # Taking minimum of abs, so only possible azimuths slews should show up. And deltaAz is signed properly. 

323 stacked_az = np.vstack([delta_az_short, delta_az_long]) 

324 indx = np.argmin(np.abs(stacked_az), axis=0) 

325 deltaAztel = np.take_along_axis(stacked_az, np.expand_dims(indx, axis=0), axis=0).squeeze(axis=0) 

326 

327 # Calculate how long the telescope will take to slew to this position. 

328 telAltSlewTime = self._uamSlewTime(deltaAlt, self.telalt_maxspeed_rad, 

329 self.telalt_accel_rad) 

330 telAzSlewTime = self._uamSlewTime(np.abs(deltaAztel), self.telaz_maxspeed_rad, 

331 self.telaz_accel_rad) 

332 totTelTime = np.maximum(telAltSlewTime, telAzSlewTime) 

333 

334 # Time for open loop optics correction 

335 olTime = deltaAlt / self.optics_ol_slope 

336 totTelTime += olTime 

337 # Add time for telescope settle. 

338 # XXX--note, this means we're going to have a settle time even for very small slews (like even a dither) 

339 settleAndOL = np.where(totTelTime > 0) 

340 totTelTime[settleAndOL] += np.maximum(0, self.mount_settletime - olTime[settleAndOL]) 

341 # And readout puts a floor on tel time 

342 if include_readtime: 

343 totTelTime = np.maximum(self.readtime, totTelTime) 

344 

345 # now compute dome slew time 

346 # I think the dome can spin all the way around, so we will let it go the shortest angle, 

347 # even if the telescope has to unwind 

348 deltaAz = np.abs(smallest_signed_angle(starting_az_rad, az_rad)) 

349 if lax_dome: 

350 # model dome creep, dome slit, and no azimuth settle 

351 # if we can fit both exposures in the dome slit, do so 

352 sameDome = np.where(deltaAlt ** 2 + deltaAz ** 2 < self.camera_fov ** 2) 

353 

354 # else, we take the minimum time from two options: 

355 # 1. assume we line up alt in the center of the dome slit so we 

356 # minimize distance we have to travel in azimuth. 

357 # 2. line up az in the center of the slit 

358 # also assume: 

359 # * that we start out going maxspeed for both alt and az 

360 # * that we only just barely have to get the new field in the 

361 # dome slit in one direction, but that we have to center the 

362 # field in the other (which depends which of the two options used) 

363 # * that we don't have to slow down until after the shutter 

364 # starts opening 

365 domDeltaAlt = deltaAlt 

366 # on each side, we can start out with the dome shifted away from 

367 # the center of the field by an amount domSlitRadius - fovRadius 

368 domSlitDiam = self.camera_fov / 2.0 

369 domDeltaAz = deltaAz - 2 * (domSlitDiam / 2 - self.camera_fov / 2) 

370 domAltSlewTime = domDeltaAlt / self.domalt_maxspeed_rad 

371 domAzSlewTime = domDeltaAz / self.domaz_maxspeed_rad 

372 totDomTime1 = np.maximum(domAltSlewTime, domAzSlewTime) 

373 

374 domDeltaAlt = deltaAlt - 2 * (domSlitDiam / 2 - self.camera_fov / 2) 

375 domDeltaAz = deltaAz 

376 domAltSlewTime = domDeltaAlt / self.domalt_maxspeed_rad 

377 domAzSlewTime = domDeltaAz / self.domaz_maxspeed_rad 

378 totDomTime2 = np.maximum(domAltSlewTime, domAzSlewTime) 

379 

380 totDomTime = np.minimum(totDomTime1, totDomTime2) 

381 totDomTime[sameDome] = 0 

382 

383 else: 

384 # the above models a dome slit and dome creep. However, it appears that 

385 # SOCS requires the dome to slew exactly to each field and settle in az 

386 domAltSlewTime = self._uamSlewTime(deltaAlt, self.domalt_maxspeed_rad, 

387 self.domalt_accel_rad) 

388 domAzSlewTime = self._uamSlewTime(deltaAz, self.domaz_maxspeed_rad, 

389 self.domaz_accel_rad) 

390 # Dome takes 1 second to settle in az 

391 domAzSlewTime = np.where(domAzSlewTime > 0, 

392 domAzSlewTime + self.domaz_settletime, 

393 domAzSlewTime) 

394 totDomTime = np.maximum(domAltSlewTime, domAzSlewTime) 

395 # Find the max of the above for slew time. 

396 slewTime = np.maximum(totTelTime, totDomTime) 

397 # include filter change time if necessary 

398 filterChange = np.where(filtername != self.current_filter) 

399 slewTime[filterChange] = np.maximum(slewTime[filterChange], 

400 self.filter_changetime) 

401 # Add closed loop optics correction 

402 # Find the limit where we must add the delay 

403 cl_limit = self.optics_cl_altlimit[1] 

404 cl_delay = self.optics_cl_delay[1] 

405 closeLoop = np.where(deltaAlt >= cl_limit) 

406 slewTime[closeLoop] += cl_delay 

407 

408 # Mask min/max altitude limits so slewtime = np.nan 

409 outsideLimits = np.where((alt_rad > self.telalt_maxpos_rad) | 

410 (alt_rad < self.telalt_minpos_rad))[0] 

411 slewTime[outsideLimits] = np.nan 

412 

413 # If we want to include the camera rotation time 

414 if (rotSkyPos is not None) | (rotTelPos is not None): 

415 if rotTelPos is None: 

416 rotTelPos = _getRotTelPos(pa, rotSkyPos) 

417 if rotSkyPos is None: 

418 rotSkyPos = _getRotSkyPos(pa, rotTelPos) 

419 # If the new rotation angle would move us out of the limits, return nan 

420 rotTelPos_ranged = rotTelPos+0 

421 over = np.where(rotTelPos > np.pi)[0] 

422 rotTelPos_ranged[over] -= TwoPi 

423 if (rotTelPos_ranged < self.telrot_minpos_rad) | (rotTelPos_ranged > self.telrot_maxpos_rad): 

424 return np.nan 

425 # If there was no kwarg for starting rotator position 

426 if starting_rotTelPos_rad is None: 

427 # If there is no current rotSkyPos, we were parked 

428 if self.current_rotSkyPos_rad is None: 

429 current_rotTelPos = self.last_rot_tel_pos_rad 

430 else: 

431 # We have been tracking, so rotTelPos needs to be updated 

432 current_rotTelPos = _getRotTelPos(pa, self.current_rotSkyPos_rad) 

433 else: 

434 # kwarg overrides if it was supplied 

435 current_rotTelPos = starting_rotTelPos_rad 

436 deltaRotation = np.abs(smallest_signed_angle(current_rotTelPos, rotTelPos)) 

437 rotator_time = self._uamSlewTime(deltaRotation, self.telrot_maxspeed_rad, self.telrot_accel_rad) 

438 slewTime = np.maximum(slewTime, rotator_time) 

439 

440 # Update the internal attributes to note that we are now pointing and tracking 

441 # at the requested RA,Dec,rotSkyPos 

442 if update_tracking: 

443 self.current_RA_rad = ra_rad 

444 self.current_dec_rad = dec_rad 

445 self.current_rotSkyPos_rad = rotSkyPos 

446 self.parked = False 

447 # Handy to keep as reference, but not used for any calculations 

448 self.last_rot_tel_pos_rad = rotTelPos 

449 self.last_az_rad = az_rad 

450 self.last_alt_rad = alt_rad 

451 self.last_pa_rad = pa 

452 # Track the cumulative azimuth 

453 self.cumulative_azimuth_rad += deltaAztel 

454 self.current_filter = filtername 

455 self.last_mjd = mjd 

456 

457 return slewTime 

458 

459 def visit_time(self, observation): 

460 # How long does it take to make an observation. Assume final read can be done during next slew. 

461 visit_time = observation['exptime'] + \ 

462 observation['nexp'] * self.shuttertime + \ 

463 max(observation['nexp'] - 1, 0) * self.readtime 

464 return visit_time 

465 

466 def observe(self, observation, mjd, rotTelPos=None): 

467 """observe a target, and return the slewtime and visit time for the action 

468 

469 If slew is not allowed, returns np.nan and does not update state. 

470 """ 

471 slewtime = self.slew_times(observation['RA'], observation['dec'], 

472 mjd, rotSkyPos=observation['rotSkyPos'], 

473 rotTelPos=rotTelPos, 

474 filtername=observation['filter'], update_tracking=True) 

475 visit_time = self.visit_time(observation) 

476 return slewtime, visit_time