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 

10from astropy.coordinates import SkyCoord 

11from astropy import units as u 

12 

13 

14__all__ = ['Base_basis_function', 'Constant_basis_function', 'Target_map_basis_function', 

15 'Avoid_long_gaps_basis_function', 

16 'Avoid_Fast_Revists', 'Visit_repeat_basis_function', 'M5_diff_basis_function', 

17 'Strict_filter_basis_function', 'Goal_Strict_filter_basis_function', 

18 'Filter_change_basis_function', 'Slewtime_basis_function', 

19 'Aggressive_Slewtime_basis_function', 'Skybrightness_limit_basis_function', 

20 'CableWrap_unwrap_basis_function', 'Cadence_enhance_basis_function', 'Azimuth_basis_function', 

21 'Az_modulo_basis_function', 'Dec_modulo_basis_function', 'Map_modulo_basis_function', 

22 'Template_generate_basis_function', 

23 'Footprint_nvis_basis_function', 'Third_observation_basis_function', 'Season_coverage_basis_function', 

24 'N_obs_per_year_basis_function', 'Cadence_in_season_basis_function', 'Near_sun_twilight_basis_function', 

25 'N_obs_high_am_basis_function', 'Good_seeing_basis_function', 'Observed_twice_basis_function', 

26 'Ecliptic_basis_function'] 

27 

28 

29class Base_basis_function(object): 

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

31 """ 

32 

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

34 

35 # Set if basis function needs to be recalculated if there is a new observation 

36 self.update_on_newobs = True 

37 # Set if basis function needs to be recalculated if conditions change 

38 self.update_on_mjd = True 

39 # Dict to hold all the features we want to track 

40 self.survey_features = {} 

41 # Keep track of the last time the basis function was called. If mjd doesn't change, use cached value 

42 self.mjd_last = None 

43 self.value = 0 

44 # list the attributes to compare to check if basis functions are equal. 

45 self.attrs_to_compare = [] 

46 # Do we need to recalculate the basis function 

47 self.recalc = True 

48 # Basis functions don't technically all need an nside, but so many do might as well set it here 

49 if nside is None: 

50 self.nside = utils.set_default_nside() 

51 else: 

52 self.nside = nside 

53 

54 self.filtername = filtername 

55 

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

57 """ 

58 Parameters 

59 ---------- 

60 observation : np.array 

61 An array with information about the input observation 

62 indx : np.array 

63 The indices of the healpix map that the observation overlaps with 

64 """ 

65 for feature in self.survey_features: 

66 self.survey_features[feature].add_observation(observation, indx=indx) 

67 if self.update_on_newobs: 

68 self.recalc = True 

69 

70 def check_feasibility(self, conditions): 

71 """If there is logic to decide if something is feasible (e.g., only if moon is down), 

72 it can be calculated here. Helps prevent full __call__ from being called more than needed. 

73 """ 

74 return True 

75 

76 def _calc_value(self, conditions, **kwarge): 

77 self.value = 0 

78 # Update the last time we had an mjd 

79 self.mjd_last = conditions.mjd + 0 

80 self.recalc = False 

81 return self.value 

82 

83 def __eq__(self): 

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

85 pass 

86 

87 def __ne__(self): 

88 pass 

89 

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

91 """ 

92 Parameters 

93 ---------- 

94 conditions : lsst.sims.featureScheduler.features.conditions object 

95 Object that has attributes for all the current conditions. 

96 

97 Return a reward healpix map or a reward scalar. 

98 """ 

99 # If we are not feasible, return -inf 

100 if not self.check_feasibility(conditions): 

101 return -np.inf 

102 if self.recalc: 

103 self.value = self._calc_value(conditions, **kwargs) 

104 if self.update_on_mjd: 

105 if conditions.mjd != self.mjd_last: 

106 self.value = self._calc_value(conditions, **kwargs) 

107 return self.value 

108 

109 

110class Constant_basis_function(Base_basis_function): 

111 """Just add a constant 

112 """ 

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

114 return 1 

115 

116 

117class Avoid_long_gaps_basis_function(Base_basis_function): 

118 """ 

119 Boost the reward on parts of the survey that haven't been observed for a while 

120 """ 

121 

122 def __init__(self, filtername=None, nside=None, footprint=None, min_gap=4., max_gap=40., 

123 ha_limit=3.5): 

124 super(Avoid_long_gaps_basis_function, self).__init__(nside=nside, filtername=filtername) 

125 self.min_gap = min_gap 

126 self.max_gap = max_gap 

127 self.filtername = filtername 

128 self.footprint = footprint 

129 self.ha_limit = 2.*np.pi*ha_limit/24. # To radians 

130 self.survey_features = {} 

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

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

133 

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

135 result = self.result.copy() 

136 

137 gap = conditions.mjd - self.survey_features['last_observed'].feature 

138 in_range = np.where((gap > self.min_gap) & (gap < self.max_gap) & (self.footprint > 0)) 

139 result[in_range] = 1 

140 

141 # mask out areas beyond the hour angle limit. 

142 out_ha = np.where((conditions.HA > self.ha_limit) & (conditions.HA < (2.*np.pi - self.ha_limit)))[0] 

143 result[out_ha] = 0 

144 

145 return result 

146 

147 

148class Target_map_basis_function(Base_basis_function): 

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

150 

151 Parameters 

152 ---------- 

153 filtername: (string 'r') 

154 The name of the filter for this target map. 

155 nside: int (default_nside) 

156 The healpix resolution. 

157 target_map : numpy array (None) 

158 A healpix map showing the ratio of observations desired for all points on the sky 

159 norm_factor : float (0.00010519) 

160 for converting target map to number of observations. Should be the area of the camera 

161 divided by the area of a healpixel divided by the sum of all your goal maps. Default 

162 value assumes LSST foV has 1.75 degree radius and the standard goal maps. If using 

163 mulitple filters, see lsst.sims.featureScheduler.utils.calc_norm_factor for a utility 

164 that computes norm_factor. 

165 out_of_bounds_val : float (-10.) 

166 Reward value to give regions where there are no observations requested (unitless). 

167 """ 

168 def __init__(self, filtername='r', nside=None, target_map=None, 

169 norm_factor=None, 

170 out_of_bounds_val=-10.): 

171 

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

173 

174 if norm_factor is None: 

175 warnings.warn('No norm_factor set, use utils.calc_norm_factor if using multiple filters.') 

176 self.norm_factor = 0.00010519 

177 else: 

178 self.norm_factor = norm_factor 

179 

180 self.survey_features = {} 

181 # Map of the number of observations in filter 

182 self.survey_features['N_obs'] = features.N_observations(filtername=filtername, nside=self.nside) 

183 # Count of all the observations 

184 self.survey_features['N_obs_count_all'] = features.N_obs_count(filtername=None) 

185 if target_map is None: 

186 self.target_map = utils.generate_goal_map(filtername=filtername, nside=self.nside) 

187 else: 

188 self.target_map = target_map 

189 self.out_of_bounds_area = np.where(self.target_map == 0)[0] 

190 self.out_of_bounds_val = out_of_bounds_val 

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

192 self.all_indx = np.arange(self.result.size) 

193 

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

195 """ 

196 Parameters 

197 ---------- 

198 indx : list (None) 

199 Index values to compute, if None, full map is computed 

200 Returns 

201 ------- 

202 Healpix reward map 

203 """ 

204 result = self.result.copy() 

205 if indx is None: 

206 indx = self.all_indx 

207 

208 # Find out how many observations we want now at those points 

209 goal_N = self.target_map[indx] * self.survey_features['N_obs_count_all'].feature * self.norm_factor 

210 

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

212 result[self.out_of_bounds_area] = self.out_of_bounds_val 

213 

214 return result 

215 

216 

217def azRelPoint(azs, pointAz): 

218 azRelMoon = (azs - pointAz) % (2.0*np.pi) 

219 if isinstance(azs, np.ndarray): 

220 over = np.where(azRelMoon > np.pi) 

221 azRelMoon[over] = 2. * np.pi - azRelMoon[over] 

222 else: 

223 if azRelMoon > np.pi: 

224 azRelMoon = 2.0 * np.pi - azRelMoon 

225 return azRelMoon 

226 

227 

228class N_obs_high_am_basis_function(Base_basis_function): 

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

230 """ 

231 

232 def __init__(self, nside=None, filtername='r', footprint=None, n_obs=3, season=300., 

233 am_limits=[1.5, 2.2], out_of_bounds_val=np.nan): 

234 super(N_obs_high_am_basis_function, self).__init__(nside=nside, filtername=filtername) 

235 self.footprint = footprint 

236 self.out_footprint = np.where((footprint == 0) | np.isnan(footprint)) 

237 self.am_limits = am_limits 

238 self.season = season 

239 self.survey_features['last_n_mjds'] = features.Last_N_obs_times(nside=nside, filtername=filtername, 

240 n_obs=n_obs) 

241 

242 self.result = np.zeros(hp.nside2npix(self.nside), dtype=float) + out_of_bounds_val 

243 self.out_of_bounds_val = out_of_bounds_val 

244 

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

246 """ 

247 Parameters 

248 ---------- 

249 observation : np.array 

250 An array with information about the input observation 

251 indx : np.array 

252 The indices of the healpix map that the observation overlaps with 

253 """ 

254 

255 # Only count the observations if they are at the airmass limits 

256 if (observation['airmass'] > np.min(self.am_limits)) & (observation['airmass'] < np.max(self.am_limits)): 

257 for feature in self.survey_features: 

258 self.survey_features[feature].add_observation(observation, indx=indx) 

259 if self.update_on_newobs: 

260 self.recalc = True 

261 

262 def check_feasibility(self, conditions): 

263 """If there is logic to decide if something is feasible (e.g., only if moon is down), 

264 it can be calculated here. Helps prevent full __call__ from being called more than needed. 

265 """ 

266 result = True 

267 reward = self._calc_value(conditions) 

268 # If there are no non-NaN values, we're not feasible now 

269 if True not in np.isfinite(reward): 

270 result = False 

271 

272 return result 

273 

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

275 result = self.result.copy() 

276 behind_pix = np.where((int_rounded(conditions.mjd-self.survey_features['last_n_mjds'].feature[0]) > int_rounded(self.season)) & 

277 (int_rounded(conditions.airmass) > int_rounded(np.min(self.am_limits))) & 

278 (int_rounded(conditions.airmass) < int_rounded(np.max(self.am_limits)))) 

279 result[behind_pix] = 1 

280 result[self.out_footprint] = self.out_of_bounds_val 

281 

282 # Update the last time we had an mjd 

283 self.mjd_last = conditions.mjd + 0 

284 self.recalc = False 

285 self.value = result 

286 

287 return result 

288 

289 

290class Ecliptic_basis_function(Base_basis_function): 

291 """Mark the area around the ecliptic 

292 """ 

293 

294 def __init__(self, nside=None, distance_to_eclip=25.): 

295 super(Ecliptic_basis_function, self).__init__(nside=nside) 

296 self.distance_to_eclip = np.radians(distance_to_eclip) 

297 ra, dec = _hpid2RaDec(nside, np.arange(hp.nside2npix(self.nside))) 

298 self.result = np.zeros(ra.size) 

299 coord = SkyCoord(ra=ra*u.rad, dec=dec*u.rad) 

300 eclip_lat = coord.barycentrictrueecliptic.lat.radian 

301 good = np.where(np.abs(eclip_lat) < self.distance_to_eclip) 

302 self.result[good] += 1 

303 

304 def __call__(self, conditions, indx=None): 

305 return self.result 

306 

307 

308class N_obs_per_year_basis_function(Base_basis_function): 

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

310 

311 Parameters 

312 ---------- 

313 filtername : str ('r') 

314 The filter to track 

315 footprint : np.array 

316 Should be a HEALpix map. Values of 0 or np.nan will be ignored. 

317 n_obs : int (3) 

318 The number of observations to demand 

319 season : float (300) 

320 The amount of time to allow pass before marking a region as "behind". Default 365.25 (days). 

321 season_start_hour : float (-2) 

322 When to start the season relative to RA 180 degrees away from the sun (hours) 

323 season_end_hour : float (2) 

324 When to consider a season ending, the RA relative to the sun + 180 degrees. (hours) 

325 """ 

326 def __init__(self, filtername='r', nside=None, footprint=None, n_obs=3, season=300, 

327 season_start_hour=-4., season_end_hour=2.): 

328 super(N_obs_per_year_basis_function, self).__init__(nside=nside, filtername=filtername) 

329 self.footprint = footprint 

330 self.n_obs = n_obs 

331 self.season = season 

332 self.season_start_hour = (season_start_hour)*np.pi/12. # To radians 

333 self.season_end_hour = season_end_hour*np.pi/12. # To radians 

334 

335 self.survey_features['last_n_mjds'] = features.Last_N_obs_times(nside=nside, filtername=filtername, 

336 n_obs=n_obs) 

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

338 self.out_footprint = np.where((footprint == 0) | np.isnan(footprint)) 

339 

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

341 

342 result = self.result.copy() 

343 behind_pix = np.where((conditions.mjd-self.survey_features['last_n_mjds'].feature[0]) > self.season) 

344 result[behind_pix] = 1 

345 

346 # let's ramp up the weight depending on how far into the observing season the healpix is 

347 mid_season_ra = (conditions.sunRA + np.pi) % (2.*np.pi) 

348 # relative RA 

349 relative_ra = (conditions.ra - mid_season_ra) % (2.*np.pi) 

350 relative_ra = (self.season_end_hour - relative_ra) % (2.*np.pi) 

351 # ok, now  

352 relative_ra[np.where(int_rounded(relative_ra) > int_rounded(self.season_end_hour-self.season_start_hour))] = 0 

353 

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

355 result *= weight 

356 

357 # mask off anything outside the footprint 

358 result[self.out_footprint] = 0 

359 

360 return result 

361 

362 

363class Cadence_in_season_basis_function(Base_basis_function): 

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

365 

366 Parameters 

367 ---------- 

368 drive_map : np.array 

369 A HEALpix map with values of 1 where the cadence should be driven. 

370 filtername : str 

371 The filters that can count 

372 season_span : float (2.5) 

373 How long to consider a spot "in_season" (hours) 

374 cadence : float (2.5) 

375 How long to wait before activating the basis function (days) 

376 """ 

377 

378 def __init__(self, drive_map, filtername='griz', season_span=2.5, cadence=2.5, nside=None): 

379 super(Cadence_in_season_basis_function, self).__init__(nside=nside, filtername=filtername) 

380 self.drive_map = drive_map 

381 self.season_span = season_span/12.*np.pi # To radians 

382 self.cadence = cadence 

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

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

385 

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

387 result = self.result.copy() 

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

389 

390 angle_to_mid_season = np.abs(conditions.ra - ra_mid_season) 

391 over = np.where(int_rounded(angle_to_mid_season) > int_rounded(np.pi)) 

392 angle_to_mid_season[over] = 2.*np.pi - angle_to_mid_season[over] 

393 

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

395 

396 active_pix = np.where((int_rounded(days_lag) >= int_rounded(self.cadence)) & 

397 (self.drive_map == 1) & 

398 (int_rounded(angle_to_mid_season) < int_rounded(self.season_span))) 

399 result[active_pix] = 1. 

400 

401 return result 

402 

403 

404class Season_coverage_basis_function(Base_basis_function): 

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

406 

407 Parameters 

408 ---------- 

409 footprint : healpix map (None) 

410 The footprint where one should demand coverage every season 

411 n_per_season : int (3) 

412 The number of observations to attempt to gather every season 

413 offset : healpix map 

414 The offset to apply when computing the current season over the sky. utils.create_season_offset 

415 is helpful for making this 

416 season_frac_start : float (0.5) 

417 Only start trying to gather observations after a season is fractionally this far over. 

418 """ 

419 def __init__(self, filtername='r', nside=None, footprint=None, n_per_season=3, offset=None, 

420 season_frac_start=0.5): 

421 super(Season_coverage_basis_function, self).__init__(nside=nside, filtername=filtername) 

422 

423 self.n_per_season = n_per_season 

424 self.footprint = footprint 

425 self.survey_features['n_obs_season'] = features.N_observations_current_season(filtername=filtername, 

426 nside=nside, offset=offset) 

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

428 self.season_frac_start = season_frac_start 

429 self.offset = offset 

430 

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

432 result = self.result.copy() 

433 season = utils.season_calc(conditions.night, offset=self.offset, floor=False) 

434 # Find the area that still needs observation 

435 feature = self.survey_features['n_obs_season'].feature 

436 not_enough = np.where((self.footprint > 0) & (feature < self.n_per_season) & 

437 ((int_rounded(season-np.floor(season)) > int_rounded(self.season_frac_start))) & 

438 (season >= 0)) 

439 result[not_enough] = 1 

440 return result 

441 

442 

443class Footprint_nvis_basis_function(Base_basis_function): 

444 """Basis function to drive observations of a given footprint. Good to target of opportunity targets 

445 where one might want to observe a region 3 times. 

446 

447 Parameters 

448 ---------- 

449 footprint : np.array 

450 A healpix array (1 for desired, 0 for not desired) of the target footprint. 

451 nvis : int (1) 

452 The number of visits to try and gather 

453 """ 

454 def __init__(self, filtername='r', nside=None, footprint=None, 

455 nvis=1, out_of_bounds_val=np.nan): 

456 super(Footprint_nvis_basis_function, self).__init__(nside=nside, filtername=filtername) 

457 self.footprint = footprint 

458 self.nvis = nvis 

459 

460 # Have a feature that tracks how many observations we have 

461 self.survey_features = {} 

462 # Map of the number of observations in filter 

463 self.survey_features['N_obs'] = features.N_observations(filtername=filtername, nside=self.nside) 

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

465 self.result.fill(out_of_bounds_val) 

466 self.out_of_bounds_val = out_of_bounds_val 

467 

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

469 result = self.result.copy() 

470 diff = int_rounded(self.footprint*self.nvis - self.survey_features['N_obs'].feature) 

471 

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

473 

474 # Any spot where we have enough visits is out of bounds now. 

475 result[np.where(diff <= 0)] = self.out_of_bounds_val 

476 return result 

477 

478 

479class Third_observation_basis_function(Base_basis_function): 

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

481 

482 Parameters 

483 ---------- 

484 gap_min : float (40.) 

485 The minimum time gap to consider a pixel good (minutes) 

486 gap_max : float (120) 

487 The maximum time to consider going for a pair (minutes) 

488 """ 

489 

490 def __init__(self, nside=32, filtername1='r', filtername2='z', gap_min=40., gap_max=120.): 

491 super(Third_observation_basis_function, self).__init__(nside=nside) 

492 self.filtername1 = filtername1 

493 self.filtername2 = filtername2 

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

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

496 

497 self.survey_features = {} 

498 self.survey_features['last_obs_f1'] = features.Last_observed(filtername=filtername1, nside=nside) 

499 self.survey_features['last_obs_f2'] = features.Last_observed(filtername=filtername2, nside=nside) 

500 self.result = np.empty(hp.nside2npix(self.nside)) 

501 self.result.fill(np.nan) 

502 

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

504 result = self.result.copy() 

505 d1 = int_rounded(conditions.mjd - self.survey_features['last_obs_f1'].feature) 

506 d2 = int_rounded(conditions.mjd - self.survey_features['last_obs_f2'].feature) 

507 good = np.where((d1 > self.gap_min) & (d1 < self.gap_max) & 

508 (d2 > self.gap_min) & (d2 < self.gap_max)) 

509 result[good] = 1 

510 return result 

511 

512 

513class Avoid_Fast_Revists(Base_basis_function): 

514 """Marks targets as unseen if they are in a specified time window in order to avoid fast revisits. 

515 

516 Parameters 

517 ---------- 

518 filtername: (string 'r') 

519 The name of the filter for this target map. 

520 gap_min : float (25.) 

521 Minimum time for the gap (minutes). 

522 nside: int (default_nside) 

523 The healpix resolution. 

524 penalty_val : float (np.nan) 

525 The reward value to use for regions to penalize. Will be masked if set to np.nan (default). 

526 """ 

527 def __init__(self, filtername='r', nside=None, gap_min=25., 

528 penalty_val=np.nan): 

529 super(Avoid_Fast_Revists, self).__init__(nside=nside, filtername=filtername) 

530 

531 self.filtername = filtername 

532 self.penalty_val = penalty_val 

533 

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

535 self.nside = nside 

536 

537 self.survey_features = dict() 

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

539 

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

541 result = np.ones(hp.nside2npix(self.nside), dtype=float) 

542 if indx is None: 

543 indx = np.arange(result.size) 

544 diff = int_rounded(conditions.mjd - self.survey_features['Last_observed'].feature[indx]) 

545 bad = np.where(diff < self.gap_min)[0] 

546 result[indx[bad]] = self.penalty_val 

547 return result 

548 

549 

550class Near_sun_twilight_basis_function(Base_basis_function): 

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

552 

553 Parameters 

554 ---------- 

555 max_airmass : float (2.5) 

556 The maximum airmass to try and observe (unitless) 

557 """ 

558 

559 def __init__(self, nside=None, max_airmass=2.5): 

560 super(Near_sun_twilight_basis_function, self).__init__(nside=nside) 

561 self.max_airmass = int_rounded(max_airmass) 

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

563 

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

565 result = self.result.copy() 

566 good_pix = np.where((int_rounded(conditions.airmass) < self.max_airmass) & 

567 (int_rounded(conditions.az_to_sun) < int_rounded(np.pi/2.))) 

568 result[good_pix] = conditions.airmass[good_pix] / self.max_airmass.value 

569 return result 

570 

571 

572class Visit_repeat_basis_function(Base_basis_function): 

573 """ 

574 Basis function to reward re-visiting an area on the sky. Looking for Solar System objects. 

575 

576 Parameters 

577 ---------- 

578 gap_min : float (15.) 

579 Minimum time for the gap (minutes) 

580 gap_max : float (45.) 

581 Maximum time for a gap 

582 filtername : str ('r') 

583 The filter(s) to count with pairs 

584 npairs : int (1) 

585 The number of pairs of observations to attempt to gather 

586 """ 

587 def __init__(self, gap_min=25., gap_max=45., 

588 filtername='r', nside=None, npairs=1): 

589 

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

591 

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

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

594 self.npairs = npairs 

595 

596 self.survey_features = {} 

597 # Track the number of pairs that have been taken in a night 

598 self.survey_features['Pair_in_night'] = features.Pair_in_night(filtername=filtername, 

599 gap_min=gap_min, gap_max=gap_max, 

600 nside=nside) 

601 # When was it last observed 

602 # XXX--since this feature is also in Pair_in_night, I should just access that one! 

603 self.survey_features['Last_observed'] = features.Last_observed(filtername=filtername, 

604 nside=nside) 

605 

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

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

608 if indx is None: 

609 indx = np.arange(result.size) 

610 diff = int_rounded(conditions.mjd - self.survey_features['Last_observed'].feature[indx]) 

611 good = np.where((diff >= self.gap_min) & (diff <= self.gap_max) & 

612 (self.survey_features['Pair_in_night'].feature[indx] < self.npairs))[0] 

613 result[indx[good]] += 1. 

614 return result 

615 

616 

617class M5_diff_basis_function(Base_basis_function): 

618 """Basis function based on the 5-sigma depth. 

619 Look up the best depth a healpixel achieves, and compute 

620 the limiting depth difference given current conditions 

621 """ 

622 def __init__(self, filtername='r', nside=None): 

623 

624 super(M5_diff_basis_function, self).__init__(nside=nside, filtername=filtername) 

625 # Need to look up the deepest m5 values for all the healpixels 

626 m5p = M5percentiles() 

627 self.dark_map = m5p.dark_map(filtername=filtername, nside_out=self.nside) 

628 

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

630 # No way to get the sign on this right the first time. 

631 result = conditions.M5Depth[self.filtername] - self.dark_map 

632 return result 

633 

634 

635class Strict_filter_basis_function(Base_basis_function): 

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

637 

638 If the moon rises/sets or twilight starts/ends, it makes a lot of sense to consider 

639 a filter change. This basis function rewards if it matches the current filter, the moon rises or sets, 

640 twilight starts or stops, or there has been a large gap since the last observation. 

641 

642 Paramters 

643 --------- 

644 time_lag : float (10.) 

645 If there is a gap between observations longer than this, let the filter change (minutes) 

646 twi_change : float (-18.) 

647 The sun altitude to consider twilight starting/ending (degrees) 

648 note_free : str ('DD') 

649 No penalty for changing filters if the last observation note field includes string.  

650 Useful for giving a free filter change after deep drilling sequence 

651 """ 

652 def __init__(self, time_lag=10., filtername='r', twi_change=-18., note_free='DD'): 

653 

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

655 

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

657 self.twi_change = np.radians(twi_change) 

658 

659 self.survey_features = {} 

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

661 self.note_free = note_free 

662 

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

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

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

666 

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

668 in_filter = (conditions.current_filter == self.filtername) | (conditions.current_filter is None) 

669 

670 # Has enough time past? 

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

672 

673 # Did twilight start/end? 

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

675 

676 # Did we just finish a DD sequence 

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

678 

679 # Is the filter mounted? 

680 mounted = self.filtername in conditions.mounted_filters 

681 

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

683 result = 1. 

684 else: 

685 result = 0. 

686 

687 return result 

688 

689 

690class Goal_Strict_filter_basis_function(Base_basis_function): 

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

692 

693 If the moon rises/sets or twilight starts/ends, it makes a lot of sense to consider 

694 a filter change. This basis function rewards if it matches the current filter, the moon rises or sets, 

695 twilight starts or stops, or there has been a large gap since the last observation. 

696 

697 Parameters 

698 --------- 

699 time_lag_min: Minimum time after a filter change for which a new filter change will receive zero reward, or 

700 be denied at all (see unseen_before_lag). 

701 time_lag_max: Time after a filter change where the reward for changing filters achieve its maximum. 

702 time_lag_boost: Time after a filter change to apply a boost on the reward. 

703 boost_gain: A multiplier factor for the reward after time_lag_boost. 

704 unseen_before_lag: If True will make it impossible to switch filter before time_lag has passed. 

705 filtername: The filter for which this basis function will be used. 

706 tag: When using filter proportion use only regions with this tag to count for observations. 

707 twi_change: Switch reward on when twilight changes. 

708 proportion: The expected filter proportion distribution. 

709 aways_available: If this is true the basis function will aways be computed regardless of the feasibility. If 

710 False a more detailed feasibility check is performed. When set to False, it may speed up the computation 

711 process by avoiding to compute the reward functions paired with this bf, when observation is not feasible. 

712 

713 """ 

714 

715 def __init__(self, time_lag_min=10., time_lag_max=30., 

716 time_lag_boost=60., boost_gain=2.0, unseen_before_lag=False, 

717 filtername='r', tag=None, twi_change=-18., proportion=1.0, aways_available=False): 

718 

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

720 

721 self.time_lag_min = time_lag_min / 60. / 24. # Convert to days 

722 self.time_lag_max = time_lag_max / 60. / 24. # Convert to days 

723 self.time_lag_boost = time_lag_boost / 60. / 24. 

724 self.boost_gain = boost_gain 

725 self.unseen_before_lag = unseen_before_lag 

726 

727 self.twi_change = np.radians(twi_change) 

728 self.proportion = proportion 

729 self.aways_available = aways_available 

730 

731 self.survey_features = {} 

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

733 self.survey_features['Last_filter_change'] = features.LastFilterChange() 

734 self.survey_features['N_obs_all'] = features.N_obs_count(filtername=None) 

735 self.survey_features['N_obs'] = features.N_obs_count(filtername=filtername, 

736 tag=tag) 

737 

738 def filter_change_bonus(self, time): 

739 

740 lag_min = self.time_lag_min 

741 lag_max = self.time_lag_max 

742 

743 a = 1. / (lag_max - lag_min) 

744 b = -a * lag_min 

745 

746 bonus = a * time + b 

747 # How far behind we are with respect to proportion? 

748 nobs = self.survey_features['N_obs'].feature 

749 nobs_all = self.survey_features['N_obs_all'].feature 

750 goal = self.proportion 

751 # need = 1. - nobs / nobs_all + goal if nobs_all > 0 else 1. + goal 

752 need = goal / nobs * nobs_all if nobs > 0 else 1. 

753 # need /= goal 

754 if hasattr(time, '__iter__'): 

755 before_lag = np.where(time <= lag_min) 

756 bonus[before_lag] = -np.inf if self.unseen_before_lag else 0. 

757 after_lag = np.where(time >= lag_max) 

758 bonus[after_lag] = 1. if time < self.time_lag_boost else self.boost_gain 

759 elif int_rounded(time) <= int_rounded(lag_min): 

760 return -np.inf if self.unseen_before_lag else 0. 

761 elif int_rounded(time) >= int_rounded(lag_max): 

762 return 1. if int_rounded(time) < int_rounded(self.time_lag_boost) else self.boost_gain 

763 

764 return bonus * need 

765 

766 def check_feasibility(self, conditions): 

767 """ 

768 This method makes a pre-check of the feasibility of this basis function. If a basis function return False 

769 on the feasibility check, it won't computed at all. 

770 

771 :return: 

772 """ 

773 

774 # Make a quick check about the feasibility of this basis function. If current filter is none, telescope 

775 # is parked and we could, in principle, switch to any filter. If this basis function computes reward for 

776 # the current filter, then it is also feasible. At last we check for an "aways_available" flag. Meaning, we 

777 # force this basis function to be aways be computed. 

778 if conditions.current_filter is None or conditions.current_filter == self.filtername or self.aways_available: 

779 return True 

780 

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

782 

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

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

785 

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

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

788 

789 # Has enough time past? 

790 lag = conditions.mjd - self.survey_features['Last_filter_change'].feature['mjd'] 

791 time_past = int_rounded(lag) > int_rounded(self.time_lag_min) 

792 

793 # Did twilight start/end? 

794 twi_changed = (conditions.sunAlt - self.twi_change) * \ 

795 (self.survey_features['Last_observation'].feature['sunAlt'] - self.twi_change) < 0 

796 

797 # Did we just finish a DD sequence 

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

799 

800 # Is the filter mounted? 

801 mounted = self.filtername in conditions.mounted_filters 

802 

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

804 return True 

805 else: 

806 return False 

807 

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

809 

810 if conditions.current_filter is None: 

811 return 0. # no bonus if no filter is mounted 

812 # elif self.condition_features['Current_filter'].feature == self.filtername: 

813 # return 0. # no bonus if on the filter already 

814 

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

816 moon_changed = conditions.moonAlt * \ 

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

818 

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

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

821 

822 # Has enough time past? 

823 lag = conditions.mjd - self.survey_features['Last_filter_change'].feature['mjd'] 

824 time_past = lag > self.time_lag_min 

825 

826 # Did twilight start/end? 

827 twi_changed = (conditions.sunAlt - self.twi_change) * ( 

828 self.survey_features['Last_observation'].feature['sunAlt'] - self.twi_change) < 0 

829 

830 # Did we just finish a DD sequence 

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

832 

833 # Is the filter mounted? 

834 mounted = self.filtername in conditions.mounted_filters 

835 

836 if (moon_changed | time_past | twi_changed | wasDD) & mounted: 

837 result = self.filter_change_bonus(lag) if time_past else 0. 

838 else: 

839 result = -100. if self.unseen_before_lag else 0. 

840 

841 return result 

842 

843 

844class Filter_change_basis_function(Base_basis_function): 

845 """Reward staying in the current filter. 

846 """ 

847 def __init__(self, filtername='r'): 

848 super(Filter_change_basis_function, self).__init__(filtername=filtername) 

849 

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

851 

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

853 result = 1. 

854 else: 

855 result = 0. 

856 return result 

857 

858 

859class Slewtime_basis_function(Base_basis_function): 

860 """Reward slews that take little time 

861 

862 Parameters 

863 ---------- 

864 max_time : float (135) 

865 The estimated maximum slewtime (seconds). Used to normalize so the basis function 

866 spans ~ -1-0 in reward units. 

867 """ 

868 def __init__(self, max_time=135., filtername='r', nside=None): 

869 super(Slewtime_basis_function, self).__init__(nside=nside, filtername=filtername) 

870 

871 self.maxtime = max_time 

872 self.nside = nside 

873 self.filtername = filtername 

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

875 

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

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

878 pass 

879 

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

881 # If we are in a different filter, the Filter_change_basis_function will take it 

882 if conditions.current_filter != self.filtername: 

883 result = 0 

884 else: 

885 # Need to make sure smaller slewtime is larger reward. 

886 if np.size(conditions.slewtime) > 1: 

887 result = self.result.copy() 

888 good = ~np.isnan(conditions.slewtime) 

889 result[good] = -conditions.slewtime[good]/self.maxtime 

890 else: 

891 result = -conditions.slewtime/self.maxtime 

892 return result 

893 

894 

895class Aggressive_Slewtime_basis_function(Base_basis_function): 

896 """Reward slews that take little time 

897 

898 XXX--not sure how this is different from Slewtime_basis_function? 

899 Looks like it's checking the slewtime to the field position rather than the healpix maybe? 

900 """ 

901 

902 def __init__(self, max_time=135., order=1., hard_max=None, filtername='r', nside=None): 

903 super(Aggressive_Slewtime_basis_function, self).__init__(nside=nside, filtername=filtername) 

904 

905 self.maxtime = max_time 

906 self.hard_max = hard_max 

907 self.order = order 

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

909 

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

911 # If we are in a different filter, the Filter_change_basis_function will take it 

912 if conditions.current_filter != self.filtername: 

913 result = 0. 

914 else: 

915 # Need to make sure smaller slewtime is larger reward. 

916 if np.size(self.condition_features['slewtime'].feature) > 1: 

917 result = self.result.copy() 

918 result.fill(np.nan) 

919 

920 good = np.where(np.bitwise_and(conditions.slewtime > 0., 

921 conditions.slewtime < self.maxtime)) 

922 result[good] = ((self.maxtime - conditions.slewtime[good]) / 

923 self.maxtime) ** self.order 

924 if self.hard_max is not None: 

925 not_so_good = np.where(conditions.slewtime > self.hard_max) 

926 result[not_so_good] -= 10. 

927 fields = np.unique(conditions.hp2fields[good]) 

928 for field in fields: 

929 hp_indx = np.where(conditions.hp2fields == field) 

930 result[hp_indx] = np.min(result[hp_indx]) 

931 else: 

932 result = (self.maxtime - conditions.slewtime) / self.maxtime 

933 return result 

934 

935 

936class Skybrightness_limit_basis_function(Base_basis_function): 

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

938 

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

940 

941 Parameters 

942 ---------- 

943 min : float (20.) 

944 The minimum sky brightness (mags). 

945 max : float (30.) 

946 The maximum sky brightness (mags). 

947 

948 """ 

949 def __init__(self, nside=None, filtername='r', sbmin=20., sbmax=30.): 

950 super(Skybrightness_limit_basis_function, self).__init__(nside=nside, filtername=filtername) 

951 

952 self.min = int_rounded(sbmin) 

953 self.max = int_rounded(sbmax) 

954 self.result = np.empty(hp.nside2npix(self.nside), dtype=float) 

955 self.result.fill(np.nan) 

956 

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

958 result = self.result.copy() 

959 

960 good = np.where(np.bitwise_and(int_rounded(conditions.skybrightness[self.filtername]) > self.min, 

961 int_rounded(conditions.skybrightness[self.filtername]) < self.max)) 

962 result[good] = 1.0 

963 

964 return result 

965 

966 

967class CableWrap_unwrap_basis_function(Base_basis_function): 

968 """ 

969 Parameters 

970 ---------- 

971 minAz : float (20.) 

972 The minimum azimuth to activate bf (degrees) 

973 maxAz : float (82.) 

974 The maximum azimuth to activate bf (degrees) 

975 unwrap_until: float (90.) 

976 The window in which the bf is activated (degrees) 

977 """ 

978 def __init__(self, nside=None, minAz=-270., maxAz=270., minAlt=20., maxAlt=82., 

979 activate_tol=20., delta_unwrap=1.2, unwrap_until=70., max_duration=30.): 

980 super(CableWrap_unwrap_basis_function, self).__init__(nside=nside) 

981 

982 self.minAz = np.radians(minAz) 

983 self.maxAz = np.radians(maxAz) 

984 

985 self.activate_tol = np.radians(activate_tol) 

986 self.delta_unwrap = np.radians(delta_unwrap) 

987 self.unwrap_until = np.radians(unwrap_until) 

988 

989 self.minAlt = np.radians(minAlt) 

990 self.maxAlt = np.radians(maxAlt) 

991 # Convert to half-width for convienence 

992 self.nside = nside 

993 self.active = False 

994 self.unwrap_direction = 0. # either -1., 0., 1. 

995 self.max_duration = max_duration/60./24. # Convert to days 

996 self.activation_time = None 

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

998 

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

1000 

1001 result = self.result.copy() 

1002 

1003 current_abs_rad = np.radians(conditions.az) 

1004 unseen = np.where(np.bitwise_or(conditions.alt < self.minAlt, 

1005 conditions.alt > self.maxAlt)) 

1006 result[unseen] = np.nan 

1007 

1008 if (self.minAz + self.activate_tol < current_abs_rad < self.maxAz - self.activate_tol) and not self.active: 

1009 return result 

1010 elif self.active and self.unwrap_direction == 1 and current_abs_rad > self.minAz+self.unwrap_until: 

1011 self.active = False 

1012 self.unwrap_direction = 0. 

1013 self.activation_time = None 

1014 return result 

1015 elif self.active and self.unwrap_direction == -1 and current_abs_rad < self.maxAz-self.unwrap_until: 

1016 self.active = False 

1017 self.unwrap_direction = 0. 

1018 self.activation_time = None 

1019 return result 

1020 elif (self.activation_time is not None and 

1021 conditions.mjd - self.activation_time > self.max_duration): 

1022 self.active = False 

1023 self.unwrap_direction = 0. 

1024 self.activation_time = None 

1025 return result 

1026 

1027 if not self.active: 

1028 self.activation_time = conditions.mjd 

1029 if current_abs_rad < 0.: 

1030 self.unwrap_direction = 1 # clock-wise unwrap 

1031 else: 

1032 self.unwrap_direction = -1 # counter-clock-wise unwrap 

1033 

1034 self.active = True 

1035 

1036 max_abs_rad = self.maxAz 

1037 min_abs_rad = self.minAz 

1038 

1039 TWOPI = 2.*np.pi 

1040 

1041 # Compute distance and accumulated az. 

1042 norm_az_rad = np.divmod(conditions.az - min_abs_rad, TWOPI)[1] + min_abs_rad 

1043 distance_rad = divmod(norm_az_rad - current_abs_rad, TWOPI)[1] 

1044 get_shorter = np.where(distance_rad > np.pi) 

1045 distance_rad[get_shorter] -= TWOPI 

1046 accum_abs_rad = current_abs_rad + distance_rad 

1047 

1048 # Compute wrap regions and fix distances 

1049 mask_max = np.where(accum_abs_rad > max_abs_rad) 

1050 distance_rad[mask_max] -= TWOPI 

1051 mask_min = np.where(accum_abs_rad < min_abs_rad) 

1052 distance_rad[mask_min] += TWOPI 

1053 

1054 # Step-2: Repeat but now with compute reward to unwrap using specified delta_unwrap 

1055 unwrap_current_abs_rad = current_abs_rad - (np.abs(self.delta_unwrap) if self.unwrap_direction > 0 

1056 else -np.abs(self.delta_unwrap)) 

1057 unwrap_distance_rad = divmod(norm_az_rad - unwrap_current_abs_rad, TWOPI)[1] 

1058 unwrap_get_shorter = np.where(unwrap_distance_rad > np.pi) 

1059 unwrap_distance_rad[unwrap_get_shorter] -= TWOPI 

1060 unwrap_distance_rad = np.abs(unwrap_distance_rad) 

1061 

1062 if self.unwrap_direction < 0: 

1063 mask = np.where(accum_abs_rad > unwrap_current_abs_rad) 

1064 else: 

1065 mask = np.where(accum_abs_rad < unwrap_current_abs_rad) 

1066 

1067 # Finally build reward map 

1068 result = (1. - unwrap_distance_rad/np.max(unwrap_distance_rad))**2. 

1069 result[mask] = 0. 

1070 result[unseen] = np.nan 

1071 

1072 return result 

1073 

1074 

1075class Cadence_enhance_basis_function(Base_basis_function): 

1076 """Drive a certain cadence 

1077 Parameters 

1078 ---------- 

1079 filtername : str ('gri') 

1080 The filter(s) that should be grouped together 

1081 supress_window : list of float 

1082 The start and stop window for when observations should be repressed (days) 

1083 apply_area : healpix map 

1084 The area over which to try and drive the cadence. Good values as 1, no candece drive 0. 

1085 Probably works as a bool array too.""" 

1086 def __init__(self, filtername='gri', nside=None, 

1087 supress_window=[0, 1.8], supress_val=-0.5, 

1088 enhance_window=[2.1, 3.2], enhance_val=1., 

1089 apply_area=None): 

1090 super(Cadence_enhance_basis_function, self).__init__(nside=nside, filtername=filtername) 

1091 

1092 self.supress_window = np.sort(supress_window) 

1093 self.supress_val = supress_val 

1094 self.enhance_window = np.sort(enhance_window) 

1095 self.enhance_val = enhance_val 

1096 

1097 self.survey_features = {} 

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

1099 

1100 self.empty = np.zeros(hp.nside2npix(self.nside), dtype=float) 

1101 # No map, try to drive the whole area 

1102 if apply_area is None: 

1103 self.apply_indx = np.arange(self.empty.size) 

1104 else: 

1105 self.apply_indx = np.where(apply_area != 0)[0] 

1106 

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

1108 # copy an empty array 

1109 result = self.empty.copy() 

1110 if indx is not None: 

1111 ind = np.intersect1d(indx, self.apply_indx) 

1112 else: 

1113 ind = self.apply_indx 

1114 if np.size(ind) == 0: 

1115 result = 0 

1116 else: 

1117 mjd_diff = conditions.mjd - self.survey_features['last_observed'].feature[ind] 

1118 to_supress = np.where((int_rounded(mjd_diff) > int_rounded(self.supress_window[0])) & 

1119 (int_rounded(mjd_diff) < int_rounded(self.supress_window[1]))) 

1120 result[ind[to_supress]] = self.supress_val 

1121 to_enhance = np.where((int_rounded(mjd_diff) > int_rounded(self.enhance_window[0])) & 

1122 (int_rounded(mjd_diff) < int_rounded(self.enhance_window[1]))) 

1123 result[ind[to_enhance]] = self.enhance_val 

1124 return result 

1125 

1126 

1127class Azimuth_basis_function(Base_basis_function): 

1128 """Reward staying in the same azimuth range. Possibly better than using slewtime, especially when selecting a large area of sky. 

1129 

1130 Parameters 

1131 ---------- 

1132 

1133 """ 

1134 

1135 def __init__(self, nside=None): 

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

1137 

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

1139 az_dist = conditions.az - conditions.telAz 

1140 az_dist = az_dist % (2.*np.pi) 

1141 over = np.where(az_dist > np.pi) 

1142 az_dist[over] = 2. * np.pi - az_dist[over] 

1143 # Normalize sp between 0 and 1 

1144 result = az_dist/np.pi 

1145 return result 

1146 

1147 

1148class Az_modulo_basis_function(Base_basis_function): 

1149 """Try to replicate the Rothchild et al cadence forcing by only observing on limited az ranges per night. 

1150 

1151 Parameters 

1152 ---------- 

1153 az_limits : list of float pairs (None) 

1154 The azimuth limits (degrees) to use. 

1155 """ 

1156 def __init__(self, nside=None, az_limits=None, out_of_bounds_val=-1.): 

1157 super(Az_modulo_basis_function, self).__init__(nside=nside) 

1158 self.result = np.ones(hp.nside2npix(self.nside)) 

1159 if az_limits is None: 

1160 spread = 100./2. 

1161 self.az_limits = np.radians([[360-spread, spread], 

1162 [90.-spread, 90.+spread], 

1163 [180.-spread, 180.+spread]]) 

1164 else: 

1165 self.az_limits = np.radians(az_limits) 

1166 self.mod_val = len(self.az_limits) 

1167 self.out_of_bounds_val = out_of_bounds_val 

1168 

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

1170 result = self.result.copy() 

1171 az_lim = self.az_limits[np.max(conditions.night) % self.mod_val] 

1172 

1173 if az_lim[0] < az_lim[1]: 

1174 out_pix = np.where((int_rounded(conditions.az) < int_rounded(az_lim[0])) | 

1175 (int_rounded(conditions.az) > int_rounded(az_lim[1]))) 

1176 else: 

1177 out_pix = np.where((int_rounded(conditions.az) < int_rounded(az_lim[0])) | 

1178 (int_rounded(conditions.az) > int_rounded(az_lim[1])))[0] 

1179 result[out_pix] = self.out_of_bounds_val 

1180 return result 

1181 

1182 

1183class Dec_modulo_basis_function(Base_basis_function): 

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

1185 

1186 Parameters 

1187 ---------- 

1188 dec_limits : list of float pairs (None) 

1189 The azimuth limits (degrees) to use. 

1190 """ 

1191 def __init__(self, nside=None, dec_limits=None, out_of_bounds_val=-1.): 

1192 super(Dec_modulo_basis_function, self).__init__(nside=nside) 

1193 

1194 npix = hp.nside2npix(nside) 

1195 hpids = np.arange(npix) 

1196 ra, dec = _hpid2RaDec(nside, hpids) 

1197 

1198 self.results = [] 

1199 

1200 if dec_limits is None: 

1201 self.dec_limits = np.radians([[-90., -32.8], 

1202 [-32.8, -12.], 

1203 [-12., 35.]]) 

1204 else: 

1205 self.dec_limits = np.radians(dec_limits) 

1206 self.mod_val = len(self.dec_limits) 

1207 self.out_of_bounds_val = out_of_bounds_val 

1208 

1209 for limits in self.dec_limits: 

1210 good = np.where((dec >= limits[0]) & (dec < limits[1]))[0] 

1211 tmp = np.zeros(npix) 

1212 tmp[good] = 1 

1213 self.results.append(tmp) 

1214 

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

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

1217 result = self.results[night_index] 

1218 

1219 return result 

1220 

1221 

1222class Map_modulo_basis_function(Base_basis_function): 

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

1224 

1225 Parameters 

1226 ---------- 

1227 inmaps : list of hp arrays 

1228 """ 

1229 def __init__(self, inmaps): 

1230 nside = hp.npix2nside(np.size(inmaps[0])) 

1231 super(Map_modulo_basis_function, self).__init__(nside=nside) 

1232 self.maps = inmaps 

1233 self.mod_val = len(inmaps) 

1234 

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

1236 indx = np.max(conditions.night % self.mod_val) 

1237 result = self.maps[indx] 

1238 return result 

1239 

1240 

1241class Good_seeing_basis_function(Base_basis_function): 

1242 """Drive observations in good seeing conditions""" 

1243 

1244 def __init__(self, nside=None, filtername='r', footprint=None, FWHMeff_limit=0.8, 

1245 mag_diff=0.75): 

1246 super(Good_seeing_basis_function, self).__init__(nside=nside) 

1247 

1248 self.filtername = filtername 

1249 self.FWHMeff_limit = int_rounded(FWHMeff_limit) 

1250 if footprint is None: 

1251 fp = utils.standard_goals(nside=nside)[filtername] 

1252 else: 

1253 fp = footprint 

1254 self.out_of_bounds = np.where(fp == 0)[0] 

1255 self.result = fp*0 

1256 

1257 self.mag_diff = int_rounded(mag_diff) 

1258 self.survey_features = {} 

1259 self.survey_features['coadd_depth_all'] = features.Coadded_depth(filtername=filtername, 

1260 nside=nside) 

1261 self.survey_features['coadd_depth_good'] = features.Coadded_depth(filtername=filtername, 

1262 nside=nside, 

1263 FWHMeff_limit=FWHMeff_limit) 

1264 

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

1266 # Seeing is "bad" 

1267 if int_rounded(conditions.FWHMeff[self.filtername].min()) > self.FWHMeff_limit: 

1268 return 0 

1269 result = self.result.copy() 

1270 

1271 diff = self.survey_features['coadd_depth_all'].feature - self.survey_features['coadd_depth_good'].feature 

1272 # Where are there things we want to observe? 

1273 good_pix = np.where((int_rounded(diff) > self.mag_diff) & 

1274 (int_rounded(conditions.FWHMeff[self.filtername]) <= self.FWHMeff_limit)) 

1275 # Hm, should this scale by the mag differences? Probably. 

1276 result[good_pix] = diff[good_pix] 

1277 result[self.out_of_bounds] = 0 

1278 

1279 return result 

1280 

1281 

1282class Template_generate_basis_function(Base_basis_function): 

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

1284 

1285 Parameters 

1286 ---------- 

1287 day_gap : float (250.) 

1288 How long to wait before boosting the reward (days) 

1289 footprint : np.array (None) 

1290 The indices of the healpixels to apply the boost to. Uses the default footprint if None 

1291 """ 

1292 def __init__(self, nside=None, day_gap=250., filtername='r', footprint=None): 

1293 super(Template_generate_basis_function, self).__init__(nside=nside) 

1294 self.day_gap = day_gap 

1295 self.filtername = filtername 

1296 self.survey_features = {} 

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

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

1299 if footprint is None: 

1300 fp = utils.standard_goals(nside=nside)[filtername] 

1301 else: 

1302 fp = footprint 

1303 self.out_of_bounds = np.where(fp == 0) 

1304 

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

1306 result = self.result.copy() 

1307 overdue = np.where((int_rounded(conditions.mjd - self.survey_features['Last_observed'].feature)) > int_rounded(self.day_gap)) 

1308 result[overdue] = 1 

1309 result[self.out_of_bounds] = 0 

1310 

1311 return result 

1312 

1313 

1314class Observed_twice_basis_function(Base_basis_function): 

1315 """Mask out pixels that haven't been observed in the night 

1316 """ 

1317 def __init__(self, nside=None, filtername='r', n_obs_needed=2, n_obs_in_filt_needed=1): 

1318 super(Observed_twice_basis_function, self).__init__(nside=nside) 

1319 self.n_obs_needed = n_obs_needed 

1320 self.n_obs_in_filt_needed = n_obs_in_filt_needed 

1321 self.filtername = filtername 

1322 self.survey_features = {} 

1323 self.survey_features['N_obs_infilt'] = features.N_obs_night(nside=nside, filtername=filtername) 

1324 self.survey_features['N_obs_all'] = features.N_obs_night(nside=nside, filtername='') 

1325 

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

1327 

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

1329 result = self.result.copy() 

1330 good_pix = np.where((self.survey_features['N_obs_infilt'].feature >= self.n_obs_in_filt_needed) & 

1331 (self.survey_features['N_obs_all'].feature >= self.n_obs_needed))[0] 

1332 result[good_pix] = 1 

1333 

1334 return result