Coverage for python/lsst/sims/featureScheduler/basis_functions/basis_functions.py : 15%

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.featureScheduler import features
3from lsst.sims.featureScheduler import utils
4from lsst.sims.featureScheduler.utils import int_rounded
5import healpy as hp
6from lsst.sims.skybrightness_pre import M5percentiles
7import matplotlib.pylab as plt
8import warnings
9from lsst.sims.utils import _hpid2RaDec
10from astropy.coordinates import SkyCoord
11from astropy import units as u
14__all__ = ['Base_basis_function', 'Constant_basis_function', 'Target_map_basis_function',
15 'Avoid_long_gaps_basis_function',
16 'Avoid_Fast_Revists', 'Visit_repeat_basis_function', 'M5_diff_basis_function',
17 'Strict_filter_basis_function', 'Goal_Strict_filter_basis_function',
18 'Filter_change_basis_function', 'Slewtime_basis_function',
19 'Aggressive_Slewtime_basis_function', 'Skybrightness_limit_basis_function',
20 'CableWrap_unwrap_basis_function', 'Cadence_enhance_basis_function',
21 'Cadence_enhance_trapezoid_basis_function', 'Azimuth_basis_function',
22 'Az_modulo_basis_function', 'Dec_modulo_basis_function', 'Map_modulo_basis_function',
23 'Template_generate_basis_function',
24 'Footprint_nvis_basis_function', 'Third_observation_basis_function', 'Season_coverage_basis_function',
25 'N_obs_per_year_basis_function', 'Cadence_in_season_basis_function', 'Near_sun_twilight_basis_function',
26 'N_obs_high_am_basis_function', 'Good_seeing_basis_function', 'Observed_twice_basis_function',
27 'Ecliptic_basis_function']
30class Base_basis_function(object):
31 """Class that takes features and computes a reward function when called.
32 """
34 def __init__(self, nside=None, filtername=None, **kwargs):
36 # Set if basis function needs to be recalculated if there is a new observation
37 self.update_on_newobs = True
38 # Set if basis function needs to be recalculated if conditions change
39 self.update_on_mjd = True
40 # Dict to hold all the features we want to track
41 self.survey_features = {}
42 # Keep track of the last time the basis function was called. If mjd doesn't change, use cached value
43 self.mjd_last = None
44 self.value = 0
45 # list the attributes to compare to check if basis functions are equal.
46 self.attrs_to_compare = []
47 # Do we need to recalculate the basis function
48 self.recalc = True
49 # Basis functions don't technically all need an nside, but so many do might as well set it here
50 if nside is None:
51 self.nside = utils.set_default_nside()
52 else:
53 self.nside = nside
55 self.filtername = filtername
57 def add_observation(self, observation, indx=None):
58 """
59 Parameters
60 ----------
61 observation : np.array
62 An array with information about the input observation
63 indx : np.array
64 The indices of the healpix map that the observation overlaps with
65 """
66 for feature in self.survey_features:
67 self.survey_features[feature].add_observation(observation, indx=indx)
68 if self.update_on_newobs:
69 self.recalc = True
71 def check_feasibility(self, conditions):
72 """If there is logic to decide if something is feasible (e.g., only if moon is down),
73 it can be calculated here. Helps prevent full __call__ from being called more than needed.
74 """
75 return True
77 def _calc_value(self, conditions, **kwarge):
78 self.value = 0
79 # Update the last time we had an mjd
80 self.mjd_last = conditions.mjd + 0
81 self.recalc = False
82 return self.value
84 def __eq__(self):
85 # XXX--to work on if we need to make a registry of basis functions.
86 pass
88 def __ne__(self):
89 pass
91 def __call__(self, conditions, **kwargs):
92 """
93 Parameters
94 ----------
95 conditions : lsst.sims.featureScheduler.features.conditions object
96 Object that has attributes for all the current conditions.
98 Return a reward healpix map or a reward scalar.
99 """
100 # If we are not feasible, return -inf
101 if not self.check_feasibility(conditions):
102 return -np.inf
103 if self.recalc:
104 self.value = self._calc_value(conditions, **kwargs)
105 if self.update_on_mjd:
106 if conditions.mjd != self.mjd_last:
107 self.value = self._calc_value(conditions, **kwargs)
108 return self.value
111class Constant_basis_function(Base_basis_function):
112 """Just add a constant
113 """
114 def __call__(self, conditions, **kwargs):
115 return 1
118class Avoid_long_gaps_basis_function(Base_basis_function):
119 """
120 Boost the reward on parts of the survey that haven't been observed for a while
121 """
123 def __init__(self, filtername=None, nside=None, footprint=None, min_gap=4., max_gap=40.,
124 ha_limit=3.5):
125 super(Avoid_long_gaps_basis_function, self).__init__(nside=nside, filtername=filtername)
126 self.min_gap = min_gap
127 self.max_gap = max_gap
128 self.filtername = filtername
129 self.footprint = footprint
130 self.ha_limit = 2.*np.pi*ha_limit/24. # To radians
131 self.survey_features = {}
132 self.survey_features['last_observed'] = features.Last_observed(nside=nside, filtername=filtername)
133 self.result = np.zeros(hp.nside2npix(self.nside))
135 def _calc_value(self, conditions, indx=None):
136 result = self.result.copy()
138 gap = conditions.mjd - self.survey_features['last_observed'].feature
139 in_range = np.where((gap > self.min_gap) & (gap < self.max_gap) & (self.footprint > 0))
140 result[in_range] = 1
142 # mask out areas beyond the hour angle limit.
143 out_ha = np.where((conditions.HA > self.ha_limit) & (conditions.HA < (2.*np.pi - self.ha_limit)))[0]
144 result[out_ha] = 0
146 return result
149class Target_map_basis_function(Base_basis_function):
150 """Basis function that tracks number of observations and tries to match a specified spatial distribution
152 Parameters
153 ----------
154 filtername: (string 'r')
155 The name of the filter for this target map.
156 nside: int (default_nside)
157 The healpix resolution.
158 target_map : numpy array (None)
159 A healpix map showing the ratio of observations desired for all points on the sky
160 norm_factor : float (0.00010519)
161 for converting target map to number of observations. Should be the area of the camera
162 divided by the area of a healpixel divided by the sum of all your goal maps. Default
163 value assumes LSST foV has 1.75 degree radius and the standard goal maps. If using
164 mulitple filters, see lsst.sims.featureScheduler.utils.calc_norm_factor for a utility
165 that computes norm_factor.
166 out_of_bounds_val : float (-10.)
167 Reward value to give regions where there are no observations requested (unitless).
168 """
169 def __init__(self, filtername='r', nside=None, target_map=None,
170 norm_factor=None,
171 out_of_bounds_val=-10.):
173 super(Target_map_basis_function, self).__init__(nside=nside, filtername=filtername)
175 if norm_factor is None:
176 warnings.warn('No norm_factor set, use utils.calc_norm_factor if using multiple filters.')
177 self.norm_factor = 0.00010519
178 else:
179 self.norm_factor = norm_factor
181 self.survey_features = {}
182 # Map of the number of observations in filter
183 self.survey_features['N_obs'] = features.N_observations(filtername=filtername, nside=self.nside)
184 # Count of all the observations
185 self.survey_features['N_obs_count_all'] = features.N_obs_count(filtername=None)
186 if target_map is None:
187 self.target_map = utils.generate_goal_map(filtername=filtername, nside=self.nside)
188 else:
189 self.target_map = target_map
190 self.out_of_bounds_area = np.where(self.target_map == 0)[0]
191 self.out_of_bounds_val = out_of_bounds_val
192 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
193 self.all_indx = np.arange(self.result.size)
195 def _calc_value(self, conditions, indx=None):
196 """
197 Parameters
198 ----------
199 indx : list (None)
200 Index values to compute, if None, full map is computed
201 Returns
202 -------
203 Healpix reward map
204 """
205 result = self.result.copy()
206 if indx is None:
207 indx = self.all_indx
209 # Find out how many observations we want now at those points
210 goal_N = self.target_map[indx] * self.survey_features['N_obs_count_all'].feature * self.norm_factor
212 result[indx] = goal_N - self.survey_features['N_obs'].feature[indx]
213 result[self.out_of_bounds_area] = self.out_of_bounds_val
215 return result
218def azRelPoint(azs, pointAz):
219 azRelMoon = (azs - pointAz) % (2.0*np.pi)
220 if isinstance(azs, np.ndarray):
221 over = np.where(azRelMoon > np.pi)
222 azRelMoon[over] = 2. * np.pi - azRelMoon[over]
223 else:
224 if azRelMoon > np.pi:
225 azRelMoon = 2.0 * np.pi - azRelMoon
226 return azRelMoon
229class N_obs_high_am_basis_function(Base_basis_function):
230 """Reward only reward/count observations at high airmass
231 """
233 def __init__(self, nside=None, filtername='r', footprint=None, n_obs=3, season=300.,
234 am_limits=[1.5, 2.2], out_of_bounds_val=np.nan):
235 super(N_obs_high_am_basis_function, self).__init__(nside=nside, filtername=filtername)
236 self.footprint = footprint
237 self.out_footprint = np.where((footprint == 0) | np.isnan(footprint))
238 self.am_limits = am_limits
239 self.season = season
240 self.survey_features['last_n_mjds'] = features.Last_N_obs_times(nside=nside, filtername=filtername,
241 n_obs=n_obs)
243 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float) + out_of_bounds_val
244 self.out_of_bounds_val = out_of_bounds_val
246 def add_observation(self, observation, indx=None):
247 """
248 Parameters
249 ----------
250 observation : np.array
251 An array with information about the input observation
252 indx : np.array
253 The indices of the healpix map that the observation overlaps with
254 """
256 # Only count the observations if they are at the airmass limits
257 if (observation['airmass'] > np.min(self.am_limits)) & (observation['airmass'] < np.max(self.am_limits)):
258 for feature in self.survey_features:
259 self.survey_features[feature].add_observation(observation, indx=indx)
260 if self.update_on_newobs:
261 self.recalc = True
263 def check_feasibility(self, conditions):
264 """If there is logic to decide if something is feasible (e.g., only if moon is down),
265 it can be calculated here. Helps prevent full __call__ from being called more than needed.
266 """
267 result = True
268 reward = self._calc_value(conditions)
269 # If there are no non-NaN values, we're not feasible now
270 if True not in np.isfinite(reward):
271 result = False
273 return result
275 def _calc_value(self, conditions, indx=None):
276 result = self.result.copy()
277 behind_pix = np.where((int_rounded(conditions.mjd-self.survey_features['last_n_mjds'].feature[0]) > int_rounded(self.season)) &
278 (int_rounded(conditions.airmass) > int_rounded(np.min(self.am_limits))) &
279 (int_rounded(conditions.airmass) < int_rounded(np.max(self.am_limits))))
280 result[behind_pix] = 1
281 result[self.out_footprint] = self.out_of_bounds_val
283 # Update the last time we had an mjd
284 self.mjd_last = conditions.mjd + 0
285 self.recalc = False
286 self.value = result
288 return result
291class Ecliptic_basis_function(Base_basis_function):
292 """Mark the area around the ecliptic
293 """
295 def __init__(self, nside=None, distance_to_eclip=25.):
296 super(Ecliptic_basis_function, self).__init__(nside=nside)
297 self.distance_to_eclip = np.radians(distance_to_eclip)
298 ra, dec = _hpid2RaDec(nside, np.arange(hp.nside2npix(self.nside)))
299 self.result = np.zeros(ra.size)
300 coord = SkyCoord(ra=ra*u.rad, dec=dec*u.rad)
301 eclip_lat = coord.barycentrictrueecliptic.lat.radian
302 good = np.where(np.abs(eclip_lat) < self.distance_to_eclip)
303 self.result[good] += 1
305 def __call__(self, conditions, indx=None):
306 return self.result
309class N_obs_per_year_basis_function(Base_basis_function):
310 """Reward areas that have not been observed N-times in the last year
312 Parameters
313 ----------
314 filtername : str ('r')
315 The filter to track
316 footprint : np.array
317 Should be a HEALpix map. Values of 0 or np.nan will be ignored.
318 n_obs : int (3)
319 The number of observations to demand
320 season : float (300)
321 The amount of time to allow pass before marking a region as "behind". Default 365.25 (days).
322 season_start_hour : float (-2)
323 When to start the season relative to RA 180 degrees away from the sun (hours)
324 season_end_hour : float (2)
325 When to consider a season ending, the RA relative to the sun + 180 degrees. (hours)
326 """
327 def __init__(self, filtername='r', nside=None, footprint=None, n_obs=3, season=300,
328 season_start_hour=-4., season_end_hour=2.):
329 super(N_obs_per_year_basis_function, self).__init__(nside=nside, filtername=filtername)
330 self.footprint = footprint
331 self.n_obs = n_obs
332 self.season = season
333 self.season_start_hour = (season_start_hour)*np.pi/12. # To radians
334 self.season_end_hour = season_end_hour*np.pi/12. # To radians
336 self.survey_features['last_n_mjds'] = features.Last_N_obs_times(nside=nside, filtername=filtername,
337 n_obs=n_obs)
338 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
339 self.out_footprint = np.where((footprint == 0) | np.isnan(footprint))
341 def _calc_value(self, conditions, indx=None):
343 result = self.result.copy()
344 behind_pix = np.where((conditions.mjd-self.survey_features['last_n_mjds'].feature[0]) > self.season)
345 result[behind_pix] = 1
347 # let's ramp up the weight depending on how far into the observing season the healpix is
348 mid_season_ra = (conditions.sunRA + np.pi) % (2.*np.pi)
349 # relative RA
350 relative_ra = (conditions.ra - mid_season_ra) % (2.*np.pi)
351 relative_ra = (self.season_end_hour - relative_ra) % (2.*np.pi)
352 # ok, now
353 relative_ra[np.where(int_rounded(relative_ra) > int_rounded(self.season_end_hour-self.season_start_hour))] = 0
355 weight = relative_ra/(self.season_end_hour - self.season_start_hour)
356 result *= weight
358 # mask off anything outside the footprint
359 result[self.out_footprint] = 0
361 return result
364class Cadence_in_season_basis_function(Base_basis_function):
365 """Drive observations at least every N days in a given area
367 Parameters
368 ----------
369 drive_map : np.array
370 A HEALpix map with values of 1 where the cadence should be driven.
371 filtername : str
372 The filters that can count
373 season_span : float (2.5)
374 How long to consider a spot "in_season" (hours)
375 cadence : float (2.5)
376 How long to wait before activating the basis function (days)
377 """
379 def __init__(self, drive_map, filtername='griz', season_span=2.5, cadence=2.5, nside=None):
380 super(Cadence_in_season_basis_function, self).__init__(nside=nside, filtername=filtername)
381 self.drive_map = drive_map
382 self.season_span = season_span/12.*np.pi # To radians
383 self.cadence = cadence
384 self.survey_features['last_observed'] = features.Last_observed(nside=nside, filtername=filtername)
385 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
387 def _calc_value(self, conditions, indx=None):
388 result = self.result.copy()
389 ra_mid_season = (conditions.sunRA + np.pi) % (2.*np.pi)
391 angle_to_mid_season = np.abs(conditions.ra - ra_mid_season)
392 over = np.where(int_rounded(angle_to_mid_season) > int_rounded(np.pi))
393 angle_to_mid_season[over] = 2.*np.pi - angle_to_mid_season[over]
395 days_lag = conditions.mjd - self.survey_features['last_observed'].feature
397 active_pix = np.where((int_rounded(days_lag) >= int_rounded(self.cadence)) &
398 (self.drive_map == 1) &
399 (int_rounded(angle_to_mid_season) < int_rounded(self.season_span)))
400 result[active_pix] = 1.
402 return result
405class Season_coverage_basis_function(Base_basis_function):
406 """Basis function to encourage N observations per observing season
408 Parameters
409 ----------
410 footprint : healpix map (None)
411 The footprint where one should demand coverage every season
412 n_per_season : int (3)
413 The number of observations to attempt to gather every season
414 offset : healpix map
415 The offset to apply when computing the current season over the sky. utils.create_season_offset
416 is helpful for making this
417 season_frac_start : float (0.5)
418 Only start trying to gather observations after a season is fractionally this far over.
419 """
420 def __init__(self, filtername='r', nside=None, footprint=None, n_per_season=3, offset=None,
421 season_frac_start=0.5):
422 super(Season_coverage_basis_function, self).__init__(nside=nside, filtername=filtername)
424 self.n_per_season = n_per_season
425 self.footprint = footprint
426 self.survey_features['n_obs_season'] = features.N_observations_current_season(filtername=filtername,
427 nside=nside, offset=offset)
428 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
429 self.season_frac_start = season_frac_start
430 self.offset = offset
432 def _calc_value(self, conditions, indx=None):
433 result = self.result.copy()
434 season = utils.season_calc(conditions.night, offset=self.offset, floor=False)
435 # Find the area that still needs observation
436 feature = self.survey_features['n_obs_season'].feature
437 not_enough = np.where((self.footprint > 0) & (feature < self.n_per_season) &
438 ((int_rounded(season-np.floor(season)) > int_rounded(self.season_frac_start))) &
439 (season >= 0))
440 result[not_enough] = 1
441 return result
444class Footprint_nvis_basis_function(Base_basis_function):
445 """Basis function to drive observations of a given footprint. Good to target of opportunity targets
446 where one might want to observe a region 3 times.
448 Parameters
449 ----------
450 footprint : np.array
451 A healpix array (1 for desired, 0 for not desired) of the target footprint.
452 nvis : int (1)
453 The number of visits to try and gather
454 """
455 def __init__(self, filtername='r', nside=None, footprint=None,
456 nvis=1, out_of_bounds_val=np.nan):
457 super(Footprint_nvis_basis_function, self).__init__(nside=nside, filtername=filtername)
458 self.footprint = footprint
459 self.nvis = nvis
461 # Have a feature that tracks how many observations we have
462 self.survey_features = {}
463 # Map of the number of observations in filter
464 self.survey_features['N_obs'] = features.N_observations(filtername=filtername, nside=self.nside)
465 self.result = np.zeros(hp.nside2npix(nside))
466 self.result.fill(out_of_bounds_val)
467 self.out_of_bounds_val = out_of_bounds_val
469 def _calc_value(self, conditions, indx=None):
470 result = self.result.copy()
471 diff = int_rounded(self.footprint*self.nvis - self.survey_features['N_obs'].feature)
473 result[np.where(diff > 0)] = 1
475 # Any spot where we have enough visits is out of bounds now.
476 result[np.where(diff <= 0)] = self.out_of_bounds_val
477 return result
480class Third_observation_basis_function(Base_basis_function):
481 """If there have been observations in two filters long enough ago, go for a third
483 Parameters
484 ----------
485 gap_min : float (40.)
486 The minimum time gap to consider a pixel good (minutes)
487 gap_max : float (120)
488 The maximum time to consider going for a pair (minutes)
489 """
491 def __init__(self, nside=32, filtername1='r', filtername2='z', gap_min=40., gap_max=120.):
492 super(Third_observation_basis_function, self).__init__(nside=nside)
493 self.filtername1 = filtername1
494 self.filtername2 = filtername2
495 self.gap_min = int_rounded(gap_min/60./24.)
496 self.gap_max = int_rounded(gap_max/60./24.)
498 self.survey_features = {}
499 self.survey_features['last_obs_f1'] = features.Last_observed(filtername=filtername1, nside=nside)
500 self.survey_features['last_obs_f2'] = features.Last_observed(filtername=filtername2, nside=nside)
501 self.result = np.empty(hp.nside2npix(self.nside))
502 self.result.fill(np.nan)
504 def _calc_value(self, conditions, indx=None):
505 result = self.result.copy()
506 d1 = int_rounded(conditions.mjd - self.survey_features['last_obs_f1'].feature)
507 d2 = int_rounded(conditions.mjd - self.survey_features['last_obs_f2'].feature)
508 good = np.where((d1 > self.gap_min) & (d1 < self.gap_max) &
509 (d2 > self.gap_min) & (d2 < self.gap_max))
510 result[good] = 1
511 return result
514class Avoid_Fast_Revists(Base_basis_function):
515 """Marks targets as unseen if they are in a specified time window in order to avoid fast revisits.
517 Parameters
518 ----------
519 filtername: (string 'r')
520 The name of the filter for this target map.
521 gap_min : float (25.)
522 Minimum time for the gap (minutes).
523 nside: int (default_nside)
524 The healpix resolution.
525 penalty_val : float (np.nan)
526 The reward value to use for regions to penalize. Will be masked if set to np.nan (default).
527 """
528 def __init__(self, filtername='r', nside=None, gap_min=25.,
529 penalty_val=np.nan):
530 super(Avoid_Fast_Revists, self).__init__(nside=nside, filtername=filtername)
532 self.filtername = filtername
533 self.penalty_val = penalty_val
535 self.gap_min = int_rounded(gap_min/60./24.)
536 self.nside = nside
538 self.survey_features = dict()
539 self.survey_features['Last_observed'] = features.Last_observed(filtername=filtername, nside=nside)
541 def _calc_value(self, conditions, indx=None):
542 result = np.ones(hp.nside2npix(self.nside), dtype=float)
543 if indx is None:
544 indx = np.arange(result.size)
545 diff = int_rounded(conditions.mjd - self.survey_features['Last_observed'].feature[indx])
546 bad = np.where(diff < self.gap_min)[0]
547 result[indx[bad]] = self.penalty_val
548 return result
551class Near_sun_twilight_basis_function(Base_basis_function):
552 """Reward looking into the twilight for NEOs at high airmass
554 Parameters
555 ----------
556 max_airmass : float (2.5)
557 The maximum airmass to try and observe (unitless)
558 """
560 def __init__(self, nside=None, max_airmass=2.5):
561 super(Near_sun_twilight_basis_function, self).__init__(nside=nside)
562 self.max_airmass = int_rounded(max_airmass)
563 self.result = np.zeros(hp.nside2npix(self.nside))
565 def _calc_value(self, conditions, indx=None):
566 result = self.result.copy()
567 good_pix = np.where((int_rounded(conditions.airmass) < self.max_airmass) &
568 (int_rounded(conditions.az_to_sun) < int_rounded(np.pi/2.)))
569 result[good_pix] = conditions.airmass[good_pix] / self.max_airmass.value
570 return result
573class Visit_repeat_basis_function(Base_basis_function):
574 """
575 Basis function to reward re-visiting an area on the sky. Looking for Solar System objects.
577 Parameters
578 ----------
579 gap_min : float (15.)
580 Minimum time for the gap (minutes)
581 gap_max : float (45.)
582 Maximum time for a gap
583 filtername : str ('r')
584 The filter(s) to count with pairs
585 npairs : int (1)
586 The number of pairs of observations to attempt to gather
587 """
588 def __init__(self, gap_min=25., gap_max=45.,
589 filtername='r', nside=None, npairs=1):
591 super(Visit_repeat_basis_function, self).__init__(nside=nside, filtername=filtername)
593 self.gap_min = int_rounded(gap_min/60./24.)
594 self.gap_max = int_rounded(gap_max/60./24.)
595 self.npairs = npairs
597 self.survey_features = {}
598 # Track the number of pairs that have been taken in a night
599 self.survey_features['Pair_in_night'] = features.Pair_in_night(filtername=filtername,
600 gap_min=gap_min, gap_max=gap_max,
601 nside=nside)
602 # When was it last observed
603 # XXX--since this feature is also in Pair_in_night, I should just access that one!
604 self.survey_features['Last_observed'] = features.Last_observed(filtername=filtername,
605 nside=nside)
607 def _calc_value(self, conditions, indx=None):
608 result = np.zeros(hp.nside2npix(self.nside), dtype=float)
609 if indx is None:
610 indx = np.arange(result.size)
611 diff = int_rounded(conditions.mjd - self.survey_features['Last_observed'].feature[indx])
612 good = np.where((diff >= self.gap_min) & (diff <= self.gap_max) &
613 (self.survey_features['Pair_in_night'].feature[indx] < self.npairs))[0]
614 result[indx[good]] += 1.
615 return result
618class M5_diff_basis_function(Base_basis_function):
619 """Basis function based on the 5-sigma depth.
620 Look up the best depth a healpixel achieves, and compute
621 the limiting depth difference given current conditions
622 """
623 def __init__(self, filtername='r', nside=None):
625 super(M5_diff_basis_function, self).__init__(nside=nside, filtername=filtername)
626 # Need to look up the deepest m5 values for all the healpixels
627 m5p = M5percentiles()
628 self.dark_map = m5p.dark_map(filtername=filtername, nside_out=self.nside)
630 def _calc_value(self, conditions, indx=None):
631 # No way to get the sign on this right the first time.
632 result = conditions.M5Depth[self.filtername] - self.dark_map
633 return result
636class Strict_filter_basis_function(Base_basis_function):
637 """Remove the bonus for staying in the same filter if certain conditions are met.
639 If the moon rises/sets or twilight starts/ends, it makes a lot of sense to consider
640 a filter change. This basis function rewards if it matches the current filter, the moon rises or sets,
641 twilight starts or stops, or there has been a large gap since the last observation.
643 Paramters
644 ---------
645 time_lag : float (10.)
646 If there is a gap between observations longer than this, let the filter change (minutes)
647 twi_change : float (-18.)
648 The sun altitude to consider twilight starting/ending (degrees)
649 note_free : str ('DD')
650 No penalty for changing filters if the last observation note field includes string.
651 Useful for giving a free filter change after deep drilling sequence
652 """
653 def __init__(self, time_lag=10., filtername='r', twi_change=-18., note_free='DD'):
655 super(Strict_filter_basis_function, self).__init__(filtername=filtername)
657 self.time_lag = time_lag/60./24. # Convert to days
658 self.twi_change = np.radians(twi_change)
660 self.survey_features = {}
661 self.survey_features['Last_observation'] = features.Last_observation()
662 self.note_free = note_free
664 def _calc_value(self, conditions, **kwargs):
665 # Did the moon set or rise since last observation?
666 moon_changed = conditions.moonAlt * self.survey_features['Last_observation'].feature['moonAlt'] < 0
668 # Are we already in the filter (or at start of night)?
669 in_filter = (conditions.current_filter == self.filtername) | (conditions.current_filter is None)
671 # Has enough time past?
672 time_past = int_rounded(conditions.mjd - self.survey_features['Last_observation'].feature['mjd']) > int_rounded(self.time_lag)
674 # Did twilight start/end?
675 twi_changed = (conditions.sunAlt - self.twi_change) * (self.survey_features['Last_observation'].feature['sunAlt']- self.twi_change) < 0
677 # Did we just finish a DD sequence
678 wasDD = self.note_free in self.survey_features['Last_observation'].feature['note']
680 # Is the filter mounted?
681 mounted = self.filtername in conditions.mounted_filters
683 if (moon_changed | in_filter | time_past | twi_changed | wasDD) & mounted:
684 result = 1.
685 else:
686 result = 0.
688 return result
691class Goal_Strict_filter_basis_function(Base_basis_function):
692 """Remove the bonus for staying in the same filter if certain conditions are met.
694 If the moon rises/sets or twilight starts/ends, it makes a lot of sense to consider
695 a filter change. This basis function rewards if it matches the current filter, the moon rises or sets,
696 twilight starts or stops, or there has been a large gap since the last observation.
698 Parameters
699 ---------
700 time_lag_min: Minimum time after a filter change for which a new filter change will receive zero reward, or
701 be denied at all (see unseen_before_lag).
702 time_lag_max: Time after a filter change where the reward for changing filters achieve its maximum.
703 time_lag_boost: Time after a filter change to apply a boost on the reward.
704 boost_gain: A multiplier factor for the reward after time_lag_boost.
705 unseen_before_lag: If True will make it impossible to switch filter before time_lag has passed.
706 filtername: The filter for which this basis function will be used.
707 tag: When using filter proportion use only regions with this tag to count for observations.
708 twi_change: Switch reward on when twilight changes.
709 proportion: The expected filter proportion distribution.
710 aways_available: If this is true the basis function will aways be computed regardless of the feasibility. If
711 False a more detailed feasibility check is performed. When set to False, it may speed up the computation
712 process by avoiding to compute the reward functions paired with this bf, when observation is not feasible.
714 """
716 def __init__(self, time_lag_min=10., time_lag_max=30.,
717 time_lag_boost=60., boost_gain=2.0, unseen_before_lag=False,
718 filtername='r', tag=None, twi_change=-18., proportion=1.0, aways_available=False):
720 super(Goal_Strict_filter_basis_function, self).__init__(filtername=filtername)
722 self.time_lag_min = time_lag_min / 60. / 24. # Convert to days
723 self.time_lag_max = time_lag_max / 60. / 24. # Convert to days
724 self.time_lag_boost = time_lag_boost / 60. / 24.
725 self.boost_gain = boost_gain
726 self.unseen_before_lag = unseen_before_lag
728 self.twi_change = np.radians(twi_change)
729 self.proportion = proportion
730 self.aways_available = aways_available
732 self.survey_features = {}
733 self.survey_features['Last_observation'] = features.Last_observation()
734 self.survey_features['Last_filter_change'] = features.LastFilterChange()
735 self.survey_features['N_obs_all'] = features.N_obs_count(filtername=None)
736 self.survey_features['N_obs'] = features.N_obs_count(filtername=filtername,
737 tag=tag)
739 def filter_change_bonus(self, time):
741 lag_min = self.time_lag_min
742 lag_max = self.time_lag_max
744 a = 1. / (lag_max - lag_min)
745 b = -a * lag_min
747 bonus = a * time + b
748 # How far behind we are with respect to proportion?
749 nobs = self.survey_features['N_obs'].feature
750 nobs_all = self.survey_features['N_obs_all'].feature
751 goal = self.proportion
752 # need = 1. - nobs / nobs_all + goal if nobs_all > 0 else 1. + goal
753 need = goal / nobs * nobs_all if nobs > 0 else 1.
754 # need /= goal
755 if hasattr(time, '__iter__'):
756 before_lag = np.where(time <= lag_min)
757 bonus[before_lag] = -np.inf if self.unseen_before_lag else 0.
758 after_lag = np.where(time >= lag_max)
759 bonus[after_lag] = 1. if time < self.time_lag_boost else self.boost_gain
760 elif int_rounded(time) <= int_rounded(lag_min):
761 return -np.inf if self.unseen_before_lag else 0.
762 elif int_rounded(time) >= int_rounded(lag_max):
763 return 1. if int_rounded(time) < int_rounded(self.time_lag_boost) else self.boost_gain
765 return bonus * need
767 def check_feasibility(self, conditions):
768 """
769 This method makes a pre-check of the feasibility of this basis function. If a basis function return False
770 on the feasibility check, it won't computed at all.
772 :return:
773 """
775 # Make a quick check about the feasibility of this basis function. If current filter is none, telescope
776 # is parked and we could, in principle, switch to any filter. If this basis function computes reward for
777 # the current filter, then it is also feasible. At last we check for an "aways_available" flag. Meaning, we
778 # force this basis function to be aways be computed.
779 if conditions.current_filter is None or conditions.current_filter == self.filtername or self.aways_available:
780 return True
782 # If we arrive here, we make some extra checks to make sure this bf is feasible and should be computed.
784 # Did the moon set or rise since last observation?
785 moon_changed = conditions.moonAlt * self.survey_features['Last_observation'].feature['moonAlt'] < 0
787 # Are we already in the filter (or at start of night)?
788 not_in_filter = (conditions.current_filter != self.filtername)
790 # Has enough time past?
791 lag = conditions.mjd - self.survey_features['Last_filter_change'].feature['mjd']
792 time_past = int_rounded(lag) > int_rounded(self.time_lag_min)
794 # Did twilight start/end?
795 twi_changed = (conditions.sunAlt - self.twi_change) * \
796 (self.survey_features['Last_observation'].feature['sunAlt'] - self.twi_change) < 0
798 # Did we just finish a DD sequence
799 wasDD = self.survey_features['Last_observation'].feature['note'] == 'DD'
801 # Is the filter mounted?
802 mounted = self.filtername in conditions.mounted_filters
804 if (moon_changed | time_past | twi_changed | wasDD) & mounted & not_in_filter:
805 return True
806 else:
807 return False
809 def _calc_value(self, conditions, **kwargs):
811 if conditions.current_filter is None:
812 return 0. # no bonus if no filter is mounted
813 # elif self.condition_features['Current_filter'].feature == self.filtername:
814 # return 0. # no bonus if on the filter already
816 # Did the moon set or rise since last observation?
817 moon_changed = conditions.moonAlt * \
818 self.survey_features['Last_observation'].feature['moonAlt'] < 0
820 # Are we already in the filter (or at start of night)?
821 # not_in_filter = (self.condition_features['Current_filter'].feature != self.filtername)
823 # Has enough time past?
824 lag = conditions.mjd - self.survey_features['Last_filter_change'].feature['mjd']
825 time_past = lag > self.time_lag_min
827 # Did twilight start/end?
828 twi_changed = (conditions.sunAlt - self.twi_change) * (
829 self.survey_features['Last_observation'].feature['sunAlt'] - self.twi_change) < 0
831 # Did we just finish a DD sequence
832 wasDD = self.survey_features['Last_observation'].feature['note'] == 'DD'
834 # Is the filter mounted?
835 mounted = self.filtername in conditions.mounted_filters
837 if (moon_changed | time_past | twi_changed | wasDD) & mounted:
838 result = self.filter_change_bonus(lag) if time_past else 0.
839 else:
840 result = -100. if self.unseen_before_lag else 0.
842 return result
845class Filter_change_basis_function(Base_basis_function):
846 """Reward staying in the current filter.
847 """
848 def __init__(self, filtername='r'):
849 super(Filter_change_basis_function, self).__init__(filtername=filtername)
851 def _calc_value(self, conditions, **kwargs):
853 if (conditions.current_filter == self.filtername) | (conditions.current_filter is None):
854 result = 1.
855 else:
856 result = 0.
857 return result
860class Slewtime_basis_function(Base_basis_function):
861 """Reward slews that take little time
863 Parameters
864 ----------
865 max_time : float (135)
866 The estimated maximum slewtime (seconds). Used to normalize so the basis function
867 spans ~ -1-0 in reward units.
868 """
869 def __init__(self, max_time=135., filtername='r', nside=None):
870 super(Slewtime_basis_function, self).__init__(nside=nside, filtername=filtername)
872 self.maxtime = max_time
873 self.nside = nside
874 self.filtername = filtername
875 self.result = np.zeros(hp.nside2npix(nside), dtype=float)
877 def add_observation(self, observation, indx=None):
878 # No tracking of observations in this basis function. Purely based on conditions.
879 pass
881 def _calc_value(self, conditions, indx=None):
882 # If we are in a different filter, the Filter_change_basis_function will take it
883 if conditions.current_filter != self.filtername:
884 result = 0
885 else:
886 # Need to make sure smaller slewtime is larger reward.
887 if np.size(conditions.slewtime) > 1:
888 result = self.result.copy()
889 good = ~np.isnan(conditions.slewtime)
890 result[good] = -conditions.slewtime[good]/self.maxtime
891 else:
892 result = -conditions.slewtime/self.maxtime
893 return result
896class Aggressive_Slewtime_basis_function(Base_basis_function):
897 """Reward slews that take little time
899 XXX--not sure how this is different from Slewtime_basis_function?
900 Looks like it's checking the slewtime to the field position rather than the healpix maybe?
901 """
903 def __init__(self, max_time=135., order=1., hard_max=None, filtername='r', nside=None):
904 super(Aggressive_Slewtime_basis_function, self).__init__(nside=nside, filtername=filtername)
906 self.maxtime = max_time
907 self.hard_max = hard_max
908 self.order = order
909 self.result = np.zeros(hp.nside2npix(nside), dtype=float)
911 def _calc_value(self, conditions, indx=None):
912 # If we are in a different filter, the Filter_change_basis_function will take it
913 if conditions.current_filter != self.filtername:
914 result = 0.
915 else:
916 # Need to make sure smaller slewtime is larger reward.
917 if np.size(self.condition_features['slewtime'].feature) > 1:
918 result = self.result.copy()
919 result.fill(np.nan)
921 good = np.where(np.bitwise_and(conditions.slewtime > 0.,
922 conditions.slewtime < self.maxtime))
923 result[good] = ((self.maxtime - conditions.slewtime[good]) /
924 self.maxtime) ** self.order
925 if self.hard_max is not None:
926 not_so_good = np.where(conditions.slewtime > self.hard_max)
927 result[not_so_good] -= 10.
928 fields = np.unique(conditions.hp2fields[good])
929 for field in fields:
930 hp_indx = np.where(conditions.hp2fields == field)
931 result[hp_indx] = np.min(result[hp_indx])
932 else:
933 result = (self.maxtime - conditions.slewtime) / self.maxtime
934 return result
937class Skybrightness_limit_basis_function(Base_basis_function):
938 """Mask regions that are outside a sky brightness limit
940 XXX--TODO: This should probably go to the mask basis functions.
942 Parameters
943 ----------
944 min : float (20.)
945 The minimum sky brightness (mags).
946 max : float (30.)
947 The maximum sky brightness (mags).
949 """
950 def __init__(self, nside=None, filtername='r', sbmin=20., sbmax=30.):
951 super(Skybrightness_limit_basis_function, self).__init__(nside=nside, filtername=filtername)
953 self.min = int_rounded(sbmin)
954 self.max = int_rounded(sbmax)
955 self.result = np.empty(hp.nside2npix(self.nside), dtype=float)
956 self.result.fill(np.nan)
958 def _calc_value(self, conditions, indx=None):
959 result = self.result.copy()
961 good = np.where(np.bitwise_and(int_rounded(conditions.skybrightness[self.filtername]) > self.min,
962 int_rounded(conditions.skybrightness[self.filtername]) < self.max))
963 result[good] = 1.0
965 return result
968class CableWrap_unwrap_basis_function(Base_basis_function):
969 """
970 Parameters
971 ----------
972 minAz : float (20.)
973 The minimum azimuth to activate bf (degrees)
974 maxAz : float (82.)
975 The maximum azimuth to activate bf (degrees)
976 unwrap_until: float (90.)
977 The window in which the bf is activated (degrees)
978 """
979 def __init__(self, nside=None, minAz=-270., maxAz=270., minAlt=20., maxAlt=82.,
980 activate_tol=20., delta_unwrap=1.2, unwrap_until=70., max_duration=30.):
981 super(CableWrap_unwrap_basis_function, self).__init__(nside=nside)
983 self.minAz = np.radians(minAz)
984 self.maxAz = np.radians(maxAz)
986 self.activate_tol = np.radians(activate_tol)
987 self.delta_unwrap = np.radians(delta_unwrap)
988 self.unwrap_until = np.radians(unwrap_until)
990 self.minAlt = np.radians(minAlt)
991 self.maxAlt = np.radians(maxAlt)
992 # Convert to half-width for convienence
993 self.nside = nside
994 self.active = False
995 self.unwrap_direction = 0. # either -1., 0., 1.
996 self.max_duration = max_duration/60./24. # Convert to days
997 self.activation_time = None
998 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
1000 def _calc_value(self, conditions, indx=None):
1002 result = self.result.copy()
1004 current_abs_rad = np.radians(conditions.az)
1005 unseen = np.where(np.bitwise_or(conditions.alt < self.minAlt,
1006 conditions.alt > self.maxAlt))
1007 result[unseen] = np.nan
1009 if (self.minAz + self.activate_tol < current_abs_rad < self.maxAz - self.activate_tol) and not self.active:
1010 return result
1011 elif self.active and self.unwrap_direction == 1 and current_abs_rad > self.minAz+self.unwrap_until:
1012 self.active = False
1013 self.unwrap_direction = 0.
1014 self.activation_time = None
1015 return result
1016 elif self.active and self.unwrap_direction == -1 and current_abs_rad < self.maxAz-self.unwrap_until:
1017 self.active = False
1018 self.unwrap_direction = 0.
1019 self.activation_time = None
1020 return result
1021 elif (self.activation_time is not None and
1022 conditions.mjd - self.activation_time > self.max_duration):
1023 self.active = False
1024 self.unwrap_direction = 0.
1025 self.activation_time = None
1026 return result
1028 if not self.active:
1029 self.activation_time = conditions.mjd
1030 if current_abs_rad < 0.:
1031 self.unwrap_direction = 1 # clock-wise unwrap
1032 else:
1033 self.unwrap_direction = -1 # counter-clock-wise unwrap
1035 self.active = True
1037 max_abs_rad = self.maxAz
1038 min_abs_rad = self.minAz
1040 TWOPI = 2.*np.pi
1042 # Compute distance and accumulated az.
1043 norm_az_rad = np.divmod(conditions.az - min_abs_rad, TWOPI)[1] + min_abs_rad
1044 distance_rad = divmod(norm_az_rad - current_abs_rad, TWOPI)[1]
1045 get_shorter = np.where(distance_rad > np.pi)
1046 distance_rad[get_shorter] -= TWOPI
1047 accum_abs_rad = current_abs_rad + distance_rad
1049 # Compute wrap regions and fix distances
1050 mask_max = np.where(accum_abs_rad > max_abs_rad)
1051 distance_rad[mask_max] -= TWOPI
1052 mask_min = np.where(accum_abs_rad < min_abs_rad)
1053 distance_rad[mask_min] += TWOPI
1055 # Step-2: Repeat but now with compute reward to unwrap using specified delta_unwrap
1056 unwrap_current_abs_rad = current_abs_rad - (np.abs(self.delta_unwrap) if self.unwrap_direction > 0
1057 else -np.abs(self.delta_unwrap))
1058 unwrap_distance_rad = divmod(norm_az_rad - unwrap_current_abs_rad, TWOPI)[1]
1059 unwrap_get_shorter = np.where(unwrap_distance_rad > np.pi)
1060 unwrap_distance_rad[unwrap_get_shorter] -= TWOPI
1061 unwrap_distance_rad = np.abs(unwrap_distance_rad)
1063 if self.unwrap_direction < 0:
1064 mask = np.where(accum_abs_rad > unwrap_current_abs_rad)
1065 else:
1066 mask = np.where(accum_abs_rad < unwrap_current_abs_rad)
1068 # Finally build reward map
1069 result = (1. - unwrap_distance_rad/np.max(unwrap_distance_rad))**2.
1070 result[mask] = 0.
1071 result[unseen] = np.nan
1073 return result
1076class Cadence_enhance_basis_function(Base_basis_function):
1077 """Drive a certain cadence
1078 Parameters
1079 ----------
1080 filtername : str ('gri')
1081 The filter(s) that should be grouped together
1082 supress_window : list of float
1083 The start and stop window for when observations should be repressed (days)
1084 apply_area : healpix map
1085 The area over which to try and drive the cadence. Good values as 1, no candece drive 0.
1086 Probably works as a bool array too."""
1087 def __init__(self, filtername='gri', nside=None,
1088 supress_window=[0, 1.8], supress_val=-0.5,
1089 enhance_window=[2.1, 3.2], enhance_val=1.,
1090 apply_area=None):
1091 super(Cadence_enhance_basis_function, self).__init__(nside=nside, filtername=filtername)
1093 self.supress_window = np.sort(supress_window)
1094 self.supress_val = supress_val
1095 self.enhance_window = np.sort(enhance_window)
1096 self.enhance_val = enhance_val
1098 self.survey_features = {}
1099 self.survey_features['last_observed'] = features.Last_observed(filtername=filtername)
1101 self.empty = np.zeros(hp.nside2npix(self.nside), dtype=float)
1102 # No map, try to drive the whole area
1103 if apply_area is None:
1104 self.apply_indx = np.arange(self.empty.size)
1105 else:
1106 self.apply_indx = np.where(apply_area != 0)[0]
1108 def _calc_value(self, conditions, indx=None):
1109 # copy an empty array
1110 result = self.empty.copy()
1111 if indx is not None:
1112 ind = np.intersect1d(indx, self.apply_indx)
1113 else:
1114 ind = self.apply_indx
1115 if np.size(ind) == 0:
1116 result = 0
1117 else:
1118 mjd_diff = conditions.mjd - self.survey_features['last_observed'].feature[ind]
1119 to_supress = np.where((int_rounded(mjd_diff) > int_rounded(self.supress_window[0])) &
1120 (int_rounded(mjd_diff) < int_rounded(self.supress_window[1])))
1121 result[ind[to_supress]] = self.supress_val
1122 to_enhance = np.where((int_rounded(mjd_diff) > int_rounded(self.enhance_window[0])) &
1123 (int_rounded(mjd_diff) < int_rounded(self.enhance_window[1])))
1124 result[ind[to_enhance]] = self.enhance_val
1125 return result
1128# https://docs.astropy.org/en/stable/_modules/astropy/modeling/functional_models.html#Trapezoid1D
1129def trapezoid(x, amplitude, x_0, width, slope):
1130 """One dimensional Trapezoid model function"""
1131 # Compute the four points where the trapezoid changes slope
1132 # x1 <= x2 <= x3 <= x4
1133 x2 = x_0 - width / 2.
1134 x3 = x_0 + width / 2.
1135 x1 = x2 - amplitude / slope
1136 x4 = x3 + amplitude / slope
1138 result = x*0
1140 # Compute model values in pieces between the change points
1141 range_a = np.logical_and(x >= x1, x < x2)
1142 range_b = np.logical_and(x >= x2, x < x3)
1143 range_c = np.logical_and(x >= x3, x < x4)
1145 result[range_a] = slope * (x[range_a] - x1)
1146 result[range_b] = amplitude
1147 result[range_c] = slope * (x4 - x[range_c])
1149 return result
1152class Cadence_enhance_trapezoid_basis_function(Base_basis_function):
1153 """Drive a certain cadence, like Cadence_enhance_basis_function but with smooth transitions
1154 Parameters
1155 ----------
1156 filtername : str ('gri')
1157 The filter(s) that should be grouped together
1159 XXX--fill out doc string!
1160 """
1161 def __init__(self, filtername='gri', nside=None,
1162 delay_width=2, delay_slope=2., delay_peak=0, delay_amp=0.5,
1163 enhance_width=3., enhance_slope=2., enhance_peak=4., enhance_amp=1.,
1164 apply_area=None, season_limit=None):
1165 super(Cadence_enhance_trapezoid_basis_function, self).__init__(nside=nside, filtername=filtername)
1167 self.delay_width = delay_width
1168 self.delay_slope = delay_slope
1169 self.delay_peak = delay_peak
1170 self.delay_amp = delay_amp
1171 self.enhance_width = enhance_width
1172 self.enhance_slope = enhance_slope
1173 self.enhance_peak = enhance_peak
1174 self.enhance_amp = enhance_amp
1176 self.season_limit = season_limit/12*np.pi # To radians
1178 self.survey_features = {}
1179 self.survey_features['last_observed'] = features.Last_observed(filtername=filtername)
1181 self.empty = np.zeros(hp.nside2npix(self.nside), dtype=float)
1182 # No map, try to drive the whole area
1183 if apply_area is None:
1184 self.apply_indx = np.arange(self.empty.size)
1185 else:
1186 self.apply_indx = np.where(apply_area != 0)[0]
1188 def suppress_enhance(self, x):
1189 result = x*0
1190 result -= trapezoid(x, self.delay_amp, self.delay_peak, self.delay_width, self.delay_slope)
1191 result += trapezoid(x, self.enhance_amp, self.enhance_peak, self.enhance_width, self.enhance_slope)
1193 return result
1195 def season_calc(self, conditions):
1196 ra_mid_season = (conditions.sunRA + np.pi) % (2.*np.pi)
1197 angle_to_mid_season = np.abs(conditions.ra - ra_mid_season)
1198 over = np.where(int_rounded(angle_to_mid_season) > int_rounded(np.pi))
1199 angle_to_mid_season[over] = 2.*np.pi - angle_to_mid_season[over]
1201 return angle_to_mid_season
1203 def _calc_value(self, conditions, indx=None):
1204 # copy an empty array
1205 result = self.empty.copy()
1206 if indx is not None:
1207 ind = np.intersect1d(indx, self.apply_indx)
1208 else:
1209 ind = self.apply_indx
1210 if np.size(ind) == 0:
1211 result = 0
1212 else:
1213 mjd_diff = conditions.mjd - self.survey_features['last_observed'].feature[ind]
1214 result[ind] += self.suppress_enhance(mjd_diff)
1216 if self.season_limit is not None:
1217 radians_to_midseason = self.season_calc(conditions)
1218 outside_season = np.where(radians_to_midseason > self.season_limit)
1219 result[outside_season] = 0
1222 return result
1225class Azimuth_basis_function(Base_basis_function):
1226 """Reward staying in the same azimuth range. Possibly better than using slewtime, especially when selecting a large area of sky.
1228 Parameters
1229 ----------
1231 """
1233 def __init__(self, nside=None):
1234 super(Azimuth_basis_function, self).__init__(nside=nside)
1236 def _calc_value(self, conditions, indx=None):
1237 az_dist = conditions.az - conditions.telAz
1238 az_dist = az_dist % (2.*np.pi)
1239 over = np.where(az_dist > np.pi)
1240 az_dist[over] = 2. * np.pi - az_dist[over]
1241 # Normalize sp between 0 and 1
1242 result = az_dist/np.pi
1243 return result
1246class Az_modulo_basis_function(Base_basis_function):
1247 """Try to replicate the Rothchild et al cadence forcing by only observing on limited az ranges per night.
1249 Parameters
1250 ----------
1251 az_limits : list of float pairs (None)
1252 The azimuth limits (degrees) to use.
1253 """
1254 def __init__(self, nside=None, az_limits=None, out_of_bounds_val=-1.):
1255 super(Az_modulo_basis_function, self).__init__(nside=nside)
1256 self.result = np.ones(hp.nside2npix(self.nside))
1257 if az_limits is None:
1258 spread = 100./2.
1259 self.az_limits = np.radians([[360-spread, spread],
1260 [90.-spread, 90.+spread],
1261 [180.-spread, 180.+spread]])
1262 else:
1263 self.az_limits = np.radians(az_limits)
1264 self.mod_val = len(self.az_limits)
1265 self.out_of_bounds_val = out_of_bounds_val
1267 def _calc_value(self, conditions, indx=None):
1268 result = self.result.copy()
1269 az_lim = self.az_limits[np.max(conditions.night) % self.mod_val]
1271 if az_lim[0] < az_lim[1]:
1272 out_pix = np.where((int_rounded(conditions.az) < int_rounded(az_lim[0])) |
1273 (int_rounded(conditions.az) > int_rounded(az_lim[1])))
1274 else:
1275 out_pix = np.where((int_rounded(conditions.az) < int_rounded(az_lim[0])) |
1276 (int_rounded(conditions.az) > int_rounded(az_lim[1])))[0]
1277 result[out_pix] = self.out_of_bounds_val
1278 return result
1281class Dec_modulo_basis_function(Base_basis_function):
1282 """Emphasize dec bands on a nightly varying basis
1284 Parameters
1285 ----------
1286 dec_limits : list of float pairs (None)
1287 The azimuth limits (degrees) to use.
1288 """
1289 def __init__(self, nside=None, dec_limits=None, out_of_bounds_val=-1.):
1290 super(Dec_modulo_basis_function, self).__init__(nside=nside)
1292 npix = hp.nside2npix(nside)
1293 hpids = np.arange(npix)
1294 ra, dec = _hpid2RaDec(nside, hpids)
1296 self.results = []
1298 if dec_limits is None:
1299 self.dec_limits = np.radians([[-90., -32.8],
1300 [-32.8, -12.],
1301 [-12., 35.]])
1302 else:
1303 self.dec_limits = np.radians(dec_limits)
1304 self.mod_val = len(self.dec_limits)
1305 self.out_of_bounds_val = out_of_bounds_val
1307 for limits in self.dec_limits:
1308 good = np.where((dec >= limits[0]) & (dec < limits[1]))[0]
1309 tmp = np.zeros(npix)
1310 tmp[good] = 1
1311 self.results.append(tmp)
1313 def _calc_value(self, conditions, indx=None):
1314 night_index = np.max(conditions.night % self.mod_val)
1315 result = self.results[night_index]
1317 return result
1320class Map_modulo_basis_function(Base_basis_function):
1321 """Similar to Dec_modulo, but now use input masks
1323 Parameters
1324 ----------
1325 inmaps : list of hp arrays
1326 """
1327 def __init__(self, inmaps):
1328 nside = hp.npix2nside(np.size(inmaps[0]))
1329 super(Map_modulo_basis_function, self).__init__(nside=nside)
1330 self.maps = inmaps
1331 self.mod_val = len(inmaps)
1333 def _calc_value(self, conditions, indx=None):
1334 indx = np.max(conditions.night % self.mod_val)
1335 result = self.maps[indx]
1336 return result
1339class Good_seeing_basis_function(Base_basis_function):
1340 """Drive observations in good seeing conditions"""
1342 def __init__(self, nside=None, filtername='r', footprint=None, FWHMeff_limit=0.8,
1343 mag_diff=0.75):
1344 super(Good_seeing_basis_function, self).__init__(nside=nside)
1346 self.filtername = filtername
1347 self.FWHMeff_limit = int_rounded(FWHMeff_limit)
1348 if footprint is None:
1349 fp = utils.standard_goals(nside=nside)[filtername]
1350 else:
1351 fp = footprint
1352 self.out_of_bounds = np.where(fp == 0)[0]
1353 self.result = fp*0
1355 self.mag_diff = int_rounded(mag_diff)
1356 self.survey_features = {}
1357 self.survey_features['coadd_depth_all'] = features.Coadded_depth(filtername=filtername,
1358 nside=nside)
1359 self.survey_features['coadd_depth_good'] = features.Coadded_depth(filtername=filtername,
1360 nside=nside,
1361 FWHMeff_limit=FWHMeff_limit)
1363 def _calc_value(self, conditions, **kwargs):
1364 # Seeing is "bad"
1365 if int_rounded(conditions.FWHMeff[self.filtername].min()) > self.FWHMeff_limit:
1366 return 0
1367 result = self.result.copy()
1369 diff = self.survey_features['coadd_depth_all'].feature - self.survey_features['coadd_depth_good'].feature
1370 # Where are there things we want to observe?
1371 good_pix = np.where((int_rounded(diff) > self.mag_diff) &
1372 (int_rounded(conditions.FWHMeff[self.filtername]) <= self.FWHMeff_limit))
1373 # Hm, should this scale by the mag differences? Probably.
1374 result[good_pix] = diff[good_pix]
1375 result[self.out_of_bounds] = 0
1377 return result
1380class Template_generate_basis_function(Base_basis_function):
1381 """Emphasize areas that have not been observed in a long time
1383 Parameters
1384 ----------
1385 day_gap : float (250.)
1386 How long to wait before boosting the reward (days)
1387 footprint : np.array (None)
1388 The indices of the healpixels to apply the boost to. Uses the default footprint if None
1389 """
1390 def __init__(self, nside=None, day_gap=250., filtername='r', footprint=None):
1391 super(Template_generate_basis_function, self).__init__(nside=nside)
1392 self.day_gap = day_gap
1393 self.filtername = filtername
1394 self.survey_features = {}
1395 self.survey_features['Last_observed'] = features.Last_observed(filtername=filtername)
1396 self.result = np.zeros(hp.nside2npix(self.nside))
1397 if footprint is None:
1398 fp = utils.standard_goals(nside=nside)[filtername]
1399 else:
1400 fp = footprint
1401 self.out_of_bounds = np.where(fp == 0)
1403 def _calc_value(self, conditions, **kwargs):
1404 result = self.result.copy()
1405 overdue = np.where((int_rounded(conditions.mjd - self.survey_features['Last_observed'].feature)) > int_rounded(self.day_gap))
1406 result[overdue] = 1
1407 result[self.out_of_bounds] = 0
1409 return result
1412class Observed_twice_basis_function(Base_basis_function):
1413 """Mask out pixels that haven't been observed in the night
1414 """
1415 def __init__(self, nside=None, filtername='r', n_obs_needed=2, n_obs_in_filt_needed=1):
1416 super(Observed_twice_basis_function, self).__init__(nside=nside)
1417 self.n_obs_needed = n_obs_needed
1418 self.n_obs_in_filt_needed = n_obs_in_filt_needed
1419 self.filtername = filtername
1420 self.survey_features = {}
1421 self.survey_features['N_obs_infilt'] = features.N_obs_night(nside=nside, filtername=filtername)
1422 self.survey_features['N_obs_all'] = features.N_obs_night(nside=nside, filtername='')
1424 self.result = np.zeros(hp.nside2npix(self.nside))
1426 def _calc_value(self, conditions, **kwargs):
1427 result = self.result.copy()
1428 good_pix = np.where((self.survey_features['N_obs_infilt'].feature >= self.n_obs_in_filt_needed) &
1429 (self.survey_features['N_obs_all'].feature >= self.n_obs_needed))[0]
1430 result[good_pix] = 1
1432 return result