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', 'Limit_repeat_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, **kwargs):
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, penalty=np.nan):
561 super(Near_sun_twilight_basis_function, self).__init__(nside=nside)
562 self.max_airmass = int_rounded(max_airmass)
563 self.result = np.empty(hp.nside2npix(self.nside))
564 self.result.fill(penalty)
566 def _calc_value(self, conditions, indx=None):
567 result = self.result.copy()
568 good_pix = np.where((conditions.airmass >= 1.) &
569 (int_rounded(conditions.airmass) < self.max_airmass) &
570 (int_rounded(np.abs(conditions.az_to_sun)) < int_rounded(np.pi/2.)))
571 result[good_pix] = conditions.airmass[good_pix] / self.max_airmass.initial
572 return result
575class Visit_repeat_basis_function(Base_basis_function):
576 """
577 Basis function to reward re-visiting an area on the sky. Looking for Solar System objects.
579 Parameters
580 ----------
581 gap_min : float (15.)
582 Minimum time for the gap (minutes)
583 gap_max : float (45.)
584 Maximum time for a gap
585 filtername : str ('r')
586 The filter(s) to count with pairs
587 npairs : int (1)
588 The number of pairs of observations to attempt to gather
589 """
590 def __init__(self, gap_min=25., gap_max=45.,
591 filtername='r', nside=None, npairs=1):
593 super(Visit_repeat_basis_function, self).__init__(nside=nside, filtername=filtername)
595 self.gap_min = int_rounded(gap_min/60./24.)
596 self.gap_max = int_rounded(gap_max/60./24.)
597 self.npairs = npairs
599 self.survey_features = {}
600 # Track the number of pairs that have been taken in a night
601 self.survey_features['Pair_in_night'] = features.Pair_in_night(filtername=filtername,
602 gap_min=gap_min, gap_max=gap_max,
603 nside=nside)
604 # When was it last observed
605 # XXX--since this feature is also in Pair_in_night, I should just access that one!
606 self.survey_features['Last_observed'] = features.Last_observed(filtername=filtername,
607 nside=nside)
609 def _calc_value(self, conditions, indx=None):
610 result = np.zeros(hp.nside2npix(self.nside), dtype=float)
611 if indx is None:
612 indx = np.arange(result.size)
613 diff = int_rounded(conditions.mjd - self.survey_features['Last_observed'].feature[indx])
614 good = np.where((diff >= self.gap_min) & (diff <= self.gap_max) &
615 (self.survey_features['Pair_in_night'].feature[indx] < self.npairs))[0]
616 result[indx[good]] += 1.
617 return result
620class M5_diff_basis_function(Base_basis_function):
621 """Basis function based on the 5-sigma depth.
622 Look up the best depth a healpixel achieves, and compute
623 the limiting depth difference given current conditions
624 """
625 def __init__(self, filtername='r', nside=None):
627 super(M5_diff_basis_function, self).__init__(nside=nside, filtername=filtername)
628 # Need to look up the deepest m5 values for all the healpixels
629 m5p = M5percentiles()
630 self.dark_map = m5p.dark_map(filtername=filtername, nside_out=self.nside)
632 def _calc_value(self, conditions, indx=None):
633 # No way to get the sign on this right the first time.
634 result = conditions.M5Depth[self.filtername] - self.dark_map
635 return result
638class Strict_filter_basis_function(Base_basis_function):
639 """Remove the bonus for staying in the same filter if certain conditions are met.
641 If the moon rises/sets or twilight starts/ends, it makes a lot of sense to consider
642 a filter change. This basis function rewards if it matches the current filter, the moon rises or sets,
643 twilight starts or stops, or there has been a large gap since the last observation.
645 Paramters
646 ---------
647 time_lag : float (10.)
648 If there is a gap between observations longer than this, let the filter change (minutes)
649 twi_change : float (-18.)
650 The sun altitude to consider twilight starting/ending (degrees)
651 note_free : str ('DD')
652 No penalty for changing filters if the last observation note field includes string.
653 Useful for giving a free filter change after deep drilling sequence
654 """
655 def __init__(self, time_lag=10., filtername='r', twi_change=-18., note_free='DD'):
657 super(Strict_filter_basis_function, self).__init__(filtername=filtername)
659 self.time_lag = time_lag/60./24. # Convert to days
660 self.twi_change = np.radians(twi_change)
662 self.survey_features = {}
663 self.survey_features['Last_observation'] = features.Last_observation()
664 self.note_free = note_free
666 def _calc_value(self, conditions, **kwargs):
667 # Did the moon set or rise since last observation?
668 moon_changed = conditions.moonAlt * self.survey_features['Last_observation'].feature['moonAlt'] < 0
670 # Are we already in the filter (or at start of night)?
671 in_filter = (conditions.current_filter == self.filtername) | (conditions.current_filter is None)
673 # Has enough time past?
674 time_past = int_rounded(conditions.mjd - self.survey_features['Last_observation'].feature['mjd']) > int_rounded(self.time_lag)
676 # Did twilight start/end?
677 twi_changed = (conditions.sunAlt - self.twi_change) * (self.survey_features['Last_observation'].feature['sunAlt']- self.twi_change) < 0
679 # Did we just finish a DD sequence
680 wasDD = self.note_free in self.survey_features['Last_observation'].feature['note']
682 # Is the filter mounted?
683 mounted = self.filtername in conditions.mounted_filters
685 if (moon_changed | in_filter | time_past | twi_changed | wasDD) & mounted:
686 result = 1.
687 else:
688 result = 0.
690 return result
693class Goal_Strict_filter_basis_function(Base_basis_function):
694 """Remove the bonus for staying in the same filter if certain conditions are met.
696 If the moon rises/sets or twilight starts/ends, it makes a lot of sense to consider
697 a filter change. This basis function rewards if it matches the current filter, the moon rises or sets,
698 twilight starts or stops, or there has been a large gap since the last observation.
700 Parameters
701 ---------
702 time_lag_min: Minimum time after a filter change for which a new filter change will receive zero reward, or
703 be denied at all (see unseen_before_lag).
704 time_lag_max: Time after a filter change where the reward for changing filters achieve its maximum.
705 time_lag_boost: Time after a filter change to apply a boost on the reward.
706 boost_gain: A multiplier factor for the reward after time_lag_boost.
707 unseen_before_lag: If True will make it impossible to switch filter before time_lag has passed.
708 filtername: The filter for which this basis function will be used.
709 tag: When using filter proportion use only regions with this tag to count for observations.
710 twi_change: Switch reward on when twilight changes.
711 proportion: The expected filter proportion distribution.
712 aways_available: If this is true the basis function will aways be computed regardless of the feasibility. If
713 False a more detailed feasibility check is performed. When set to False, it may speed up the computation
714 process by avoiding to compute the reward functions paired with this bf, when observation is not feasible.
716 """
718 def __init__(self, time_lag_min=10., time_lag_max=30.,
719 time_lag_boost=60., boost_gain=2.0, unseen_before_lag=False,
720 filtername='r', tag=None, twi_change=-18., proportion=1.0, aways_available=False):
722 super(Goal_Strict_filter_basis_function, self).__init__(filtername=filtername)
724 self.time_lag_min = time_lag_min / 60. / 24. # Convert to days
725 self.time_lag_max = time_lag_max / 60. / 24. # Convert to days
726 self.time_lag_boost = time_lag_boost / 60. / 24.
727 self.boost_gain = boost_gain
728 self.unseen_before_lag = unseen_before_lag
730 self.twi_change = np.radians(twi_change)
731 self.proportion = proportion
732 self.aways_available = aways_available
734 self.survey_features = {}
735 self.survey_features['Last_observation'] = features.Last_observation()
736 self.survey_features['Last_filter_change'] = features.LastFilterChange()
737 self.survey_features['N_obs_all'] = features.N_obs_count(filtername=None)
738 self.survey_features['N_obs'] = features.N_obs_count(filtername=filtername,
739 tag=tag)
741 def filter_change_bonus(self, time):
743 lag_min = self.time_lag_min
744 lag_max = self.time_lag_max
746 a = 1. / (lag_max - lag_min)
747 b = -a * lag_min
749 bonus = a * time + b
750 # How far behind we are with respect to proportion?
751 nobs = self.survey_features['N_obs'].feature
752 nobs_all = self.survey_features['N_obs_all'].feature
753 goal = self.proportion
754 # need = 1. - nobs / nobs_all + goal if nobs_all > 0 else 1. + goal
755 need = goal / nobs * nobs_all if nobs > 0 else 1.
756 # need /= goal
757 if hasattr(time, '__iter__'):
758 before_lag = np.where(time <= lag_min)
759 bonus[before_lag] = -np.inf if self.unseen_before_lag else 0.
760 after_lag = np.where(time >= lag_max)
761 bonus[after_lag] = 1. if time < self.time_lag_boost else self.boost_gain
762 elif int_rounded(time) <= int_rounded(lag_min):
763 return -np.inf if self.unseen_before_lag else 0.
764 elif int_rounded(time) >= int_rounded(lag_max):
765 return 1. if int_rounded(time) < int_rounded(self.time_lag_boost) else self.boost_gain
767 return bonus * need
769 def check_feasibility(self, conditions):
770 """
771 This method makes a pre-check of the feasibility of this basis function. If a basis function return False
772 on the feasibility check, it won't computed at all.
774 :return:
775 """
777 # Make a quick check about the feasibility of this basis function. If current filter is none, telescope
778 # is parked and we could, in principle, switch to any filter. If this basis function computes reward for
779 # the current filter, then it is also feasible. At last we check for an "aways_available" flag. Meaning, we
780 # force this basis function to be aways be computed.
781 if conditions.current_filter is None or conditions.current_filter == self.filtername or self.aways_available:
782 return True
784 # If we arrive here, we make some extra checks to make sure this bf is feasible and should be computed.
786 # Did the moon set or rise since last observation?
787 moon_changed = conditions.moonAlt * self.survey_features['Last_observation'].feature['moonAlt'] < 0
789 # Are we already in the filter (or at start of night)?
790 not_in_filter = (conditions.current_filter != self.filtername)
792 # Has enough time past?
793 lag = conditions.mjd - self.survey_features['Last_filter_change'].feature['mjd']
794 time_past = int_rounded(lag) > int_rounded(self.time_lag_min)
796 # Did twilight start/end?
797 twi_changed = (conditions.sunAlt - self.twi_change) * \
798 (self.survey_features['Last_observation'].feature['sunAlt'] - self.twi_change) < 0
800 # Did we just finish a DD sequence
801 wasDD = self.survey_features['Last_observation'].feature['note'] == 'DD'
803 # Is the filter mounted?
804 mounted = self.filtername in conditions.mounted_filters
806 if (moon_changed | time_past | twi_changed | wasDD) & mounted & not_in_filter:
807 return True
808 else:
809 return False
811 def _calc_value(self, conditions, **kwargs):
813 if conditions.current_filter is None:
814 return 0. # no bonus if no filter is mounted
815 # elif self.condition_features['Current_filter'].feature == self.filtername:
816 # return 0. # no bonus if on the filter already
818 # Did the moon set or rise since last observation?
819 moon_changed = conditions.moonAlt * \
820 self.survey_features['Last_observation'].feature['moonAlt'] < 0
822 # Are we already in the filter (or at start of night)?
823 # not_in_filter = (self.condition_features['Current_filter'].feature != self.filtername)
825 # Has enough time past?
826 lag = conditions.mjd - self.survey_features['Last_filter_change'].feature['mjd']
827 time_past = lag > self.time_lag_min
829 # Did twilight start/end?
830 twi_changed = (conditions.sunAlt - self.twi_change) * (
831 self.survey_features['Last_observation'].feature['sunAlt'] - self.twi_change) < 0
833 # Did we just finish a DD sequence
834 wasDD = self.survey_features['Last_observation'].feature['note'] == 'DD'
836 # Is the filter mounted?
837 mounted = self.filtername in conditions.mounted_filters
839 if (moon_changed | time_past | twi_changed | wasDD) & mounted:
840 result = self.filter_change_bonus(lag) if time_past else 0.
841 else:
842 result = -100. if self.unseen_before_lag else 0.
844 return result
847class Filter_change_basis_function(Base_basis_function):
848 """Reward staying in the current filter.
849 """
850 def __init__(self, filtername='r'):
851 super(Filter_change_basis_function, self).__init__(filtername=filtername)
853 def _calc_value(self, conditions, **kwargs):
855 if (conditions.current_filter == self.filtername) | (conditions.current_filter is None):
856 result = 1.
857 else:
858 result = 0.
859 return result
862class Slewtime_basis_function(Base_basis_function):
863 """Reward slews that take little time
865 Parameters
866 ----------
867 max_time : float (135)
868 The estimated maximum slewtime (seconds). Used to normalize so the basis function
869 spans ~ -1-0 in reward units.
870 """
871 def __init__(self, max_time=135., filtername='r', nside=None):
872 super(Slewtime_basis_function, self).__init__(nside=nside, filtername=filtername)
874 self.maxtime = max_time
875 self.nside = nside
876 self.filtername = filtername
877 self.result = np.zeros(hp.nside2npix(nside), dtype=float)
879 def add_observation(self, observation, indx=None):
880 # No tracking of observations in this basis function. Purely based on conditions.
881 pass
883 def _calc_value(self, conditions, indx=None):
884 # If we are in a different filter, the Filter_change_basis_function will take it
885 if conditions.current_filter != self.filtername:
886 result = 0
887 else:
888 # Need to make sure smaller slewtime is larger reward.
889 if np.size(conditions.slewtime) > 1:
890 result = self.result.copy()
891 good = ~np.isnan(conditions.slewtime)
892 result[good] = -conditions.slewtime[good]/self.maxtime
893 else:
894 result = -conditions.slewtime/self.maxtime
895 return result
898class Aggressive_Slewtime_basis_function(Base_basis_function):
899 """Reward slews that take little time
901 XXX--not sure how this is different from Slewtime_basis_function?
902 Looks like it's checking the slewtime to the field position rather than the healpix maybe?
903 """
905 def __init__(self, max_time=135., order=1., hard_max=None, filtername='r', nside=None):
906 super(Aggressive_Slewtime_basis_function, self).__init__(nside=nside, filtername=filtername)
908 self.maxtime = max_time
909 self.hard_max = hard_max
910 self.order = order
911 self.result = np.zeros(hp.nside2npix(nside), dtype=float)
913 def _calc_value(self, conditions, indx=None):
914 # If we are in a different filter, the Filter_change_basis_function will take it
915 if conditions.current_filter != self.filtername:
916 result = 0.
917 else:
918 # Need to make sure smaller slewtime is larger reward.
919 if np.size(self.condition_features['slewtime'].feature) > 1:
920 result = self.result.copy()
921 result.fill(np.nan)
923 good = np.where(np.bitwise_and(conditions.slewtime > 0.,
924 conditions.slewtime < self.maxtime))
925 result[good] = ((self.maxtime - conditions.slewtime[good]) /
926 self.maxtime) ** self.order
927 if self.hard_max is not None:
928 not_so_good = np.where(conditions.slewtime > self.hard_max)
929 result[not_so_good] -= 10.
930 fields = np.unique(conditions.hp2fields[good])
931 for field in fields:
932 hp_indx = np.where(conditions.hp2fields == field)
933 result[hp_indx] = np.min(result[hp_indx])
934 else:
935 result = (self.maxtime - conditions.slewtime) / self.maxtime
936 return result
939class Skybrightness_limit_basis_function(Base_basis_function):
940 """Mask regions that are outside a sky brightness limit
942 XXX--TODO: This should probably go to the mask basis functions.
944 Parameters
945 ----------
946 min : float (20.)
947 The minimum sky brightness (mags).
948 max : float (30.)
949 The maximum sky brightness (mags).
951 """
952 def __init__(self, nside=None, filtername='r', sbmin=20., sbmax=30.):
953 super(Skybrightness_limit_basis_function, self).__init__(nside=nside, filtername=filtername)
955 self.min = int_rounded(sbmin)
956 self.max = int_rounded(sbmax)
957 self.result = np.empty(hp.nside2npix(self.nside), dtype=float)
958 self.result.fill(np.nan)
960 def _calc_value(self, conditions, indx=None):
961 result = self.result.copy()
963 good = np.where(np.bitwise_and(int_rounded(conditions.skybrightness[self.filtername]) > self.min,
964 int_rounded(conditions.skybrightness[self.filtername]) < self.max))
965 result[good] = 1.0
967 return result
970class CableWrap_unwrap_basis_function(Base_basis_function):
971 """
972 Parameters
973 ----------
974 minAz : float (20.)
975 The minimum azimuth to activate bf (degrees)
976 maxAz : float (82.)
977 The maximum azimuth to activate bf (degrees)
978 unwrap_until: float (90.)
979 The window in which the bf is activated (degrees)
980 """
981 def __init__(self, nside=None, minAz=-270., maxAz=270., minAlt=20., maxAlt=82.,
982 activate_tol=20., delta_unwrap=1.2, unwrap_until=70., max_duration=30.):
983 super(CableWrap_unwrap_basis_function, self).__init__(nside=nside)
985 self.minAz = np.radians(minAz)
986 self.maxAz = np.radians(maxAz)
988 self.activate_tol = np.radians(activate_tol)
989 self.delta_unwrap = np.radians(delta_unwrap)
990 self.unwrap_until = np.radians(unwrap_until)
992 self.minAlt = np.radians(minAlt)
993 self.maxAlt = np.radians(maxAlt)
994 # Convert to half-width for convienence
995 self.nside = nside
996 self.active = False
997 self.unwrap_direction = 0. # either -1., 0., 1.
998 self.max_duration = max_duration/60./24. # Convert to days
999 self.activation_time = None
1000 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
1002 def _calc_value(self, conditions, indx=None):
1004 result = self.result.copy()
1006 current_abs_rad = np.radians(conditions.az)
1007 unseen = np.where(np.bitwise_or(conditions.alt < self.minAlt,
1008 conditions.alt > self.maxAlt))
1009 result[unseen] = np.nan
1011 if (self.minAz + self.activate_tol < current_abs_rad < self.maxAz - self.activate_tol) and not self.active:
1012 return result
1013 elif self.active and self.unwrap_direction == 1 and current_abs_rad > self.minAz+self.unwrap_until:
1014 self.active = False
1015 self.unwrap_direction = 0.
1016 self.activation_time = None
1017 return result
1018 elif self.active and self.unwrap_direction == -1 and current_abs_rad < self.maxAz-self.unwrap_until:
1019 self.active = False
1020 self.unwrap_direction = 0.
1021 self.activation_time = None
1022 return result
1023 elif (self.activation_time is not None and
1024 conditions.mjd - self.activation_time > self.max_duration):
1025 self.active = False
1026 self.unwrap_direction = 0.
1027 self.activation_time = None
1028 return result
1030 if not self.active:
1031 self.activation_time = conditions.mjd
1032 if current_abs_rad < 0.:
1033 self.unwrap_direction = 1 # clock-wise unwrap
1034 else:
1035 self.unwrap_direction = -1 # counter-clock-wise unwrap
1037 self.active = True
1039 max_abs_rad = self.maxAz
1040 min_abs_rad = self.minAz
1042 TWOPI = 2.*np.pi
1044 # Compute distance and accumulated az.
1045 norm_az_rad = np.divmod(conditions.az - min_abs_rad, TWOPI)[1] + min_abs_rad
1046 distance_rad = divmod(norm_az_rad - current_abs_rad, TWOPI)[1]
1047 get_shorter = np.where(distance_rad > np.pi)
1048 distance_rad[get_shorter] -= TWOPI
1049 accum_abs_rad = current_abs_rad + distance_rad
1051 # Compute wrap regions and fix distances
1052 mask_max = np.where(accum_abs_rad > max_abs_rad)
1053 distance_rad[mask_max] -= TWOPI
1054 mask_min = np.where(accum_abs_rad < min_abs_rad)
1055 distance_rad[mask_min] += TWOPI
1057 # Step-2: Repeat but now with compute reward to unwrap using specified delta_unwrap
1058 unwrap_current_abs_rad = current_abs_rad - (np.abs(self.delta_unwrap) if self.unwrap_direction > 0
1059 else -np.abs(self.delta_unwrap))
1060 unwrap_distance_rad = divmod(norm_az_rad - unwrap_current_abs_rad, TWOPI)[1]
1061 unwrap_get_shorter = np.where(unwrap_distance_rad > np.pi)
1062 unwrap_distance_rad[unwrap_get_shorter] -= TWOPI
1063 unwrap_distance_rad = np.abs(unwrap_distance_rad)
1065 if self.unwrap_direction < 0:
1066 mask = np.where(accum_abs_rad > unwrap_current_abs_rad)
1067 else:
1068 mask = np.where(accum_abs_rad < unwrap_current_abs_rad)
1070 # Finally build reward map
1071 result = (1. - unwrap_distance_rad/np.max(unwrap_distance_rad))**2.
1072 result[mask] = 0.
1073 result[unseen] = np.nan
1075 return result
1078class Cadence_enhance_basis_function(Base_basis_function):
1079 """Drive a certain cadence
1080 Parameters
1081 ----------
1082 filtername : str ('gri')
1083 The filter(s) that should be grouped together
1084 supress_window : list of float
1085 The start and stop window for when observations should be repressed (days)
1086 apply_area : healpix map
1087 The area over which to try and drive the cadence. Good values as 1, no candece drive 0.
1088 Probably works as a bool array too."""
1089 def __init__(self, filtername='gri', nside=None,
1090 supress_window=[0, 1.8], supress_val=-0.5,
1091 enhance_window=[2.1, 3.2], enhance_val=1.,
1092 apply_area=None):
1093 super(Cadence_enhance_basis_function, self).__init__(nside=nside, filtername=filtername)
1095 self.supress_window = np.sort(supress_window)
1096 self.supress_val = supress_val
1097 self.enhance_window = np.sort(enhance_window)
1098 self.enhance_val = enhance_val
1100 self.survey_features = {}
1101 self.survey_features['last_observed'] = features.Last_observed(filtername=filtername)
1103 self.empty = np.zeros(hp.nside2npix(self.nside), dtype=float)
1104 # No map, try to drive the whole area
1105 if apply_area is None:
1106 self.apply_indx = np.arange(self.empty.size)
1107 else:
1108 self.apply_indx = np.where(apply_area != 0)[0]
1110 def _calc_value(self, conditions, indx=None):
1111 # copy an empty array
1112 result = self.empty.copy()
1113 if indx is not None:
1114 ind = np.intersect1d(indx, self.apply_indx)
1115 else:
1116 ind = self.apply_indx
1117 if np.size(ind) == 0:
1118 result = 0
1119 else:
1120 mjd_diff = conditions.mjd - self.survey_features['last_observed'].feature[ind]
1121 to_supress = np.where((int_rounded(mjd_diff) > int_rounded(self.supress_window[0])) &
1122 (int_rounded(mjd_diff) < int_rounded(self.supress_window[1])))
1123 result[ind[to_supress]] = self.supress_val
1124 to_enhance = np.where((int_rounded(mjd_diff) > int_rounded(self.enhance_window[0])) &
1125 (int_rounded(mjd_diff) < int_rounded(self.enhance_window[1])))
1126 result[ind[to_enhance]] = self.enhance_val
1127 return result
1130# https://docs.astropy.org/en/stable/_modules/astropy/modeling/functional_models.html#Trapezoid1D
1131def trapezoid(x, amplitude, x_0, width, slope):
1132 """One dimensional Trapezoid model function"""
1133 # Compute the four points where the trapezoid changes slope
1134 # x1 <= x2 <= x3 <= x4
1135 x2 = x_0 - width / 2.
1136 x3 = x_0 + width / 2.
1137 x1 = x2 - amplitude / slope
1138 x4 = x3 + amplitude / slope
1140 result = x*0
1142 # Compute model values in pieces between the change points
1143 range_a = np.logical_and(x >= x1, x < x2)
1144 range_b = np.logical_and(x >= x2, x < x3)
1145 range_c = np.logical_and(x >= x3, x < x4)
1147 result[range_a] = slope * (x[range_a] - x1)
1148 result[range_b] = amplitude
1149 result[range_c] = slope * (x4 - x[range_c])
1151 return result
1154class Cadence_enhance_trapezoid_basis_function(Base_basis_function):
1155 """Drive a certain cadence, like Cadence_enhance_basis_function but with smooth transitions
1156 Parameters
1157 ----------
1158 filtername : str ('gri')
1159 The filter(s) that should be grouped together
1161 XXX--fill out doc string!
1162 """
1163 def __init__(self, filtername='gri', nside=None,
1164 delay_width=2, delay_slope=2., delay_peak=0, delay_amp=0.5,
1165 enhance_width=3., enhance_slope=2., enhance_peak=4., enhance_amp=1.,
1166 apply_area=None, season_limit=None):
1167 super(Cadence_enhance_trapezoid_basis_function, self).__init__(nside=nside, filtername=filtername)
1169 self.delay_width = delay_width
1170 self.delay_slope = delay_slope
1171 self.delay_peak = delay_peak
1172 self.delay_amp = delay_amp
1173 self.enhance_width = enhance_width
1174 self.enhance_slope = enhance_slope
1175 self.enhance_peak = enhance_peak
1176 self.enhance_amp = enhance_amp
1178 self.season_limit = season_limit/12*np.pi # To radians
1180 self.survey_features = {}
1181 self.survey_features['last_observed'] = features.Last_observed(filtername=filtername)
1183 self.empty = np.zeros(hp.nside2npix(self.nside), dtype=float)
1184 # No map, try to drive the whole area
1185 if apply_area is None:
1186 self.apply_indx = np.arange(self.empty.size)
1187 else:
1188 self.apply_indx = np.where(apply_area != 0)[0]
1190 def suppress_enhance(self, x):
1191 result = x*0
1192 result -= trapezoid(x, self.delay_amp, self.delay_peak, self.delay_width, self.delay_slope)
1193 result += trapezoid(x, self.enhance_amp, self.enhance_peak, self.enhance_width, self.enhance_slope)
1195 return result
1197 def season_calc(self, conditions):
1198 ra_mid_season = (conditions.sunRA + np.pi) % (2.*np.pi)
1199 angle_to_mid_season = np.abs(conditions.ra - ra_mid_season)
1200 over = np.where(int_rounded(angle_to_mid_season) > int_rounded(np.pi))
1201 angle_to_mid_season[over] = 2.*np.pi - angle_to_mid_season[over]
1203 return angle_to_mid_season
1205 def _calc_value(self, conditions, indx=None):
1206 # copy an empty array
1207 result = self.empty.copy()
1208 if indx is not None:
1209 ind = np.intersect1d(indx, self.apply_indx)
1210 else:
1211 ind = self.apply_indx
1212 if np.size(ind) == 0:
1213 result = 0
1214 else:
1215 mjd_diff = conditions.mjd - self.survey_features['last_observed'].feature[ind]
1216 result[ind] += self.suppress_enhance(mjd_diff)
1218 if self.season_limit is not None:
1219 radians_to_midseason = self.season_calc(conditions)
1220 outside_season = np.where(radians_to_midseason > self.season_limit)
1221 result[outside_season] = 0
1224 return result
1227class Azimuth_basis_function(Base_basis_function):
1228 """Reward staying in the same azimuth range. Possibly better than using slewtime, especially when selecting a large area of sky.
1230 Parameters
1231 ----------
1233 """
1235 def __init__(self, nside=None):
1236 super(Azimuth_basis_function, self).__init__(nside=nside)
1238 def _calc_value(self, conditions, indx=None):
1239 az_dist = conditions.az - conditions.telAz
1240 az_dist = az_dist % (2.*np.pi)
1241 over = np.where(az_dist > np.pi)
1242 az_dist[over] = 2. * np.pi - az_dist[over]
1243 # Normalize sp between 0 and 1
1244 result = az_dist/np.pi
1245 return result
1248class Az_modulo_basis_function(Base_basis_function):
1249 """Try to replicate the Rothchild et al cadence forcing by only observing on limited az ranges per night.
1251 Parameters
1252 ----------
1253 az_limits : list of float pairs (None)
1254 The azimuth limits (degrees) to use.
1255 """
1256 def __init__(self, nside=None, az_limits=None, out_of_bounds_val=-1.):
1257 super(Az_modulo_basis_function, self).__init__(nside=nside)
1258 self.result = np.ones(hp.nside2npix(self.nside))
1259 if az_limits is None:
1260 spread = 100./2.
1261 self.az_limits = np.radians([[360-spread, spread],
1262 [90.-spread, 90.+spread],
1263 [180.-spread, 180.+spread]])
1264 else:
1265 self.az_limits = np.radians(az_limits)
1266 self.mod_val = len(self.az_limits)
1267 self.out_of_bounds_val = out_of_bounds_val
1269 def _calc_value(self, conditions, indx=None):
1270 result = self.result.copy()
1271 az_lim = self.az_limits[np.max(conditions.night) % self.mod_val]
1273 if az_lim[0] < az_lim[1]:
1274 out_pix = np.where((int_rounded(conditions.az) < int_rounded(az_lim[0])) |
1275 (int_rounded(conditions.az) > int_rounded(az_lim[1])))
1276 else:
1277 out_pix = np.where((int_rounded(conditions.az) < int_rounded(az_lim[0])) |
1278 (int_rounded(conditions.az) > int_rounded(az_lim[1])))[0]
1279 result[out_pix] = self.out_of_bounds_val
1280 return result
1283class Dec_modulo_basis_function(Base_basis_function):
1284 """Emphasize dec bands on a nightly varying basis
1286 Parameters
1287 ----------
1288 dec_limits : list of float pairs (None)
1289 The azimuth limits (degrees) to use.
1290 """
1291 def __init__(self, nside=None, dec_limits=None, out_of_bounds_val=-1.):
1292 super(Dec_modulo_basis_function, self).__init__(nside=nside)
1294 npix = hp.nside2npix(nside)
1295 hpids = np.arange(npix)
1296 ra, dec = _hpid2RaDec(nside, hpids)
1298 self.results = []
1300 if dec_limits is None:
1301 self.dec_limits = np.radians([[-90., -32.8],
1302 [-32.8, -12.],
1303 [-12., 35.]])
1304 else:
1305 self.dec_limits = np.radians(dec_limits)
1306 self.mod_val = len(self.dec_limits)
1307 self.out_of_bounds_val = out_of_bounds_val
1309 for limits in self.dec_limits:
1310 good = np.where((dec >= limits[0]) & (dec < limits[1]))[0]
1311 tmp = np.zeros(npix)
1312 tmp[good] = 1
1313 self.results.append(tmp)
1315 def _calc_value(self, conditions, indx=None):
1316 night_index = np.max(conditions.night % self.mod_val)
1317 result = self.results[night_index]
1319 return result
1322class Map_modulo_basis_function(Base_basis_function):
1323 """Similar to Dec_modulo, but now use input masks
1325 Parameters
1326 ----------
1327 inmaps : list of hp arrays
1328 """
1329 def __init__(self, inmaps):
1330 nside = hp.npix2nside(np.size(inmaps[0]))
1331 super(Map_modulo_basis_function, self).__init__(nside=nside)
1332 self.maps = inmaps
1333 self.mod_val = len(inmaps)
1335 def _calc_value(self, conditions, indx=None):
1336 indx = np.max(conditions.night % self.mod_val)
1337 result = self.maps[indx]
1338 return result
1341class Good_seeing_basis_function(Base_basis_function):
1342 """Drive observations in good seeing conditions"""
1344 def __init__(self, nside=None, filtername='r', footprint=None, FWHMeff_limit=0.8,
1345 mag_diff=0.75):
1346 super(Good_seeing_basis_function, self).__init__(nside=nside)
1348 self.filtername = filtername
1349 self.FWHMeff_limit = int_rounded(FWHMeff_limit)
1350 if footprint is None:
1351 fp = utils.standard_goals(nside=nside)[filtername]
1352 else:
1353 fp = footprint
1354 self.out_of_bounds = np.where(fp == 0)[0]
1355 self.result = fp*0
1357 self.mag_diff = int_rounded(mag_diff)
1358 self.survey_features = {}
1359 self.survey_features['coadd_depth_all'] = features.Coadded_depth(filtername=filtername,
1360 nside=nside)
1361 self.survey_features['coadd_depth_good'] = features.Coadded_depth(filtername=filtername,
1362 nside=nside,
1363 FWHMeff_limit=FWHMeff_limit)
1365 def _calc_value(self, conditions, **kwargs):
1366 # Seeing is "bad"
1367 if int_rounded(conditions.FWHMeff[self.filtername].min()) > self.FWHMeff_limit:
1368 return 0
1369 result = self.result.copy()
1371 diff = self.survey_features['coadd_depth_all'].feature - self.survey_features['coadd_depth_good'].feature
1372 # Where are there things we want to observe?
1373 good_pix = np.where((int_rounded(diff) > self.mag_diff) &
1374 (int_rounded(conditions.FWHMeff[self.filtername]) <= self.FWHMeff_limit))
1375 # Hm, should this scale by the mag differences? Probably.
1376 result[good_pix] = diff[good_pix]
1377 result[self.out_of_bounds] = 0
1379 return result
1382class Template_generate_basis_function(Base_basis_function):
1383 """Emphasize areas that have not been observed in a long time
1385 Parameters
1386 ----------
1387 day_gap : float (250.)
1388 How long to wait before boosting the reward (days)
1389 footprint : np.array (None)
1390 The indices of the healpixels to apply the boost to. Uses the default footprint if None
1391 """
1392 def __init__(self, nside=None, day_gap=250., filtername='r', footprint=None):
1393 super(Template_generate_basis_function, self).__init__(nside=nside)
1394 self.day_gap = day_gap
1395 self.filtername = filtername
1396 self.survey_features = {}
1397 self.survey_features['Last_observed'] = features.Last_observed(filtername=filtername)
1398 self.result = np.zeros(hp.nside2npix(self.nside))
1399 if footprint is None:
1400 fp = utils.standard_goals(nside=nside)[filtername]
1401 else:
1402 fp = footprint
1403 self.out_of_bounds = np.where(fp == 0)
1405 def _calc_value(self, conditions, **kwargs):
1406 result = self.result.copy()
1407 overdue = np.where((int_rounded(conditions.mjd - self.survey_features['Last_observed'].feature)) > int_rounded(self.day_gap))
1408 result[overdue] = 1
1409 result[self.out_of_bounds] = 0
1411 return result
1414class Limit_repeat_basis_function(Base_basis_function):
1415 """Mask out pixels that haven't been observed in the night
1416 """
1417 def __init__(self, nside=None, filtername='r', n_limit=2):
1418 super(Limit_repeat_basis_function, self).__init__(nside=nside)
1419 self.filtername = filtername
1420 self.n_limit = n_limit
1421 self.survey_features = {}
1422 self.survey_features['N_obs'] = features.N_obs_night(nside=nside, filtername=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'].feature >= self.n_limit)[0]
1429 result[good_pix] = 1
1431 return result
1434class Observed_twice_basis_function(Base_basis_function):
1435 """Mask out pixels that haven't been observed in the night
1436 """
1437 def __init__(self, nside=None, filtername='r', n_obs_needed=2, n_obs_in_filt_needed=1):
1438 super(Observed_twice_basis_function, self).__init__(nside=nside)
1439 self.n_obs_needed = n_obs_needed
1440 self.n_obs_in_filt_needed = n_obs_in_filt_needed
1441 self.filtername = filtername
1442 self.survey_features = {}
1443 self.survey_features['N_obs_infilt'] = features.N_obs_night(nside=nside, filtername=filtername)
1444 self.survey_features['N_obs_all'] = features.N_obs_night(nside=nside, filtername='')
1446 self.result = np.zeros(hp.nside2npix(self.nside))
1448 def _calc_value(self, conditions, **kwargs):
1449 result = self.result.copy()
1450 good_pix = np.where((self.survey_features['N_obs_infilt'].feature >= self.n_obs_in_filt_needed) &
1451 (self.survey_features['N_obs_all'].feature >= self.n_obs_needed))[0]
1452 result[good_pix] = 1
1454 return result