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

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