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