Hide keyboard shortcuts

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 

10 

11 

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'] 

23 

24 

25class Base_basis_function(object): 

26 """Class that takes features and computes a reward function when called. 

27 """ 

28 

29 def __init__(self, nside=None, filtername=None, **kwargs): 

30 

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 

49 

50 self.filtername = filtername 

51 

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 

65 

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 

71 

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 

78 

79 def __eq__(self): 

80 # XXX--to work on if we need to make a registry of basis functions. 

81 pass 

82 

83 def __ne__(self): 

84 pass 

85 

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. 

92 

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 

104 

105 

106class Constant_basis_function(Base_basis_function): 

107 """Just add a constant 

108 """ 

109 def __call__(self, conditions, **kwargs): 

110 return 1 

111 

112 

113class Target_map_basis_function(Base_basis_function): 

114 """Basis function that tracks number of observations and tries to match a specified spatial distribution 

115 

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.): 

136 

137 super(Target_map_basis_function, self).__init__(nside=nside, filtername=filtername) 

138 

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 

144 

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) 

158 

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 

172 

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 

175 

176 result[indx] = goal_N - self.survey_features['N_obs'].feature[indx] 

177 result[self.out_of_bounds_area] = self.out_of_bounds_val 

178 

179 return result 

180 

181 

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 

191 

192 

193class N_obs_high_am_basis_function(Base_basis_function): 

194 """Reward only reward/count observations at high airmass 

195 """ 

196 

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) 

206 

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 

209 

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 """ 

219 

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 

226 

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 

236 

237 return result 

238 

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 

246 

247 # Update the last time we had an mjd 

248 self.mjd_last = conditions.mjd + 0 

249 self.recalc = False 

250 self.value = result 

251 

252 return result 

253 

254 

255class N_obs_per_year_basis_function(Base_basis_function): 

256 """Reward areas that have not been observed N-times in the last year 

257 

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 

281 

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)) 

286 

287 def _calc_value(self, conditions, indx=None): 

288 

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 

292 

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 

300 

301 weight = relative_ra/(self.season_end_hour - self.season_start_hour) 

302 result *= weight 

303 

304 # mask off anything outside the footprint 

305 result[self.out_footprint] = 0 

306 

307 return result 

308 

309 

310class Cadence_in_season_basis_function(Base_basis_function): 

311 """Drive observations at least every N days in a given area 

312 

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 """ 

324 

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) 

332 

333 def _calc_value(self, conditions, indx=None): 

334 result = self.result.copy() 

335 ra_mid_season = (conditions.sunRA + np.pi) % (2.*np.pi) 

336 

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] 

340 

341 days_lag = conditions.mjd - self.survey_features['last_observed'].feature 

342 

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. 

347 

348 return result 

349 

350 

351class Season_coverage_basis_function(Base_basis_function): 

352 """Basis function to encourage N observations per observing season 

353 

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) 

369 

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 

377 

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 

388 

389 

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. 

393 

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 

406 

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 

414 

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) 

418 

419 result[np.where(diff > 0)] = 1 

420 

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 

424 

425 

426class Third_observation_basis_function(Base_basis_function): 

427 """If there have been observations in two filters long enough ago, go for a third 

428 

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 """ 

436 

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.) 

443 

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) 

449 

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 

458 

459 

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. 

462 

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) 

477 

478 self.filtername = filtername 

479 self.penalty_val = penalty_val 

480 

481 self.gap_min = int_rounded(gap_min/60./24.) 

482 self.nside = nside 

483 

484 self.survey_features = dict() 

485 self.survey_features['Last_observed'] = features.Last_observed(filtername=filtername, nside=nside) 

486 

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 

495 

496 

497class Near_sun_twilight_basis_function(Base_basis_function): 

498 """Reward looking into the twilight for NEOs at high airmass 

499 

500 Parameters 

501 ---------- 

502 max_airmass : float (2.5) 

503 The maximum airmass to try and observe (unitless) 

504 """ 

505 

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)) 

510 

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 

517 

518 

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. 

522 

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): 

536 

537 super(Visit_repeat_basis_function, self).__init__(nside=nside, filtername=filtername) 

538 

539 self.gap_min = int_rounded(gap_min/60./24.) 

540 self.gap_max = int_rounded(gap_max/60./24.) 

541 self.npairs = npairs 

542 

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) 

552 

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 

562 

563 

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): 

570 

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) 

575 

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 

580 

581 

582class Strict_filter_basis_function(Base_basis_function): 

583 """Remove the bonus for staying in the same filter if certain conditions are met. 

584 

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. 

588 

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'): 

600 

601 super(Strict_filter_basis_function, self).__init__(filtername=filtername) 

602 

603 self.time_lag = time_lag/60./24. # Convert to days 

604 self.twi_change = np.radians(twi_change) 

605 

606 self.survey_features = {} 

607 self.survey_features['Last_observation'] = features.Last_observation() 

608 self.note_free = note_free 

609 

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 

613 

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) 

616 

617 # Has enough time past? 

618 time_past = int_rounded(conditions.mjd - self.survey_features['Last_observation'].feature['mjd']) > int_rounded(self.time_lag) 

619 

620 # Did twilight start/end? 

621 twi_changed = (conditions.sunAlt - self.twi_change) * (self.survey_features['Last_observation'].feature['sunAlt']- self.twi_change) < 0 

622 

623 # Did we just finish a DD sequence 

624 wasDD = self.note_free in self.survey_features['Last_observation'].feature['note'] 

625 

626 # Is the filter mounted? 

627 mounted = self.filtername in conditions.mounted_filters 

628 

629 if (moon_changed | in_filter | time_past | twi_changed | wasDD) & mounted: 

630 result = 1. 

631 else: 

632 result = 0. 

633 

634 return result 

635 

636 

637class Goal_Strict_filter_basis_function(Base_basis_function): 

638 """Remove the bonus for staying in the same filter if certain conditions are met. 

639 

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. 

643 

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. 

659 

660 """ 

661 

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): 

665 

666 super(Goal_Strict_filter_basis_function, self).__init__(filtername=filtername) 

667 

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 

673 

674 self.twi_change = np.radians(twi_change) 

675 self.proportion = proportion 

676 self.aways_available = aways_available 

677 

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) 

684 

685 def filter_change_bonus(self, time): 

686 

687 lag_min = self.time_lag_min 

688 lag_max = self.time_lag_max 

689 

690 a = 1. / (lag_max - lag_min) 

691 b = -a * lag_min 

692 

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 

710 

711 return bonus * need 

712 

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. 

717 

718 :return: 

719 """ 

720 

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 

727 

728 # If we arrive here, we make some extra checks to make sure this bf is feasible and should be computed. 

729 

730 # Did the moon set or rise since last observation? 

731 moon_changed = conditions.moonAlt * self.survey_features['Last_observation'].feature['moonAlt'] < 0 

732 

733 # Are we already in the filter (or at start of night)? 

734 not_in_filter = (conditions.current_filter != self.filtername) 

735 

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) 

739 

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 

743 

744 # Did we just finish a DD sequence 

745 wasDD = self.survey_features['Last_observation'].feature['note'] == 'DD' 

746 

747 # Is the filter mounted? 

748 mounted = self.filtername in conditions.mounted_filters 

749 

750 if (moon_changed | time_past | twi_changed | wasDD) & mounted & not_in_filter: 

751 return True 

752 else: 

753 return False 

754 

755 def _calc_value(self, conditions, **kwargs): 

756 

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 

761 

762 # Did the moon set or rise since last observation? 

763 moon_changed = conditions.moonAlt * \ 

764 self.survey_features['Last_observation'].feature['moonAlt'] < 0 

765 

766 # Are we already in the filter (or at start of night)? 

767 # not_in_filter = (self.condition_features['Current_filter'].feature != self.filtername) 

768 

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 

772 

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 

776 

777 # Did we just finish a DD sequence 

778 wasDD = self.survey_features['Last_observation'].feature['note'] == 'DD' 

779 

780 # Is the filter mounted? 

781 mounted = self.filtername in conditions.mounted_filters 

782 

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. 

787 

788 return result 

789 

790 

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) 

796 

797 def _calc_value(self, conditions, **kwargs): 

798 

799 if (conditions.current_filter == self.filtername) | (conditions.current_filter is None): 

800 result = 1. 

801 else: 

802 result = 0. 

803 return result 

804 

805 

806class Slewtime_basis_function(Base_basis_function): 

807 """Reward slews that take little time 

808 

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) 

817 

818 self.maxtime = max_time 

819 self.nside = nside 

820 self.filtername = filtername 

821 self.result = np.zeros(hp.nside2npix(nside), dtype=float) 

822 

823 def add_observation(self, observation, indx=None): 

824 # No tracking of observations in this basis function. Purely based on conditions. 

825 pass 

826 

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 

840 

841 

842class Aggressive_Slewtime_basis_function(Base_basis_function): 

843 """Reward slews that take little time 

844 

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 """ 

848 

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) 

851 

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) 

856 

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) 

866 

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 

881 

882 

883class Skybrightness_limit_basis_function(Base_basis_function): 

884 """Mask regions that are outside a sky brightness limit 

885 

886 XXX--TODO: This should probably go to the mask basis functions. 

887 

888 Parameters 

889 ---------- 

890 min : float (20.) 

891 The minimum sky brightness (mags). 

892 max : float (30.) 

893 The maximum sky brightness (mags). 

894 

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) 

898 

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) 

903 

904 def _calc_value(self, conditions, indx=None): 

905 result = self.result.copy() 

906 

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 

910 

911 return result 

912 

913 

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) 

928 

929 self.minAz = np.radians(minAz) 

930 self.maxAz = np.radians(maxAz) 

931 

932 self.activate_tol = np.radians(activate_tol) 

933 self.delta_unwrap = np.radians(delta_unwrap) 

934 self.unwrap_until = np.radians(unwrap_until) 

935 

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) 

945 

946 def _calc_value(self, conditions, indx=None): 

947 

948 result = self.result.copy() 

949 

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 

954 

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 

973 

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 

980 

981 self.active = True 

982 

983 max_abs_rad = self.maxAz 

984 min_abs_rad = self.minAz 

985 

986 TWOPI = 2.*np.pi 

987 

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 

994 

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 

1000 

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) 

1008 

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) 

1013 

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 

1018 

1019 return result 

1020 

1021 

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) 

1038 

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 

1043 

1044 self.survey_features = {} 

1045 self.survey_features['last_observed'] = features.Last_observed(filtername=filtername) 

1046 

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] 

1053 

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 

1072 

1073 

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. 

1076 

1077 Parameters 

1078 ---------- 

1079 

1080 """ 

1081 

1082 def __init__(self, nside=None): 

1083 super(Azimuth_basis_function, self).__init__(nside=nside) 

1084 

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 

1093 

1094 

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. 

1097 

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 

1115 

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] 

1119 

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 

1128 

1129 

1130class Dec_modulo_basis_function(Base_basis_function): 

1131 """Emphasize dec bands on a nightly varying basis 

1132 

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) 

1140 

1141 npix = hp.nside2npix(nside) 

1142 hpids = np.arange(npix) 

1143 ra, dec = _hpid2RaDec(nside, hpids) 

1144 

1145 self.results = [] 

1146 

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 

1155 

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) 

1161 

1162 def _calc_value(self, conditions, indx=None): 

1163 night_index = np.max(conditions.night % self.mod_val) 

1164 result = self.results[night_index] 

1165 

1166 return result 

1167 

1168 

1169class Map_modulo_basis_function(Base_basis_function): 

1170 """Similar to Dec_modulo, but now use input masks 

1171 

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) 

1181 

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 

1186 

1187 

1188class Good_seeing_basis_function(Base_basis_function): 

1189 """Drive observations in good seeing conditions""" 

1190 

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) 

1194 

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 

1203 

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) 

1211 

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() 

1217 

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 

1225 

1226 return result 

1227 

1228 

1229class Template_generate_basis_function(Base_basis_function): 

1230 """Emphasize areas that have not been observed in a long time 

1231 

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) 

1251 

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 

1257 

1258 return result 

1259 

1260 

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='') 

1272 

1273 self.result = np.zeros(hp.nside2npix(self.nside)) 

1274 

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 

1280 

1281 return result