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', 'Azimuth_basis_function',
21 'Az_modulo_basis_function', 'Dec_modulo_basis_function', 'Map_modulo_basis_function',
22 'Template_generate_basis_function',
23 'Footprint_nvis_basis_function', 'Third_observation_basis_function', 'Season_coverage_basis_function',
24 'N_obs_per_year_basis_function', 'Cadence_in_season_basis_function', 'Near_sun_twilight_basis_function',
25 'N_obs_high_am_basis_function', 'Good_seeing_basis_function', 'Observed_twice_basis_function',
26 'Ecliptic_basis_function']
29class Base_basis_function(object):
30 """Class that takes features and computes a reward function when called.
31 """
33 def __init__(self, nside=None, filtername=None, **kwargs):
35 # Set if basis function needs to be recalculated if there is a new observation
36 self.update_on_newobs = True
37 # Set if basis function needs to be recalculated if conditions change
38 self.update_on_mjd = True
39 # Dict to hold all the features we want to track
40 self.survey_features = {}
41 # Keep track of the last time the basis function was called. If mjd doesn't change, use cached value
42 self.mjd_last = None
43 self.value = 0
44 # list the attributes to compare to check if basis functions are equal.
45 self.attrs_to_compare = []
46 # Do we need to recalculate the basis function
47 self.recalc = True
48 # Basis functions don't technically all need an nside, but so many do might as well set it here
49 if nside is None:
50 self.nside = utils.set_default_nside()
51 else:
52 self.nside = nside
54 self.filtername = filtername
56 def add_observation(self, observation, indx=None):
57 """
58 Parameters
59 ----------
60 observation : np.array
61 An array with information about the input observation
62 indx : np.array
63 The indices of the healpix map that the observation overlaps with
64 """
65 for feature in self.survey_features:
66 self.survey_features[feature].add_observation(observation, indx=indx)
67 if self.update_on_newobs:
68 self.recalc = True
70 def check_feasibility(self, conditions):
71 """If there is logic to decide if something is feasible (e.g., only if moon is down),
72 it can be calculated here. Helps prevent full __call__ from being called more than needed.
73 """
74 return True
76 def _calc_value(self, conditions, **kwarge):
77 self.value = 0
78 # Update the last time we had an mjd
79 self.mjd_last = conditions.mjd + 0
80 self.recalc = False
81 return self.value
83 def __eq__(self):
84 # XXX--to work on if we need to make a registry of basis functions.
85 pass
87 def __ne__(self):
88 pass
90 def __call__(self, conditions, **kwargs):
91 """
92 Parameters
93 ----------
94 conditions : lsst.sims.featureScheduler.features.conditions object
95 Object that has attributes for all the current conditions.
97 Return a reward healpix map or a reward scalar.
98 """
99 # If we are not feasible, return -inf
100 if not self.check_feasibility(conditions):
101 return -np.inf
102 if self.recalc:
103 self.value = self._calc_value(conditions, **kwargs)
104 if self.update_on_mjd:
105 if conditions.mjd != self.mjd_last:
106 self.value = self._calc_value(conditions, **kwargs)
107 return self.value
110class Constant_basis_function(Base_basis_function):
111 """Just add a constant
112 """
113 def __call__(self, conditions, **kwargs):
114 return 1
117class Avoid_long_gaps_basis_function(Base_basis_function):
118 """
119 Boost the reward on parts of the survey that haven't been observed for a while
120 """
122 def __init__(self, filtername=None, nside=None, footprint=None, min_gap=4., max_gap=40.,
123 ha_limit=3.5):
124 super(Avoid_long_gaps_basis_function, self).__init__(nside=nside, filtername=filtername)
125 self.min_gap = min_gap
126 self.max_gap = max_gap
127 self.filtername = filtername
128 self.footprint = footprint
129 self.ha_limit = 2.*np.pi*ha_limit/24. # To radians
130 self.survey_features = {}
131 self.survey_features['last_observed'] = features.Last_observed(nside=nside, filtername=filtername)
132 self.result = np.zeros(hp.nside2npix(self.nside))
134 def _calc_value(self, conditions, indx=None):
135 result = self.result.copy()
137 gap = conditions.mjd - self.survey_features['last_observed'].feature
138 in_range = np.where((gap > self.min_gap) & (gap < self.max_gap) & (self.footprint > 0))
139 result[in_range] = 1
141 # mask out areas beyond the hour angle limit.
142 out_ha = np.where((conditions.HA > self.ha_limit) & (conditions.HA < (2.*np.pi - self.ha_limit)))[0]
143 result[out_ha] = 0
145 return result
148class Target_map_basis_function(Base_basis_function):
149 """Basis function that tracks number of observations and tries to match a specified spatial distribution
151 Parameters
152 ----------
153 filtername: (string 'r')
154 The name of the filter for this target map.
155 nside: int (default_nside)
156 The healpix resolution.
157 target_map : numpy array (None)
158 A healpix map showing the ratio of observations desired for all points on the sky
159 norm_factor : float (0.00010519)
160 for converting target map to number of observations. Should be the area of the camera
161 divided by the area of a healpixel divided by the sum of all your goal maps. Default
162 value assumes LSST foV has 1.75 degree radius and the standard goal maps. If using
163 mulitple filters, see lsst.sims.featureScheduler.utils.calc_norm_factor for a utility
164 that computes norm_factor.
165 out_of_bounds_val : float (-10.)
166 Reward value to give regions where there are no observations requested (unitless).
167 """
168 def __init__(self, filtername='r', nside=None, target_map=None,
169 norm_factor=None,
170 out_of_bounds_val=-10.):
172 super(Target_map_basis_function, self).__init__(nside=nside, filtername=filtername)
174 if norm_factor is None:
175 warnings.warn('No norm_factor set, use utils.calc_norm_factor if using multiple filters.')
176 self.norm_factor = 0.00010519
177 else:
178 self.norm_factor = norm_factor
180 self.survey_features = {}
181 # Map of the number of observations in filter
182 self.survey_features['N_obs'] = features.N_observations(filtername=filtername, nside=self.nside)
183 # Count of all the observations
184 self.survey_features['N_obs_count_all'] = features.N_obs_count(filtername=None)
185 if target_map is None:
186 self.target_map = utils.generate_goal_map(filtername=filtername, nside=self.nside)
187 else:
188 self.target_map = target_map
189 self.out_of_bounds_area = np.where(self.target_map == 0)[0]
190 self.out_of_bounds_val = out_of_bounds_val
191 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
192 self.all_indx = np.arange(self.result.size)
194 def _calc_value(self, conditions, indx=None):
195 """
196 Parameters
197 ----------
198 indx : list (None)
199 Index values to compute, if None, full map is computed
200 Returns
201 -------
202 Healpix reward map
203 """
204 result = self.result.copy()
205 if indx is None:
206 indx = self.all_indx
208 # Find out how many observations we want now at those points
209 goal_N = self.target_map[indx] * self.survey_features['N_obs_count_all'].feature * self.norm_factor
211 result[indx] = goal_N - self.survey_features['N_obs'].feature[indx]
212 result[self.out_of_bounds_area] = self.out_of_bounds_val
214 return result
217def azRelPoint(azs, pointAz):
218 azRelMoon = (azs - pointAz) % (2.0*np.pi)
219 if isinstance(azs, np.ndarray):
220 over = np.where(azRelMoon > np.pi)
221 azRelMoon[over] = 2. * np.pi - azRelMoon[over]
222 else:
223 if azRelMoon > np.pi:
224 azRelMoon = 2.0 * np.pi - azRelMoon
225 return azRelMoon
228class N_obs_high_am_basis_function(Base_basis_function):
229 """Reward only reward/count observations at high airmass
230 """
232 def __init__(self, nside=None, filtername='r', footprint=None, n_obs=3, season=300.,
233 am_limits=[1.5, 2.2], out_of_bounds_val=np.nan):
234 super(N_obs_high_am_basis_function, self).__init__(nside=nside, filtername=filtername)
235 self.footprint = footprint
236 self.out_footprint = np.where((footprint == 0) | np.isnan(footprint))
237 self.am_limits = am_limits
238 self.season = season
239 self.survey_features['last_n_mjds'] = features.Last_N_obs_times(nside=nside, filtername=filtername,
240 n_obs=n_obs)
242 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float) + out_of_bounds_val
243 self.out_of_bounds_val = out_of_bounds_val
245 def add_observation(self, observation, indx=None):
246 """
247 Parameters
248 ----------
249 observation : np.array
250 An array with information about the input observation
251 indx : np.array
252 The indices of the healpix map that the observation overlaps with
253 """
255 # Only count the observations if they are at the airmass limits
256 if (observation['airmass'] > np.min(self.am_limits)) & (observation['airmass'] < np.max(self.am_limits)):
257 for feature in self.survey_features:
258 self.survey_features[feature].add_observation(observation, indx=indx)
259 if self.update_on_newobs:
260 self.recalc = True
262 def check_feasibility(self, conditions):
263 """If there is logic to decide if something is feasible (e.g., only if moon is down),
264 it can be calculated here. Helps prevent full __call__ from being called more than needed.
265 """
266 result = True
267 reward = self._calc_value(conditions)
268 # If there are no non-NaN values, we're not feasible now
269 if True not in np.isfinite(reward):
270 result = False
272 return result
274 def _calc_value(self, conditions, indx=None):
275 result = self.result.copy()
276 behind_pix = np.where((int_rounded(conditions.mjd-self.survey_features['last_n_mjds'].feature[0]) > int_rounded(self.season)) &
277 (int_rounded(conditions.airmass) > int_rounded(np.min(self.am_limits))) &
278 (int_rounded(conditions.airmass) < int_rounded(np.max(self.am_limits))))
279 result[behind_pix] = 1
280 result[self.out_footprint] = self.out_of_bounds_val
282 # Update the last time we had an mjd
283 self.mjd_last = conditions.mjd + 0
284 self.recalc = False
285 self.value = result
287 return result
290class Ecliptic_basis_function(Base_basis_function):
291 """Mark the area around the ecliptic
292 """
294 def __init__(self, nside=None, distance_to_eclip=25.):
295 super(Ecliptic_basis_function, self).__init__(nside=nside)
296 self.distance_to_eclip = np.radians(distance_to_eclip)
297 ra, dec = _hpid2RaDec(nside, np.arange(hp.nside2npix(self.nside)))
298 self.result = np.zeros(ra.size)
299 coord = SkyCoord(ra=ra*u.rad, dec=dec*u.rad)
300 eclip_lat = coord.barycentrictrueecliptic.lat.radian
301 good = np.where(np.abs(eclip_lat) < self.distance_to_eclip)
302 self.result[good] += 1
304 def __call__(self, conditions, indx=None):
305 return self.result
308class N_obs_per_year_basis_function(Base_basis_function):
309 """Reward areas that have not been observed N-times in the last year
311 Parameters
312 ----------
313 filtername : str ('r')
314 The filter to track
315 footprint : np.array
316 Should be a HEALpix map. Values of 0 or np.nan will be ignored.
317 n_obs : int (3)
318 The number of observations to demand
319 season : float (300)
320 The amount of time to allow pass before marking a region as "behind". Default 365.25 (days).
321 season_start_hour : float (-2)
322 When to start the season relative to RA 180 degrees away from the sun (hours)
323 season_end_hour : float (2)
324 When to consider a season ending, the RA relative to the sun + 180 degrees. (hours)
325 """
326 def __init__(self, filtername='r', nside=None, footprint=None, n_obs=3, season=300,
327 season_start_hour=-4., season_end_hour=2.):
328 super(N_obs_per_year_basis_function, self).__init__(nside=nside, filtername=filtername)
329 self.footprint = footprint
330 self.n_obs = n_obs
331 self.season = season
332 self.season_start_hour = (season_start_hour)*np.pi/12. # To radians
333 self.season_end_hour = season_end_hour*np.pi/12. # To radians
335 self.survey_features['last_n_mjds'] = features.Last_N_obs_times(nside=nside, filtername=filtername,
336 n_obs=n_obs)
337 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
338 self.out_footprint = np.where((footprint == 0) | np.isnan(footprint))
340 def _calc_value(self, conditions, indx=None):
342 result = self.result.copy()
343 behind_pix = np.where((conditions.mjd-self.survey_features['last_n_mjds'].feature[0]) > self.season)
344 result[behind_pix] = 1
346 # let's ramp up the weight depending on how far into the observing season the healpix is
347 mid_season_ra = (conditions.sunRA + np.pi) % (2.*np.pi)
348 # relative RA
349 relative_ra = (conditions.ra - mid_season_ra) % (2.*np.pi)
350 relative_ra = (self.season_end_hour - relative_ra) % (2.*np.pi)
351 # ok, now
352 relative_ra[np.where(int_rounded(relative_ra) > int_rounded(self.season_end_hour-self.season_start_hour))] = 0
354 weight = relative_ra/(self.season_end_hour - self.season_start_hour)
355 result *= weight
357 # mask off anything outside the footprint
358 result[self.out_footprint] = 0
360 return result
363class Cadence_in_season_basis_function(Base_basis_function):
364 """Drive observations at least every N days in a given area
366 Parameters
367 ----------
368 drive_map : np.array
369 A HEALpix map with values of 1 where the cadence should be driven.
370 filtername : str
371 The filters that can count
372 season_span : float (2.5)
373 How long to consider a spot "in_season" (hours)
374 cadence : float (2.5)
375 How long to wait before activating the basis function (days)
376 """
378 def __init__(self, drive_map, filtername='griz', season_span=2.5, cadence=2.5, nside=None):
379 super(Cadence_in_season_basis_function, self).__init__(nside=nside, filtername=filtername)
380 self.drive_map = drive_map
381 self.season_span = season_span/12.*np.pi # To radians
382 self.cadence = cadence
383 self.survey_features['last_observed'] = features.Last_observed(nside=nside, filtername=filtername)
384 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
386 def _calc_value(self, conditions, indx=None):
387 result = self.result.copy()
388 ra_mid_season = (conditions.sunRA + np.pi) % (2.*np.pi)
390 angle_to_mid_season = np.abs(conditions.ra - ra_mid_season)
391 over = np.where(int_rounded(angle_to_mid_season) > int_rounded(np.pi))
392 angle_to_mid_season[over] = 2.*np.pi - angle_to_mid_season[over]
394 days_lag = conditions.mjd - self.survey_features['last_observed'].feature
396 active_pix = np.where((int_rounded(days_lag) >= int_rounded(self.cadence)) &
397 (self.drive_map == 1) &
398 (int_rounded(angle_to_mid_season) < int_rounded(self.season_span)))
399 result[active_pix] = 1.
401 return result
404class Season_coverage_basis_function(Base_basis_function):
405 """Basis function to encourage N observations per observing season
407 Parameters
408 ----------
409 footprint : healpix map (None)
410 The footprint where one should demand coverage every season
411 n_per_season : int (3)
412 The number of observations to attempt to gather every season
413 offset : healpix map
414 The offset to apply when computing the current season over the sky. utils.create_season_offset
415 is helpful for making this
416 season_frac_start : float (0.5)
417 Only start trying to gather observations after a season is fractionally this far over.
418 """
419 def __init__(self, filtername='r', nside=None, footprint=None, n_per_season=3, offset=None,
420 season_frac_start=0.5):
421 super(Season_coverage_basis_function, self).__init__(nside=nside, filtername=filtername)
423 self.n_per_season = n_per_season
424 self.footprint = footprint
425 self.survey_features['n_obs_season'] = features.N_observations_current_season(filtername=filtername,
426 nside=nside, offset=offset)
427 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
428 self.season_frac_start = season_frac_start
429 self.offset = offset
431 def _calc_value(self, conditions, indx=None):
432 result = self.result.copy()
433 season = utils.season_calc(conditions.night, offset=self.offset, floor=False)
434 # Find the area that still needs observation
435 feature = self.survey_features['n_obs_season'].feature
436 not_enough = np.where((self.footprint > 0) & (feature < self.n_per_season) &
437 ((int_rounded(season-np.floor(season)) > int_rounded(self.season_frac_start))) &
438 (season >= 0))
439 result[not_enough] = 1
440 return result
443class Footprint_nvis_basis_function(Base_basis_function):
444 """Basis function to drive observations of a given footprint. Good to target of opportunity targets
445 where one might want to observe a region 3 times.
447 Parameters
448 ----------
449 footprint : np.array
450 A healpix array (1 for desired, 0 for not desired) of the target footprint.
451 nvis : int (1)
452 The number of visits to try and gather
453 """
454 def __init__(self, filtername='r', nside=None, footprint=None,
455 nvis=1, out_of_bounds_val=np.nan):
456 super(Footprint_nvis_basis_function, self).__init__(nside=nside, filtername=filtername)
457 self.footprint = footprint
458 self.nvis = nvis
460 # Have a feature that tracks how many observations we have
461 self.survey_features = {}
462 # Map of the number of observations in filter
463 self.survey_features['N_obs'] = features.N_observations(filtername=filtername, nside=self.nside)
464 self.result = np.zeros(hp.nside2npix(nside))
465 self.result.fill(out_of_bounds_val)
466 self.out_of_bounds_val = out_of_bounds_val
468 def _calc_value(self, conditions, indx=None):
469 result = self.result.copy()
470 diff = int_rounded(self.footprint*self.nvis - self.survey_features['N_obs'].feature)
472 result[np.where(diff > 0)] = 1
474 # Any spot where we have enough visits is out of bounds now.
475 result[np.where(diff <= 0)] = self.out_of_bounds_val
476 return result
479class Third_observation_basis_function(Base_basis_function):
480 """If there have been observations in two filters long enough ago, go for a third
482 Parameters
483 ----------
484 gap_min : float (40.)
485 The minimum time gap to consider a pixel good (minutes)
486 gap_max : float (120)
487 The maximum time to consider going for a pair (minutes)
488 """
490 def __init__(self, nside=32, filtername1='r', filtername2='z', gap_min=40., gap_max=120.):
491 super(Third_observation_basis_function, self).__init__(nside=nside)
492 self.filtername1 = filtername1
493 self.filtername2 = filtername2
494 self.gap_min = int_rounded(gap_min/60./24.)
495 self.gap_max = int_rounded(gap_max/60./24.)
497 self.survey_features = {}
498 self.survey_features['last_obs_f1'] = features.Last_observed(filtername=filtername1, nside=nside)
499 self.survey_features['last_obs_f2'] = features.Last_observed(filtername=filtername2, nside=nside)
500 self.result = np.empty(hp.nside2npix(self.nside))
501 self.result.fill(np.nan)
503 def _calc_value(self, conditions, indx=None):
504 result = self.result.copy()
505 d1 = int_rounded(conditions.mjd - self.survey_features['last_obs_f1'].feature)
506 d2 = int_rounded(conditions.mjd - self.survey_features['last_obs_f2'].feature)
507 good = np.where((d1 > self.gap_min) & (d1 < self.gap_max) &
508 (d2 > self.gap_min) & (d2 < self.gap_max))
509 result[good] = 1
510 return result
513class Avoid_Fast_Revists(Base_basis_function):
514 """Marks targets as unseen if they are in a specified time window in order to avoid fast revisits.
516 Parameters
517 ----------
518 filtername: (string 'r')
519 The name of the filter for this target map.
520 gap_min : float (25.)
521 Minimum time for the gap (minutes).
522 nside: int (default_nside)
523 The healpix resolution.
524 penalty_val : float (np.nan)
525 The reward value to use for regions to penalize. Will be masked if set to np.nan (default).
526 """
527 def __init__(self, filtername='r', nside=None, gap_min=25.,
528 penalty_val=np.nan):
529 super(Avoid_Fast_Revists, self).__init__(nside=nside, filtername=filtername)
531 self.filtername = filtername
532 self.penalty_val = penalty_val
534 self.gap_min = int_rounded(gap_min/60./24.)
535 self.nside = nside
537 self.survey_features = dict()
538 self.survey_features['Last_observed'] = features.Last_observed(filtername=filtername, nside=nside)
540 def _calc_value(self, conditions, indx=None):
541 result = np.ones(hp.nside2npix(self.nside), dtype=float)
542 if indx is None:
543 indx = np.arange(result.size)
544 diff = int_rounded(conditions.mjd - self.survey_features['Last_observed'].feature[indx])
545 bad = np.where(diff < self.gap_min)[0]
546 result[indx[bad]] = self.penalty_val
547 return result
550class Near_sun_twilight_basis_function(Base_basis_function):
551 """Reward looking into the twilight for NEOs at high airmass
553 Parameters
554 ----------
555 max_airmass : float (2.5)
556 The maximum airmass to try and observe (unitless)
557 """
559 def __init__(self, nside=None, max_airmass=2.5):
560 super(Near_sun_twilight_basis_function, self).__init__(nside=nside)
561 self.max_airmass = int_rounded(max_airmass)
562 self.result = np.zeros(hp.nside2npix(self.nside))
564 def _calc_value(self, conditions, indx=None):
565 result = self.result.copy()
566 good_pix = np.where((int_rounded(conditions.airmass) < self.max_airmass) &
567 (int_rounded(conditions.az_to_sun) < int_rounded(np.pi/2.)))
568 result[good_pix] = conditions.airmass[good_pix] / self.max_airmass.value
569 return result
572class Visit_repeat_basis_function(Base_basis_function):
573 """
574 Basis function to reward re-visiting an area on the sky. Looking for Solar System objects.
576 Parameters
577 ----------
578 gap_min : float (15.)
579 Minimum time for the gap (minutes)
580 gap_max : float (45.)
581 Maximum time for a gap
582 filtername : str ('r')
583 The filter(s) to count with pairs
584 npairs : int (1)
585 The number of pairs of observations to attempt to gather
586 """
587 def __init__(self, gap_min=25., gap_max=45.,
588 filtername='r', nside=None, npairs=1):
590 super(Visit_repeat_basis_function, self).__init__(nside=nside, filtername=filtername)
592 self.gap_min = int_rounded(gap_min/60./24.)
593 self.gap_max = int_rounded(gap_max/60./24.)
594 self.npairs = npairs
596 self.survey_features = {}
597 # Track the number of pairs that have been taken in a night
598 self.survey_features['Pair_in_night'] = features.Pair_in_night(filtername=filtername,
599 gap_min=gap_min, gap_max=gap_max,
600 nside=nside)
601 # When was it last observed
602 # XXX--since this feature is also in Pair_in_night, I should just access that one!
603 self.survey_features['Last_observed'] = features.Last_observed(filtername=filtername,
604 nside=nside)
606 def _calc_value(self, conditions, indx=None):
607 result = np.zeros(hp.nside2npix(self.nside), dtype=float)
608 if indx is None:
609 indx = np.arange(result.size)
610 diff = int_rounded(conditions.mjd - self.survey_features['Last_observed'].feature[indx])
611 good = np.where((diff >= self.gap_min) & (diff <= self.gap_max) &
612 (self.survey_features['Pair_in_night'].feature[indx] < self.npairs))[0]
613 result[indx[good]] += 1.
614 return result
617class M5_diff_basis_function(Base_basis_function):
618 """Basis function based on the 5-sigma depth.
619 Look up the best depth a healpixel achieves, and compute
620 the limiting depth difference given current conditions
621 """
622 def __init__(self, filtername='r', nside=None):
624 super(M5_diff_basis_function, self).__init__(nside=nside, filtername=filtername)
625 # Need to look up the deepest m5 values for all the healpixels
626 m5p = M5percentiles()
627 self.dark_map = m5p.dark_map(filtername=filtername, nside_out=self.nside)
629 def _calc_value(self, conditions, indx=None):
630 # No way to get the sign on this right the first time.
631 result = conditions.M5Depth[self.filtername] - self.dark_map
632 return result
635class Strict_filter_basis_function(Base_basis_function):
636 """Remove the bonus for staying in the same filter if certain conditions are met.
638 If the moon rises/sets or twilight starts/ends, it makes a lot of sense to consider
639 a filter change. This basis function rewards if it matches the current filter, the moon rises or sets,
640 twilight starts or stops, or there has been a large gap since the last observation.
642 Paramters
643 ---------
644 time_lag : float (10.)
645 If there is a gap between observations longer than this, let the filter change (minutes)
646 twi_change : float (-18.)
647 The sun altitude to consider twilight starting/ending (degrees)
648 note_free : str ('DD')
649 No penalty for changing filters if the last observation note field includes string.
650 Useful for giving a free filter change after deep drilling sequence
651 """
652 def __init__(self, time_lag=10., filtername='r', twi_change=-18., note_free='DD'):
654 super(Strict_filter_basis_function, self).__init__(filtername=filtername)
656 self.time_lag = time_lag/60./24. # Convert to days
657 self.twi_change = np.radians(twi_change)
659 self.survey_features = {}
660 self.survey_features['Last_observation'] = features.Last_observation()
661 self.note_free = note_free
663 def _calc_value(self, conditions, **kwargs):
664 # Did the moon set or rise since last observation?
665 moon_changed = conditions.moonAlt * self.survey_features['Last_observation'].feature['moonAlt'] < 0
667 # Are we already in the filter (or at start of night)?
668 in_filter = (conditions.current_filter == self.filtername) | (conditions.current_filter is None)
670 # Has enough time past?
671 time_past = int_rounded(conditions.mjd - self.survey_features['Last_observation'].feature['mjd']) > int_rounded(self.time_lag)
673 # Did twilight start/end?
674 twi_changed = (conditions.sunAlt - self.twi_change) * (self.survey_features['Last_observation'].feature['sunAlt']- self.twi_change) < 0
676 # Did we just finish a DD sequence
677 wasDD = self.note_free in self.survey_features['Last_observation'].feature['note']
679 # Is the filter mounted?
680 mounted = self.filtername in conditions.mounted_filters
682 if (moon_changed | in_filter | time_past | twi_changed | wasDD) & mounted:
683 result = 1.
684 else:
685 result = 0.
687 return result
690class Goal_Strict_filter_basis_function(Base_basis_function):
691 """Remove the bonus for staying in the same filter if certain conditions are met.
693 If the moon rises/sets or twilight starts/ends, it makes a lot of sense to consider
694 a filter change. This basis function rewards if it matches the current filter, the moon rises or sets,
695 twilight starts or stops, or there has been a large gap since the last observation.
697 Parameters
698 ---------
699 time_lag_min: Minimum time after a filter change for which a new filter change will receive zero reward, or
700 be denied at all (see unseen_before_lag).
701 time_lag_max: Time after a filter change where the reward for changing filters achieve its maximum.
702 time_lag_boost: Time after a filter change to apply a boost on the reward.
703 boost_gain: A multiplier factor for the reward after time_lag_boost.
704 unseen_before_lag: If True will make it impossible to switch filter before time_lag has passed.
705 filtername: The filter for which this basis function will be used.
706 tag: When using filter proportion use only regions with this tag to count for observations.
707 twi_change: Switch reward on when twilight changes.
708 proportion: The expected filter proportion distribution.
709 aways_available: If this is true the basis function will aways be computed regardless of the feasibility. If
710 False a more detailed feasibility check is performed. When set to False, it may speed up the computation
711 process by avoiding to compute the reward functions paired with this bf, when observation is not feasible.
713 """
715 def __init__(self, time_lag_min=10., time_lag_max=30.,
716 time_lag_boost=60., boost_gain=2.0, unseen_before_lag=False,
717 filtername='r', tag=None, twi_change=-18., proportion=1.0, aways_available=False):
719 super(Goal_Strict_filter_basis_function, self).__init__(filtername=filtername)
721 self.time_lag_min = time_lag_min / 60. / 24. # Convert to days
722 self.time_lag_max = time_lag_max / 60. / 24. # Convert to days
723 self.time_lag_boost = time_lag_boost / 60. / 24.
724 self.boost_gain = boost_gain
725 self.unseen_before_lag = unseen_before_lag
727 self.twi_change = np.radians(twi_change)
728 self.proportion = proportion
729 self.aways_available = aways_available
731 self.survey_features = {}
732 self.survey_features['Last_observation'] = features.Last_observation()
733 self.survey_features['Last_filter_change'] = features.LastFilterChange()
734 self.survey_features['N_obs_all'] = features.N_obs_count(filtername=None)
735 self.survey_features['N_obs'] = features.N_obs_count(filtername=filtername,
736 tag=tag)
738 def filter_change_bonus(self, time):
740 lag_min = self.time_lag_min
741 lag_max = self.time_lag_max
743 a = 1. / (lag_max - lag_min)
744 b = -a * lag_min
746 bonus = a * time + b
747 # How far behind we are with respect to proportion?
748 nobs = self.survey_features['N_obs'].feature
749 nobs_all = self.survey_features['N_obs_all'].feature
750 goal = self.proportion
751 # need = 1. - nobs / nobs_all + goal if nobs_all > 0 else 1. + goal
752 need = goal / nobs * nobs_all if nobs > 0 else 1.
753 # need /= goal
754 if hasattr(time, '__iter__'):
755 before_lag = np.where(time <= lag_min)
756 bonus[before_lag] = -np.inf if self.unseen_before_lag else 0.
757 after_lag = np.where(time >= lag_max)
758 bonus[after_lag] = 1. if time < self.time_lag_boost else self.boost_gain
759 elif int_rounded(time) <= int_rounded(lag_min):
760 return -np.inf if self.unseen_before_lag else 0.
761 elif int_rounded(time) >= int_rounded(lag_max):
762 return 1. if int_rounded(time) < int_rounded(self.time_lag_boost) else self.boost_gain
764 return bonus * need
766 def check_feasibility(self, conditions):
767 """
768 This method makes a pre-check of the feasibility of this basis function. If a basis function return False
769 on the feasibility check, it won't computed at all.
771 :return:
772 """
774 # Make a quick check about the feasibility of this basis function. If current filter is none, telescope
775 # is parked and we could, in principle, switch to any filter. If this basis function computes reward for
776 # the current filter, then it is also feasible. At last we check for an "aways_available" flag. Meaning, we
777 # force this basis function to be aways be computed.
778 if conditions.current_filter is None or conditions.current_filter == self.filtername or self.aways_available:
779 return True
781 # If we arrive here, we make some extra checks to make sure this bf is feasible and should be computed.
783 # Did the moon set or rise since last observation?
784 moon_changed = conditions.moonAlt * self.survey_features['Last_observation'].feature['moonAlt'] < 0
786 # Are we already in the filter (or at start of night)?
787 not_in_filter = (conditions.current_filter != self.filtername)
789 # Has enough time past?
790 lag = conditions.mjd - self.survey_features['Last_filter_change'].feature['mjd']
791 time_past = int_rounded(lag) > int_rounded(self.time_lag_min)
793 # Did twilight start/end?
794 twi_changed = (conditions.sunAlt - self.twi_change) * \
795 (self.survey_features['Last_observation'].feature['sunAlt'] - self.twi_change) < 0
797 # Did we just finish a DD sequence
798 wasDD = self.survey_features['Last_observation'].feature['note'] == 'DD'
800 # Is the filter mounted?
801 mounted = self.filtername in conditions.mounted_filters
803 if (moon_changed | time_past | twi_changed | wasDD) & mounted & not_in_filter:
804 return True
805 else:
806 return False
808 def _calc_value(self, conditions, **kwargs):
810 if conditions.current_filter is None:
811 return 0. # no bonus if no filter is mounted
812 # elif self.condition_features['Current_filter'].feature == self.filtername:
813 # return 0. # no bonus if on the filter already
815 # Did the moon set or rise since last observation?
816 moon_changed = conditions.moonAlt * \
817 self.survey_features['Last_observation'].feature['moonAlt'] < 0
819 # Are we already in the filter (or at start of night)?
820 # not_in_filter = (self.condition_features['Current_filter'].feature != self.filtername)
822 # Has enough time past?
823 lag = conditions.mjd - self.survey_features['Last_filter_change'].feature['mjd']
824 time_past = lag > self.time_lag_min
826 # Did twilight start/end?
827 twi_changed = (conditions.sunAlt - self.twi_change) * (
828 self.survey_features['Last_observation'].feature['sunAlt'] - self.twi_change) < 0
830 # Did we just finish a DD sequence
831 wasDD = self.survey_features['Last_observation'].feature['note'] == 'DD'
833 # Is the filter mounted?
834 mounted = self.filtername in conditions.mounted_filters
836 if (moon_changed | time_past | twi_changed | wasDD) & mounted:
837 result = self.filter_change_bonus(lag) if time_past else 0.
838 else:
839 result = -100. if self.unseen_before_lag else 0.
841 return result
844class Filter_change_basis_function(Base_basis_function):
845 """Reward staying in the current filter.
846 """
847 def __init__(self, filtername='r'):
848 super(Filter_change_basis_function, self).__init__(filtername=filtername)
850 def _calc_value(self, conditions, **kwargs):
852 if (conditions.current_filter == self.filtername) | (conditions.current_filter is None):
853 result = 1.
854 else:
855 result = 0.
856 return result
859class Slewtime_basis_function(Base_basis_function):
860 """Reward slews that take little time
862 Parameters
863 ----------
864 max_time : float (135)
865 The estimated maximum slewtime (seconds). Used to normalize so the basis function
866 spans ~ -1-0 in reward units.
867 """
868 def __init__(self, max_time=135., filtername='r', nside=None):
869 super(Slewtime_basis_function, self).__init__(nside=nside, filtername=filtername)
871 self.maxtime = max_time
872 self.nside = nside
873 self.filtername = filtername
874 self.result = np.zeros(hp.nside2npix(nside), dtype=float)
876 def add_observation(self, observation, indx=None):
877 # No tracking of observations in this basis function. Purely based on conditions.
878 pass
880 def _calc_value(self, conditions, indx=None):
881 # If we are in a different filter, the Filter_change_basis_function will take it
882 if conditions.current_filter != self.filtername:
883 result = 0
884 else:
885 # Need to make sure smaller slewtime is larger reward.
886 if np.size(conditions.slewtime) > 1:
887 result = self.result.copy()
888 good = ~np.isnan(conditions.slewtime)
889 result[good] = -conditions.slewtime[good]/self.maxtime
890 else:
891 result = -conditions.slewtime/self.maxtime
892 return result
895class Aggressive_Slewtime_basis_function(Base_basis_function):
896 """Reward slews that take little time
898 XXX--not sure how this is different from Slewtime_basis_function?
899 Looks like it's checking the slewtime to the field position rather than the healpix maybe?
900 """
902 def __init__(self, max_time=135., order=1., hard_max=None, filtername='r', nside=None):
903 super(Aggressive_Slewtime_basis_function, self).__init__(nside=nside, filtername=filtername)
905 self.maxtime = max_time
906 self.hard_max = hard_max
907 self.order = order
908 self.result = np.zeros(hp.nside2npix(nside), dtype=float)
910 def _calc_value(self, conditions, indx=None):
911 # If we are in a different filter, the Filter_change_basis_function will take it
912 if conditions.current_filter != self.filtername:
913 result = 0.
914 else:
915 # Need to make sure smaller slewtime is larger reward.
916 if np.size(self.condition_features['slewtime'].feature) > 1:
917 result = self.result.copy()
918 result.fill(np.nan)
920 good = np.where(np.bitwise_and(conditions.slewtime > 0.,
921 conditions.slewtime < self.maxtime))
922 result[good] = ((self.maxtime - conditions.slewtime[good]) /
923 self.maxtime) ** self.order
924 if self.hard_max is not None:
925 not_so_good = np.where(conditions.slewtime > self.hard_max)
926 result[not_so_good] -= 10.
927 fields = np.unique(conditions.hp2fields[good])
928 for field in fields:
929 hp_indx = np.where(conditions.hp2fields == field)
930 result[hp_indx] = np.min(result[hp_indx])
931 else:
932 result = (self.maxtime - conditions.slewtime) / self.maxtime
933 return result
936class Skybrightness_limit_basis_function(Base_basis_function):
937 """Mask regions that are outside a sky brightness limit
939 XXX--TODO: This should probably go to the mask basis functions.
941 Parameters
942 ----------
943 min : float (20.)
944 The minimum sky brightness (mags).
945 max : float (30.)
946 The maximum sky brightness (mags).
948 """
949 def __init__(self, nside=None, filtername='r', sbmin=20., sbmax=30.):
950 super(Skybrightness_limit_basis_function, self).__init__(nside=nside, filtername=filtername)
952 self.min = int_rounded(sbmin)
953 self.max = int_rounded(sbmax)
954 self.result = np.empty(hp.nside2npix(self.nside), dtype=float)
955 self.result.fill(np.nan)
957 def _calc_value(self, conditions, indx=None):
958 result = self.result.copy()
960 good = np.where(np.bitwise_and(int_rounded(conditions.skybrightness[self.filtername]) > self.min,
961 int_rounded(conditions.skybrightness[self.filtername]) < self.max))
962 result[good] = 1.0
964 return result
967class CableWrap_unwrap_basis_function(Base_basis_function):
968 """
969 Parameters
970 ----------
971 minAz : float (20.)
972 The minimum azimuth to activate bf (degrees)
973 maxAz : float (82.)
974 The maximum azimuth to activate bf (degrees)
975 unwrap_until: float (90.)
976 The window in which the bf is activated (degrees)
977 """
978 def __init__(self, nside=None, minAz=-270., maxAz=270., minAlt=20., maxAlt=82.,
979 activate_tol=20., delta_unwrap=1.2, unwrap_until=70., max_duration=30.):
980 super(CableWrap_unwrap_basis_function, self).__init__(nside=nside)
982 self.minAz = np.radians(minAz)
983 self.maxAz = np.radians(maxAz)
985 self.activate_tol = np.radians(activate_tol)
986 self.delta_unwrap = np.radians(delta_unwrap)
987 self.unwrap_until = np.radians(unwrap_until)
989 self.minAlt = np.radians(minAlt)
990 self.maxAlt = np.radians(maxAlt)
991 # Convert to half-width for convienence
992 self.nside = nside
993 self.active = False
994 self.unwrap_direction = 0. # either -1., 0., 1.
995 self.max_duration = max_duration/60./24. # Convert to days
996 self.activation_time = None
997 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float)
999 def _calc_value(self, conditions, indx=None):
1001 result = self.result.copy()
1003 current_abs_rad = np.radians(conditions.az)
1004 unseen = np.where(np.bitwise_or(conditions.alt < self.minAlt,
1005 conditions.alt > self.maxAlt))
1006 result[unseen] = np.nan
1008 if (self.minAz + self.activate_tol < current_abs_rad < self.maxAz - self.activate_tol) and not self.active:
1009 return result
1010 elif self.active and self.unwrap_direction == 1 and current_abs_rad > self.minAz+self.unwrap_until:
1011 self.active = False
1012 self.unwrap_direction = 0.
1013 self.activation_time = None
1014 return result
1015 elif self.active and self.unwrap_direction == -1 and current_abs_rad < self.maxAz-self.unwrap_until:
1016 self.active = False
1017 self.unwrap_direction = 0.
1018 self.activation_time = None
1019 return result
1020 elif (self.activation_time is not None and
1021 conditions.mjd - self.activation_time > self.max_duration):
1022 self.active = False
1023 self.unwrap_direction = 0.
1024 self.activation_time = None
1025 return result
1027 if not self.active:
1028 self.activation_time = conditions.mjd
1029 if current_abs_rad < 0.:
1030 self.unwrap_direction = 1 # clock-wise unwrap
1031 else:
1032 self.unwrap_direction = -1 # counter-clock-wise unwrap
1034 self.active = True
1036 max_abs_rad = self.maxAz
1037 min_abs_rad = self.minAz
1039 TWOPI = 2.*np.pi
1041 # Compute distance and accumulated az.
1042 norm_az_rad = np.divmod(conditions.az - min_abs_rad, TWOPI)[1] + min_abs_rad
1043 distance_rad = divmod(norm_az_rad - current_abs_rad, TWOPI)[1]
1044 get_shorter = np.where(distance_rad > np.pi)
1045 distance_rad[get_shorter] -= TWOPI
1046 accum_abs_rad = current_abs_rad + distance_rad
1048 # Compute wrap regions and fix distances
1049 mask_max = np.where(accum_abs_rad > max_abs_rad)
1050 distance_rad[mask_max] -= TWOPI
1051 mask_min = np.where(accum_abs_rad < min_abs_rad)
1052 distance_rad[mask_min] += TWOPI
1054 # Step-2: Repeat but now with compute reward to unwrap using specified delta_unwrap
1055 unwrap_current_abs_rad = current_abs_rad - (np.abs(self.delta_unwrap) if self.unwrap_direction > 0
1056 else -np.abs(self.delta_unwrap))
1057 unwrap_distance_rad = divmod(norm_az_rad - unwrap_current_abs_rad, TWOPI)[1]
1058 unwrap_get_shorter = np.where(unwrap_distance_rad > np.pi)
1059 unwrap_distance_rad[unwrap_get_shorter] -= TWOPI
1060 unwrap_distance_rad = np.abs(unwrap_distance_rad)
1062 if self.unwrap_direction < 0:
1063 mask = np.where(accum_abs_rad > unwrap_current_abs_rad)
1064 else:
1065 mask = np.where(accum_abs_rad < unwrap_current_abs_rad)
1067 # Finally build reward map
1068 result = (1. - unwrap_distance_rad/np.max(unwrap_distance_rad))**2.
1069 result[mask] = 0.
1070 result[unseen] = np.nan
1072 return result
1075class Cadence_enhance_basis_function(Base_basis_function):
1076 """Drive a certain cadence
1077 Parameters
1078 ----------
1079 filtername : str ('gri')
1080 The filter(s) that should be grouped together
1081 supress_window : list of float
1082 The start and stop window for when observations should be repressed (days)
1083 apply_area : healpix map
1084 The area over which to try and drive the cadence. Good values as 1, no candece drive 0.
1085 Probably works as a bool array too."""
1086 def __init__(self, filtername='gri', nside=None,
1087 supress_window=[0, 1.8], supress_val=-0.5,
1088 enhance_window=[2.1, 3.2], enhance_val=1.,
1089 apply_area=None):
1090 super(Cadence_enhance_basis_function, self).__init__(nside=nside, filtername=filtername)
1092 self.supress_window = np.sort(supress_window)
1093 self.supress_val = supress_val
1094 self.enhance_window = np.sort(enhance_window)
1095 self.enhance_val = enhance_val
1097 self.survey_features = {}
1098 self.survey_features['last_observed'] = features.Last_observed(filtername=filtername)
1100 self.empty = np.zeros(hp.nside2npix(self.nside), dtype=float)
1101 # No map, try to drive the whole area
1102 if apply_area is None:
1103 self.apply_indx = np.arange(self.empty.size)
1104 else:
1105 self.apply_indx = np.where(apply_area != 0)[0]
1107 def _calc_value(self, conditions, indx=None):
1108 # copy an empty array
1109 result = self.empty.copy()
1110 if indx is not None:
1111 ind = np.intersect1d(indx, self.apply_indx)
1112 else:
1113 ind = self.apply_indx
1114 if np.size(ind) == 0:
1115 result = 0
1116 else:
1117 mjd_diff = conditions.mjd - self.survey_features['last_observed'].feature[ind]
1118 to_supress = np.where((int_rounded(mjd_diff) > int_rounded(self.supress_window[0])) &
1119 (int_rounded(mjd_diff) < int_rounded(self.supress_window[1])))
1120 result[ind[to_supress]] = self.supress_val
1121 to_enhance = np.where((int_rounded(mjd_diff) > int_rounded(self.enhance_window[0])) &
1122 (int_rounded(mjd_diff) < int_rounded(self.enhance_window[1])))
1123 result[ind[to_enhance]] = self.enhance_val
1124 return result
1127class Azimuth_basis_function(Base_basis_function):
1128 """Reward staying in the same azimuth range. Possibly better than using slewtime, especially when selecting a large area of sky.
1130 Parameters
1131 ----------
1133 """
1135 def __init__(self, nside=None):
1136 super(Azimuth_basis_function, self).__init__(nside=nside)
1138 def _calc_value(self, conditions, indx=None):
1139 az_dist = conditions.az - conditions.telAz
1140 az_dist = az_dist % (2.*np.pi)
1141 over = np.where(az_dist > np.pi)
1142 az_dist[over] = 2. * np.pi - az_dist[over]
1143 # Normalize sp between 0 and 1
1144 result = az_dist/np.pi
1145 return result
1148class Az_modulo_basis_function(Base_basis_function):
1149 """Try to replicate the Rothchild et al cadence forcing by only observing on limited az ranges per night.
1151 Parameters
1152 ----------
1153 az_limits : list of float pairs (None)
1154 The azimuth limits (degrees) to use.
1155 """
1156 def __init__(self, nside=None, az_limits=None, out_of_bounds_val=-1.):
1157 super(Az_modulo_basis_function, self).__init__(nside=nside)
1158 self.result = np.ones(hp.nside2npix(self.nside))
1159 if az_limits is None:
1160 spread = 100./2.
1161 self.az_limits = np.radians([[360-spread, spread],
1162 [90.-spread, 90.+spread],
1163 [180.-spread, 180.+spread]])
1164 else:
1165 self.az_limits = np.radians(az_limits)
1166 self.mod_val = len(self.az_limits)
1167 self.out_of_bounds_val = out_of_bounds_val
1169 def _calc_value(self, conditions, indx=None):
1170 result = self.result.copy()
1171 az_lim = self.az_limits[np.max(conditions.night) % self.mod_val]
1173 if az_lim[0] < az_lim[1]:
1174 out_pix = np.where((int_rounded(conditions.az) < int_rounded(az_lim[0])) |
1175 (int_rounded(conditions.az) > int_rounded(az_lim[1])))
1176 else:
1177 out_pix = np.where((int_rounded(conditions.az) < int_rounded(az_lim[0])) |
1178 (int_rounded(conditions.az) > int_rounded(az_lim[1])))[0]
1179 result[out_pix] = self.out_of_bounds_val
1180 return result
1183class Dec_modulo_basis_function(Base_basis_function):
1184 """Emphasize dec bands on a nightly varying basis
1186 Parameters
1187 ----------
1188 dec_limits : list of float pairs (None)
1189 The azimuth limits (degrees) to use.
1190 """
1191 def __init__(self, nside=None, dec_limits=None, out_of_bounds_val=-1.):
1192 super(Dec_modulo_basis_function, self).__init__(nside=nside)
1194 npix = hp.nside2npix(nside)
1195 hpids = np.arange(npix)
1196 ra, dec = _hpid2RaDec(nside, hpids)
1198 self.results = []
1200 if dec_limits is None:
1201 self.dec_limits = np.radians([[-90., -32.8],
1202 [-32.8, -12.],
1203 [-12., 35.]])
1204 else:
1205 self.dec_limits = np.radians(dec_limits)
1206 self.mod_val = len(self.dec_limits)
1207 self.out_of_bounds_val = out_of_bounds_val
1209 for limits in self.dec_limits:
1210 good = np.where((dec >= limits[0]) & (dec < limits[1]))[0]
1211 tmp = np.zeros(npix)
1212 tmp[good] = 1
1213 self.results.append(tmp)
1215 def _calc_value(self, conditions, indx=None):
1216 night_index = np.max(conditions.night % self.mod_val)
1217 result = self.results[night_index]
1219 return result
1222class Map_modulo_basis_function(Base_basis_function):
1223 """Similar to Dec_modulo, but now use input masks
1225 Parameters
1226 ----------
1227 inmaps : list of hp arrays
1228 """
1229 def __init__(self, inmaps):
1230 nside = hp.npix2nside(np.size(inmaps[0]))
1231 super(Map_modulo_basis_function, self).__init__(nside=nside)
1232 self.maps = inmaps
1233 self.mod_val = len(inmaps)
1235 def _calc_value(self, conditions, indx=None):
1236 indx = np.max(conditions.night % self.mod_val)
1237 result = self.maps[indx]
1238 return result
1241class Good_seeing_basis_function(Base_basis_function):
1242 """Drive observations in good seeing conditions"""
1244 def __init__(self, nside=None, filtername='r', footprint=None, FWHMeff_limit=0.8,
1245 mag_diff=0.75):
1246 super(Good_seeing_basis_function, self).__init__(nside=nside)
1248 self.filtername = filtername
1249 self.FWHMeff_limit = int_rounded(FWHMeff_limit)
1250 if footprint is None:
1251 fp = utils.standard_goals(nside=nside)[filtername]
1252 else:
1253 fp = footprint
1254 self.out_of_bounds = np.where(fp == 0)[0]
1255 self.result = fp*0
1257 self.mag_diff = int_rounded(mag_diff)
1258 self.survey_features = {}
1259 self.survey_features['coadd_depth_all'] = features.Coadded_depth(filtername=filtername,
1260 nside=nside)
1261 self.survey_features['coadd_depth_good'] = features.Coadded_depth(filtername=filtername,
1262 nside=nside,
1263 FWHMeff_limit=FWHMeff_limit)
1265 def _calc_value(self, conditions, **kwargs):
1266 # Seeing is "bad"
1267 if int_rounded(conditions.FWHMeff[self.filtername].min()) > self.FWHMeff_limit:
1268 return 0
1269 result = self.result.copy()
1271 diff = self.survey_features['coadd_depth_all'].feature - self.survey_features['coadd_depth_good'].feature
1272 # Where are there things we want to observe?
1273 good_pix = np.where((int_rounded(diff) > self.mag_diff) &
1274 (int_rounded(conditions.FWHMeff[self.filtername]) <= self.FWHMeff_limit))
1275 # Hm, should this scale by the mag differences? Probably.
1276 result[good_pix] = diff[good_pix]
1277 result[self.out_of_bounds] = 0
1279 return result
1282class Template_generate_basis_function(Base_basis_function):
1283 """Emphasize areas that have not been observed in a long time
1285 Parameters
1286 ----------
1287 day_gap : float (250.)
1288 How long to wait before boosting the reward (days)
1289 footprint : np.array (None)
1290 The indices of the healpixels to apply the boost to. Uses the default footprint if None
1291 """
1292 def __init__(self, nside=None, day_gap=250., filtername='r', footprint=None):
1293 super(Template_generate_basis_function, self).__init__(nside=nside)
1294 self.day_gap = day_gap
1295 self.filtername = filtername
1296 self.survey_features = {}
1297 self.survey_features['Last_observed'] = features.Last_observed(filtername=filtername)
1298 self.result = np.zeros(hp.nside2npix(self.nside))
1299 if footprint is None:
1300 fp = utils.standard_goals(nside=nside)[filtername]
1301 else:
1302 fp = footprint
1303 self.out_of_bounds = np.where(fp == 0)
1305 def _calc_value(self, conditions, **kwargs):
1306 result = self.result.copy()
1307 overdue = np.where((int_rounded(conditions.mjd - self.survey_features['Last_observed'].feature)) > int_rounded(self.day_gap))
1308 result[overdue] = 1
1309 result[self.out_of_bounds] = 0
1311 return result
1314class Observed_twice_basis_function(Base_basis_function):
1315 """Mask out pixels that haven't been observed in the night
1316 """
1317 def __init__(self, nside=None, filtername='r', n_obs_needed=2, n_obs_in_filt_needed=1):
1318 super(Observed_twice_basis_function, self).__init__(nside=nside)
1319 self.n_obs_needed = n_obs_needed
1320 self.n_obs_in_filt_needed = n_obs_in_filt_needed
1321 self.filtername = filtername
1322 self.survey_features = {}
1323 self.survey_features['N_obs_infilt'] = features.N_obs_night(nside=nside, filtername=filtername)
1324 self.survey_features['N_obs_all'] = features.N_obs_night(nside=nside, filtername='')
1326 self.result = np.zeros(hp.nside2npix(self.nside))
1328 def _calc_value(self, conditions, **kwargs):
1329 result = self.result.copy()
1330 good_pix = np.where((self.survey_features['N_obs_infilt'].feature >= self.n_obs_in_filt_needed) &
1331 (self.survey_features['N_obs_all'].feature >= self.n_obs_needed))[0]
1332 result[good_pix] = 1
1334 return result