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_Fast_Revists', 'Visit_repeat_basis_function', 'M5_diff_basis_function',
16 'Strict_filter_basis_function', 'Goal_Strict_filter_basis_function',
17 'Filter_change_basis_function', 'Slewtime_basis_function',
18 'Aggressive_Slewtime_basis_function', 'Skybrightness_limit_basis_function',
19 'CableWrap_unwrap_basis_function', 'Cadence_enhance_basis_function', 'Azimuth_basis_function',
20 'Az_modulo_basis_function', 'Dec_modulo_basis_function', 'Map_modulo_basis_function',
21 'Template_generate_basis_function',
22 'Footprint_nvis_basis_function', 'Third_observation_basis_function', 'Season_coverage_basis_function',
23 'N_obs_per_year_basis_function', 'Cadence_in_season_basis_function', 'Near_sun_twilight_basis_function',
24 'N_obs_high_am_basis_function', 'Good_seeing_basis_function', 'Observed_twice_basis_function',
25 'Ecliptic_basis_function']
28class Base_basis_function(object):
29 """Class that takes features and computes a reward function when called.
30 """
32 def __init__(self, nside=None, filtername=None, **kwargs):
34 # Set if basis function needs to be recalculated if there is a new observation
35 self.update_on_newobs = True
36 # Set if basis function needs to be recalculated if conditions change
37 self.update_on_mjd = True
38 # Dict to hold all the features we want to track
39 self.survey_features = {}
40 # Keep track of the last time the basis function was called. If mjd doesn't change, use cached value
41 self.mjd_last = None
42 self.value = 0
43 # list the attributes to compare to check if basis functions are equal.
44 self.attrs_to_compare = []
45 # Do we need to recalculate the basis function
46 self.recalc = True
47 # Basis functions don't technically all need an nside, but so many do might as well set it here
48 if nside is None:
49 self.nside = utils.set_default_nside()
50 else:
51 self.nside = nside
53 self.filtername = filtername
55 def add_observation(self, observation, indx=None):
56 """
57 Parameters
58 ----------
59 observation : np.array
60 An array with information about the input observation
61 indx : np.array
62 The indices of the healpix map that the observation overlaps with
63 """
64 for feature in self.survey_features:
65 self.survey_features[feature].add_observation(observation, indx=indx)
66 if self.update_on_newobs:
67 self.recalc = True
69 def check_feasibility(self, conditions):
70 """If there is logic to decide if something is feasible (e.g., only if moon is down),
71 it can be calculated here. Helps prevent full __call__ from being called more than needed.
72 """
73 return True
75 def _calc_value(self, conditions, **kwarge):
76 self.value = 0
77 # Update the last time we had an mjd
78 self.mjd_last = conditions.mjd + 0
79 self.recalc = False
80 return self.value
82 def __eq__(self):
83 # XXX--to work on if we need to make a registry of basis functions.
84 pass
86 def __ne__(self):
87 pass
89 def __call__(self, conditions, **kwargs):
90 """
91 Parameters
92 ----------
93 conditions : lsst.sims.featureScheduler.features.conditions object
94 Object that has attributes for all the current conditions.
96 Return a reward healpix map or a reward scalar.
97 """
98 # If we are not feasible, return -inf
99 if not self.check_feasibility(conditions):
100 return -np.inf
101 if self.recalc:
102 self.value = self._calc_value(conditions, **kwargs)
103 if self.update_on_mjd:
104 if conditions.mjd != self.mjd_last:
105 self.value = self._calc_value(conditions, **kwargs)
106 return self.value
109class Constant_basis_function(Base_basis_function):
110 """Just add a constant
111 """
112 def __call__(self, conditions, **kwargs):
113 return 1
116class Target_map_basis_function(Base_basis_function):
117 """Basis function that tracks number of observations and tries to match a specified spatial distribution
119 Parameters
120 ----------
121 filtername: (string 'r')
122 The name of the filter for this target map.
123 nside: int (default_nside)
124 The healpix resolution.
125 target_map : numpy array (None)
126 A healpix map showing the ratio of observations desired for all points on the sky
127 norm_factor : float (0.00010519)
128 for converting target map to number of observations. Should be the area of the camera
129 divided by the area of a healpixel divided by the sum of all your goal maps. Default
130 value assumes LSST foV has 1.75 degree radius and the standard goal maps. If using
131 mulitple filters, see lsst.sims.featureScheduler.utils.calc_norm_factor for a utility
132 that computes norm_factor.
133 out_of_bounds_val : float (-10.)
134 Reward value to give regions where there are no observations requested (unitless).
135 """
136 def __init__(self, filtername='r', nside=None, target_map=None,
137 norm_factor=None,
138 out_of_bounds_val=-10.):
140 super(Target_map_basis_function, self).__init__(nside=nside, filtername=filtername)
142 if norm_factor is None:
143 warnings.warn('No norm_factor set, use utils.calc_norm_factor if using multiple filters.')
144 self.norm_factor = 0.00010519
145 else:
146 self.norm_factor = norm_factor
148 self.survey_features = {}
149 # Map of the number of observations in filter
150 self.survey_features['N_obs'] = features.N_observations(filtername=filtername, nside=self.nside)
151 # Count of all the observations
152 self.survey_features['N_obs_count_all'] = features.N_obs_count(filtername=None)
153 if target_map is None:
154 self.target_map = utils.generate_goal_map(filtername=filtername, nside=self.nside)
155 else:
156 self.target_map = target_map
157 self.out_of_bounds_area = np.where(self.target_map == 0)[0]
158 self.out_of_bounds_val = out_of_bounds_val
159 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
160 self.all_indx = np.arange(self.result.size)
162 def _calc_value(self, conditions, indx=None):
163 """
164 Parameters
165 ----------
166 indx : list (None)
167 Index values to compute, if None, full map is computed
168 Returns
169 -------
170 Healpix reward map
171 """
172 result = self.result.copy()
173 if indx is None:
174 indx = self.all_indx
176 # Find out how many observations we want now at those points
177 goal_N = self.target_map[indx] * self.survey_features['N_obs_count_all'].feature * self.norm_factor
179 result[indx] = goal_N - self.survey_features['N_obs'].feature[indx]
180 result[self.out_of_bounds_area] = self.out_of_bounds_val
182 return result
185def azRelPoint(azs, pointAz):
186 azRelMoon = (azs - pointAz) % (2.0*np.pi)
187 if isinstance(azs, np.ndarray):
188 over = np.where(azRelMoon > np.pi)
189 azRelMoon[over] = 2. * np.pi - azRelMoon[over]
190 else:
191 if azRelMoon > np.pi:
192 azRelMoon = 2.0 * np.pi - azRelMoon
193 return azRelMoon
196class N_obs_high_am_basis_function(Base_basis_function):
197 """Reward only reward/count observations at high airmass
198 """
200 def __init__(self, nside=None, filtername='r', footprint=None, n_obs=3, season=300.,
201 am_limits=[1.5, 2.2], out_of_bounds_val=np.nan):
202 super(N_obs_high_am_basis_function, self).__init__(nside=nside, filtername=filtername)
203 self.footprint = footprint
204 self.out_footprint = np.where((footprint == 0) | np.isnan(footprint))
205 self.am_limits = am_limits
206 self.season = season
207 self.survey_features['last_n_mjds'] = features.Last_N_obs_times(nside=nside, filtername=filtername,
208 n_obs=n_obs)
210 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float) + out_of_bounds_val
211 self.out_of_bounds_val = out_of_bounds_val
213 def add_observation(self, observation, indx=None):
214 """
215 Parameters
216 ----------
217 observation : np.array
218 An array with information about the input observation
219 indx : np.array
220 The indices of the healpix map that the observation overlaps with
221 """
223 # Only count the observations if they are at the airmass limits
224 if (observation['airmass'] > np.min(self.am_limits)) & (observation['airmass'] < np.max(self.am_limits)):
225 for feature in self.survey_features:
226 self.survey_features[feature].add_observation(observation, indx=indx)
227 if self.update_on_newobs:
228 self.recalc = True
230 def check_feasibility(self, conditions):
231 """If there is logic to decide if something is feasible (e.g., only if moon is down),
232 it can be calculated here. Helps prevent full __call__ from being called more than needed.
233 """
234 result = True
235 reward = self._calc_value(conditions)
236 # If there are no non-NaN values, we're not feasible now
237 if True not in np.isfinite(reward):
238 result = False
240 return result
242 def _calc_value(self, conditions, indx=None):
243 result = self.result.copy()
244 behind_pix = np.where((int_rounded(conditions.mjd-self.survey_features['last_n_mjds'].feature[0]) > int_rounded(self.season)) &
245 (int_rounded(conditions.airmass) > int_rounded(np.min(self.am_limits))) &
246 (int_rounded(conditions.airmass) < int_rounded(np.max(self.am_limits))))
247 result[behind_pix] = 1
248 result[self.out_footprint] = self.out_of_bounds_val
250 # Update the last time we had an mjd
251 self.mjd_last = conditions.mjd + 0
252 self.recalc = False
253 self.value = result
255 return result
258class Ecliptic_basis_function(Base_basis_function):
259 """Mark the area around the ecliptic
260 """
262 def __init__(self, nside=None, distance_to_eclip=25.):
263 super(Ecliptic_basis_function, self).__init__(nside=nside)
264 self.distance_to_eclip = np.radians(distance_to_eclip)
265 ra, dec = _hpid2RaDec(nside, np.arange(hp.nside2npix(self.nside)))
266 self.result = np.zeros(ra.size)
267 coord = SkyCoord(ra=ra*u.rad, dec=dec*u.rad)
268 eclip_lat = coord.barycentrictrueecliptic.lat.radian
269 good = np.where(np.abs(eclip_lat) < self.distance_to_eclip)
270 self.result[good] += 1
272 def __call__(self, conditions, indx=None):
273 return self.result
276class N_obs_per_year_basis_function(Base_basis_function):
277 """Reward areas that have not been observed N-times in the last year
279 Parameters
280 ----------
281 filtername : str ('r')
282 The filter to track
283 footprint : np.array
284 Should be a HEALpix map. Values of 0 or np.nan will be ignored.
285 n_obs : int (3)
286 The number of observations to demand
287 season : float (300)
288 The amount of time to allow pass before marking a region as "behind". Default 365.25 (days).
289 season_start_hour : float (-2)
290 When to start the season relative to RA 180 degrees away from the sun (hours)
291 season_end_hour : float (2)
292 When to consider a season ending, the RA relative to the sun + 180 degrees. (hours)
293 """
294 def __init__(self, filtername='r', nside=None, footprint=None, n_obs=3, season=300,
295 season_start_hour=-4., season_end_hour=2.):
296 super(N_obs_per_year_basis_function, self).__init__(nside=nside, filtername=filtername)
297 self.footprint = footprint
298 self.n_obs = n_obs
299 self.season = season
300 self.season_start_hour = (season_start_hour)*np.pi/12. # To radians
301 self.season_end_hour = season_end_hour*np.pi/12. # To radians
303 self.survey_features['last_n_mjds'] = features.Last_N_obs_times(nside=nside, filtername=filtername,
304 n_obs=n_obs)
305 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
306 self.out_footprint = np.where((footprint == 0) | np.isnan(footprint))
308 def _calc_value(self, conditions, indx=None):
310 result = self.result.copy()
311 behind_pix = np.where((conditions.mjd-self.survey_features['last_n_mjds'].feature[0]) > self.season)
312 result[behind_pix] = 1
314 # let's ramp up the weight depending on how far into the observing season the healpix is
315 mid_season_ra = (conditions.sunRA + np.pi) % (2.*np.pi)
316 # relative RA
317 relative_ra = (conditions.ra - mid_season_ra) % (2.*np.pi)
318 relative_ra = (self.season_end_hour - relative_ra) % (2.*np.pi)
319 # ok, now
320 relative_ra[np.where(int_rounded(relative_ra) > int_rounded(self.season_end_hour-self.season_start_hour))] = 0
322 weight = relative_ra/(self.season_end_hour - self.season_start_hour)
323 result *= weight
325 # mask off anything outside the footprint
326 result[self.out_footprint] = 0
328 return result
331class Cadence_in_season_basis_function(Base_basis_function):
332 """Drive observations at least every N days in a given area
334 Parameters
335 ----------
336 drive_map : np.array
337 A HEALpix map with values of 1 where the cadence should be driven.
338 filtername : str
339 The filters that can count
340 season_span : float (2.5)
341 How long to consider a spot "in_season" (hours)
342 cadence : float (2.5)
343 How long to wait before activating the basis function (days)
344 """
346 def __init__(self, drive_map, filtername='griz', season_span=2.5, cadence=2.5, nside=None):
347 super(Cadence_in_season_basis_function, self).__init__(nside=nside, filtername=filtername)
348 self.drive_map = drive_map
349 self.season_span = season_span/12.*np.pi # To radians
350 self.cadence = cadence
351 self.survey_features['last_observed'] = features.Last_observed(nside=nside, filtername=filtername)
352 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
354 def _calc_value(self, conditions, indx=None):
355 result = self.result.copy()
356 ra_mid_season = (conditions.sunRA + np.pi) % (2.*np.pi)
358 angle_to_mid_season = np.abs(conditions.ra - ra_mid_season)
359 over = np.where(int_rounded(angle_to_mid_season) > int_rounded(np.pi))
360 angle_to_mid_season[over] = 2.*np.pi - angle_to_mid_season[over]
362 days_lag = conditions.mjd - self.survey_features['last_observed'].feature
364 active_pix = np.where((int_rounded(days_lag) >= int_rounded(self.cadence)) &
365 (self.drive_map == 1) &
366 (int_rounded(angle_to_mid_season) < int_rounded(self.season_span)))
367 result[active_pix] = 1.
369 return result
372class Season_coverage_basis_function(Base_basis_function):
373 """Basis function to encourage N observations per observing season
375 Parameters
376 ----------
377 footprint : healpix map (None)
378 The footprint where one should demand coverage every season
379 n_per_season : int (3)
380 The number of observations to attempt to gather every season
381 offset : healpix map
382 The offset to apply when computing the current season over the sky. utils.create_season_offset
383 is helpful for making this
384 season_frac_start : float (0.5)
385 Only start trying to gather observations after a season is fractionally this far over.
386 """
387 def __init__(self, filtername='r', nside=None, footprint=None, n_per_season=3, offset=None,
388 season_frac_start=0.5):
389 super(Season_coverage_basis_function, self).__init__(nside=nside, filtername=filtername)
391 self.n_per_season = n_per_season
392 self.footprint = footprint
393 self.survey_features['n_obs_season'] = features.N_observations_current_season(filtername=filtername,
394 nside=nside, offset=offset)
395 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
396 self.season_frac_start = season_frac_start
397 self.offset = offset
399 def _calc_value(self, conditions, indx=None):
400 result = self.result.copy()
401 season = utils.season_calc(conditions.night, offset=self.offset, floor=False)
402 # Find the area that still needs observation
403 feature = self.survey_features['n_obs_season'].feature
404 not_enough = np.where((self.footprint > 0) & (feature < self.n_per_season) &
405 ((int_rounded(season-np.floor(season)) > int_rounded(self.season_frac_start))) &
406 (season >= 0))
407 result[not_enough] = 1
408 return result
411class Footprint_nvis_basis_function(Base_basis_function):
412 """Basis function to drive observations of a given footprint. Good to target of opportunity targets
413 where one might want to observe a region 3 times.
415 Parameters
416 ----------
417 footprint : np.array
418 A healpix array (1 for desired, 0 for not desired) of the target footprint.
419 nvis : int (1)
420 The number of visits to try and gather
421 """
422 def __init__(self, filtername='r', nside=None, footprint=None,
423 nvis=1, out_of_bounds_val=np.nan):
424 super(Footprint_nvis_basis_function, self).__init__(nside=nside, filtername=filtername)
425 self.footprint = footprint
426 self.nvis = nvis
428 # Have a feature that tracks how many observations we have
429 self.survey_features = {}
430 # Map of the number of observations in filter
431 self.survey_features['N_obs'] = features.N_observations(filtername=filtername, nside=self.nside)
432 self.result = np.zeros(hp.nside2npix(nside))
433 self.result.fill(out_of_bounds_val)
434 self.out_of_bounds_val = out_of_bounds_val
436 def _calc_value(self, conditions, indx=None):
437 result = self.result.copy()
438 diff = int_rounded(self.footprint*self.nvis - self.survey_features['N_obs'].feature)
440 result[np.where(diff > 0)] = 1
442 # Any spot where we have enough visits is out of bounds now.
443 result[np.where(diff <= 0)] = self.out_of_bounds_val
444 return result
447class Third_observation_basis_function(Base_basis_function):
448 """If there have been observations in two filters long enough ago, go for a third
450 Parameters
451 ----------
452 gap_min : float (40.)
453 The minimum time gap to consider a pixel good (minutes)
454 gap_max : float (120)
455 The maximum time to consider going for a pair (minutes)
456 """
458 def __init__(self, nside=32, filtername1='r', filtername2='z', gap_min=40., gap_max=120.):
459 super(Third_observation_basis_function, self).__init__(nside=nside)
460 self.filtername1 = filtername1
461 self.filtername2 = filtername2
462 self.gap_min = int_rounded(gap_min/60./24.)
463 self.gap_max = int_rounded(gap_max/60./24.)
465 self.survey_features = {}
466 self.survey_features['last_obs_f1'] = features.Last_observed(filtername=filtername1, nside=nside)
467 self.survey_features['last_obs_f2'] = features.Last_observed(filtername=filtername2, nside=nside)
468 self.result = np.empty(hp.nside2npix(self.nside))
469 self.result.fill(np.nan)
471 def _calc_value(self, conditions, indx=None):
472 result = self.result.copy()
473 d1 = int_rounded(conditions.mjd - self.survey_features['last_obs_f1'].feature)
474 d2 = int_rounded(conditions.mjd - self.survey_features['last_obs_f2'].feature)
475 good = np.where((d1 > self.gap_min) & (d1 < self.gap_max) &
476 (d2 > self.gap_min) & (d2 < self.gap_max))
477 result[good] = 1
478 return result
481class Avoid_Fast_Revists(Base_basis_function):
482 """Marks targets as unseen if they are in a specified time window in order to avoid fast revisits.
484 Parameters
485 ----------
486 filtername: (string 'r')
487 The name of the filter for this target map.
488 gap_min : float (25.)
489 Minimum time for the gap (minutes).
490 nside: int (default_nside)
491 The healpix resolution.
492 penalty_val : float (np.nan)
493 The reward value to use for regions to penalize. Will be masked if set to np.nan (default).
494 """
495 def __init__(self, filtername='r', nside=None, gap_min=25.,
496 penalty_val=np.nan):
497 super(Avoid_Fast_Revists, self).__init__(nside=nside, filtername=filtername)
499 self.filtername = filtername
500 self.penalty_val = penalty_val
502 self.gap_min = int_rounded(gap_min/60./24.)
503 self.nside = nside
505 self.survey_features = dict()
506 self.survey_features['Last_observed'] = features.Last_observed(filtername=filtername, nside=nside)
508 def _calc_value(self, conditions, indx=None):
509 result = np.ones(hp.nside2npix(self.nside), dtype=float)
510 if indx is None:
511 indx = np.arange(result.size)
512 diff = int_rounded(conditions.mjd - self.survey_features['Last_observed'].feature[indx])
513 bad = np.where(diff < self.gap_min)[0]
514 result[indx[bad]] = self.penalty_val
515 return result
518class Near_sun_twilight_basis_function(Base_basis_function):
519 """Reward looking into the twilight for NEOs at high airmass
521 Parameters
522 ----------
523 max_airmass : float (2.5)
524 The maximum airmass to try and observe (unitless)
525 """
527 def __init__(self, nside=None, max_airmass=2.5):
528 super(Near_sun_twilight_basis_function, self).__init__(nside=nside)
529 self.max_airmass = int_rounded(max_airmass)
530 self.result = np.zeros(hp.nside2npix(self.nside))
532 def _calc_value(self, conditions, indx=None):
533 result = self.result.copy()
534 good_pix = np.where((int_rounded(conditions.airmass) < self.max_airmass) &
535 (int_rounded(conditions.az_to_sun) < int_rounded(np.pi/2.)))
536 result[good_pix] = conditions.airmass[good_pix] / self.max_airmass.value
537 return result
540class Visit_repeat_basis_function(Base_basis_function):
541 """
542 Basis function to reward re-visiting an area on the sky. Looking for Solar System objects.
544 Parameters
545 ----------
546 gap_min : float (15.)
547 Minimum time for the gap (minutes)
548 gap_max : float (45.)
549 Maximum time for a gap
550 filtername : str ('r')
551 The filter(s) to count with pairs
552 npairs : int (1)
553 The number of pairs of observations to attempt to gather
554 """
555 def __init__(self, gap_min=25., gap_max=45.,
556 filtername='r', nside=None, npairs=1):
558 super(Visit_repeat_basis_function, self).__init__(nside=nside, filtername=filtername)
560 self.gap_min = int_rounded(gap_min/60./24.)
561 self.gap_max = int_rounded(gap_max/60./24.)
562 self.npairs = npairs
564 self.survey_features = {}
565 # Track the number of pairs that have been taken in a night
566 self.survey_features['Pair_in_night'] = features.Pair_in_night(filtername=filtername,
567 gap_min=gap_min, gap_max=gap_max,
568 nside=nside)
569 # When was it last observed
570 # XXX--since this feature is also in Pair_in_night, I should just access that one!
571 self.survey_features['Last_observed'] = features.Last_observed(filtername=filtername,
572 nside=nside)
574 def _calc_value(self, conditions, indx=None):
575 result = np.zeros(hp.nside2npix(self.nside), dtype=float)
576 if indx is None:
577 indx = np.arange(result.size)
578 diff = int_rounded(conditions.mjd - self.survey_features['Last_observed'].feature[indx])
579 good = np.where((diff >= self.gap_min) & (diff <= self.gap_max) &
580 (self.survey_features['Pair_in_night'].feature[indx] < self.npairs))[0]
581 result[indx[good]] += 1.
582 return result
585class M5_diff_basis_function(Base_basis_function):
586 """Basis function based on the 5-sigma depth.
587 Look up the best depth a healpixel achieves, and compute
588 the limiting depth difference given current conditions
589 """
590 def __init__(self, filtername='r', nside=None):
592 super(M5_diff_basis_function, self).__init__(nside=nside, filtername=filtername)
593 # Need to look up the deepest m5 values for all the healpixels
594 m5p = M5percentiles()
595 self.dark_map = m5p.dark_map(filtername=filtername, nside_out=self.nside)
597 def _calc_value(self, conditions, indx=None):
598 # No way to get the sign on this right the first time.
599 result = conditions.M5Depth[self.filtername] - self.dark_map
600 return result
603class Strict_filter_basis_function(Base_basis_function):
604 """Remove the bonus for staying in the same filter if certain conditions are met.
606 If the moon rises/sets or twilight starts/ends, it makes a lot of sense to consider
607 a filter change. This basis function rewards if it matches the current filter, the moon rises or sets,
608 twilight starts or stops, or there has been a large gap since the last observation.
610 Paramters
611 ---------
612 time_lag : float (10.)
613 If there is a gap between observations longer than this, let the filter change (minutes)
614 twi_change : float (-18.)
615 The sun altitude to consider twilight starting/ending (degrees)
616 note_free : str ('DD')
617 No penalty for changing filters if the last observation note field includes string.
618 Useful for giving a free filter change after deep drilling sequence
619 """
620 def __init__(self, time_lag=10., filtername='r', twi_change=-18., note_free='DD'):
622 super(Strict_filter_basis_function, self).__init__(filtername=filtername)
624 self.time_lag = time_lag/60./24. # Convert to days
625 self.twi_change = np.radians(twi_change)
627 self.survey_features = {}
628 self.survey_features['Last_observation'] = features.Last_observation()
629 self.note_free = note_free
631 def _calc_value(self, conditions, **kwargs):
632 # Did the moon set or rise since last observation?
633 moon_changed = conditions.moonAlt * self.survey_features['Last_observation'].feature['moonAlt'] < 0
635 # Are we already in the filter (or at start of night)?
636 in_filter = (conditions.current_filter == self.filtername) | (conditions.current_filter is None)
638 # Has enough time past?
639 time_past = int_rounded(conditions.mjd - self.survey_features['Last_observation'].feature['mjd']) > int_rounded(self.time_lag)
641 # Did twilight start/end?
642 twi_changed = (conditions.sunAlt - self.twi_change) * (self.survey_features['Last_observation'].feature['sunAlt']- self.twi_change) < 0
644 # Did we just finish a DD sequence
645 wasDD = self.note_free in self.survey_features['Last_observation'].feature['note']
647 # Is the filter mounted?
648 mounted = self.filtername in conditions.mounted_filters
650 if (moon_changed | in_filter | time_past | twi_changed | wasDD) & mounted:
651 result = 1.
652 else:
653 result = 0.
655 return result
658class Goal_Strict_filter_basis_function(Base_basis_function):
659 """Remove the bonus for staying in the same filter if certain conditions are met.
661 If the moon rises/sets or twilight starts/ends, it makes a lot of sense to consider
662 a filter change. This basis function rewards if it matches the current filter, the moon rises or sets,
663 twilight starts or stops, or there has been a large gap since the last observation.
665 Parameters
666 ---------
667 time_lag_min: Minimum time after a filter change for which a new filter change will receive zero reward, or
668 be denied at all (see unseen_before_lag).
669 time_lag_max: Time after a filter change where the reward for changing filters achieve its maximum.
670 time_lag_boost: Time after a filter change to apply a boost on the reward.
671 boost_gain: A multiplier factor for the reward after time_lag_boost.
672 unseen_before_lag: If True will make it impossible to switch filter before time_lag has passed.
673 filtername: The filter for which this basis function will be used.
674 tag: When using filter proportion use only regions with this tag to count for observations.
675 twi_change: Switch reward on when twilight changes.
676 proportion: The expected filter proportion distribution.
677 aways_available: If this is true the basis function will aways be computed regardless of the feasibility. If
678 False a more detailed feasibility check is performed. When set to False, it may speed up the computation
679 process by avoiding to compute the reward functions paired with this bf, when observation is not feasible.
681 """
683 def __init__(self, time_lag_min=10., time_lag_max=30.,
684 time_lag_boost=60., boost_gain=2.0, unseen_before_lag=False,
685 filtername='r', tag=None, twi_change=-18., proportion=1.0, aways_available=False):
687 super(Goal_Strict_filter_basis_function, self).__init__(filtername=filtername)
689 self.time_lag_min = time_lag_min / 60. / 24. # Convert to days
690 self.time_lag_max = time_lag_max / 60. / 24. # Convert to days
691 self.time_lag_boost = time_lag_boost / 60. / 24.
692 self.boost_gain = boost_gain
693 self.unseen_before_lag = unseen_before_lag
695 self.twi_change = np.radians(twi_change)
696 self.proportion = proportion
697 self.aways_available = aways_available
699 self.survey_features = {}
700 self.survey_features['Last_observation'] = features.Last_observation()
701 self.survey_features['Last_filter_change'] = features.LastFilterChange()
702 self.survey_features['N_obs_all'] = features.N_obs_count(filtername=None)
703 self.survey_features['N_obs'] = features.N_obs_count(filtername=filtername,
704 tag=tag)
706 def filter_change_bonus(self, time):
708 lag_min = self.time_lag_min
709 lag_max = self.time_lag_max
711 a = 1. / (lag_max - lag_min)
712 b = -a * lag_min
714 bonus = a * time + b
715 # How far behind we are with respect to proportion?
716 nobs = self.survey_features['N_obs'].feature
717 nobs_all = self.survey_features['N_obs_all'].feature
718 goal = self.proportion
719 # need = 1. - nobs / nobs_all + goal if nobs_all > 0 else 1. + goal
720 need = goal / nobs * nobs_all if nobs > 0 else 1.
721 # need /= goal
722 if hasattr(time, '__iter__'):
723 before_lag = np.where(time <= lag_min)
724 bonus[before_lag] = -np.inf if self.unseen_before_lag else 0.
725 after_lag = np.where(time >= lag_max)
726 bonus[after_lag] = 1. if time < self.time_lag_boost else self.boost_gain
727 elif int_rounded(time) <= int_rounded(lag_min):
728 return -np.inf if self.unseen_before_lag else 0.
729 elif int_rounded(time) >= int_rounded(lag_max):
730 return 1. if int_rounded(time) < int_rounded(self.time_lag_boost) else self.boost_gain
732 return bonus * need
734 def check_feasibility(self, conditions):
735 """
736 This method makes a pre-check of the feasibility of this basis function. If a basis function return False
737 on the feasibility check, it won't computed at all.
739 :return:
740 """
742 # Make a quick check about the feasibility of this basis function. If current filter is none, telescope
743 # is parked and we could, in principle, switch to any filter. If this basis function computes reward for
744 # the current filter, then it is also feasible. At last we check for an "aways_available" flag. Meaning, we
745 # force this basis function to be aways be computed.
746 if conditions.current_filter is None or conditions.current_filter == self.filtername or self.aways_available:
747 return True
749 # If we arrive here, we make some extra checks to make sure this bf is feasible and should be computed.
751 # Did the moon set or rise since last observation?
752 moon_changed = conditions.moonAlt * self.survey_features['Last_observation'].feature['moonAlt'] < 0
754 # Are we already in the filter (or at start of night)?
755 not_in_filter = (conditions.current_filter != self.filtername)
757 # Has enough time past?
758 lag = conditions.mjd - self.survey_features['Last_filter_change'].feature['mjd']
759 time_past = int_rounded(lag) > int_rounded(self.time_lag_min)
761 # Did twilight start/end?
762 twi_changed = (conditions.sunAlt - self.twi_change) * \
763 (self.survey_features['Last_observation'].feature['sunAlt'] - self.twi_change) < 0
765 # Did we just finish a DD sequence
766 wasDD = self.survey_features['Last_observation'].feature['note'] == 'DD'
768 # Is the filter mounted?
769 mounted = self.filtername in conditions.mounted_filters
771 if (moon_changed | time_past | twi_changed | wasDD) & mounted & not_in_filter:
772 return True
773 else:
774 return False
776 def _calc_value(self, conditions, **kwargs):
778 if conditions.current_filter is None:
779 return 0. # no bonus if no filter is mounted
780 # elif self.condition_features['Current_filter'].feature == self.filtername:
781 # return 0. # no bonus if on the filter already
783 # Did the moon set or rise since last observation?
784 moon_changed = conditions.moonAlt * \
785 self.survey_features['Last_observation'].feature['moonAlt'] < 0
787 # Are we already in the filter (or at start of night)?
788 # not_in_filter = (self.condition_features['Current_filter'].feature != self.filtername)
790 # Has enough time past?
791 lag = conditions.mjd - self.survey_features['Last_filter_change'].feature['mjd']
792 time_past = lag > 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:
805 result = self.filter_change_bonus(lag) if time_past else 0.
806 else:
807 result = -100. if self.unseen_before_lag else 0.
809 return result
812class Filter_change_basis_function(Base_basis_function):
813 """Reward staying in the current filter.
814 """
815 def __init__(self, filtername='r'):
816 super(Filter_change_basis_function, self).__init__(filtername=filtername)
818 def _calc_value(self, conditions, **kwargs):
820 if (conditions.current_filter == self.filtername) | (conditions.current_filter is None):
821 result = 1.
822 else:
823 result = 0.
824 return result
827class Slewtime_basis_function(Base_basis_function):
828 """Reward slews that take little time
830 Parameters
831 ----------
832 max_time : float (135)
833 The estimated maximum slewtime (seconds). Used to normalize so the basis function
834 spans ~ -1-0 in reward units.
835 """
836 def __init__(self, max_time=135., filtername='r', nside=None):
837 super(Slewtime_basis_function, self).__init__(nside=nside, filtername=filtername)
839 self.maxtime = max_time
840 self.nside = nside
841 self.filtername = filtername
842 self.result = np.zeros(hp.nside2npix(nside), dtype=float)
844 def add_observation(self, observation, indx=None):
845 # No tracking of observations in this basis function. Purely based on conditions.
846 pass
848 def _calc_value(self, conditions, indx=None):
849 # If we are in a different filter, the Filter_change_basis_function will take it
850 if conditions.current_filter != self.filtername:
851 result = 0
852 else:
853 # Need to make sure smaller slewtime is larger reward.
854 if np.size(conditions.slewtime) > 1:
855 result = self.result.copy()
856 good = ~np.isnan(conditions.slewtime)
857 result[good] = -conditions.slewtime[good]/self.maxtime
858 else:
859 result = -conditions.slewtime/self.maxtime
860 return result
863class Aggressive_Slewtime_basis_function(Base_basis_function):
864 """Reward slews that take little time
866 XXX--not sure how this is different from Slewtime_basis_function?
867 Looks like it's checking the slewtime to the field position rather than the healpix maybe?
868 """
870 def __init__(self, max_time=135., order=1., hard_max=None, filtername='r', nside=None):
871 super(Aggressive_Slewtime_basis_function, self).__init__(nside=nside, filtername=filtername)
873 self.maxtime = max_time
874 self.hard_max = hard_max
875 self.order = order
876 self.result = np.zeros(hp.nside2npix(nside), dtype=float)
878 def _calc_value(self, conditions, indx=None):
879 # If we are in a different filter, the Filter_change_basis_function will take it
880 if conditions.current_filter != self.filtername:
881 result = 0.
882 else:
883 # Need to make sure smaller slewtime is larger reward.
884 if np.size(self.condition_features['slewtime'].feature) > 1:
885 result = self.result.copy()
886 result.fill(np.nan)
888 good = np.where(np.bitwise_and(conditions.slewtime > 0.,
889 conditions.slewtime < self.maxtime))
890 result[good] = ((self.maxtime - conditions.slewtime[good]) /
891 self.maxtime) ** self.order
892 if self.hard_max is not None:
893 not_so_good = np.where(conditions.slewtime > self.hard_max)
894 result[not_so_good] -= 10.
895 fields = np.unique(conditions.hp2fields[good])
896 for field in fields:
897 hp_indx = np.where(conditions.hp2fields == field)
898 result[hp_indx] = np.min(result[hp_indx])
899 else:
900 result = (self.maxtime - conditions.slewtime) / self.maxtime
901 return result
904class Skybrightness_limit_basis_function(Base_basis_function):
905 """Mask regions that are outside a sky brightness limit
907 XXX--TODO: This should probably go to the mask basis functions.
909 Parameters
910 ----------
911 min : float (20.)
912 The minimum sky brightness (mags).
913 max : float (30.)
914 The maximum sky brightness (mags).
916 """
917 def __init__(self, nside=None, filtername='r', sbmin=20., sbmax=30.):
918 super(Skybrightness_limit_basis_function, self).__init__(nside=nside, filtername=filtername)
920 self.min = int_rounded(sbmin)
921 self.max = int_rounded(sbmax)
922 self.result = np.empty(hp.nside2npix(self.nside), dtype=float)
923 self.result.fill(np.nan)
925 def _calc_value(self, conditions, indx=None):
926 result = self.result.copy()
928 good = np.where(np.bitwise_and(int_rounded(conditions.skybrightness[self.filtername]) > self.min,
929 int_rounded(conditions.skybrightness[self.filtername]) < self.max))
930 result[good] = 1.0
932 return result
935class CableWrap_unwrap_basis_function(Base_basis_function):
936 """
937 Parameters
938 ----------
939 minAz : float (20.)
940 The minimum azimuth to activate bf (degrees)
941 maxAz : float (82.)
942 The maximum azimuth to activate bf (degrees)
943 unwrap_until: float (90.)
944 The window in which the bf is activated (degrees)
945 """
946 def __init__(self, nside=None, minAz=-270., maxAz=270., minAlt=20., maxAlt=82.,
947 activate_tol=20., delta_unwrap=1.2, unwrap_until=70., max_duration=30.):
948 super(CableWrap_unwrap_basis_function, self).__init__(nside=nside)
950 self.minAz = np.radians(minAz)
951 self.maxAz = np.radians(maxAz)
953 self.activate_tol = np.radians(activate_tol)
954 self.delta_unwrap = np.radians(delta_unwrap)
955 self.unwrap_until = np.radians(unwrap_until)
957 self.minAlt = np.radians(minAlt)
958 self.maxAlt = np.radians(maxAlt)
959 # Convert to half-width for convienence
960 self.nside = nside
961 self.active = False
962 self.unwrap_direction = 0. # either -1., 0., 1.
963 self.max_duration = max_duration/60./24. # Convert to days
964 self.activation_time = None
965 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
967 def _calc_value(self, conditions, indx=None):
969 result = self.result.copy()
971 current_abs_rad = np.radians(conditions.az)
972 unseen = np.where(np.bitwise_or(conditions.alt < self.minAlt,
973 conditions.alt > self.maxAlt))
974 result[unseen] = np.nan
976 if (self.minAz + self.activate_tol < current_abs_rad < self.maxAz - self.activate_tol) and not self.active:
977 return result
978 elif self.active and self.unwrap_direction == 1 and current_abs_rad > self.minAz+self.unwrap_until:
979 self.active = False
980 self.unwrap_direction = 0.
981 self.activation_time = None
982 return result
983 elif self.active and self.unwrap_direction == -1 and current_abs_rad < self.maxAz-self.unwrap_until:
984 self.active = False
985 self.unwrap_direction = 0.
986 self.activation_time = None
987 return result
988 elif (self.activation_time is not None and
989 conditions.mjd - self.activation_time > self.max_duration):
990 self.active = False
991 self.unwrap_direction = 0.
992 self.activation_time = None
993 return result
995 if not self.active:
996 self.activation_time = conditions.mjd
997 if current_abs_rad < 0.:
998 self.unwrap_direction = 1 # clock-wise unwrap
999 else:
1000 self.unwrap_direction = -1 # counter-clock-wise unwrap
1002 self.active = True
1004 max_abs_rad = self.maxAz
1005 min_abs_rad = self.minAz
1007 TWOPI = 2.*np.pi
1009 # Compute distance and accumulated az.
1010 norm_az_rad = np.divmod(conditions.az - min_abs_rad, TWOPI)[1] + min_abs_rad
1011 distance_rad = divmod(norm_az_rad - current_abs_rad, TWOPI)[1]
1012 get_shorter = np.where(distance_rad > np.pi)
1013 distance_rad[get_shorter] -= TWOPI
1014 accum_abs_rad = current_abs_rad + distance_rad
1016 # Compute wrap regions and fix distances
1017 mask_max = np.where(accum_abs_rad > max_abs_rad)
1018 distance_rad[mask_max] -= TWOPI
1019 mask_min = np.where(accum_abs_rad < min_abs_rad)
1020 distance_rad[mask_min] += TWOPI
1022 # Step-2: Repeat but now with compute reward to unwrap using specified delta_unwrap
1023 unwrap_current_abs_rad = current_abs_rad - (np.abs(self.delta_unwrap) if self.unwrap_direction > 0
1024 else -np.abs(self.delta_unwrap))
1025 unwrap_distance_rad = divmod(norm_az_rad - unwrap_current_abs_rad, TWOPI)[1]
1026 unwrap_get_shorter = np.where(unwrap_distance_rad > np.pi)
1027 unwrap_distance_rad[unwrap_get_shorter] -= TWOPI
1028 unwrap_distance_rad = np.abs(unwrap_distance_rad)
1030 if self.unwrap_direction < 0:
1031 mask = np.where(accum_abs_rad > unwrap_current_abs_rad)
1032 else:
1033 mask = np.where(accum_abs_rad < unwrap_current_abs_rad)
1035 # Finally build reward map
1036 result = (1. - unwrap_distance_rad/np.max(unwrap_distance_rad))**2.
1037 result[mask] = 0.
1038 result[unseen] = np.nan
1040 return result
1043class Cadence_enhance_basis_function(Base_basis_function):
1044 """Drive a certain cadence
1045 Parameters
1046 ----------
1047 filtername : str ('gri')
1048 The filter(s) that should be grouped together
1049 supress_window : list of float
1050 The start and stop window for when observations should be repressed (days)
1051 apply_area : healpix map
1052 The area over which to try and drive the cadence. Good values as 1, no candece drive 0.
1053 Probably works as a bool array too."""
1054 def __init__(self, filtername='gri', nside=None,
1055 supress_window=[0, 1.8], supress_val=-0.5,
1056 enhance_window=[2.1, 3.2], enhance_val=1.,
1057 apply_area=None):
1058 super(Cadence_enhance_basis_function, self).__init__(nside=nside, filtername=filtername)
1060 self.supress_window = np.sort(supress_window)
1061 self.supress_val = supress_val
1062 self.enhance_window = np.sort(enhance_window)
1063 self.enhance_val = enhance_val
1065 self.survey_features = {}
1066 self.survey_features['last_observed'] = features.Last_observed(filtername=filtername)
1068 self.empty = np.zeros(hp.nside2npix(self.nside), dtype=float)
1069 # No map, try to drive the whole area
1070 if apply_area is None:
1071 self.apply_indx = np.arange(self.empty.size)
1072 else:
1073 self.apply_indx = np.where(apply_area != 0)[0]
1075 def _calc_value(self, conditions, indx=None):
1076 # copy an empty array
1077 result = self.empty.copy()
1078 if indx is not None:
1079 ind = np.intersect1d(indx, self.apply_indx)
1080 else:
1081 ind = self.apply_indx
1082 if np.size(ind) == 0:
1083 result = 0
1084 else:
1085 mjd_diff = conditions.mjd - self.survey_features['last_observed'].feature[ind]
1086 to_supress = np.where((int_rounded(mjd_diff) > int_rounded(self.supress_window[0])) &
1087 (int_rounded(mjd_diff) < int_rounded(self.supress_window[1])))
1088 result[ind[to_supress]] = self.supress_val
1089 to_enhance = np.where((int_rounded(mjd_diff) > int_rounded(self.enhance_window[0])) &
1090 (int_rounded(mjd_diff) < int_rounded(self.enhance_window[1])))
1091 result[ind[to_enhance]] = self.enhance_val
1092 return result
1095class Azimuth_basis_function(Base_basis_function):
1096 """Reward staying in the same azimuth range. Possibly better than using slewtime, especially when selecting a large area of sky.
1098 Parameters
1099 ----------
1101 """
1103 def __init__(self, nside=None):
1104 super(Azimuth_basis_function, self).__init__(nside=nside)
1106 def _calc_value(self, conditions, indx=None):
1107 az_dist = conditions.az - conditions.telAz
1108 az_dist = az_dist % (2.*np.pi)
1109 over = np.where(az_dist > np.pi)
1110 az_dist[over] = 2. * np.pi - az_dist[over]
1111 # Normalize sp between 0 and 1
1112 result = az_dist/np.pi
1113 return result
1116class Az_modulo_basis_function(Base_basis_function):
1117 """Try to replicate the Rothchild et al cadence forcing by only observing on limited az ranges per night.
1119 Parameters
1120 ----------
1121 az_limits : list of float pairs (None)
1122 The azimuth limits (degrees) to use.
1123 """
1124 def __init__(self, nside=None, az_limits=None, out_of_bounds_val=-1.):
1125 super(Az_modulo_basis_function, self).__init__(nside=nside)
1126 self.result = np.ones(hp.nside2npix(self.nside))
1127 if az_limits is None:
1128 spread = 100./2.
1129 self.az_limits = np.radians([[360-spread, spread],
1130 [90.-spread, 90.+spread],
1131 [180.-spread, 180.+spread]])
1132 else:
1133 self.az_limits = np.radians(az_limits)
1134 self.mod_val = len(self.az_limits)
1135 self.out_of_bounds_val = out_of_bounds_val
1137 def _calc_value(self, conditions, indx=None):
1138 result = self.result.copy()
1139 az_lim = self.az_limits[np.max(conditions.night) % self.mod_val]
1141 if az_lim[0] < az_lim[1]:
1142 out_pix = np.where((int_rounded(conditions.az) < int_rounded(az_lim[0])) |
1143 (int_rounded(conditions.az) > int_rounded(az_lim[1])))
1144 else:
1145 out_pix = np.where((int_rounded(conditions.az) < int_rounded(az_lim[0])) |
1146 (int_rounded(conditions.az) > int_rounded(az_lim[1])))[0]
1147 result[out_pix] = self.out_of_bounds_val
1148 return result
1151class Dec_modulo_basis_function(Base_basis_function):
1152 """Emphasize dec bands on a nightly varying basis
1154 Parameters
1155 ----------
1156 dec_limits : list of float pairs (None)
1157 The azimuth limits (degrees) to use.
1158 """
1159 def __init__(self, nside=None, dec_limits=None, out_of_bounds_val=-1.):
1160 super(Dec_modulo_basis_function, self).__init__(nside=nside)
1162 npix = hp.nside2npix(nside)
1163 hpids = np.arange(npix)
1164 ra, dec = _hpid2RaDec(nside, hpids)
1166 self.results = []
1168 if dec_limits is None:
1169 self.dec_limits = np.radians([[-90., -32.8],
1170 [-32.8, -12.],
1171 [-12., 35.]])
1172 else:
1173 self.dec_limits = np.radians(dec_limits)
1174 self.mod_val = len(self.dec_limits)
1175 self.out_of_bounds_val = out_of_bounds_val
1177 for limits in self.dec_limits:
1178 good = np.where((dec >= limits[0]) & (dec < limits[1]))[0]
1179 tmp = np.zeros(npix)
1180 tmp[good] = 1
1181 self.results.append(tmp)
1183 def _calc_value(self, conditions, indx=None):
1184 night_index = np.max(conditions.night % self.mod_val)
1185 result = self.results[night_index]
1187 return result
1190class Map_modulo_basis_function(Base_basis_function):
1191 """Similar to Dec_modulo, but now use input masks
1193 Parameters
1194 ----------
1195 inmaps : list of hp arrays
1196 """
1197 def __init__(self, inmaps):
1198 nside = hp.npix2nside(np.size(inmaps[0]))
1199 super(Map_modulo_basis_function, self).__init__(nside=nside)
1200 self.maps = inmaps
1201 self.mod_val = len(inmaps)
1203 def _calc_value(self, conditions, indx=None):
1204 indx = np.max(conditions.night % self.mod_val)
1205 result = self.maps[indx]
1206 return result
1209class Good_seeing_basis_function(Base_basis_function):
1210 """Drive observations in good seeing conditions"""
1212 def __init__(self, nside=None, filtername='r', footprint=None, FWHMeff_limit=0.8,
1213 mag_diff=0.75):
1214 super(Good_seeing_basis_function, self).__init__(nside=nside)
1216 self.filtername = filtername
1217 self.FWHMeff_limit = int_rounded(FWHMeff_limit)
1218 if footprint is None:
1219 fp = utils.standard_goals(nside=nside)[filtername]
1220 else:
1221 fp = footprint
1222 self.out_of_bounds = np.where(fp == 0)[0]
1223 self.result = fp*0
1225 self.mag_diff = int_rounded(mag_diff)
1226 self.survey_features = {}
1227 self.survey_features['coadd_depth_all'] = features.Coadded_depth(filtername=filtername,
1228 nside=nside)
1229 self.survey_features['coadd_depth_good'] = features.Coadded_depth(filtername=filtername,
1230 nside=nside,
1231 FWHMeff_limit=FWHMeff_limit)
1233 def _calc_value(self, conditions, **kwargs):
1234 # Seeing is "bad"
1235 if int_rounded(conditions.FWHMeff[self.filtername].min()) > self.FWHMeff_limit:
1236 return 0
1237 result = self.result.copy()
1239 diff = self.survey_features['coadd_depth_all'].feature - self.survey_features['coadd_depth_good'].feature
1240 # Where are there things we want to observe?
1241 good_pix = np.where((int_rounded(diff) > self.mag_diff) &
1242 (int_rounded(conditions.FWHMeff[self.filtername]) <= self.FWHMeff_limit))
1243 # Hm, should this scale by the mag differences? Probably.
1244 result[good_pix] = diff[good_pix]
1245 result[self.out_of_bounds] = 0
1247 return result
1250class Template_generate_basis_function(Base_basis_function):
1251 """Emphasize areas that have not been observed in a long time
1253 Parameters
1254 ----------
1255 day_gap : float (250.)
1256 How long to wait before boosting the reward (days)
1257 footprint : np.array (None)
1258 The indices of the healpixels to apply the boost to. Uses the default footprint if None
1259 """
1260 def __init__(self, nside=None, day_gap=250., filtername='r', footprint=None):
1261 super(Template_generate_basis_function, self).__init__(nside=nside)
1262 self.day_gap = day_gap
1263 self.filtername = filtername
1264 self.survey_features = {}
1265 self.survey_features['Last_observed'] = features.Last_observed(filtername=filtername)
1266 self.result = np.zeros(hp.nside2npix(self.nside))
1267 if footprint is None:
1268 fp = utils.standard_goals(nside=nside)[filtername]
1269 else:
1270 fp = footprint
1271 self.out_of_bounds = np.where(fp == 0)
1273 def _calc_value(self, conditions, **kwargs):
1274 result = self.result.copy()
1275 overdue = np.where((int_rounded(conditions.mjd - self.survey_features['Last_observed'].feature)) > int_rounded(self.day_gap))
1276 result[overdue] = 1
1277 result[self.out_of_bounds] = 0
1279 return result
1282class Observed_twice_basis_function(Base_basis_function):
1283 """Mask out pixels that haven't been observed in the night
1284 """
1285 def __init__(self, nside=None, filtername='r', n_obs_needed=2, n_obs_in_filt_needed=1):
1286 super(Observed_twice_basis_function, self).__init__(nside=nside)
1287 self.n_obs_needed = n_obs_needed
1288 self.n_obs_in_filt_needed = n_obs_in_filt_needed
1289 self.filtername = filtername
1290 self.survey_features = {}
1291 self.survey_features['N_obs_infilt'] = features.N_obs_night(nside=nside, filtername=filtername)
1292 self.survey_features['N_obs_all'] = features.N_obs_night(nside=nside, filtername='')
1294 self.result = np.zeros(hp.nside2npix(self.nside))
1296 def _calc_value(self, conditions, **kwargs):
1297 result = self.result.copy()
1298 good_pix = np.where((self.survey_features['N_obs_infilt'].feature >= self.n_obs_in_filt_needed) &
1299 (self.survey_features['N_obs_all'].feature >= self.n_obs_needed))[0]
1300 result[good_pix] = 1
1302 return result