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 

5 

6__all__ = ["Kinem_model"] 

7TwoPi = 2.*np.pi 

8 

9 

10class radec2altazpa(object): 

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

12 """ 

13 def __init__(self, location): 

14 self.location = location 

15 

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

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

18 return_pa=True) 

19 return alt, az, pa 

20 

21 

22def _getRotSkyPos(paRad, rotTelRad): 

23 """ 

24 Paramteres 

25 ---------- 

26 paRad : float or array 

27 The parallactic angle 

28 """ 

29 return (rotTelRad - paRad) % TwoPi 

30 

31 

32def _getRotTelPos(paRad, rotSkyRad): 

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

34 """ 

35 result = (rotSkyRad + paRad) % TwoPi 

36 return result 

37 

38 

39def smallest_signed_angle(a1, a2): 

40 """ 

41 via https://stackoverflow.com/questions/1878907/the-smallest-difference-between-2-angles""" 

42 x = a1 % TwoPi 

43 y = a2 % TwoPi 

44 a = (x - y) % TwoPi 

45 b = (y - x) % TwoPi 

46 result = b+0 

47 alb = np.where(a < b)[0] 

48 result[alb] = -1.*a[alb] 

49 return result 

50 

51 

52class Kinem_model(object): 

53 """ 

54 A Kinematic model of the telescope. 

55 

56 Parameters 

57 ---------- 

58 location : astropy.coordinates.EarthLocation object 

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

60 park_alt : float (86.5) 

61 The altitude the telescope gets parked at (degrees) 

62 start_filter : str ('r') 

63 The filter that gets loaded when the telescope is parked 

64 mjd0 : float (0) 

65 The MJD to assume we are starting from 

66 

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

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

69 """ 

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

71 self.park_alt_rad = np.radians(park_alt) 

72 self.park_az_rad = np.radians(park_az) 

73 self.current_filter = start_filter 

74 if location is None: 

75 self.location = Site('LSST') 

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

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

78 # Our RA,Dec to Alt,Az converter 

79 self.radec2altaz = radec2altazpa(self.location) 

80 

81 self.setup_camera() 

82 self.setup_dome() 

83 self.setup_telescope() 

84 self.setup_optics() 

85 

86 # Park the telescope 

87 self.park() 

88 self.last_mjd = mjd0 

89 

90 def mount_filters(self, filter_list): 

91 """Change which filters are mounted 

92 """ 

93 self.mounted_filters = filter_list 

94 

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

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

97 """ 

98 Parameters 

99 ---------- 

100 readtime : float (2) 

101 The readout time of the CCDs (seconds) 

102 shuttertime : float (1.) 

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

104 filter_changetime : float (120) 

105 The time it takes to change filters (seconds) 

106 fov : float (3.5) 

107 The camera field of view (degrees) 

108 rotator_min : float (-90) 

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

110 maxspeed : float (3.5) 

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

112 accel : float (1.0) 

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

114 """ 

115 self.readtime = readtime 

116 self.shuttertime = shuttertime 

117 self.filter_changetime = filter_changetime 

118 self.camera_fov = np.radians(fov) 

119 

120 self.telrot_minpos_rad = np.radians(rotator_min) 

121 self.telrot_maxpos_rad = np.radians(rotator_max) 

122 self.telrot_maxspeed_rad = np.radians(maxspeed) 

123 self.telrot_accel_rad = np.radians(accel) 

124 self.telrot_decel_rad = np.radians(decel) 

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

126 

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

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

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

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

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

132 """ 

133 self.domalt_maxspeed_rad = np.radians(altitude_maxspeed) 

134 self.domalt_accel_rad = np.radians(altitude_accel) 

135 self.domalt_decel_rad = np.radians(altitude_decel) 

136 self.domalt_free_range = np.radians(altitude_freerange) 

137 self.domaz_maxspeed_rad = np.radians(azimuth_maxspeed) 

138 self.domaz_accel_rad = np.radians(azimuth_accel) 

139 self.domaz_decel_rad = np.radians(azimuth_decel) 

140 self.domaz_free_range = np.radians(azimuth_freerange) 

141 self.domaz_settletime = settle_time 

142 

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

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

145 altitude_accel=3.5, altitude_decel=3.5, azimuth_maxspeed=7.0, 

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

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

148 """ 

149 self.telalt_minpos_rad = np.radians(altitude_minpos) 

150 self.telalt_maxpos_rad = np.radians(altitude_maxpos) 

151 self.telaz_minpos_rad = np.radians(azimuth_minpos) 

152 self.telaz_maxpos_rad = np.radians(azimuth_maxpos) 

153 self.telalt_maxspeed_rad = np.radians(altitude_maxspeed) 

154 self.telalt_accel_rad = np.radians(altitude_accel) 

155 self.telalt_decel_rad = np.radians(altitude_decel) 

156 self.telaz_maxspeed_rad = np.radians(azimuth_maxspeed) 

157 self.telaz_accel_rad = np.radians(azimuth_accel) 

158 self.telaz_decel_rad = np.radians(azimuth_decel) 

159 self.mount_settletime = settle_time 

160 

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

162 """ 

163 Parameters 

164 ---------- 

165 ol_slope : float 

166 seconds/degree in altitude slew. 

167 cl_delay : list ([0.0, 36]) 

168 The delays for closed optics loops (seconds) 

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

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

171 """ 

172 

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

174 self.optics_cl_delay = cl_delay 

175 self.optics_cl_altlimit = np.radians(cl_altlimit) 

176 

177 def park(self): 

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

179 """ 

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

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

182 

183 self.parked = True 

184 

185 # We have no current position we are tracking 

186 self.current_RA_rad = None 

187 self.current_dec_rad = None 

188 self.current_rotSkyPos_rad = None 

189 self.cumulative_azimuth_rad = 0 

190 

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

192 self.last_az_rad = self.park_az_rad 

193 self.last_alt_rad = self.park_alt_rad 

194 self.last_rot_tel_pos_rad = 0 

195 

196 def current_alt_az(self, mjd): 

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

198 """ 

199 if self.parked: 

200 return self.last_alt_rad, self.last_az_rad, self.last_rot_tel_pos_rad 

201 else: 

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

203 rotTelPos = _getRotTelPos(pa, self.last_rot_tel_pos_rad) 

204 return alt_rad, az_rad, rotTelPos 

205 

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

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

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

209 d = vmax**2 / accel 

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

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

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

213 

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

215 

216 Parameters 

217 ---------- 

218 distance : numpy.ndarray 

219 Distances to travel. Must be positive value. 

220 vmax : float 

221 Max velocity 

222 accel : float 

223 Acceleration (and deceleration) 

224 

225 Returns 

226 ------- 

227 numpy.ndarray 

228 """ 

229 dm = vmax**2 / accel 

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

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

232 return slewTime 

233 

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

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

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

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

238 position (stored internally). 

239 Assumptions (currently): 

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

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

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

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

244 

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

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

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

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

249 

250 Parameters 

251 ---------- 

252 ra_rad : np.ndarray 

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

254 dec_rad : np.ndarray 

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

256 mjd : float 

257 The current moodified julian date (days) 

258 rotSkyPos : np.ndarray 

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

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

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

262 rotTelPos : np.ndarray 

263 The desired rotTelPos(s) (radians). 

264 filtername : str 

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

266 alt_rad : np.ndarray 

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

268 az_rad : np.ndarray 

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

270 lax_dome : boolean, default False 

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

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

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

274 starting_alt_rad : float (None) 

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

276 starting_az_rad : float (None) 

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

278 starting_rotTelPos_rad : float (None) 

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

280 update_tracking : bool (False) 

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

282 include_readtime : bool (True) 

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

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

285 

286 Returns 

287 ------- 

288 np.ndarray 

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

290 """ 

291 if filtername not in self.mounted_filters: 

292 return np.nan 

293 

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

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

296 if np.isfinite(rotTelPos): 

297 rotSkyPos = None 

298 else: 

299 rotTelPos = None 

300 

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

302 if alt_rad is None: 

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

304 else: 

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

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

307 self.location.lon_rad, mjd) 

308 

309 if starting_alt_rad is None: 

310 if self.parked: 

311 starting_alt_rad = self.park_alt_rad 

312 starting_az_rad = self.park_az_rad 

313 else: 

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

315 self.current_dec_rad, mjd) 

316 

317 deltaAlt = np.abs(alt_rad - starting_alt_rad) 

318 delta_az_short = smallest_signed_angle(starting_az_rad, az_rad) 

319 delta_az_long = delta_az_short - TwoPi 

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

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

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

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

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

325 cummulative_az_short = delta_az_short + self.cumulative_azimuth_rad 

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

327 # Set out of bounds azimuths to infinite distance 

328 delta_az_short[oob] = np.inf 

329 cummulative_az_long = delta_az_long + self.cumulative_azimuth_rad 

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

331 delta_az_long[oob] = np.inf 

332 

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

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

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

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

337 

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

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

340 self.telalt_accel_rad) 

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

342 self.telaz_accel_rad) 

343 totTelTime = np.maximum(telAltSlewTime, telAzSlewTime) 

344 

345 # Time for open loop optics correction 

346 olTime = deltaAlt / self.optics_ol_slope 

347 totTelTime += olTime 

348 # Add time for telescope settle. 

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

350 settleAndOL = np.where(totTelTime > 0) 

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

352 # And readout puts a floor on tel time 

353 if include_readtime: 

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

355 

356 # now compute dome slew time 

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

358 # even if the telescope has to unwind 

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

360 if lax_dome: 

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

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

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

364 

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

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

367 # minimize distance we have to travel in azimuth. 

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

369 # also assume: 

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

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

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

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

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

375 # starts opening 

376 domDeltaAlt = deltaAlt 

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

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

379 domSlitDiam = self.camera_fov / 2.0 

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

381 domAltSlewTime = domDeltaAlt / self.domalt_maxspeed_rad 

382 domAzSlewTime = domDeltaAz / self.domaz_maxspeed_rad 

383 totDomTime1 = np.maximum(domAltSlewTime, domAzSlewTime) 

384 

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

386 domDeltaAz = deltaAz 

387 domAltSlewTime = domDeltaAlt / self.domalt_maxspeed_rad 

388 domAzSlewTime = domDeltaAz / self.domaz_maxspeed_rad 

389 totDomTime2 = np.maximum(domAltSlewTime, domAzSlewTime) 

390 

391 totDomTime = np.minimum(totDomTime1, totDomTime2) 

392 totDomTime[sameDome] = 0 

393 

394 else: 

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

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

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

398 self.domalt_accel_rad) 

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

400 self.domaz_accel_rad) 

401 # Dome takes 1 second to settle in az 

402 domAzSlewTime = np.where(domAzSlewTime > 0, 

403 domAzSlewTime + self.domaz_settletime, 

404 domAzSlewTime) 

405 totDomTime = np.maximum(domAltSlewTime, domAzSlewTime) 

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

407 slewTime = np.maximum(totTelTime, totDomTime) 

408 # include filter change time if necessary 

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

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

411 self.filter_changetime) 

412 # Add closed loop optics correction 

413 # Find the limit where we must add the delay 

414 cl_limit = self.optics_cl_altlimit[1] 

415 cl_delay = self.optics_cl_delay[1] 

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

417 slewTime[closeLoop] += cl_delay 

418 

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

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

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

422 slewTime[outsideLimits] = np.nan 

423 

424 # If we want to include the camera rotation time 

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

426 if rotTelPos is None: 

427 rotTelPos = _getRotTelPos(pa, rotSkyPos) 

428 if rotSkyPos is None: 

429 rotSkyPos = _getRotSkyPos(pa, rotTelPos) 

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

431 rotTelPos_ranged = rotTelPos+0 

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

433 rotTelPos_ranged[over] -= TwoPi 

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

435 return np.nan 

436 # If there was no kwarg for starting rotator position 

437 if starting_rotTelPos_rad is None: 

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

439 if self.current_rotSkyPos_rad is None: 

440 current_rotTelPos = self.last_rot_tel_pos_rad 

441 else: 

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

443 current_rotTelPos = _getRotTelPos(pa, self.current_rotSkyPos_rad) 

444 else: 

445 # kwarg overrides if it was supplied 

446 current_rotTelPos = starting_rotTelPos_rad 

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

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

449 slewTime = np.maximum(slewTime, rotator_time) 

450 

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

452 # at the requested RA,Dec,rotSkyPos 

453 if update_tracking: 

454 self.current_RA_rad = ra_rad 

455 self.current_dec_rad = dec_rad 

456 self.current_rotSkyPos_rad = rotSkyPos 

457 self.parked = False 

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

459 self.last_rot_tel_pos_rad = rotTelPos 

460 self.last_az_rad = az_rad 

461 self.last_alt_rad = alt_rad 

462 self.last_pa_rad = pa 

463 # Track the cumulative azimuth 

464 self.cumulative_azimuth_rad += deltaAztel 

465 self.current_filter = filtername 

466 self.last_mjd = mjd 

467 

468 return slewTime 

469 

470 def visit_time(self, observation): 

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

472 visit_time = observation['exptime'] + \ 

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

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

475 return visit_time 

476 

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

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

479 

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

481 """ 

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

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

484 rotTelPos=rotTelPos, 

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

486 visit_time = self.visit_time(observation) 

487 return slewtime, visit_time