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 logging 

2import math 

3import numpy as np 

4import os 

5 

6import palpy as pal 

7 

8from lsst.ts.dateloc import DateProfile, ObservatoryLocation 

9from lsst.ts.observatory.model import ObservatoryModelParameters 

10from lsst.ts.observatory.model import ObservatoryPosition 

11from lsst.ts.observatory.model import ObservatoryState 

12from lsst.ts.observatory.model import read_conf_file 

13 

14__all__ = ["ObservatoryModel"] 

15 

16TWOPI = 2 * np.pi 

17 

18class ObservatoryModel(object): 

19 """Class for modeling the observatory. 

20 """ 

21 

22 def __init__(self, location=None, log_level=logging.DEBUG): 

23 """Initialize the class. 

24 

25 Parameters 

26 ---------- 

27 location : lsst.ts.dateloc.ObservatoryLocation, optional 

28 An instance of the observatory location. Default is None, 

29 but this sets up the LSST as the location. 

30 log_level : int 

31 Set the logging level for the class. Default is logging.DEBUG. 

32 """ 

33 self.log = logging.getLogger("ObservatoryModel") 

34 self.log_level = log_level 

35 

36 self.params = ObservatoryModelParameters() 

37 if location is None: 

38 self.location = ObservatoryLocation() 

39 self.location.for_lsst() 

40 else: 

41 self.location = location 

42 self.park_state = ObservatoryState() 

43 self.current_state = ObservatoryState() 

44 

45 self.dateprofile = DateProfile(0.0, self.location) 

46 

47 self.filters = ["u", "g", "r", "i", "z", "y"] 

48 

49 self.activities = ["telalt", 

50 "telaz", 

51 "telrot", 

52 "telsettle", 

53 "telopticsopenloop", 

54 "telopticsclosedloop", 

55 "domalt", 

56 "domaz", 

57 "domazsettle", 

58 "filter", 

59 "readout", 

60 "exposures"] 

61 self.function_get_delay_for = {} 

62 self.delay_for = {} 

63 self.longest_prereq_for = {} 

64 for activity in self.activities: 

65 function_name = "get_delay_for_" + activity 

66 self.function_get_delay_for[activity] = getattr(self, function_name) 

67 self.delay_for[activity] = 0.0 

68 self.longest_prereq_for[activity] = "" 

69 self.lastslew_delays_dict = {} 

70 self.lastslew_criticalpath = [] 

71 self.filter_changes_list = [] 

72 

73 def __str__(self): 

74 """str: The string representation of the model.""" 

75 return str(self.current_state) 

76 

77 @classmethod 

78 def get_configure_dict(cls): 

79 """Get the configuration dictionary for the observatory model. 

80 

81 Returns 

82 ------- 

83 dict 

84 The configuration dictionary for the observatory model. 

85 """ 

86 conf_file = os.path.join(os.path.dirname(__file__), 

87 "observatory_model.conf") 

88 conf_dict = read_conf_file(conf_file) 

89 return conf_dict 

90 

91 def altaz2radecpa(self, dateprofile, alt_rad, az_rad): 

92 """Converts alt, az coordinates into ra, dec for the given time. 

93 

94 Parameters 

95 ---------- 

96 dateprofile : lsst.ts.dateloc.DateProfile 

97 Instance containing the time information. 

98 alt_rad : float 

99 The altitude in radians 

100 az_rad : float 

101 The azimuth in radians 

102 

103 Returns 

104 ------- 

105 tuple(float, float, float) 

106 (right ascension in radians, declination in radians, parallactic angle in radians) 

107 """ 

108 lst_rad = dateprofile.lst_rad 

109 

110 (ha_rad, dec_rad) = pal.dh2e(az_rad, alt_rad, self.location.latitude_rad) 

111 pa_rad = divmod(pal.pa(ha_rad, dec_rad, self.location.latitude_rad), TWOPI)[1] 

112 ra_rad = divmod(lst_rad - ha_rad, TWOPI)[1] 

113 

114 return (ra_rad, dec_rad, pa_rad) 

115 

116 @staticmethod 

117 def compute_kinematic_delay(distance, maxspeed, accel, decel, free_range=0.): 

118 """Calculate the kinematic delay. 

119 

120 This function calculates the kinematic delay for the given distance 

121 based on a simple second-order function for velocity plus an additional 

122 optional free range. The model considers that the free range is used for crawling 

123 such that: 

124 

125 1 - If the distance is less than the free_range, than the delay is zero. 

126 2 - If the distance is larger than the free_range, than the initial and final speed 

127 are equal to that possible to achieve after accelerating/decelerating for the free_range 

128 distance. 

129 

130 

131 Parameters 

132 ---------- 

133 distance : float 

134 The required distance to move. 

135 maxspeed : float 

136 The maximum speed for the calculation. 

137 accel : float 

138 The acceleration for the calculation. 

139 decel : float 

140 The deceleration for the calculation. 

141 free_range : float 

142 The free range in which the kinematic model returns zero delay. 

143 

144 Returns 

145 ------- 

146 tuple(float, float) 

147 (delay time in seconds, peak velocity in radians/sec) 

148 """ 

149 d = abs(distance) 

150 vpeak_free_range = (2 * free_range / (1 / accel + 1 / decel)) ** 0.5 

151 if vpeak_free_range > maxspeed: 

152 vpeak_free_range = maxspeed 

153 

154 if free_range > d: 

155 return 0., vpeak_free_range 

156 

157 vpeak = (2 * d / (1 / accel + 1 / decel)) ** 0.5 

158 

159 if vpeak <= maxspeed: 

160 delay = (vpeak - vpeak_free_range) * (1. / accel + 1. / decel) 

161 else: 

162 d1 = 0.5 * (maxspeed * maxspeed) / accel - free_range * accel / (accel + decel) 

163 d3 = 0.5 * (maxspeed * maxspeed) / decel - free_range * decel / (accel + decel) 

164 

165 if d1 < 0.: 

166 # This means it is possible to ramp up to max speed in less than the free range 

167 d1 = 0. 

168 if d3 < 0.: 

169 # This means it is possible to break down to zero in less than the free range 

170 d3 = 0. 

171 

172 d2 = d - d1 - d3 - free_range 

173 

174 t1 = (maxspeed - vpeak_free_range) / accel 

175 t3 = (maxspeed - vpeak_free_range) / decel 

176 t2 = d2 / maxspeed 

177 

178 delay = t1 + t2 + t3 

179 vpeak = maxspeed 

180 

181 if distance < 0.: 

182 vpeak *= -1. 

183 

184 return delay, vpeak 

185 

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

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

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

189 d = vmax**2 / accel 

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

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

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

193 

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

195 

196 Parameters 

197 ---------- 

198 distance : numpy.ndarray 

199 Distances to travel. Must be positive value. 

200 vmax : float 

201 Max velocity 

202 accel : float 

203 Acceleration (and deceleration) 

204 

205 Returns 

206 ------- 

207 numpy.ndarray 

208 """ 

209 dm = vmax**2 / accel 

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

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

212 return slewTime 

213 

214 def get_approximate_slew_delay(self, alt_rad, az_rad, goal_filter, lax_dome=False): 

215 """Calculates ``slew'' time to a series of alt/az/filter positions. 

216 Assumptions (currently): 

217 assumes rotator is not moved (no rotator included) 

218 assumes we never max out cable wrap-around! 

219 

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

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

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

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

224 

225 Parameters 

226 ---------- 

227 alt_rad : np.ndarray 

228 The altitude of the destination pointing in RADIANS. 

229 az_rad : np.ndarray 

230 The azimuth of the destination pointing in RADIANS. 

231 goal_filter : np.ndarray 

232 The filter to be used in the destination observation. 

233 lax_dome : boolean, default False 

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

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

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

237 

238 Returns 

239 ------- 

240 np.ndarray 

241 The number of seconds between the two specified exposures. 

242 """ 

243 # FYI this takes on the order of 285 us to calculate slew times to 2293 pts (.12us per pointing) 

244 # Find the distances to all the other fields. 

245 deltaAlt = np.abs(alt_rad - self.current_state.alt_rad) 

246 deltaAz = np.abs(az_rad - self.current_state.az_rad) 

247 deltaAz = np.minimum(deltaAz, np.abs(deltaAz - 2 * np.pi)) 

248 

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

250 telAltSlewTime = self._uamSlewTime(deltaAlt, self.params.telalt_maxspeed_rad, 

251 self.params.telalt_accel_rad ) 

252 telAzSlewTime = self._uamSlewTime(deltaAz, self.params.telaz_maxspeed_rad, 

253 self.params.telaz_accel_rad) 

254 totTelTime = np.maximum(telAltSlewTime, telAzSlewTime) 

255 # Time for open loop optics correction 

256 olTime = deltaAlt / self.params.optics_ol_slope 

257 totTelTime += olTime 

258 # Add time for telescope settle. 

259 settleAndOL = np.where(totTelTime > 0) 

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

261 # And readout puts a floor on tel time 

262 totTelTime = np.maximum(self.params.readouttime, totTelTime) 

263 

264 # now compute dome slew time 

265 if lax_dome: 

266 totDomeTime = np.zeros(len(alt_rad), float) 

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

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

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

270 

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

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

273 # minimize distance we have to travel in azimuth. 

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

275 # also assume: 

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

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

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

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

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

281 # starts opening 

282 domDeltaAlt = deltaAlt 

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

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

285 domSlitDiam = self.params.camera_fov / 2.0 

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

287 domAltSlewTime = domDeltaAlt / self.params.domalt_maxspeed_rad 

288 domAzSlewTime = domDeltaAz / self.params.domaz_maxspeed_rad 

289 totDomTime1 = np.maximum(domAltSlewTime, domAzSlewTime) 

290 

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

292 domDeltaAz = deltaAz 

293 domAltSlewTime = domDeltaAlt / self.params.domalt_maxspeed_rad 

294 domAzSlewTime = domDeltaAz / self.params.domaz_maxspeed_rad 

295 totDomTime2 = np.maximum(domAltSlewTime, domAzSlewTime) 

296 

297 totDomTime = np.minimum(totDomTime1, totDomTime2) 

298 totDomTime[sameDome] = 0 

299 

300 else: 

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

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

303 domAltSlewTime = self._uamSlewTime(deltaAlt, self.params.domalt_maxspeed_rad, 

304 self.params.domalt_accel_rad) 

305 domAzSlewTime = self._uamSlewTime(deltaAz, self.params.domaz_maxspeed_rad, 

306 self.params.domaz_accel_rad) 

307 # Dome takes 1 second to settle in az 

308 domAzSlewTime = np.where(domAzSlewTime > 0, 

309 domAzSlewTime + self.params.domaz_settletime, 

310 domAzSlewTime) 

311 totDomTime = np.maximum(domAltSlewTime, domAzSlewTime) 

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

313 slewTime = np.maximum(totTelTime, totDomTime) 

314 # include filter change time if necessary 

315 filterChange = np.where(goal_filter != self.current_state.filter) 

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

317 self.params.filter_changetime) 

318 # Add closed loop optics correction 

319 # Find the limit where we must add the delay 

320 cl_limit = self.params.optics_cl_altlimit[1] 

321 cl_delay = self.params.optics_cl_delay[1] 

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

323 slewTime[closeLoop] += cl_delay 

324 

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

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

327 (alt_rad < self.params.telalt_minpos_rad)) 

328 slewTime[outsideLimits] = -1 

329 return slewTime 

330 

331 def configure(self, confdict): 

332 """Configure the observatory model. 

333 

334 Parameters 

335 ---------- 

336 confdict : dict 

337 The configuration dictionary containing the parameters for 

338 the observatory model. 

339 """ 

340 self.configure_telescope(confdict) 

341 self.configure_rotator(confdict) 

342 self.configure_dome(confdict) 

343 self.configure_optics(confdict) 

344 self.configure_camera(confdict) 

345 self.configure_slew(confdict) 

346 self.configure_park(confdict) 

347 

348 self.current_state.mountedfilters = self.params.filter_init_mounted_list 

349 self.current_state.unmountedfilters = self.params.filter_init_unmounted_list 

350 self.park_state.mountedfilters = self.current_state.mountedfilters 

351 self.park_state.unmountedfilters = self.current_state.unmountedfilters 

352 

353 self.reset() 

354 

355 def configure_camera(self, confdict): 

356 """Configure the camera parameters. 

357 

358 Parameters 

359 ---------- 

360 confdict : dict 

361 The set of configuration parameters for the camera. 

362 """ 

363 self.params.configure_camera(confdict) 

364 

365 self.log.log(self.log_level, 

366 "configure_camera: filter_changetime=%.1f" % 

367 (self.params.filter_changetime)) 

368 self.log.log(self.log_level, 

369 "configure_camera: readouttime=%.1f" % 

370 (self.params.readouttime)) 

371 self.log.log(self.log_level, 

372 "configure_camera: shuttertime=%.1f" % 

373 (self.params.shuttertime)) 

374 self.log.log(self.log_level, 

375 "configure_camera: filter_removable=%s" % 

376 (self.params.filter_removable_list)) 

377 self.log.log(self.log_level, 

378 "configure_camera: filter_max_changes_burst_num=%i" % 

379 (self.params.filter_max_changes_burst_num)) 

380 self.log.log(self.log_level, 

381 "configure_camera: filter_max_changes_burst_time=%.1f" % 

382 (self.params.filter_max_changes_burst_time)) 

383 self.log.log(self.log_level, 

384 "configure_camera: filter_max_changes_avg_num=%i" % 

385 (self.params.filter_max_changes_avg_num)) 

386 self.log.log(self.log_level, 

387 "configure_camera: filter_max_changes_avg_time=%.1f" % 

388 (self.params.filter_max_changes_avg_time)) 

389 self.log.log(self.log_level, 

390 "configure_camera: filter_init_mounted=%s" % 

391 (self.params.filter_init_mounted_list)) 

392 self.log.log(self.log_level, 

393 "configure_camera: filter_init_unmounted=%s" % 

394 (self.params.filter_init_unmounted_list)) 

395 

396 def configure_dome(self, confdict): 

397 """Configure the dome parameters. 

398 

399 Parameters 

400 ---------- 

401 confdict : dict 

402 The set of configuration parameters for the dome. 

403 """ 

404 self.params.configure_dome(confdict) 

405 

406 self.log.log(self.log_level, 

407 "configure_dome: domalt_maxspeed=%.3f" % 

408 (math.degrees(self.params.domalt_maxspeed_rad))) 

409 self.log.log(self.log_level, 

410 "configure_dome: domalt_accel=%.3f" % 

411 (math.degrees(self.params.domalt_accel_rad))) 

412 self.log.log(self.log_level, 

413 "configure_dome: domalt_decel=%.3f" % 

414 (math.degrees(self.params.domalt_decel_rad))) 

415 self.log.log(self.log_level, 

416 "configure_dome: domalt_freerange=%.3f" % 

417 (math.degrees(self.params.domalt_free_range))) 

418 self.log.log(self.log_level, 

419 "configure_dome: domaz_maxspeed=%.3f" % 

420 (math.degrees(self.params.domaz_maxspeed_rad))) 

421 self.log.log(self.log_level, 

422 "configure_dome: domaz_accel=%.3f" % 

423 (math.degrees(self.params.domaz_accel_rad))) 

424 self.log.log(self.log_level, 

425 "configure_dome: domaz_decel=%.3f" % 

426 (math.degrees(self.params.domaz_decel_rad))) 

427 self.log.log(self.log_level, 

428 "configure_dome: domaz_freerange=%.3f" % 

429 (math.degrees(self.params.domaz_free_range))) 

430 self.log.log(self.log_level, 

431 "configure_dome: domaz_settletime=%.3f" % 

432 (self.params.domaz_settletime)) 

433 

434 def configure_from_module(self, conf_file=None): 

435 """Configure the observatory model from the module stored \ 

436 configuration. 

437 

438 Parameters 

439 ---------- 

440 conf_file : str, optional 

441 The configuration file to use. 

442 """ 

443 if conf_file is None: 

444 conf_file = os.path.join(os.path.dirname(__file__), 

445 "observatory_model.conf") 

446 conf_dict = read_conf_file(conf_file) 

447 self.configure(conf_dict) 

448 

449 def configure_optics(self, confdict): 

450 """Configure the optics parameters. 

451 

452 Parameters 

453 ---------- 

454 confdict : dict 

455 The set of configuration parameters for the optics. 

456 """ 

457 self.params.configure_optics(confdict) 

458 

459 self.log.log(self.log_level, 

460 "configure_optics: optics_ol_slope=%.3f" % 

461 (self.params.optics_ol_slope)) 

462 self.log.log(self.log_level, 

463 "configure_optics: optics_cl_delay=%s" % 

464 (self.params.optics_cl_delay)) 

465 self.log.log(self.log_level, 

466 "configure_optics: optics_cl_altlimit=%s" % 

467 ([math.degrees(x) for x in self.params.optics_cl_altlimit])) 

468 

469 def configure_park(self, confdict): 

470 """Configure the telescope park state parameters. 

471 

472 Parameters 

473 ---------- 

474 confdict : dict 

475 The set of configuration parameters for the telescope park state. 

476 """ 

477 self.park_state.alt_rad = math.radians(confdict["park"]["telescope_altitude"]) 

478 self.park_state.az_rad = math.radians(confdict["park"]["telescope_azimuth"]) 

479 self.park_state.rot_rad = math.radians(confdict["park"]["telescope_rotator"]) 

480 self.park_state.telalt_rad = math.radians(confdict["park"]["telescope_altitude"]) 

481 self.park_state.telaz_rad = math.radians(confdict["park"]["telescope_azimuth"]) 

482 self.park_state.telrot_rad = math.radians(confdict["park"]["telescope_rotator"]) 

483 self.park_state.domalt_rad = math.radians(confdict["park"]["dome_altitude"]) 

484 self.park_state.domaz_rad = math.radians(confdict["park"]["dome_azimuth"]) 

485 self.park_state.filter = confdict["park"]["filter_position"] 

486 self.park_state.mountedfilters = self.current_state.mountedfilters 

487 self.park_state.unmountedfilters = self.current_state.unmountedfilters 

488 self.park_state.tracking = False 

489 

490 self.log.log(self.log_level, 

491 "configure_park: park_telalt_rad=%.3f" % (self.park_state.telalt_rad)) 

492 self.log.log(self.log_level, 

493 "configure_park: park_telaz_rad=%.3f" % (self.park_state.telaz_rad)) 

494 self.log.log(self.log_level, 

495 "configure_park: park_telrot_rad=%.3f" % (self.park_state.telrot_rad)) 

496 self.log.log(self.log_level, 

497 "configure_park: park_domalt_rad=%.3f" % (self.park_state.domalt_rad)) 

498 self.log.log(self.log_level, 

499 "configure_park: park_domaz_rad=%.3f" % (self.park_state.domaz_rad)) 

500 self.log.log(self.log_level, 

501 "configure_park: park_filter=%s" % (self.park_state.filter)) 

502 

503 def configure_rotator(self, confdict): 

504 """Configure the telescope rotator parameters. 

505 

506 Parameters 

507 ---------- 

508 confdict : dict 

509 The set of configuration parameters for the telescope rotator. 

510 """ 

511 self.params.configure_rotator(confdict) 

512 

513 self.log.log(self.log_level, 

514 "configure_rotator: telrot_minpos=%.3f" % 

515 (math.degrees(self.params.telrot_minpos_rad))) 

516 self.log.log(self.log_level, 

517 "configure_rotator: telrot_maxpos=%.3f" % 

518 (math.degrees(self.params.telrot_maxpos_rad))) 

519 self.log.log(self.log_level, 

520 "configure_rotator: telrot_maxspeed=%.3f" % 

521 (math.degrees(self.params.telrot_maxspeed_rad))) 

522 self.log.log(self.log_level, 

523 "configure_rotator: telrot_accel=%.3f" % 

524 (math.degrees(self.params.telrot_accel_rad))) 

525 self.log.log(self.log_level, 

526 "configure_rotator: telrot_decel=%.3f" % 

527 (math.degrees(self.params.telrot_decel_rad))) 

528 self.log.log(self.log_level, 

529 "configure_rotator: telrot_filterchangepos=%.3f" % 

530 (math.degrees(self.params.telrot_filterchangepos_rad))) 

531 self.log.log(self.log_level, 

532 "configure_rotator: rotator_followsky=%s" % 

533 (self.params.rotator_followsky)) 

534 self.log.log(self.log_level, 

535 "configure_rotator: rotator_resumeangle=%s" % 

536 (self.params.rotator_resumeangle)) 

537 

538 def configure_slew(self, confdict): 

539 """Configure the slew parameters. 

540 

541 Parameters 

542 ---------- 

543 confdict : dict 

544 The set of configuration parameters for the slew. 

545 """ 

546 self.params.configure_slew(confdict, self.activities) 

547 

548 for activity in self.activities: 

549 self.log.log(self.log_level, "configure_slew: prerequisites[%s]=%s" % 

550 (activity, self.params.prerequisites[activity])) 

551 

552 def configure_telescope(self, confdict): 

553 """Configure the telescope parameters. 

554 

555 Parameters 

556 ---------- 

557 confdict : dict 

558 The set of configuration parameters for the telescope. 

559 """ 

560 self.params.configure_telescope(confdict) 

561 

562 self.log.log(self.log_level, 

563 "configure_telescope: telalt_minpos=%.3f" % 

564 (math.degrees(self.params.telalt_minpos_rad))) 

565 self.log.log(self.log_level, 

566 "configure_telescope: telalt_maxpos=%.3f" % 

567 (math.degrees(self.params.telalt_maxpos_rad))) 

568 self.log.log(self.log_level, 

569 "configure_telescope: telaz_minpos=%.3f" % 

570 (math.degrees(self.params.telaz_minpos_rad))) 

571 self.log.log(self.log_level, 

572 "configure_telescope: telaz_maxpos=%.3f" % 

573 (math.degrees(self.params.telaz_maxpos_rad))) 

574 self.log.log(self.log_level, 

575 "configure_telescope: telalt_maxspeed=%.3f" % 

576 (math.degrees(self.params.telalt_maxspeed_rad))) 

577 self.log.log(self.log_level, 

578 "configure_telescope: telalt_accel=%.3f" % 

579 (math.degrees(self.params.telalt_accel_rad))) 

580 self.log.log(self.log_level, 

581 "configure_telescope: telalt_decel=%.3f" % 

582 (math.degrees(self.params.telalt_decel_rad))) 

583 self.log.log(self.log_level, 

584 "configure_telescope: telaz_maxspeed=%.3f" % 

585 (math.degrees(self.params.telaz_maxspeed_rad))) 

586 self.log.log(self.log_level, 

587 "configure_telescope: telaz_accel=%.3f" % 

588 (math.degrees(self.params.telaz_accel_rad))) 

589 self.log.log(self.log_level, 

590 "configure_telescope: telaz_decel=%.3f" % 

591 (math.degrees(self.params.telaz_decel_rad))) 

592 self.log.log(self.log_level, 

593 "configure_telescope: mount_settletime=%.3f" % 

594 (self.params.mount_settletime)) 

595 

596 def get_closest_angle_distance(self, target_rad, current_abs_rad, 

597 min_abs_rad=None, max_abs_rad=None): 

598 """Calculate the closest angular distance including handling \ 

599 cable wrap if necessary. 

600 

601 Parameters 

602 ---------- 

603 target_rad : float 

604 The destination angle (radians). 

605 current_abs_rad : float 

606 The current angle (radians). 

607 min_abs_rad : float, optional 

608 The minimum constraint angle (radians). 

609 max_abs_rad : float, optional 

610 The maximum constraint angle (radians). 

611 

612 Returns 

613 ------- 

614 tuple(float, float) 

615 (accumulated angle in radians, distance angle in radians) 

616 """ 

617 # if there are wrap limits, normalizes the target angle 

618 if min_abs_rad is not None: 

619 norm_target_rad = divmod(target_rad - min_abs_rad, TWOPI)[1] + min_abs_rad 

620 if max_abs_rad is not None: 

621 # if the target angle is unreachable 

622 # then sets an arbitrary value 

623 if norm_target_rad > max_abs_rad: 

624 norm_target_rad = max(min_abs_rad, norm_target_rad - math.pi) 

625 else: 

626 norm_target_rad = target_rad 

627 

628 # computes the distance clockwise 

629 distance_rad = divmod(norm_target_rad - current_abs_rad, TWOPI)[1] 

630 

631 # take the counter-clockwise distance if shorter 

632 if distance_rad > math.pi: 

633 distance_rad = distance_rad - TWOPI 

634 

635 # if there are wrap limits 

636 if (min_abs_rad is not None) and (max_abs_rad is not None): 

637 # compute accumulated angle 

638 accum_abs_rad = current_abs_rad + distance_rad 

639 

640 # if limits reached chose the other direction 

641 if accum_abs_rad > max_abs_rad: 

642 distance_rad = distance_rad - TWOPI 

643 if accum_abs_rad < min_abs_rad: 

644 distance_rad = distance_rad + TWOPI 

645 

646 # compute final accumulated angle 

647 final_abs_rad = current_abs_rad + distance_rad 

648 

649 return (final_abs_rad, distance_rad) 

650 

651 def get_closest_state(self, targetposition, istracking=False): 

652 """Find the closest observatory state for the given target position. 

653 

654 Parameters 

655 ---------- 

656 targetposition : :class:`.ObservatoryPosition` 

657 A target position instance. 

658 istracking : bool, optional 

659 Flag for saying if the observatory is tracking. Default is False. 

660 

661 Returns 

662 ------- 

663 :class:`.ObservatoryState` 

664 The state that is closest to the current observatory state. 

665 

666 Binary schema 

667 ------------- 

668 The binary schema used to determine the state of a proposed target. A 

669 value of 1 indicates that is it failing. A value of 0 indicates that the 

670 state is passing. 

671 ___ ___ ___ ___ ___ ___ 

672 | | | | | | 

673 rot rot az az alt alt 

674 max min max min max min 

675 

676 For example, if a proposed target exceeds the rotators maximum value, 

677 and is below the minimum azimuth we would have a binary value of; 

678 

679 0 1 0 1 0 0 

680 

681 If the target passed, then no limitations would occur; 

682 

683 0 0 0 0 0 0 

684 """ 

685 

686 valid_state = True 

687 fail_record = self.current_state.fail_record 

688 self.current_state.fail_state = 0 

689 

690 if targetposition.alt_rad < self.params.telalt_minpos_rad: 

691 telalt_rad = self.params.telalt_minpos_rad 

692 domalt_rad = self.params.telalt_minpos_rad 

693 valid_state = False 

694 

695 if "telalt_minpos_rad" in fail_record: 

696 fail_record["telalt_minpos_rad"] += 1 

697 else: 

698 fail_record["telalt_minpos_rad"] = 1 

699 

700 self.current_state.fail_state = self.current_state.fail_state | \ 

701 self.current_state.fail_value_table["altEmin"] 

702 

703 elif targetposition.alt_rad > self.params.telalt_maxpos_rad: 

704 telalt_rad = self.params.telalt_maxpos_rad 

705 domalt_rad = self.params.telalt_maxpos_rad 

706 valid_state = False 

707 if "telalt_maxpos_rad" in fail_record: 

708 fail_record["telalt_maxpos_rad"] += 1 

709 else: 

710 fail_record["telalt_maxpos_rad"] = 1 

711 

712 self.current_state.fail_state = self.current_state.fail_state | \ 

713 self.current_state.fail_value_table["altEmax"] 

714 

715 else: 

716 telalt_rad = targetposition.alt_rad 

717 domalt_rad = targetposition.alt_rad 

718 

719 if istracking: 

720 (telaz_rad, delta) = self.get_closest_angle_distance(targetposition.az_rad, 

721 self.current_state.telaz_rad) 

722 if telaz_rad < self.params.telaz_minpos_rad: 

723 telaz_rad = self.params.telaz_minpos_rad 

724 valid_state = False 

725 if "telaz_minpos_rad" in fail_record: 

726 fail_record["telaz_minpos_rad"] += 1 

727 else: 

728 fail_record["telaz_minpos_rad"] = 1 

729 

730 self.current_state.fail_state = self.current_state.fail_state | \ 

731 self.current_state.fail_value_table["azEmin"] 

732 

733 elif telaz_rad > self.params.telaz_maxpos_rad: 

734 telaz_rad = self.params.telaz_maxpos_rad 

735 valid_state = False 

736 if "telaz_maxpos_rad" in fail_record: 

737 fail_record["telaz_maxpos_rad"] += 1 

738 else: 

739 fail_record["telaz_maxpos_rad"] = 1 

740 

741 self.current_state.fail_state = self.current_state.fail_state | \ 

742 self.current_state.fail_value_table["azEmax"] 

743 

744 else: 

745 (telaz_rad, delta) = self.get_closest_angle_distance(targetposition.az_rad, 

746 self.current_state.telaz_rad, 

747 self.params.telaz_minpos_rad, 

748 self.params.telaz_maxpos_rad) 

749 

750 (domaz_rad, delta) = self.get_closest_angle_distance(targetposition.az_rad, 

751 self.current_state.domaz_rad) 

752 

753 if istracking: 

754 (telrot_rad, delta) = self.get_closest_angle_distance(targetposition.rot_rad, 

755 self.current_state.telrot_rad) 

756 if telrot_rad < self.params.telrot_minpos_rad: 

757 telrot_rad = self.params.telrot_minpos_rad 

758 valid_state = False 

759 if "telrot_minpos_rad" in fail_record: 

760 fail_record["telrot_minpos_rad"] += 1 

761 else: 

762 fail_record["telrot_minpos_rad"] = 1 

763 

764 self.current_state.fail_state = self.current_state.fail_state | \ 

765 self.current_state.fail_value_table["rotEmin"] 

766 

767 elif telrot_rad > self.params.telrot_maxpos_rad: 

768 telrot_rad = self.params.telrot_maxpos_rad 

769 valid_state = False 

770 if "telrot_maxpos_rad" in fail_record: 

771 fail_record["telrot_maxpos_rad"] += 1 

772 else: 

773 fail_record["telrot_maxpos_rad"] = 1 

774 

775 self.current_state.fail_state = self.current_state.fail_state | \ 

776 self.current_state.fail_value_table["rotEmax"] 

777 else: 

778 # if the target rotator angle is unreachable 

779 # then sets an arbitrary value (opposite) 

780 norm_rot_rad = divmod(targetposition.rot_rad - self.params.telrot_minpos_rad, TWOPI)[1] \ 

781 + self.params.telrot_minpos_rad 

782 if norm_rot_rad > self.params.telrot_maxpos_rad: 

783 targetposition.rot_rad = norm_rot_rad - math.pi 

784 (telrot_rad, delta) = self.get_closest_angle_distance(targetposition.rot_rad, 

785 self.current_state.telrot_rad, 

786 self.params.telrot_minpos_rad, 

787 self.params.telrot_maxpos_rad) 

788 targetposition.ang_rad = divmod(targetposition.pa_rad - telrot_rad, TWOPI)[1] 

789 

790 targetstate = ObservatoryState() 

791 targetstate.set_position(targetposition) 

792 targetstate.telalt_rad = telalt_rad 

793 targetstate.telaz_rad = telaz_rad 

794 targetstate.telrot_rad = telrot_rad 

795 targetstate.domalt_rad = domalt_rad 

796 targetstate.domaz_rad = domaz_rad 

797 if istracking: 

798 targetstate.tracking = valid_state 

799 

800 self.current_state.fail_record = fail_record 

801 

802 return targetstate 

803 

804 def get_deep_drilling_time(self, target): 

805 """Get the observing time for a deep drilling target. 

806 

807 Parameters 

808 ---------- 

809 target : :class:`.Target` 

810 The instance containing the target information. 

811 

812 Returns 

813 ------- 

814 float 

815 The total observation time. 

816 """ 

817 ddtime = target.dd_exptime + \ 

818 target.dd_exposures * self.params.shuttertime + \ 

819 max(target.dd_exposures - 1, 0) * self.params.readouttime + \ 

820 target.dd_filterchanges * (self.params.filter_changetime - self.params.readouttime) 

821 

822 return ddtime 

823 

824 def get_delay_after(self, activity, targetstate, initstate): 

825 """Calculate slew delay for activity. 

826 

827 This function calculates the slew delay for a given activity based on 

828 the requested target state and the current observatory state. 

829 

830 Parameters 

831 ---------- 

832 activity : str 

833 The name of the activity for slew delay calculation. 

834 targetstate : :class:`.ObservatoryState` 

835 The instance containing the target state. 

836 initstate : :class:`.ObservatoryState` 

837 The instance containing the current state of the observatory. 

838 

839 Returns 

840 ------- 

841 float 

842 The slew delay for the activity. 

843 """ 

844 activity_delay = self.function_get_delay_for[activity](targetstate, initstate) 

845 

846 prereq_list = self.params.prerequisites[activity] 

847 

848 longest_previous_delay = 0.0 

849 longest_prereq = "" 

850 for prereq in prereq_list: 

851 previous_delay = self.get_delay_after(prereq, targetstate, initstate) 

852 if previous_delay > longest_previous_delay: 

853 longest_previous_delay = previous_delay 

854 longest_prereq = prereq 

855 self.longest_prereq_for[activity] = longest_prereq 

856 self.delay_for[activity] = activity_delay 

857 

858 return activity_delay + longest_previous_delay 

859 

860 def get_delay_for_domalt(self, targetstate, initstate): 

861 """Calculate slew delay for domalt activity. 

862 

863 This function calculates the slew delay for the domalt activity 

864 based on the requested target state and the current observatory 

865 state. 

866 

867 Parameters 

868 ---------- 

869 targetstate : :class:`.ObservatoryState` 

870 The instance containing the target state. 

871 initstate : :class:`.ObservatoryState` 

872 The instance containing the current state of the observatory. 

873 

874 Returns 

875 ------- 

876 float 

877 The slew delay for the domalt activity. 

878 """ 

879 distance = targetstate.domalt_rad - initstate.domalt_rad 

880 maxspeed = self.params.domalt_maxspeed_rad 

881 accel = self.params.domalt_accel_rad 

882 decel = self.params.domalt_decel_rad 

883 free_range = self.params.domalt_free_range 

884 

885 (delay, peakspeed) = self.compute_kinematic_delay(distance, maxspeed, accel, decel, free_range) 

886 targetstate.domalt_peakspeed_rad = peakspeed 

887 

888 return delay 

889 

890 def get_delay_for_domaz(self, targetstate, initstate): 

891 """Calculate slew delay for domaz activity. 

892 

893 This function calculates the slew delay for the domaz activity 

894 based on the requested target state and the current observatory 

895 state. 

896 

897 Parameters 

898 ---------- 

899 targetstate : :class:`.ObservatoryState` 

900 The instance containing the target state. 

901 initstate : :class:`.ObservatoryState` 

902 The instance containing the current state of the observatory. 

903 

904 Returns 

905 ------- 

906 float 

907 The slew delay for the domaz activity. 

908 """ 

909 distance = targetstate.domaz_rad - initstate.domaz_rad 

910 maxspeed = self.params.domaz_maxspeed_rad 

911 accel = self.params.domaz_accel_rad 

912 decel = self.params.domaz_decel_rad 

913 free_range = self.params.domaz_free_range 

914 

915 (delay, peakspeed) = self.compute_kinematic_delay(distance, maxspeed, accel, decel, free_range) 

916 targetstate.domaz_peakspeed_rad = peakspeed 

917 

918 return delay 

919 

920 def get_delay_for_domazsettle(self, targetstate, initstate): 

921 """Calculate slew delay for domazsettle activity. 

922 

923 This function calculates the slew delay for the domazsettle activity 

924 based on the requested target state and the current observatory 

925 state. 

926 

927 Parameters 

928 ---------- 

929 targetstate : :class:`.ObservatoryState` 

930 The instance containing the target state. 

931 initstate : :class:`.ObservatoryState` 

932 The instance containing the current state of the observatory. 

933 

934 Returns 

935 ------- 

936 float 

937 The slew delay for the domazsettle activity. 

938 """ 

939 distance = abs(targetstate.domaz_rad - initstate.domaz_rad) 

940 

941 if distance > 1e-6: 

942 delay = self.params.domaz_settletime 

943 else: 

944 delay = 0 

945 

946 return delay 

947 

948 def get_delay_for_exposures(self, targetstate, initstate): 

949 """Calculate slew delay for exposures activity. 

950 

951 This function calculates the slew delay for the exposures activity 

952 based on the requested target state and the current observatory 

953 state. 

954 

955 Parameters 

956 ---------- 

957 targetstate : :class:`.ObservatoryState` 

958 The instance containing the target state. 

959 initstate : :class:`.ObservatoryState` 

960 The instance containing the current state of the observatory. 

961 

962 Returns 

963 ------- 

964 float 

965 The slew delay for the exposures activity. 

966 """ 

967 return 0.0 

968 

969 def get_delay_for_filter(self, targetstate, initstate): 

970 """Calculate slew delay for filter activity. 

971 

972 This function calculates the slew delay for the filter activity 

973 based on the requested target state and the current observatory 

974 state. 

975 

976 Parameters 

977 ---------- 

978 targetstate : :class:`.ObservatoryState` 

979 The instance containing the target state. 

980 initstate : :class:`.ObservatoryState` 

981 The instance containing the current state of the observatory. 

982 

983 Returns 

984 ------- 

985 float 

986 The slew delay for the filter activity. 

987 """ 

988 if targetstate.filter != initstate.filter: 

989 # filter change needed 

990 delay = self.params.filter_changetime 

991 else: 

992 delay = 0.0 

993 

994 return delay 

995 

996 def get_delay_for_readout(self, targetstate, initstate): 

997 """Calculate slew delay for readout activity. 

998 

999 This function calculates the slew delay for the readout activity 

1000 based on the requested target state and the current observatory 

1001 state. 

1002 

1003 Parameters 

1004 ---------- 

1005 targetstate : :class:`.ObservatoryState` 

1006 The instance containing the target state. 

1007 initstate : :class:`.ObservatoryState` 

1008 The instance containing the current state of the observatory. 

1009 

1010 Returns 

1011 ------- 

1012 float 

1013 The slew delay for the readout activity. 

1014 """ 

1015 return self.params.readouttime 

1016 

1017 def get_delay_for_telalt(self, targetstate, initstate): 

1018 """Calculate slew delay for telalt activity. 

1019 

1020 This function calculates the slew delay for the telalt activity 

1021 based on the requested target state and the current observatory 

1022 state. 

1023 

1024 Parameters 

1025 ---------- 

1026 targetstate : :class:`.ObservatoryState` 

1027 The instance containing the target state. 

1028 initstate : :class:`.ObservatoryState` 

1029 The instance containing the current state of the observatory. 

1030 

1031 Returns 

1032 ------- 

1033 float 

1034 The slew delay for the telalt activity. 

1035 """ 

1036 distance = targetstate.telalt_rad - initstate.telalt_rad 

1037 maxspeed = self.params.telalt_maxspeed_rad 

1038 accel = self.params.telalt_accel_rad 

1039 decel = self.params.telalt_decel_rad 

1040 

1041 (delay, peakspeed) = self.compute_kinematic_delay(distance, maxspeed, accel, decel) 

1042 targetstate.telalt_peakspeed_rad = peakspeed 

1043 

1044 return delay 

1045 

1046 def get_delay_for_telaz(self, targetstate, initstate): 

1047 """Calculate slew delay for telaz activity. 

1048 

1049 This function calculates the slew delay for the telaz activity 

1050 based on the requested target state and the current observatory 

1051 state. 

1052 

1053 Parameters 

1054 ---------- 

1055 targetstate : :class:`.ObservatoryState` 

1056 The instance containing the target state. 

1057 initstate : :class:`.ObservatoryState` 

1058 The instance containing the current state of the observatory. 

1059 

1060 Returns 

1061 ------- 

1062 float 

1063 The slew delay for the telaz activity. 

1064 """ 

1065 distance = targetstate.telaz_rad - initstate.telaz_rad 

1066 maxspeed = self.params.telaz_maxspeed_rad 

1067 accel = self.params.telaz_accel_rad 

1068 decel = self.params.telaz_decel_rad 

1069 

1070 (delay, peakspeed) = self.compute_kinematic_delay(distance, maxspeed, accel, decel) 

1071 targetstate.telaz_peakspeed_rad = peakspeed 

1072 

1073 return delay 

1074 

1075 def get_delay_for_telopticsclosedloop(self, targetstate, initstate): 

1076 """Calculate slew delay for telopticsclosedloop activity. 

1077 

1078 This function calculates the slew delay for the telopticsclosedloop 

1079 activity based on the requested target state and the current 

1080 observatory state. 

1081 

1082 Parameters 

1083 ---------- 

1084 targetstate : :class:`.ObservatoryState` 

1085 The instance containing the target state. 

1086 initstate : :class:`.ObservatoryState` 

1087 The instance containing the current state of the observatory. 

1088 

1089 Returns 

1090 ------- 

1091 float 

1092 The slew delay for the telopticsclosedloop activity. 

1093 """ 

1094 distance = abs(targetstate.telalt_rad - initstate.telalt_rad) 

1095 

1096 delay = 0.0 

1097 for k, cl_delay in enumerate(self.params.optics_cl_delay): 

1098 if self.params.optics_cl_altlimit[k] <= distance < self.params.optics_cl_altlimit[k + 1]: 

1099 delay = cl_delay 

1100 break 

1101 

1102 return delay 

1103 

1104 def get_delay_for_telopticsopenloop(self, targetstate, initstate): 

1105 """Calculate slew delay for telopticsopenloop activity. 

1106 

1107 This function calculates the slew delay for the telopticsopenloop 

1108 activity based on the requested target state and the current 

1109 observatory state. 

1110 

1111 Parameters 

1112 ---------- 

1113 targetstate : :class:`.ObservatoryState` 

1114 The instance containing the target state. 

1115 initstate : :class:`.ObservatoryState` 

1116 The instance containing the current state of the observatory. 

1117 

1118 Returns 

1119 ------- 

1120 float 

1121 The slew delay for the telopticsopenloop activity. 

1122 """ 

1123 distance = abs(targetstate.telalt_rad - initstate.telalt_rad) 

1124 

1125 if distance > 1e-6: 

1126 delay = distance * self.params.optics_ol_slope 

1127 else: 

1128 delay = 0 

1129 

1130 return delay 

1131 

1132 def get_delay_for_telrot(self, targetstate, initstate): 

1133 """Calculate slew delay for telrot activity. 

1134 

1135 This function calculates the slew delay for the telrot activity 

1136 based on the requested target state and the current observatory 

1137 state. 

1138 

1139 Parameters 

1140 ---------- 

1141 targetstate : :class:`.ObservatoryState` 

1142 The instance containing the target state. 

1143 initstate : :class:`.ObservatoryState` 

1144 The instance containing the current state of the observatory. 

1145 

1146 Returns 

1147 ------- 

1148 float 

1149 The slew delay for the telrot activity. 

1150 """ 

1151 distance = targetstate.telrot_rad - initstate.telrot_rad 

1152 maxspeed = self.params.telrot_maxspeed_rad 

1153 accel = self.params.telrot_accel_rad 

1154 decel = self.params.telrot_decel_rad 

1155 

1156 (delay, peakspeed) = self.compute_kinematic_delay(distance, maxspeed, accel, decel) 

1157 targetstate.telrot_peakspeed_rad = peakspeed 

1158 

1159 return delay 

1160 

1161 def get_delay_for_telsettle(self, targetstate, initstate): 

1162 """Calculate slew delay for telsettle activity. 

1163 

1164 This function calculates the slew delay for the telsettle activity 

1165 based on the requested target state and the current observatory 

1166 state. 

1167 

1168 Parameters 

1169 ---------- 

1170 targetstate : :class:`.ObservatoryState` 

1171 The instance containing the target state. 

1172 initstate : :class:`.ObservatoryState` 

1173 The instance containing the current state of the observatory. 

1174 

1175 Returns 

1176 ------- 

1177 float 

1178 The slew delay for the telsettle activity. 

1179 """ 

1180 distance = abs(targetstate.telalt_rad - initstate.telalt_rad) + \ 

1181 abs(targetstate.telaz_rad - initstate.telaz_rad) 

1182 

1183 if distance > 1e-6: 

1184 delay = self.params.mount_settletime 

1185 else: 

1186 delay = 0 

1187 

1188 return delay 

1189 

1190 def get_delta_filter_avg(self): 

1191 """The time difference between current time and last filter change \ 

1192 for average changes. 

1193 

1194 Returns 

1195 ------- 

1196 float 

1197 """ 

1198 avg_num = self.params.filter_max_changes_avg_num 

1199 if len(self.filter_changes_list) >= avg_num: 

1200 deltatime = self.current_state.time - self.filter_changes_list[-avg_num] 

1201 else: 

1202 deltatime = 0.0 

1203 return deltatime 

1204 

1205 def get_delta_filter_burst(self): 

1206 """The time difference between current time and last filter change \ 

1207 for burst changes. 

1208 

1209 Returns 

1210 ------- 

1211 float 

1212 """ 

1213 burst_num = self.params.filter_max_changes_burst_num 

1214 if len(self.filter_changes_list) >= burst_num: 

1215 deltatime = self.current_state.time - self.filter_changes_list[-burst_num] 

1216 else: 

1217 deltatime = 0.0 

1218 return deltatime 

1219 

1220 def get_delta_last_filterchange(self): 

1221 """Get the time difference for filter changes. 

1222 

1223 This function calculates the time difference between the current time 

1224 and the time of the last filter change. 

1225 

1226 Returns 

1227 ------- 

1228 float 

1229 The time difference. 

1230 """ 

1231 if len(self.filter_changes_list) > 0: 

1232 delta = self.current_state.time - self.filter_changes_list[-1] 

1233 else: 

1234 delta = self.current_state.time 

1235 

1236 return delta 

1237 

1238 def get_number_filter_changes(self): 

1239 """The total number of stored filter changes. 

1240 

1241 Returns 

1242 ------- 

1243 int 

1244 """ 

1245 return len(self.filter_changes_list) 

1246 

1247 def get_slew_delay(self, target): 

1248 """Calculate the slew delay based on the given target. 

1249 

1250 Parameters 

1251 ---------- 

1252 target : :class:`.Target` 

1253 An instance of a target for slew calculation. 

1254 

1255 Returns 

1256 ------- 

1257 float 

1258 The total slew delay for the target. 

1259 """ 

1260 

1261 if target.filter != self.current_state.filter: 

1262 # check if filter is possible 

1263 if not self.is_filter_change_allowed_for(target.filter): 

1264 return -1.0, self.current_state.fail_value_table["filter"] 

1265 

1266 targetposition = self.radecang2position(self.dateprofile, 

1267 target.ra_rad, 

1268 target.dec_rad, 

1269 target.ang_rad, 

1270 target.filter) 

1271 if not self.params.rotator_followsky: 

1272 #override rotator position with current telrot 

1273 targetposition.rot_rad = self.current_state.telrot_rad 

1274 

1275 # check if altitude is possible 

1276 if targetposition.alt_rad < self.params.telalt_minpos_rad: 

1277 return -1.0, self.current_state.fail_value_table["altEmin"] 

1278 if targetposition.alt_rad > self.params.telalt_maxpos_rad: 

1279 return -1.0, self.current_state.fail_value_table["altEmax"] 

1280 

1281 targetstate = self.get_closest_state(targetposition) 

1282 target.ang_rad = targetstate.ang_rad 

1283 target.alt_rad = targetstate.alt_rad 

1284 target.az_rad = targetstate.az_rad 

1285 target.rot_rad = targetstate.rot_rad 

1286 target.telalt_rad = targetstate.telalt_rad 

1287 target.telaz_rad = targetstate.telaz_rad 

1288 target.telrot_rad = targetstate.telrot_rad 

1289 

1290 return self.get_slew_delay_for_state(targetstate, self.current_state, False), 0 

1291 

1292 def get_slew_delay_for_state(self, targetstate, initstate, include_slew_data=False): 

1293 """Calculate slew delay for target state from current state. 

1294 

1295 Parameters 

1296 ---------- 

1297 targetstate : :class:`.ObservatoryState` 

1298 The instance containing the target state. 

1299 initstate : :class:`.ObservatoryState` 

1300 The instance containing the current state of the observatory. 

1301 include_slew_data : bool, optional 

1302 Flag to update lastslew_delays_dict member. 

1303 

1304 Returns 

1305 ------- 

1306 float 

1307 The slew delay for the target state. 

1308 """ 

1309 last_activity = "exposures" 

1310 slew_delay = self.get_delay_after(last_activity, targetstate, initstate) 

1311 

1312 self.lastslew_delays_dict = {} 

1313 self.lastslew_criticalpath = [] 

1314 if include_slew_data: 

1315 for act in self.activities: 

1316 dt = self.delay_for[act] 

1317 if dt > 0.0: 

1318 self.lastslew_delays_dict[act] = dt 

1319 

1320 activity = last_activity 

1321 while activity != "": 

1322 dt = self.delay_for[activity] 

1323 if dt > 0: 

1324 self.lastslew_criticalpath.append(activity) 

1325 activity = self.longest_prereq_for[activity] 

1326 

1327 return slew_delay 

1328 

1329 def is_filter_change_allowed(self): 

1330 """Determine is filter change is allowed due to constraints. 

1331 

1332 Returns 

1333 ------- 

1334 bool 

1335 True is filter change is allowed, else False. 

1336 """ 

1337 burst_num = self.params.filter_max_changes_burst_num 

1338 if len(self.filter_changes_list) >= burst_num: 

1339 deltatime = self.current_state.time - self.filter_changes_list[-burst_num] 

1340 if deltatime >= self.params.filter_max_changes_burst_time: 

1341 # burst time allowed 

1342 avg_num = self.params.filter_max_changes_avg_num 

1343 if len(self.filter_changes_list) >= avg_num: 

1344 deltatime = self.current_state.time - self.filter_changes_list[-avg_num] 

1345 if deltatime >= self.params.filter_max_changes_avg_time: 

1346 # avg time allowed 

1347 allowed = True 

1348 else: 

1349 allowed = False 

1350 else: 

1351 allowed = True 

1352 else: 

1353 allowed = False 

1354 else: 

1355 allowed = True 

1356 

1357 return allowed 

1358 

1359 def is_filter_change_allowed_for(self, targetfilter): 

1360 """Determine if filter change is allowed given band filter. 

1361 

1362 Parameters 

1363 ---------- 

1364 targetfilter : str 

1365 The band filter for the target. 

1366 

1367 Returns 

1368 ------- 

1369 bool 

1370 True is filter change is allowed, else False. 

1371 """ 

1372 if targetfilter in self.current_state.mountedfilters: 

1373 # new filter is mounted 

1374 allowed = self.is_filter_change_allowed() 

1375 else: 

1376 allowed = False 

1377 return allowed 

1378 

1379 def observe(self, target): 

1380 """Run the observatory through the observing cadence for the target. 

1381 

1382 Parameters 

1383 ---------- 

1384 target : :class:`.Target` 

1385 The instance containing the target information for the 

1386 observation. 

1387 """ 

1388 self.slew(target) 

1389 visit_time = sum(target.exp_times) + \ 

1390 target.num_exp * self.params.shuttertime + \ 

1391 max(target.num_exp - 1, 0) * self.params.readouttime 

1392 self.update_state(self.current_state.time + visit_time) 

1393 

1394 def park(self): 

1395 """Put the observatory into the park state. 

1396 """ 

1397 self.park_state.filter = self.current_state.filter 

1398 slew_delay = self.get_slew_delay_for_state(self.park_state, self.current_state, True) 

1399 self.park_state.time = self.current_state.time + slew_delay 

1400 self.current_state.set(self.park_state) 

1401 self.update_state(self.park_state.time) 

1402 self.park_state.time = 0.0 

1403 

1404 def radec2altazpa(self, dateprofile, ra_rad, dec_rad): 

1405 """Converts ra, de coordinates into alt, az for given time. 

1406 

1407 Parameters 

1408 ---------- 

1409 dateprofile : lsst.ts.dateloc.DateProfile 

1410 Instance containing the time information. 

1411 ra_rad : float 

1412 The right ascension in radians 

1413 dec_rad : float 

1414 The declination in radians 

1415 

1416 Returns 

1417 ------- 

1418 tuple(float, float, float) 

1419 (altitude in radians, azimuth in radians, parallactic angle in 

1420 radians) 

1421 """ 

1422 lst_rad = dateprofile.lst_rad 

1423 ha_rad = lst_rad - ra_rad 

1424 

1425 (az_rad, alt_rad) = pal.de2h(ha_rad, dec_rad, self.location.latitude_rad) 

1426 pa_rad = divmod(pal.pa(ha_rad, dec_rad, self.location.latitude_rad), TWOPI)[1] 

1427 

1428 return (alt_rad, az_rad, pa_rad) 

1429 

1430 def radecang2position(self, dateprofile, ra_rad, dec_rad, ang_rad, band_filter): 

1431 """Convert current time, sky location and filter into observatory\ 

1432 position. 

1433 

1434 Parameters 

1435 ---------- 

1436 dateprofile : lsst.ts.dateloc.DateProfile 

1437 The instance holding the current time information. 

1438 ra_rad : float 

1439 The current right ascension (radians). 

1440 dec_rad : float 

1441 The current declination (radians). 

1442 ang_rad : float 

1443 The current sky angle (radians). 

1444 band_filter : str 

1445 The current band filter. 

1446 

1447 Returns 

1448 ------- 

1449 :class:`.ObservatoryPosition` 

1450 The observatory position information from inputs. 

1451 """ 

1452 (alt_rad, az_rad, pa_rad) = self.radec2altazpa(dateprofile, ra_rad, dec_rad) 

1453 

1454 position = ObservatoryPosition() 

1455 position.time = dateprofile.timestamp 

1456 position.tracking = True 

1457 position.ra_rad = ra_rad 

1458 position.dec_rad = dec_rad 

1459 position.ang_rad = ang_rad 

1460 position.filter = band_filter 

1461 position.alt_rad = alt_rad 

1462 position.az_rad = az_rad 

1463 position.pa_rad = pa_rad 

1464 position.rot_rad = divmod(pa_rad - ang_rad, TWOPI)[1] 

1465 

1466 return position 

1467 

1468 def reset(self): 

1469 """Reset the observatory to the parking state. 

1470 """ 

1471 self.set_state(self.park_state) 

1472 

1473 def set_state(self, new_state): 

1474 """Set observatory state from another state. 

1475 

1476 Parameters 

1477 ---------- 

1478 new_state : :class:`.ObservatoryState` 

1479 The instance containing the state to update the observatory to. 

1480 """ 

1481 if new_state.filter != self.current_state.filter: 

1482 self.filter_changes_list.append(new_state.time) 

1483 

1484 self.current_state.set(new_state) 

1485 self.dateprofile.update(new_state.time) 

1486 

1487 def slew(self, target): 

1488 """Slew the observatory to the given target location. 

1489 

1490 Parameters 

1491 ---------- 

1492 target : :class:`.Target` 

1493 The instance containing the target information for the slew. 

1494 """ 

1495 self.slew_radec(self.current_state.time, 

1496 target.ra_rad, target.dec_rad, target.ang_rad, target.filter) 

1497 

1498 def slew_altaz(self, time, alt_rad, az_rad, rot_rad, band_filter): 

1499 """Slew observatory to the given alt, az location. 

1500 

1501 Parameters 

1502 ---------- 

1503 time : float 

1504 The UTC timestamp of the request. 

1505 alt_rad : float 

1506 The altitude (radians) to slew to. 

1507 az_rad : float 

1508 The azimuth (radians) to slew to. 

1509 rot_rad : float 

1510 The telescope rotator angle (radians) for the slew. 

1511 band_filter : str 

1512 The band filter for the slew. 

1513 """ 

1514 self.update_state(time) 

1515 time = self.current_state.time 

1516 

1517 targetposition = ObservatoryPosition() 

1518 targetposition.time = time 

1519 targetposition.tracking = False 

1520 targetposition.alt_rad = alt_rad 

1521 targetposition.az_rad = az_rad 

1522 targetposition.rot_rad = rot_rad 

1523 targetposition.filter = band_filter 

1524 

1525 self.slew_to_position(targetposition) 

1526 

1527 def slew_radec(self, time, ra_rad, dec_rad, ang_rad, filter): 

1528 """Slew observatory to the given ra, dec location. 

1529 

1530 Parameters 

1531 ---------- 

1532 time : float 

1533 The UTC timestamp of the request. 

1534 ra_rad : float 

1535 The right ascension (radians) to slew to. 

1536 dec_rad : float 

1537 The declination (radians) to slew to. 

1538 ang_rad : float 

1539 The sky angle (radians) for the slew. 

1540 band_filter : str 

1541 The band filter for the slew. 

1542 """ 

1543 self.update_state(time) 

1544 time = self.current_state.time 

1545 

1546 targetposition = self.radecang2position(self.dateprofile, ra_rad, dec_rad, ang_rad, filter) 

1547 if not self.params.rotator_followsky: 

1548 targetposition.rot_rad = self.current_state.telrot_rad 

1549 

1550 self.slew_to_position(targetposition) 

1551 

1552 def slew_to_position(self, targetposition): 

1553 """Slew the observatory to a given position. 

1554 

1555 Parameters 

1556 ---------- 

1557 targetposition : :class:`.ObservatoryPosition` 

1558 The instance containing the slew position information. 

1559 """ 

1560 targetstate = self.get_closest_state(targetposition) 

1561 targetstate.mountedfilters = self.current_state.mountedfilters 

1562 targetstate.unmountedfilters = self.current_state.unmountedfilters 

1563 slew_delay = self.get_slew_delay_for_state(targetstate, self.current_state, True) 

1564 if targetposition.filter != self.current_state.filter: 

1565 self.filter_changes_list.append(targetstate.time) 

1566 targetstate.time = targetstate.time + slew_delay 

1567 self.current_state.set(targetstate) 

1568 self.update_state(targetstate.time) 

1569 

1570 def start_tracking(self, time): 

1571 """Put observatory into tracking mode. 

1572 

1573 Parameters 

1574 ---------- 

1575 time : float 

1576 The UTC timestamp. 

1577 """ 

1578 if time < self.current_state.time: 

1579 time = self.current_state.time 

1580 if not self.current_state.tracking: 

1581 self.update_state(time) 

1582 self.current_state.tracking = True 

1583 

1584 def stop_tracking(self, time): 

1585 """Remove observatory from tracking mode. 

1586 

1587 Parameters 

1588 ---------- 

1589 time : float 

1590 The UTC timestamp. 

1591 """ 

1592 if time < self.current_state.time: 

1593 time = self.current_state.time 

1594 if self.current_state.tracking: 

1595 self.update_state(time) 

1596 self.current_state.tracking = False 

1597 

1598 def swap_filter(self, filter_to_unmount): 

1599 """Perform a filter swap with the given filter. 

1600 

1601 Parameters 

1602 ---------- 

1603 filter_to_unmount : str 

1604 The band filter name to unmount. 

1605 """ 

1606 if filter_to_unmount in self.current_state.mountedfilters: 

1607 self.current_state.mountedfilters.remove(filter_to_unmount) 

1608 filter_to_mount = self.current_state.unmountedfilters.pop() 

1609 self.current_state.mountedfilters.append(filter_to_mount) 

1610 self.current_state.unmountedfilters.append(filter_to_unmount) 

1611 

1612 self.park_state.mountedfilters = self.current_state.mountedfilters 

1613 self.park_state.unmountedfilters = self.current_state.unmountedfilters 

1614 else: 

1615 self.log.info("swap_filter: REJECTED filter %s is not mounted" % 

1616 (filter_to_unmount)) 

1617 

1618 def update_state(self, time): 

1619 """Update the observatory state for the given time. 

1620 

1621 Parameters 

1622 ---------- 

1623 time : float 

1624 A UTC timestamp for updating the observatory state. 

1625 """ 

1626 if time < self.current_state.time: 

1627 time = self.current_state.time 

1628 self.dateprofile.update(time) 

1629 

1630 if self.current_state.tracking: 

1631 

1632 targetposition = self.radecang2position(self.dateprofile, 

1633 self.current_state.ra_rad, 

1634 self.current_state.dec_rad, 

1635 self.current_state.ang_rad, 

1636 self.current_state.filter) 

1637 

1638 targetstate = self.get_closest_state(targetposition, istracking=True) 

1639 

1640 self.current_state.time = targetstate.time 

1641 self.current_state.alt_rad = targetstate.alt_rad 

1642 self.current_state.az_rad = targetstate.az_rad 

1643 self.current_state.pa_rad = targetstate.pa_rad 

1644 self.current_state.rot_rad = targetstate.rot_rad 

1645 self.current_state.tracking = targetstate.tracking 

1646 

1647 self.current_state.telalt_rad = targetstate.telalt_rad 

1648 self.current_state.telaz_rad = targetstate.telaz_rad 

1649 self.current_state.telrot_rad = targetstate.telrot_rad 

1650 self.current_state.domalt_rad = targetstate.domalt_rad 

1651 self.current_state.domaz_rad = targetstate.domaz_rad 

1652 else: 

1653 (ra_rad, dec_rad, pa_rad) = self.altaz2radecpa(self.dateprofile, 

1654 self.current_state.alt_rad, 

1655 self.current_state.az_rad) 

1656 self.current_state.time = time 

1657 self.current_state.ra_rad = ra_rad 

1658 self.current_state.dec_rad = dec_rad 

1659 self.current_state.ang_rad = divmod(pa_rad - self.current_state.rot_rad, TWOPI)[1] 

1660 self.current_state.pa_rad = pa_rad