Coverage for python/lsst/ts/observatory/model/model.py : 9%

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
6import palpy as pal
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
14__all__ = ["ObservatoryModel"]
16TWOPI = 2 * np.pi
18class ObservatoryModel(object):
19 """Class for modeling the observatory.
20 """
22 def __init__(self, location=None, log_level=logging.DEBUG):
23 """Initialize the class.
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
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()
45 self.dateprofile = DateProfile(0.0, self.location)
47 self.filters = ["u", "g", "r", "i", "z", "y"]
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 = []
73 def __str__(self):
74 """str: The string representation of the model."""
75 return str(self.current_state)
77 @classmethod
78 def get_configure_dict(cls):
79 """Get the configuration dictionary for the observatory model.
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
91 def altaz2radecpa(self, dateprofile, alt_rad, az_rad):
92 """Converts alt, az coordinates into ra, dec for the given time.
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
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
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]
114 return (ra_rad, dec_rad, pa_rad)
116 @staticmethod
117 def compute_kinematic_delay(distance, maxspeed, accel, decel, free_range=0.):
118 """Calculate the kinematic delay.
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:
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.
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.
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
154 if free_range > d:
155 return 0., vpeak_free_range
157 vpeak = (2 * d / (1 / accel + 1 / decel)) ** 0.5
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)
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.
172 d2 = d - d1 - d3 - free_range
174 t1 = (maxspeed - vpeak_free_range) / accel
175 t3 = (maxspeed - vpeak_free_range) / decel
176 t2 = d2 / maxspeed
178 delay = t1 + t2 + t3
179 vpeak = maxspeed
181 if distance < 0.:
182 vpeak *= -1.
184 return delay, vpeak
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
194 This method accepts arrays of distance, and assumes acceleration == deceleration.
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)
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
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!
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.
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).
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))
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)
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)
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)
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)
297 totDomTime = np.minimum(totDomTime1, totDomTime2)
298 totDomTime[sameDome] = 0
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
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
331 def configure(self, confdict):
332 """Configure the observatory model.
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)
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
353 self.reset()
355 def configure_camera(self, confdict):
356 """Configure the camera parameters.
358 Parameters
359 ----------
360 confdict : dict
361 The set of configuration parameters for the camera.
362 """
363 self.params.configure_camera(confdict)
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))
396 def configure_dome(self, confdict):
397 """Configure the dome parameters.
399 Parameters
400 ----------
401 confdict : dict
402 The set of configuration parameters for the dome.
403 """
404 self.params.configure_dome(confdict)
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))
434 def configure_from_module(self, conf_file=None):
435 """Configure the observatory model from the module stored \
436 configuration.
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)
449 def configure_optics(self, confdict):
450 """Configure the optics parameters.
452 Parameters
453 ----------
454 confdict : dict
455 The set of configuration parameters for the optics.
456 """
457 self.params.configure_optics(confdict)
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]))
469 def configure_park(self, confdict):
470 """Configure the telescope park state parameters.
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
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))
503 def configure_rotator(self, confdict):
504 """Configure the telescope rotator parameters.
506 Parameters
507 ----------
508 confdict : dict
509 The set of configuration parameters for the telescope rotator.
510 """
511 self.params.configure_rotator(confdict)
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))
538 def configure_slew(self, confdict):
539 """Configure the slew parameters.
541 Parameters
542 ----------
543 confdict : dict
544 The set of configuration parameters for the slew.
545 """
546 self.params.configure_slew(confdict, self.activities)
548 for activity in self.activities:
549 self.log.log(self.log_level, "configure_slew: prerequisites[%s]=%s" %
550 (activity, self.params.prerequisites[activity]))
552 def configure_telescope(self, confdict):
553 """Configure the telescope parameters.
555 Parameters
556 ----------
557 confdict : dict
558 The set of configuration parameters for the telescope.
559 """
560 self.params.configure_telescope(confdict)
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))
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.
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).
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
628 # computes the distance clockwise
629 distance_rad = divmod(norm_target_rad - current_abs_rad, TWOPI)[1]
631 # take the counter-clockwise distance if shorter
632 if distance_rad > math.pi:
633 distance_rad = distance_rad - TWOPI
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
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
646 # compute final accumulated angle
647 final_abs_rad = current_abs_rad + distance_rad
649 return (final_abs_rad, distance_rad)
651 def get_closest_state(self, targetposition, istracking=False):
652 """Find the closest observatory state for the given target position.
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.
661 Returns
662 -------
663 :class:`.ObservatoryState`
664 The state that is closest to the current observatory state.
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
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;
679 0 1 0 1 0 0
681 If the target passed, then no limitations would occur;
683 0 0 0 0 0 0
684 """
686 valid_state = True
687 fail_record = self.current_state.fail_record
688 self.current_state.fail_state = 0
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
695 if "telalt_minpos_rad" in fail_record:
696 fail_record["telalt_minpos_rad"] += 1
697 else:
698 fail_record["telalt_minpos_rad"] = 1
700 self.current_state.fail_state = self.current_state.fail_state | \
701 self.current_state.fail_value_table["altEmin"]
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
712 self.current_state.fail_state = self.current_state.fail_state | \
713 self.current_state.fail_value_table["altEmax"]
715 else:
716 telalt_rad = targetposition.alt_rad
717 domalt_rad = targetposition.alt_rad
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
730 self.current_state.fail_state = self.current_state.fail_state | \
731 self.current_state.fail_value_table["azEmin"]
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
741 self.current_state.fail_state = self.current_state.fail_state | \
742 self.current_state.fail_value_table["azEmax"]
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)
750 (domaz_rad, delta) = self.get_closest_angle_distance(targetposition.az_rad,
751 self.current_state.domaz_rad)
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
764 self.current_state.fail_state = self.current_state.fail_state | \
765 self.current_state.fail_value_table["rotEmin"]
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
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]
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
800 self.current_state.fail_record = fail_record
802 return targetstate
804 def get_deep_drilling_time(self, target):
805 """Get the observing time for a deep drilling target.
807 Parameters
808 ----------
809 target : :class:`.Target`
810 The instance containing the target information.
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)
822 return ddtime
824 def get_delay_after(self, activity, targetstate, initstate):
825 """Calculate slew delay for activity.
827 This function calculates the slew delay for a given activity based on
828 the requested target state and the current observatory state.
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.
839 Returns
840 -------
841 float
842 The slew delay for the activity.
843 """
844 activity_delay = self.function_get_delay_for[activity](targetstate, initstate)
846 prereq_list = self.params.prerequisites[activity]
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
858 return activity_delay + longest_previous_delay
860 def get_delay_for_domalt(self, targetstate, initstate):
861 """Calculate slew delay for domalt activity.
863 This function calculates the slew delay for the domalt activity
864 based on the requested target state and the current observatory
865 state.
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.
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
885 (delay, peakspeed) = self.compute_kinematic_delay(distance, maxspeed, accel, decel, free_range)
886 targetstate.domalt_peakspeed_rad = peakspeed
888 return delay
890 def get_delay_for_domaz(self, targetstate, initstate):
891 """Calculate slew delay for domaz activity.
893 This function calculates the slew delay for the domaz activity
894 based on the requested target state and the current observatory
895 state.
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.
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
915 (delay, peakspeed) = self.compute_kinematic_delay(distance, maxspeed, accel, decel, free_range)
916 targetstate.domaz_peakspeed_rad = peakspeed
918 return delay
920 def get_delay_for_domazsettle(self, targetstate, initstate):
921 """Calculate slew delay for domazsettle activity.
923 This function calculates the slew delay for the domazsettle activity
924 based on the requested target state and the current observatory
925 state.
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.
934 Returns
935 -------
936 float
937 The slew delay for the domazsettle activity.
938 """
939 distance = abs(targetstate.domaz_rad - initstate.domaz_rad)
941 if distance > 1e-6:
942 delay = self.params.domaz_settletime
943 else:
944 delay = 0
946 return delay
948 def get_delay_for_exposures(self, targetstate, initstate):
949 """Calculate slew delay for exposures activity.
951 This function calculates the slew delay for the exposures activity
952 based on the requested target state and the current observatory
953 state.
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.
962 Returns
963 -------
964 float
965 The slew delay for the exposures activity.
966 """
967 return 0.0
969 def get_delay_for_filter(self, targetstate, initstate):
970 """Calculate slew delay for filter activity.
972 This function calculates the slew delay for the filter activity
973 based on the requested target state and the current observatory
974 state.
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.
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
994 return delay
996 def get_delay_for_readout(self, targetstate, initstate):
997 """Calculate slew delay for readout activity.
999 This function calculates the slew delay for the readout activity
1000 based on the requested target state and the current observatory
1001 state.
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.
1010 Returns
1011 -------
1012 float
1013 The slew delay for the readout activity.
1014 """
1015 return self.params.readouttime
1017 def get_delay_for_telalt(self, targetstate, initstate):
1018 """Calculate slew delay for telalt activity.
1020 This function calculates the slew delay for the telalt activity
1021 based on the requested target state and the current observatory
1022 state.
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.
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
1041 (delay, peakspeed) = self.compute_kinematic_delay(distance, maxspeed, accel, decel)
1042 targetstate.telalt_peakspeed_rad = peakspeed
1044 return delay
1046 def get_delay_for_telaz(self, targetstate, initstate):
1047 """Calculate slew delay for telaz activity.
1049 This function calculates the slew delay for the telaz activity
1050 based on the requested target state and the current observatory
1051 state.
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.
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
1070 (delay, peakspeed) = self.compute_kinematic_delay(distance, maxspeed, accel, decel)
1071 targetstate.telaz_peakspeed_rad = peakspeed
1073 return delay
1075 def get_delay_for_telopticsclosedloop(self, targetstate, initstate):
1076 """Calculate slew delay for telopticsclosedloop activity.
1078 This function calculates the slew delay for the telopticsclosedloop
1079 activity based on the requested target state and the current
1080 observatory state.
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.
1089 Returns
1090 -------
1091 float
1092 The slew delay for the telopticsclosedloop activity.
1093 """
1094 distance = abs(targetstate.telalt_rad - initstate.telalt_rad)
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
1102 return delay
1104 def get_delay_for_telopticsopenloop(self, targetstate, initstate):
1105 """Calculate slew delay for telopticsopenloop activity.
1107 This function calculates the slew delay for the telopticsopenloop
1108 activity based on the requested target state and the current
1109 observatory state.
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.
1118 Returns
1119 -------
1120 float
1121 The slew delay for the telopticsopenloop activity.
1122 """
1123 distance = abs(targetstate.telalt_rad - initstate.telalt_rad)
1125 if distance > 1e-6:
1126 delay = distance * self.params.optics_ol_slope
1127 else:
1128 delay = 0
1130 return delay
1132 def get_delay_for_telrot(self, targetstate, initstate):
1133 """Calculate slew delay for telrot activity.
1135 This function calculates the slew delay for the telrot activity
1136 based on the requested target state and the current observatory
1137 state.
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.
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
1156 (delay, peakspeed) = self.compute_kinematic_delay(distance, maxspeed, accel, decel)
1157 targetstate.telrot_peakspeed_rad = peakspeed
1159 return delay
1161 def get_delay_for_telsettle(self, targetstate, initstate):
1162 """Calculate slew delay for telsettle activity.
1164 This function calculates the slew delay for the telsettle activity
1165 based on the requested target state and the current observatory
1166 state.
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.
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)
1183 if distance > 1e-6:
1184 delay = self.params.mount_settletime
1185 else:
1186 delay = 0
1188 return delay
1190 def get_delta_filter_avg(self):
1191 """The time difference between current time and last filter change \
1192 for average changes.
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
1205 def get_delta_filter_burst(self):
1206 """The time difference between current time and last filter change \
1207 for burst changes.
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
1220 def get_delta_last_filterchange(self):
1221 """Get the time difference for filter changes.
1223 This function calculates the time difference between the current time
1224 and the time of the last filter change.
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
1236 return delta
1238 def get_number_filter_changes(self):
1239 """The total number of stored filter changes.
1241 Returns
1242 -------
1243 int
1244 """
1245 return len(self.filter_changes_list)
1247 def get_slew_delay(self, target):
1248 """Calculate the slew delay based on the given target.
1250 Parameters
1251 ----------
1252 target : :class:`.Target`
1253 An instance of a target for slew calculation.
1255 Returns
1256 -------
1257 float
1258 The total slew delay for the target.
1259 """
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"]
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
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"]
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
1290 return self.get_slew_delay_for_state(targetstate, self.current_state, False), 0
1292 def get_slew_delay_for_state(self, targetstate, initstate, include_slew_data=False):
1293 """Calculate slew delay for target state from current state.
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.
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)
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
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]
1327 return slew_delay
1329 def is_filter_change_allowed(self):
1330 """Determine is filter change is allowed due to constraints.
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
1357 return allowed
1359 def is_filter_change_allowed_for(self, targetfilter):
1360 """Determine if filter change is allowed given band filter.
1362 Parameters
1363 ----------
1364 targetfilter : str
1365 The band filter for the target.
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
1379 def observe(self, target):
1380 """Run the observatory through the observing cadence for the target.
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)
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
1404 def radec2altazpa(self, dateprofile, ra_rad, dec_rad):
1405 """Converts ra, de coordinates into alt, az for given time.
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
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
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]
1428 return (alt_rad, az_rad, pa_rad)
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.
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.
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)
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]
1466 return position
1468 def reset(self):
1469 """Reset the observatory to the parking state.
1470 """
1471 self.set_state(self.park_state)
1473 def set_state(self, new_state):
1474 """Set observatory state from another state.
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)
1484 self.current_state.set(new_state)
1485 self.dateprofile.update(new_state.time)
1487 def slew(self, target):
1488 """Slew the observatory to the given target location.
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)
1498 def slew_altaz(self, time, alt_rad, az_rad, rot_rad, band_filter):
1499 """Slew observatory to the given alt, az location.
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
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
1525 self.slew_to_position(targetposition)
1527 def slew_radec(self, time, ra_rad, dec_rad, ang_rad, filter):
1528 """Slew observatory to the given ra, dec location.
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
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
1550 self.slew_to_position(targetposition)
1552 def slew_to_position(self, targetposition):
1553 """Slew the observatory to a given position.
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)
1570 def start_tracking(self, time):
1571 """Put observatory into tracking mode.
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
1584 def stop_tracking(self, time):
1585 """Remove observatory from tracking mode.
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
1598 def swap_filter(self, filter_to_unmount):
1599 """Perform a filter swap with the given filter.
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)
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))
1618 def update_state(self, time):
1619 """Update the observatory state for the given time.
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)
1630 if self.current_state.tracking:
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)
1638 targetstate = self.get_closest_state(targetposition, istracking=True)
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
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