Coverage for python/lsst/sims/featureScheduler/modelObservatory/kinem_model.py : 11%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import numpy as np
2from lsst.sims.utils import Site, calcLmstLast, _approx_altAz2RaDec, _approx_altaz2pa, _approx_RaDec2AltAz
3import healpy as hp
4import matplotlib.pylab as plt
5from lsst.sims.featureScheduler.utils import smallest_signed_angle
7__all__ = ["Kinem_model"]
8TwoPi = 2.*np.pi
11class radec2altazpa(object):
12 """Class to make it easy to swap in different alt/az conversion if wanted
13 """
14 def __init__(self, location):
15 self.location = location
17 def __call__(self, ra, dec, mjd):
18 alt, az, pa = _approx_RaDec2AltAz(ra, dec, self.location.lat_rad, self.location.lon_rad, mjd,
19 return_pa=True)
20 return alt, az, pa
23def _getRotSkyPos(paRad, rotTelRad):
24 """
25 Paramteres
26 ----------
27 paRad : float or array
28 The parallactic angle
29 """
30 return (rotTelRad - paRad) % TwoPi
33def _getRotTelPos(paRad, rotSkyRad):
34 """Make it run from -180 to 180
35 """
36 result = (rotSkyRad + paRad) % TwoPi
37 return result
40class Kinem_model(object):
41 """
42 A Kinematic model of the telescope.
44 Parameters
45 ----------
46 location : astropy.coordinates.EarthLocation object
47 The location of the telescope. If None, defaults to lsst.sims.utils.Site info
48 park_alt : float (86.5)
49 The altitude the telescope gets parked at (degrees)
50 start_filter : str ('r')
51 The filter that gets loaded when the telescope is parked
52 mjd0 : float (0)
53 The MJD to assume we are starting from
55 Note there are additional parameters in the methods setup_camera, setup_dome, setup_telescope,
56 and setup_optics. Just breaking it up a bit to make it more readable.
57 """
58 def __init__(self, location=None, park_alt=86.5, park_az=0., start_filter='r', mjd0=0):
59 self.park_alt_rad = np.radians(park_alt)
60 self.park_az_rad = np.radians(park_az)
61 self.current_filter = start_filter
62 if location is None:
63 self.location = Site('LSST')
64 self.location.lat_rad = np.radians(self.location.latitude)
65 self.location.lon_rad = np.radians(self.location.longitude)
66 # Our RA,Dec to Alt,Az converter
67 self.radec2altaz = radec2altazpa(self.location)
69 self.setup_camera()
70 self.setup_dome()
71 self.setup_telescope()
72 self.setup_optics()
74 # Park the telescope
75 self.park()
76 self.last_mjd = mjd0
78 def mount_filters(self, filter_list):
79 """Change which filters are mounted
80 """
81 self.mounted_filters = filter_list
83 def setup_camera(self, readtime=2., shuttertime=1., filter_changetime=120., fov=3.5,
84 rotator_min=-90, rotator_max=90, maxspeed=3.5, accel=1.0, decel=1.0):
85 """
86 Parameters
87 ----------
88 readtime : float (2)
89 The readout time of the CCDs (seconds)
90 shuttertime : float (1.)
91 The time it takes the shutter to go from closed to fully open (seconds)
92 filter_changetime : float (120)
93 The time it takes to change filters (seconds)
94 fov : float (3.5)
95 The camera field of view (degrees)
96 rotator_min : float (-90)
97 The minimum angle the camera rotator can move to (degrees)
98 maxspeed : float (3.5)
99 The maximum speed of the rotator (degrees/s)
100 accel : float (1.0)
101 The acceleration of the rotator (degrees/s^2)
102 """
103 self.readtime = readtime
104 self.shuttertime = shuttertime
105 self.filter_changetime = filter_changetime
106 self.camera_fov = np.radians(fov)
108 self.telrot_minpos_rad = np.radians(rotator_min)
109 self.telrot_maxpos_rad = np.radians(rotator_max)
110 self.telrot_maxspeed_rad = np.radians(maxspeed)
111 self.telrot_accel_rad = np.radians(accel)
112 self.telrot_decel_rad = np.radians(decel)
113 self.mounted_filters = ['u', 'g', 'r', 'i', 'y']
115 def setup_dome(self, altitude_maxspeed=1.75, altitude_accel=0.875, altitude_decel=0.875,
116 altitude_freerange=0., azimuth_maxspeed=1.5, azimuth_accel=0.75,
117 azimuth_decel=0.75, azimuth_freerange=4.0, settle_time=1.0):
118 """input in degrees, degees/second, degrees/second**2, and seconds.
119 Freerange is the range in which there is zero delay.
120 """
121 self.domalt_maxspeed_rad = np.radians(altitude_maxspeed)
122 self.domalt_accel_rad = np.radians(altitude_accel)
123 self.domalt_decel_rad = np.radians(altitude_decel)
124 self.domalt_free_range = np.radians(altitude_freerange)
125 self.domaz_maxspeed_rad = np.radians(azimuth_maxspeed)
126 self.domaz_accel_rad = np.radians(azimuth_accel)
127 self.domaz_decel_rad = np.radians(azimuth_decel)
128 self.domaz_free_range = np.radians(azimuth_freerange)
129 self.domaz_settletime = settle_time
131 def setup_telescope(self, altitude_minpos=20.0, altitude_maxpos=86.5,
132 azimuth_minpos=-270.0, azimuth_maxpos=270.0, altitude_maxspeed=3.5,
133 altitude_accel=3.5, altitude_decel=3.5, azimuth_maxspeed=7.0,
134 azimuth_accel=7.0, azimuth_decel=7.0, settle_time=3.0):
135 """input in degrees, degees/second, degrees/second**2, and seconds.
136 """
137 self.telalt_minpos_rad = np.radians(altitude_minpos)
138 self.telalt_maxpos_rad = np.radians(altitude_maxpos)
139 self.telaz_minpos_rad = np.radians(azimuth_minpos)
140 self.telaz_maxpos_rad = np.radians(azimuth_maxpos)
141 self.telalt_maxspeed_rad = np.radians(altitude_maxspeed)
142 self.telalt_accel_rad = np.radians(altitude_accel)
143 self.telalt_decel_rad = np.radians(altitude_decel)
144 self.telaz_maxspeed_rad = np.radians(azimuth_maxspeed)
145 self.telaz_accel_rad = np.radians(azimuth_accel)
146 self.telaz_decel_rad = np.radians(azimuth_decel)
147 self.mount_settletime = settle_time
149 def setup_optics(self, ol_slope=1.0/3.5, cl_delay=[0.0, 36.], cl_altlimit=[0.0, 9.0, 90.0]):
150 """
151 Parameters
152 ----------
153 ol_slope : float
154 seconds/degree in altitude slew.
155 cl_delay : list ([0.0, 36])
156 The delays for closed optics loops (seconds)
157 cl_altlimit : list ([0.0, 9.0, 90.0])
158 The altitude limits (degrees) for performing closed optice loops. Should be one element longer than cl_delay.
159 """
161 self.optics_ol_slope = ol_slope/np.radians(1.) # ah, 1./np.radians(1)=np.pi/180
162 self.optics_cl_delay = cl_delay
163 self.optics_cl_altlimit = np.radians(cl_altlimit)
165 def park(self):
166 """Put the telescope in the park position.
167 """
168 # I'm going to ignore that the old model had the dome altitude at 90 and telescope altitude 86 for park.
169 # We should usually be dome az limited anyway, so this should be a negligible approximation.
171 self.parked = True
173 # We have no current position we are tracking
174 self.current_RA_rad = None
175 self.current_dec_rad = None
176 self.current_rotSkyPos_rad = None
177 self.cumulative_azimuth_rad = 0
179 # The last position we were at (or the current if we are parked)
180 self.last_az_rad = self.park_az_rad
181 self.last_alt_rad = self.park_alt_rad
182 self.last_rot_tel_pos_rad = 0
184 def current_alt_az(self, mjd):
185 """return the current alt az position that we have tracked to.
186 """
187 if self.parked:
188 return self.last_alt_rad, self.last_az_rad, self.last_rot_tel_pos_rad
189 else:
190 alt_rad, az_rad, pa = self.radec2altaz(self.current_RA_rad, self.current_dec_rad, mjd)
191 rotTelPos = _getRotTelPos(pa, self.last_rot_tel_pos_rad)
192 return alt_rad, az_rad, rotTelPos
194 def _uamSlewTime(self, distance, vmax, accel):
195 """Compute slew time delay assuming uniform acceleration (for any component).
196 If you accelerate uniformly to vmax, then slow down uniformly to zero, distance traveled is
197 d = vmax**2 / accel
198 To travel distance d while accelerating/decelerating at rate a, time required is t = 2 * sqrt(d / a)
199 If hit vmax, then time to acceleration to/from vmax is 2*vmax/a and distance in those
200 steps is vmax**2/a. The remaining distance is (d - vmax^2/a) and time needed is (d - vmax^2/a)/vmax
202 This method accepts arrays of distance, and assumes acceleration == deceleration.
204 Parameters
205 ----------
206 distance : numpy.ndarray
207 Distances to travel. Must be positive value.
208 vmax : float
209 Max velocity
210 accel : float
211 Acceleration (and deceleration)
213 Returns
214 -------
215 numpy.ndarray
216 """
217 dm = vmax**2 / accel
218 slewTime = np.where(distance < dm, 2 * np.sqrt(distance / accel),
219 2 * vmax / accel + (distance - dm) / vmax)
220 return slewTime
222 def slew_times(self, ra_rad, dec_rad, mjd, rotSkyPos=None, rotTelPos=None, filtername='r',
223 lax_dome=True, alt_rad=None, az_rad=None, starting_alt_rad=None, starting_az_rad=None,
224 starting_rotTelPos_rad=None, update_tracking=False, include_readtime=True):
225 """Calculates ``slew'' time to a series of alt/az/filter positions from the current
226 position (stored internally).
227 Assumptions (currently):
228 Assumes we have been tracking on ra,dec,rotSkyPos position.
229 Ignores the motion of the sky while we are slewing (this approx should probably average out over time).
230 No checks for if we have tracked beyond limits. Assumes folks put telescope in park if there's a long gap.
231 Assumes the camera rotator never needs to (or can't) do a slew over 180 degrees.
233 Calculates the ``slew'' time necessary to get from current state
234 to alt2/az2/filter2. The time returned is actually the time between
235 the end of an exposure at current location and the beginning of an exposure
236 at alt2/az2, since it includes readout time in the ``slew'' time.
238 Parameters
239 ----------
240 ra_rad : np.ndarray
241 The RA(s) of the location(s) we wish to slew to (radians)
242 dec_rad : np.ndarray
243 The declination(s) of the location(s) we wish to slew to (radians)
244 mjd : float
245 The current moodified julian date (days)
246 rotSkyPos : np.ndarray
247 The desired rotSkyPos(s) (radians). Angle between up on the chip and North. Note,
248 it is possible to set a rotSkyPos outside the allowed camera rotator range, in which case
249 the slewtime will be np.inf. If both rotSkyPos and rotTelPos are set, rotTelPos will be used.
250 rotTelPos : np.ndarray
251 The desired rotTelPos(s) (radians).
252 filtername : str
253 The filter(s) of the desired observations. Set to None to compute only telescope and dome motion times.
254 alt_rad : np.ndarray
255 The altitude(s) of the destination pointing(s) (radians). Will override ra_rad,dec_rad if provided.
256 az_rad : np.ndarray
257 The azimuth(s) of the destination pointing(s) (radians). Will override ra_rad,dec_rad if provided.
258 lax_dome : boolean, default False
259 If True, allow the dome to creep, model a dome slit, and don't
260 require the dome to settle in azimuth. If False, adhere to the way
261 SOCS calculates slew times (as of June 21 2017).
262 starting_alt_rad : float (None)
263 The starting altitude for the slew (radians). If None, will use internally stored last pointing.
264 starting_az_rad : float (None)
265 The starting azimuth for the slew (radians). If None, will use internally stored last pointing.
266 starting_rotTelPos_rad : float (None)
267 The starting camera rotation for the slew (radians). If None, will use internally stored last pointing.
268 update_tracking : bool (False)
269 If True, update the internal attributes to say we are tracking the specified RA,Dec,RotSkyPos position.
270 include_readtime : bool (True)
271 Assume the camera must be read before opening the shutter, and include that readtime in the returned slewtime.
272 Readtime will never be inclded if the telescope was parked before the slew.
274 Returns
275 -------
276 np.ndarray
277 The number of seconds between the two specified exposures. Will be np.nan or np.inf if slew is not possible.
278 """
279 if filtername not in self.mounted_filters:
280 return np.nan
282 # Don't trust folks to do pa calculation correctly, if both rotations set, rotSkyPos wins
283 if (rotTelPos is not None) & (rotSkyPos is not None):
284 if np.isfinite(rotTelPos):
285 rotSkyPos = None
286 else:
287 rotTelPos = None
289 # alt,az not provided, calculate from RA,Dec
290 if alt_rad is None:
291 alt_rad, az_rad, pa = self.radec2altaz(ra_rad, dec_rad, mjd)
292 else:
293 pa = _approx_altaz2pa(alt_rad, az_rad, self.location.lat_rad)
294 if update_tracking:
295 ra_rad, dec_rad = _approx_altAz2RaDec(alt_rad, az_rad, self.location.lat_rad,
296 self.location.lon_rad, mjd)
298 if starting_alt_rad is None:
299 if self.parked:
300 starting_alt_rad = self.park_alt_rad
301 starting_az_rad = self.park_az_rad
302 else:
303 starting_alt_rad, starting_az_rad, starting_pa = self.radec2altaz(self.current_RA_rad,
304 self.current_dec_rad, mjd)
306 deltaAlt = np.abs(alt_rad - starting_alt_rad)
307 delta_az_short = smallest_signed_angle(starting_az_rad, az_rad)
308 delta_az_long = delta_az_short - TwoPi
309 daslz = np.where(delta_az_short < 0)[0]
310 delta_az_long[daslz] = TwoPi + delta_az_short[daslz]
311 azlz = np.where(delta_az_short < 0)[0]
312 delta_az_long[azlz] = TwoPi + delta_az_short[azlz]
313 # So, for every position, we can get there by slewing long or short way
314 cummulative_az_short = delta_az_short + self.cumulative_azimuth_rad
315 oob = np.where((cummulative_az_short < self.telaz_minpos_rad) | (cummulative_az_short > self.telaz_maxpos_rad))[0]
316 # Set out of bounds azimuths to infinite distance
317 delta_az_short[oob] = np.inf
318 cummulative_az_long = delta_az_long + self.cumulative_azimuth_rad
319 oob = np.where((cummulative_az_long < self.telaz_minpos_rad) | (cummulative_az_long > self.telaz_maxpos_rad))[0]
320 delta_az_long[oob] = np.inf
322 # Taking minimum of abs, so only possible azimuths slews should show up. And deltaAz is signed properly.
323 stacked_az = np.vstack([delta_az_short, delta_az_long])
324 indx = np.argmin(np.abs(stacked_az), axis=0)
325 deltaAztel = np.take_along_axis(stacked_az, np.expand_dims(indx, axis=0), axis=0).squeeze(axis=0)
327 # Calculate how long the telescope will take to slew to this position.
328 telAltSlewTime = self._uamSlewTime(deltaAlt, self.telalt_maxspeed_rad,
329 self.telalt_accel_rad)
330 telAzSlewTime = self._uamSlewTime(np.abs(deltaAztel), self.telaz_maxspeed_rad,
331 self.telaz_accel_rad)
332 totTelTime = np.maximum(telAltSlewTime, telAzSlewTime)
334 # Time for open loop optics correction
335 olTime = deltaAlt / self.optics_ol_slope
336 totTelTime += olTime
337 # Add time for telescope settle.
338 # XXX--note, this means we're going to have a settle time even for very small slews (like even a dither)
339 settleAndOL = np.where(totTelTime > 0)
340 totTelTime[settleAndOL] += np.maximum(0, self.mount_settletime - olTime[settleAndOL])
341 # And readout puts a floor on tel time
342 if include_readtime:
343 totTelTime = np.maximum(self.readtime, totTelTime)
345 # now compute dome slew time
346 # I think the dome can spin all the way around, so we will let it go the shortest angle,
347 # even if the telescope has to unwind
348 deltaAz = np.abs(smallest_signed_angle(starting_az_rad, az_rad))
349 if lax_dome:
350 # model dome creep, dome slit, and no azimuth settle
351 # if we can fit both exposures in the dome slit, do so
352 sameDome = np.where(deltaAlt ** 2 + deltaAz ** 2 < self.camera_fov ** 2)
354 # else, we take the minimum time from two options:
355 # 1. assume we line up alt in the center of the dome slit so we
356 # minimize distance we have to travel in azimuth.
357 # 2. line up az in the center of the slit
358 # also assume:
359 # * that we start out going maxspeed for both alt and az
360 # * that we only just barely have to get the new field in the
361 # dome slit in one direction, but that we have to center the
362 # field in the other (which depends which of the two options used)
363 # * that we don't have to slow down until after the shutter
364 # starts opening
365 domDeltaAlt = deltaAlt
366 # on each side, we can start out with the dome shifted away from
367 # the center of the field by an amount domSlitRadius - fovRadius
368 domSlitDiam = self.camera_fov / 2.0
369 domDeltaAz = deltaAz - 2 * (domSlitDiam / 2 - self.camera_fov / 2)
370 domAltSlewTime = domDeltaAlt / self.domalt_maxspeed_rad
371 domAzSlewTime = domDeltaAz / self.domaz_maxspeed_rad
372 totDomTime1 = np.maximum(domAltSlewTime, domAzSlewTime)
374 domDeltaAlt = deltaAlt - 2 * (domSlitDiam / 2 - self.camera_fov / 2)
375 domDeltaAz = deltaAz
376 domAltSlewTime = domDeltaAlt / self.domalt_maxspeed_rad
377 domAzSlewTime = domDeltaAz / self.domaz_maxspeed_rad
378 totDomTime2 = np.maximum(domAltSlewTime, domAzSlewTime)
380 totDomTime = np.minimum(totDomTime1, totDomTime2)
381 totDomTime[sameDome] = 0
383 else:
384 # the above models a dome slit and dome creep. However, it appears that
385 # SOCS requires the dome to slew exactly to each field and settle in az
386 domAltSlewTime = self._uamSlewTime(deltaAlt, self.domalt_maxspeed_rad,
387 self.domalt_accel_rad)
388 domAzSlewTime = self._uamSlewTime(deltaAz, self.domaz_maxspeed_rad,
389 self.domaz_accel_rad)
390 # Dome takes 1 second to settle in az
391 domAzSlewTime = np.where(domAzSlewTime > 0,
392 domAzSlewTime + self.domaz_settletime,
393 domAzSlewTime)
394 totDomTime = np.maximum(domAltSlewTime, domAzSlewTime)
395 # Find the max of the above for slew time.
396 slewTime = np.maximum(totTelTime, totDomTime)
397 # include filter change time if necessary
398 filterChange = np.where(filtername != self.current_filter)
399 slewTime[filterChange] = np.maximum(slewTime[filterChange],
400 self.filter_changetime)
401 # Add closed loop optics correction
402 # Find the limit where we must add the delay
403 cl_limit = self.optics_cl_altlimit[1]
404 cl_delay = self.optics_cl_delay[1]
405 closeLoop = np.where(deltaAlt >= cl_limit)
406 slewTime[closeLoop] += cl_delay
408 # Mask min/max altitude limits so slewtime = np.nan
409 outsideLimits = np.where((alt_rad > self.telalt_maxpos_rad) |
410 (alt_rad < self.telalt_minpos_rad))[0]
411 slewTime[outsideLimits] = np.nan
413 # If we want to include the camera rotation time
414 if (rotSkyPos is not None) | (rotTelPos is not None):
415 if rotTelPos is None:
416 rotTelPos = _getRotTelPos(pa, rotSkyPos)
417 if rotSkyPos is None:
418 rotSkyPos = _getRotSkyPos(pa, rotTelPos)
419 # If the new rotation angle would move us out of the limits, return nan
420 rotTelPos_ranged = rotTelPos+0
421 over = np.where(rotTelPos > np.pi)[0]
422 rotTelPos_ranged[over] -= TwoPi
423 if (rotTelPos_ranged < self.telrot_minpos_rad) | (rotTelPos_ranged > self.telrot_maxpos_rad):
424 return np.nan
425 # If there was no kwarg for starting rotator position
426 if starting_rotTelPos_rad is None:
427 # If there is no current rotSkyPos, we were parked
428 if self.current_rotSkyPos_rad is None:
429 current_rotTelPos = self.last_rot_tel_pos_rad
430 else:
431 # We have been tracking, so rotTelPos needs to be updated
432 current_rotTelPos = _getRotTelPos(pa, self.current_rotSkyPos_rad)
433 else:
434 # kwarg overrides if it was supplied
435 current_rotTelPos = starting_rotTelPos_rad
436 deltaRotation = np.abs(smallest_signed_angle(current_rotTelPos, rotTelPos))
437 rotator_time = self._uamSlewTime(deltaRotation, self.telrot_maxspeed_rad, self.telrot_accel_rad)
438 slewTime = np.maximum(slewTime, rotator_time)
440 # Update the internal attributes to note that we are now pointing and tracking
441 # at the requested RA,Dec,rotSkyPos
442 if update_tracking:
443 self.current_RA_rad = ra_rad
444 self.current_dec_rad = dec_rad
445 self.current_rotSkyPos_rad = rotSkyPos
446 self.parked = False
447 # Handy to keep as reference, but not used for any calculations
448 self.last_rot_tel_pos_rad = rotTelPos
449 self.last_az_rad = az_rad
450 self.last_alt_rad = alt_rad
451 self.last_pa_rad = pa
452 # Track the cumulative azimuth
453 self.cumulative_azimuth_rad += deltaAztel
454 self.current_filter = filtername
455 self.last_mjd = mjd
457 return slewTime
459 def visit_time(self, observation):
460 # How long does it take to make an observation. Assume final read can be done during next slew.
461 visit_time = observation['exptime'] + \
462 observation['nexp'] * self.shuttertime + \
463 max(observation['nexp'] - 1, 0) * self.readtime
464 return visit_time
466 def observe(self, observation, mjd, rotTelPos=None):
467 """observe a target, and return the slewtime and visit time for the action
469 If slew is not allowed, returns np.nan and does not update state.
470 """
471 slewtime = self.slew_times(observation['RA'], observation['dec'],
472 mjd, rotSkyPos=observation['rotSkyPos'],
473 rotTelPos=rotTelPos,
474 filtername=observation['filter'], update_tracking=True)
475 visit_time = self.visit_time(observation)
476 return slewtime, visit_time