Source code for rubin_scheduler.scheduler.model_observatory.kinem_model

import warnings

import numpy as np

from rubin_scheduler.scheduler.utils import smallest_signed_angle
from rubin_scheduler.utils import (
    Site,
    _approx_alt_az2_ra_dec,
    _approx_altaz2pa,
    _approx_ra_dec2_alt_az,
    rotation_converter,
)

from .jerk import jerk_time

__all__ = (
    "tma_movement",
    "rotator_movement",
    "KinemModel",
)
two_pi = 2.0 * np.pi


[docs] def tma_movement(percent=70): """Get a dictionary of parameters to pass to `setup_telescope` defining altitude and azimuth speed, acceleration, and jerk in terms of 'percent' of total performance. Parameters ---------- percent : `float`, optional Default performance for the scheduler simulations for operations has been 70% (70, default). Expected performance at the start of comcam on-sky science operations is about 10%. Returns ------- tma : `dict` {`str`: `float`} A dictionary which can be passed as kwargs to KinematicModel.setup_telescope(**tma). """ # See https://confluence.lsstcorp.org/display/LSSTCOM/TMA+Motion+Settings # Expected performance at end of comcam on-sky is probably 10% if percent > 125: percent = 125 print("Cannot exceed 125 percent, by requirements.") tma = {} scale = percent / 100.0 tma["azimuth_maxspeed"] = np.min([10.0 * scale, 7.0]) tma["azimuth_accel"] = 10.0 * scale tma["azimuth_jerk"] = np.max([1.0, 40.0 * scale]) tma["altitude_maxspeed"] = 5.0 * scale tma["altitude_accel"] = 5.0 * scale tma["altitude_jerk"] = np.max([1.0, 20.0 * scale]) tma["settle_time"] = 3.0 return tma
[docs] def rotator_movement(percent=100): """Get a dictionary of parameters to pass to `setup_camera` defining rotator max speed, acceleration and jerk, in terms of 'percent' of total performance. Parameters ---------- percent : `float`, optional Default performance for the scheduler simulations for operations has been 100% (100, default). Expected performance at the start of comcam on-sky science operations is approximately full performance. Returns ------- rot : `dict` {`str`: `float`} A dictionary which can be passed as kwargs to KinematicModel.setup_camera(**rot). """ # Kevin and Brian say these can run 100% # and are independent of TMA movement if percent > 125: percent = 125 print("Cannot exceed 125 percent, by requirements.") rot = {} rot["maxspeed"] = 3.5 * percent / 100 rot["accel"] = 1.0 * percent / 100 rot["jerk"] = 4.0 * percent / 100 return rot
class Radec2altazpa: """Class to make it easy to swap in different alt/az conversion if wanted.""" def __init__(self, location): self.location = location def __call__(self, ra, dec, mjd): alt, az, pa = _approx_ra_dec2_alt_az( ra, dec, self.location.lat_rad, self.location.lon_rad, mjd, return_pa=True ) return alt, az, pa
[docs] class KinemModel: """A Kinematic model of the telescope. Parameters ---------- location : `astropy.coordinates.EarthLocation` The location of the telescope. If None, defaults to rubin_scheduler.utils.Site info park_alt : `float` (86.5) The altitude the telescope gets parked at (degrees) park_az : `float` (0) The azimuth for telescope park position (degrees) start_filter : `str` ('r') The filter that gets loaded when the telescope is parked mjd0 : `float` (0) The MJD to assume we are starting from telescope : `str' The telescope name to use for sky rotation conversion. Default "rubin" Note ---- Note there are additional parameters in the methods setup_camera, setup_dome, setup_telescope, and setup_optics. """ def __init__( self, location=None, park_alt=86.5, park_az=0.0, start_filter="r", mjd0=0, telescope="rubin" ): self.park_alt_rad = np.radians(park_alt) self.park_az_rad = np.radians(park_az) self.start_filter = start_filter self.current_filter = self.start_filter if location is None: self.location = Site("LSST") self.location.lat_rad = np.radians(self.location.latitude) self.location.lon_rad = np.radians(self.location.longitude) # Our RA,Dec to Alt,Az converter self.radec2altaz = Radec2altazpa(self.location) self.setup_camera() self.setup_dome() self.setup_telescope() self.setup_optics() # Park the telescope self.park() self.last_mjd = mjd0 # Rotation conversion self.rc = rotation_converter(telescope=telescope)
[docs] def mount_filters(self, filter_list): """Change which filters are mounted Parameters ---------- filter_list : `list` [`str`] List of the mounted filters. """ self.mounted_filters = filter_list # Make sure we're using one of the available filters. if self.current_filter not in self.mounted_filters: self.current_filter = self.mounted_filters[-1] if self.start_filter not in self.mounted_filters: self.start_filter = self.mounted_filters[-1]
[docs] def setup_camera( self, readtime=2.4, shuttertime=1.0, filter_changetime=120.0, fov=3.5, rotator_min=-90, rotator_max=90, maxspeed=3.5, accel=1.0, shutter_2motion_min_time=15.0, jerk=4.0, ): """Configure the camera. Parameters ---------- readtime : `float` The readout time of the CCDs (seconds) shuttertime : `float` The time it takes the shutter to go from closed to fully open (seconds) filter_changetime : `float` The time it takes to change filters (seconds) fov : `float` The camera field of view (degrees) rotator_min : `float` The minimum angle the camera rotator (rotTelPos) can move to (degrees) rotator_max : `float` The maximum angle the camera rotator (rotTelPos) can move to degrees) maxspeed : `float` The maximum speed of the rotator (degrees/s) accel : `float` The acceleration of the rotator (degrees/s^2) jerk : `float` The jerk of the rotator (degrees/s^3). None treats jerk as infinite. Default 4.0. shutter_2motion_min_time : `float` The time required for two shutter motions (seconds). If one takes a 1-snap 10s exposure, there will be a 5s of overhead before the next exposure can start. """ self.readtime = readtime self.shuttertime = shuttertime self.filter_changetime = filter_changetime self.camera_fov = np.radians(fov) self.telrot_minpos_rad = np.radians(rotator_min) self.telrot_maxpos_rad = np.radians(rotator_max) self.telrot_maxspeed_rad = np.radians(maxspeed) self.telrot_accel_rad = np.radians(accel) self.telrot_jerk_rad = np.radians(jerk) if jerk is not None else None self.shutter_2motion_min_time = shutter_2motion_min_time self.mounted_filters = ["u", "g", "r", "i", "y"]
[docs] def setup_dome( self, altitude_maxspeed=1.75, altitude_accel=0.875, altitude_jerk=None, altitude_freerange=0.0, azimuth_maxspeed=1.5, azimuth_accel=0.75, azimuth_jerk=None, azimuth_freerange=4.0, settle_time=1.0, ): """Configure the dome. Parameters ---------- altitude_maxspeed : `float` Maximum speed for altitude movement (degrees/second) altitude_accel : `float` Maximum acceleration for altitude movement (degrees/second**2) altitude_jerk : `float` The jerk for the altitude movement (degrees/second**3). None treats jerk as infinite. Default None altitude_freerange : `float` The range over which there is 0 delay azimuth_maxspeed : `float` Maximum speed for azimuth movement (degrees/second) azimuth_accel : `float` Maximum acceleration for azimuth movement (degrees/second**2) azimuth_jerk : `float` The jerk of the azimuth movement (degrees/second**3). Default of None treats jerk as infinite. azimuth_freerange : `float` The range in which there is 0 delay settle_time : `float` Settle time after movement (seconds) """ self.domalt_maxspeed_rad = np.radians(altitude_maxspeed) self.domalt_accel_rad = np.radians(altitude_accel) self.domalt_jerk_rad = np.radians(altitude_jerk) if altitude_jerk is not None else None self.domalt_free_range = np.radians(altitude_freerange) self.domaz_maxspeed_rad = np.radians(azimuth_maxspeed) self.domaz_accel_rad = np.radians(azimuth_accel) self.domaz_jerk_rad = np.radians(azimuth_jerk) if azimuth_jerk is not None else None self.domaz_free_range = np.radians(azimuth_freerange) self.domaz_settletime = settle_time
[docs] def setup_telescope( self, altitude_minpos=20.0, altitude_maxpos=86.5, azimuth_minpos=-250.0, azimuth_maxpos=250.0, altitude_maxspeed=3.5, altitude_accel=3.5, altitude_jerk=14.0, azimuth_maxspeed=7.0, azimuth_accel=7.0, azimuth_jerk=28.0, settle_time=3.0, ): """Configure the telescope (TMA) movement and position. Parameters ---------- altitude_minpos : `float` Minimum altitude for the telescope (degrees). altitude_maxpos : `float` Maximum altitude for the telescope (degrees). Maximum position of 86.5 is limited due to slew requirements near the zenith with an alt-az mount. azimuth_minpos : `float` Minimum azimuth position (degrees). Note this value is related to cumulative azimuth range, for cable wrap. azimuth_maxpos : `float` Maximum azimuth position (degrees). Note this value is related to cumulative azimuth range, for cable wrap. Defaults -250/250 include 0-360 for all-sky azimuth positions, reachable via multiple routes. altitude_maxspeed : `float` Maximum speed for altitude movement (degrees/second) altitude_accel : `float` Maximum acceleration for altitude movement (degrees/second**2) altitude_jerk : `float` Jerk for altitude movement (degrees/second**2). None will treat jerk as infinite. Default 20. azimuth_maxspeed : `float` Maximum speed for azimuth movement (degrees/second) azimuth_accel : `float` Maximum acceleration for azimuth movement (degrees/second**2) azimuth_jerk : `float` Jerk for azimuth movement (degrees/second**2). None will treat jerk as infinite. Default 40. settle_time : `float` Settle time required for telescope after movement (seconds) See also `tma_movement` for definitions for the speed, acceleration and jerk. """ self.telalt_minpos_rad = np.radians(altitude_minpos) self.telalt_maxpos_rad = np.radians(altitude_maxpos) self.telaz_minpos_rad = np.radians(azimuth_minpos) self.telaz_maxpos_rad = np.radians(azimuth_maxpos) self.telalt_maxspeed_rad = np.radians(altitude_maxspeed) self.telalt_accel_rad = np.radians(altitude_accel) self.telalt_jerk_rad = np.radians(altitude_jerk) if altitude_jerk is not None else None self.telaz_maxspeed_rad = np.radians(azimuth_maxspeed) self.telaz_accel_rad = np.radians(azimuth_accel) self.telaz_jerk_rad = np.radians(azimuth_jerk) if azimuth_jerk is not None else None self.mount_settletime = settle_time
[docs] def setup_optics(self, ol_slope=1.0 / 3.5, cl_delay=[0.0, 36.0], cl_altlimit=[0.0, 9.0, 90.0]): """Configure parameters for the optics closed and open loops. Parameters ---------- ol_slope : `float` (1.0/3.5) seconds/degree in altitude slew. cl_delay : list ([0.0, 36]) The delays for closed optics loops (seconds) cl_altlimit : list ([0.0, 9.0, 90.0]) The altitude limits (degrees) for performing closed optice loops. Should be one element longer than cl_delay. Note ---- A given movement in altitude will cover X degrees; if X > cl_altlimit[i] there is an additional delay of cl_delay[i] """ # ah, 1./np.radians(1)=np.pi/180 self.optics_ol_slope = ol_slope / np.radians(1.0) self.optics_cl_delay = cl_delay self.optics_cl_altlimit = np.radians(cl_altlimit)
[docs] def park(self): """Put the telescope in the park position.""" # I'm going to ignore that the old model had the dome altitude at 90 # and telescope altitude 86 for park. # We should usually be dome az limited anyway, so this should be a # negligible approximation. self.parked = True # We have no current position we are tracking self.current_ra_rad = None self.current_dec_rad = None self.current_rot_sky_pos_rad = None self.cumulative_azimuth_rad = 0 # The last position we were at (or the current if we are parked) self.last_az_rad = self.park_az_rad self.last_alt_rad = self.park_alt_rad self.last_rot_tel_pos_rad = 0 # Any overhead that must happen before next exposure can start. Slew # motions are allowed during the overhead time self.overhead = 0.0 # Don't leave random filter in overnight self.current_filter = self.start_filter
[docs] def current_alt_az(self, mjd): """return the current alt az position that we have tracked to.""" if self.parked: return self.last_alt_rad, self.last_az_rad, self.last_rot_tel_pos_rad else: alt_rad, az_rad, pa = self.radec2altaz(self.current_ra_rad, self.current_dec_rad, mjd) rot_tel_pos = self.rc._rotskypos2rottelpos(self.current_rot_sky_pos_rad, pa) return alt_rad, az_rad, rot_tel_pos
[docs] def slew_times( self, ra_rad, dec_rad, mjd, rot_sky_pos=None, rot_tel_pos=None, filtername="r", lax_dome=True, alt_rad=None, az_rad=None, starting_alt_rad=None, starting_az_rad=None, starting_rot_tel_pos_rad=None, update_tracking=False, ): """Calculates slew time to a series of alt/az/filter positions from the current position (stored internally). Assumptions (currently): Assumes we have been tracking on ra,dec,rot_sky_pos position. Ignores the motion of the sky while we are slewing (this approx should probably average out over time). No checks for if we have tracked beyond limits. (this assumes folks put telescope in park if there's a long gap.) Assumes the camera rotator never needs to (or can't) do a slew over 180 degrees. Calculates the slew time necessary to get from current state to alt2/az2/filter2. The time returned is actually the time between the end of an exposure at current location and the beginning of an exposure at alt2/az2, since it includes readout time in the slew time. Parameters ---------- ra_rad : `np.ndarray` The RA(s) of the location(s) we wish to slew to (radians) dec_rad : `np.ndarray` The declination(s) of the location(s) we wish to slew to (radians) mjd : `float` The current modified julian date (days) rot_sky_pos : `np.ndarray` The desired rot_sky_pos(s) (radians). Angle between up on the chip and North. Note, it is possible to set a rot_sky_pos outside the allowed camera rotator range, in which case the slewtime will be masked. If both rot_sky_pos and rot_tel_pos are set, rot_tel_pos will be used. rot_tel_pos : `np.ndarray` The desired rot_tel_pos(s) (radians). This overrides rot_sky_pos, if set. If neither rot_sky_pos nor rot_tel_pos are set, no rotation time is added (equivalent to using current rot_tel_pos). filtername : `str` or None The filter(s) of the desired observations. Set to None to compute only telescope and dome motion times. alt_rad : `np.ndarray` The altitude(s) of the destination pointing(s) (radians). Will override ra_rad,dec_rad if provided. az_rad : `np.ndarray` The azimuth(s) of the destination pointing(s) (radians). Will override ra_rad,dec_rad if provided. lax_dome : `bool`, default True If True, allow the dome to creep, model a dome slit, and don't require the dome to settle in azimuth. If False, adhere to the way SOCS calculates slew times (as of June 21 2017) and do not allow dome creep. starting_alt_rad : `float` (None) The starting altitude for the slew (radians). If None, will use internally stored last pointing. starting_az_rad : `float` (None) The starting azimuth for the slew (radians). If None, will use internally stored last pointing. starting_rot_tel_pos_rad : `float` (None) The starting camera rotation for the slew (radians). If None, will use internally stored last pointing. update_tracking : `bool` (False) If True, update the internal attributes to say we are tracking the specified RA,Dec,RotSkyPos position. Returns ------- slew_time : `np.ndarray` The number of seconds between the two specified exposures. Will be np.nan or np.inf if slew is not possible. """ if filtername is None: filtername = self.current_filter elif filtername not in self.mounted_filters: return np.nan # if alt,az not provided, then calculate from RA,Dec if alt_rad is None: alt_rad, az_rad, pa = self.radec2altaz(ra_rad, dec_rad, mjd) else: pa = _approx_altaz2pa(alt_rad, az_rad, self.location.lat_rad) if update_tracking: ra_rad, dec_rad = _approx_alt_az2_ra_dec( alt_rad, az_rad, self.location.lat_rad, self.location.lon_rad, mjd ) # If either rot_tel_pos or rot_sky_pos are set, we will # calculate slewtime with rotator movement. # Setting rot_tel_pos allows any slew position on-sky. # Setting rot_sky_pos can restrict slew range on-sky # as some rot_tel_pos values that result will be out of bounds. # Use rot_tel_pos first if available, to override rot_sky_pos. if (rot_tel_pos is not None) & (rot_sky_pos is not None): if np.isfinite(rot_tel_pos): rot_sky_pos = self.rc._rottelpos2rotskypos(rot_tel_pos, pa) else: rot_tel_pos = self.rc._rotskypos2rottelpos(rot_sky_pos, pa) # Find the current location of the telescope. if starting_alt_rad is None: if self.parked: starting_alt_rad = self.park_alt_rad starting_az_rad = self.park_az_rad else: starting_alt_rad, starting_az_rad, starting_pa = self.radec2altaz( self.current_ra_rad, self.current_dec_rad, mjd ) # Now calculate how far we need to move, # in altitude, azimuth, and camera rotator (if applicable). # Delta Altitude delta_alt = np.abs(alt_rad - starting_alt_rad) # Delta Azimuth - there are two different directions # possible to travel for azimuth. First calculate the shortest. delta_az_short = smallest_signed_angle(starting_az_rad, az_rad) # And then calculate the longer delta_az_long = np.where(delta_az_short < 0, two_pi + delta_az_short, delta_az_short - two_pi) # Slew can go long or short direction, but azimuth range # could limit which is possible. # e.g. 70 degrees reached by going the long way around from 0 means # the cumulative azimuth is 290, but if we went the short way it would # be 70 .. absolute azimuth is still 70. Direction of previous # slews is also important. # First evaluate if available azimuth range is > 360 degrees -- if np.abs(self.telaz_maxpos_rad - self.telaz_minpos_rad) >= two_pi: # Can spin past 360 degrees, track cumulative azimuth # Note that minpos will be less than maxpos always in this case. cummulative_az_short = delta_az_short + self.cumulative_azimuth_rad out_of_bounds = np.where( (cummulative_az_short < self.telaz_minpos_rad) | (cummulative_az_short > self.telaz_maxpos_rad) )[0] # Set short direction out of bounds azimuths to infinite distance. delta_az_short[out_of_bounds] = np.inf cummulative_az_long = delta_az_long + self.cumulative_azimuth_rad out_of_bounds = np.where( (cummulative_az_long < self.telaz_minpos_rad) | (cummulative_az_long > self.telaz_maxpos_rad) )[0] # Set long direction out of bounds azimuths to infinite distance delta_az_long[out_of_bounds] = np.inf # Now pick the shortest allowable direction # (use absolute value of each az, because values can be negative) delta_aztel = np.where( np.abs(delta_az_short) < np.abs(delta_az_long), delta_az_short, delta_az_long, ) az_flag = "delta" # Now evaluate the options if we have an impaired telescope with # azimuth range < 360 degrees. else: # Note that minpos will be the starting angle, but # depending on direction available - maxpos might be < minpos. az_range = (self.telaz_maxpos_rad - self.telaz_minpos_rad) % (two_pi) out_of_bounds = np.where((az_rad - self.telaz_minpos_rad) % (two_pi) > az_range)[0] d1 = (az_rad - self.telaz_minpos_rad) % (two_pi) d2 = (starting_az_rad - self.telaz_minpos_rad) % (two_pi) delta_aztel = d2 - d1 delta_aztel[out_of_bounds] = np.inf az_flag = "restricted" # Calculate time to slew to this position. tel_alt_slew_time = jerk_time( delta_alt, self.telalt_maxspeed_rad, self.telalt_accel_rad, self.telalt_jerk_rad ) tel_az_slew_time = jerk_time( np.abs(delta_aztel), self.telaz_maxspeed_rad, self.telaz_accel_rad, self.telaz_jerk_rad ) tot_tel_time = np.maximum(tel_alt_slew_time, tel_az_slew_time) # Time for open loop optics correction ol_time = delta_alt / self.optics_ol_slope tot_tel_time += ol_time # Add time for telescope settle. # note, this means we're going to have a settle time even for very # small slews like dithering. settle_and_ol = np.where(tot_tel_time > 0) tot_tel_time[settle_and_ol] += np.maximum(0, self.mount_settletime - ol_time[settle_and_ol]) # And any leftover overhead sets a minimum on the total telescope # time tot_tel_time = np.maximum(self.overhead, tot_tel_time) # now compute dome slew time # the dome can spin all the way around, so we will let it go the # shortest angle, even if the telescope has to unwind delta_az = np.abs(smallest_signed_angle(starting_az_rad, az_rad)) if lax_dome: # model dome creep, dome slit, and no azimuth settle # if we can fit both exposures in the dome slit, do so same_dome = np.where(delta_alt**2 + delta_az**2 < self.camera_fov**2) # else, we take the minimum time from two options: # 1. assume we line up alt in the center of the dome slit so we # minimize distance we have to travel in azimuth. # 2. line up az in the center of the slit # also assume: # * that we start out going maxspeed for both alt and az # * that we only just barely have to get the new field in the # dome slit in one direction, but that we have to center the # field in the other (which depends which of the two options # used) # * that we don't have to slow down until after the shutter # starts opening dom_delta_alt = delta_alt # on each side, we can start out with the dome shifted away from # the center of the field by an amount domSlitRadius - fovRadius dom_slit_diam = self.camera_fov / 2.0 dom_delta_az = delta_az - 2 * (dom_slit_diam / 2 - self.camera_fov / 2) dom_alt_slew_time = dom_delta_alt / self.domalt_maxspeed_rad dom_az_slew_time = dom_delta_az / self.domaz_maxspeed_rad tot_dom_time1 = np.maximum(dom_alt_slew_time, dom_az_slew_time) dom_delta_alt = delta_alt - 2 * (dom_slit_diam / 2 - self.camera_fov / 2) dom_delta_az = delta_az dom_alt_slew_time = dom_delta_alt / self.domalt_maxspeed_rad dom_az_slew_time = dom_delta_az / self.domaz_maxspeed_rad tot_dom_time2 = np.maximum(dom_alt_slew_time, dom_az_slew_time) tot_dom_time = np.minimum(tot_dom_time1, tot_dom_time2) tot_dom_time[same_dome] = 0 else: # the above models a dome slit and dome creep. # If this option is not available however: dom_alt_slew_time = jerk_time( delta_alt, self.domalt_maxspeed_rad, self.domalt_accel_rad, self.domalt_jerk_rad ) dom_az_slew_time = jerk_time( delta_az, self.domaz_maxspeed_rad, self.domaz_accel_rad, self.domaz_jerk_rad ) # Dome takes 1 second to settle in az dom_az_slew_time = np.where( dom_az_slew_time > 0, dom_az_slew_time + self.domaz_settletime, dom_az_slew_time, ) tot_dom_time = np.maximum(dom_alt_slew_time, dom_az_slew_time) # Find the max of the above for slew time. slew_time = np.maximum(tot_tel_time, tot_dom_time) # include filter change time if necessary. Assume no filter # change time needed if we are starting parked if not self.parked: filter_change = np.where(filtername != self.current_filter) slew_time[filter_change] = np.maximum(slew_time[filter_change], self.filter_changetime) # Add closed loop optics correction # Find the limit where we must add the delay cl_limit = self.optics_cl_altlimit[1] cl_delay = self.optics_cl_delay[1] close_loop = np.where(delta_alt >= cl_limit) slew_time[close_loop] += cl_delay # Mask min/max altitude limits so slewtime = np.nan outside_limits = np.where((alt_rad > self.telalt_maxpos_rad) | (alt_rad < self.telalt_minpos_rad))[0] slew_time[outside_limits] = np.nan # Azimuth limits already masked through cumulative azimuth # calculation above (it might be inf, not nan, so let's swap). slew_time = np.where(np.isfinite(slew_time), slew_time, np.nan) # If we want to include the camera rotation time # We will have already set rot_tel_pos above, if either # rot_sky_pos or rot_tel_pos is set. if rot_tel_pos is not None: # rot_tel_pos will be outside_limits = np.where( (rot_tel_pos < self.telrot_minpos_rad) | (rot_tel_pos > self.telrot_maxpos_rad) ) slew_time[outside_limits] = np.nan # If there was no kwarg for starting rotator position if starting_rot_tel_pos_rad is None: # If there is no current rot_sky_pos, we were parked if self.current_rot_sky_pos_rad is None: current_rot_tel_pos = self.last_rot_tel_pos_rad else: # We have been tracking, so rot_tel_pos needs to be updated current_rot_tel_pos = self.rc._rotskypos2rottelpos( self.current_rot_sky_pos_rad, starting_pa ) else: # kwarg overrides if it was supplied current_rot_tel_pos = starting_rot_tel_pos_rad delta_rotation = np.abs(smallest_signed_angle(current_rot_tel_pos, rot_tel_pos)) rotator_time = jerk_time( delta_rotation, self.telrot_maxspeed_rad, self.telrot_accel_rad, self.telrot_jerk_rad ) slew_time = np.maximum(slew_time, rotator_time) # Recreate how this happened to work previously with single targets if len(slew_time) == 1: slew_time = slew_time[0] # Update the internal attributes to note that we are now pointing # and tracking at the requested RA,Dec,rot_sky_pos if update_tracking and np.isfinite(slew_time): self.current_ra_rad = ra_rad self.current_dec_rad = dec_rad self.current_rot_sky_pos_rad = rot_sky_pos self.parked = False # Handy to keep as reference, but not used for any calculations self.last_rot_tel_pos_rad = rot_tel_pos self.last_az_rad = az_rad self.last_alt_rad = alt_rad self.last_pa_rad = pa # Track the cumulative azimuth if az_flag == "restricted": self.cumulative_azimuth_rad = az_rad else: self.cumulative_azimuth_rad += delta_aztel self.current_filter = filtername self.last_mjd = mjd return slew_time
def visit_time(self, observation): # How long does it take to make an observation. visit_time = ( observation["exptime"] + observation["nexp"] * self.shuttertime + max(observation["nexp"] - 1, 0) * self.readtime ) return visit_time
[docs] def shutter_stall(self, observation): """Time we need to stall after shutter closes to let things cool down.""" result = 0.0 delta_t = observation["exptime"] / observation["nexp"] if delta_t < self.shutter_2motion_min_time: result = self.shutter_2motion_min_time - delta_t return result
[docs] def observe(self, observation, mjd, rot_tel_pos=None, lax_dome=True): """Observe a target, and return the slewtime and visit time for the action If slew is not allowed, returns np.nan and does not update state. """ if (observation["nexp"] >= 2) & ( (observation["exptime"] / observation["nexp"] + self.readtime * (observation["nexp"] - 1)) < self.shutter_2motion_min_time ): msg = "%i exposures in %i seconds is violating number of shutter motion limit" % ( observation["nexp"], observation["exptime"], ) warnings.warn(msg) slewtime = self.slew_times( observation["RA"], observation["dec"], mjd, rot_sky_pos=observation["rotSkyPos"], rot_tel_pos=rot_tel_pos, filtername=observation["filter"], update_tracking=True, lax_dome=lax_dome, ) visit_time = self.visit_time(observation) # Compute any overhead that is left over from this if ~np.isnan(slewtime): self.overhead = np.maximum(self.readtime, self.shutter_stall(observation)) return slewtime, visit_time