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 os 

2import sqlite3 as db 

3import datetime 

4import socket 

5import numpy as np 

6import healpy as hp 

7import pandas as pd 

8import matplotlib.path as mplPath 

9from lsst.sims.utils import _hpid2RaDec, xyz_angular_radius, _buildTree, _xyz_from_ra_dec 

10from lsst.sims.featureScheduler import version 

11from lsst.sims.survey.fields import FieldsDatabase 

12 

13 

14class int_rounded(object): 

15 """ 

16 Class to help force comparisons be made on scaled up integers, 

17 preventing machine precision issues cross-platforms 

18 

19 Parameters 

20 ---------- 

21 inval : number-like thing 

22 Some number that we want to compare 

23 scale : float (1e5) 

24 How much to scale inval before rounding and converting to an int. 

25 """ 

26 def __init__(self, inval, scale=1e5): 

27 self.initial = inval 

28 self.value = np.round(inval * scale).astype(int) 

29 self.scale = scale 

30 

31 def __eq__(self, other): 

32 return self.value == other.value 

33 

34 def __ne__(self, other): 

35 return self.value != other.value 

36 

37 def __lt__(self, other): 

38 return self.value < other.value 

39 

40 def __le__(self, other): 

41 return self.value <= other.value 

42 

43 def __gt__(self, other): 

44 return self.value > other.value 

45 

46 def __ge__(self, other): 

47 return self.value >= other.value 

48 

49 def __repr__(self): 

50 return str(self.initial) 

51 

52 def __add__(self, other): 

53 out_scale = np.min([self.scale, other.scale]) 

54 result = int_rounded(self.initial + other.initial, scale=out_scale) 

55 return result 

56 

57 def __sub__(self, other): 

58 out_scale = np.min([self.scale, other.scale]) 

59 result = int_rounded(self.initial - other.initial, scale=out_scale) 

60 return result 

61 

62 def __mul__(self, other): 

63 out_scale = np.min([self.scale, other.scale]) 

64 result = int_rounded(self.initial * other.initial, scale=out_scale) 

65 return result 

66 

67 def __div__(self, other): 

68 out_scale = np.min([self.scale, other.scale]) 

69 result = int_rounded(self.initial / other.initial, scale=out_scale) 

70 return result 

71 

72 

73def set_default_nside(nside=None): 

74 """ 

75 Utility function to set a default nside value across the scheduler. 

76 

77 XXX-there might be a better way to do this. 

78 

79 Parameters 

80 ---------- 

81 nside : int (None) 

82 A valid healpixel nside. 

83 """ 

84 if not hasattr(set_default_nside, 'nside'): 84 ↛ 88line 84 didn't jump to line 88, because the condition on line 84 was never false

85 if nside is None: 85 ↛ 87line 85 didn't jump to line 87, because the condition on line 85 was never false

86 nside = 32 

87 set_default_nside.nside = nside 

88 if nside is not None: 88 ↛ 90line 88 didn't jump to line 90, because the condition on line 88 was never false

89 set_default_nside.nside = nside 

90 return set_default_nside.nside 

91 

92 

93def int_binned_stat(ids, values, statistic=np.mean): 

94 """ 

95 Like scipy.binned_statistic, but for unique int ids 

96 """ 

97 

98 uids = np.unique(ids) 

99 order = np.argsort(ids) 

100 

101 ordered_ids = ids[order] 

102 ordered_values = values[order] 

103 

104 left = np.searchsorted(ordered_ids, uids, side='left') 

105 right = np.searchsorted(ordered_ids, uids, side='right') 

106 

107 stat_results = [] 

108 for le, ri in zip(left, right): 

109 stat_results.append(statistic(ordered_values[le:ri])) 

110 

111 return uids, np.array(stat_results) 

112 

113 

114def gnomonic_project_toxy(RA1, Dec1, RAcen, Deccen): 

115 """Calculate x/y projection of RA1/Dec1 in system with center at RAcen, Deccen. 

116 Input radians. Grabbed from sims_selfcal""" 

117 # also used in Global Telescope Network website 

118 cosc = np.sin(Deccen) * np.sin(Dec1) + np.cos(Deccen) * np.cos(Dec1) * np.cos(RA1-RAcen) 

119 x = np.cos(Dec1) * np.sin(RA1-RAcen) / cosc 

120 y = (np.cos(Deccen)*np.sin(Dec1) - np.sin(Deccen)*np.cos(Dec1)*np.cos(RA1-RAcen)) / cosc 

121 return x, y 

122 

123 

124def gnomonic_project_tosky(x, y, RAcen, Deccen): 

125 """Calculate RA/Dec on sky of object with x/y and RA/Cen of field of view. 

126 Returns Ra/Dec in radians.""" 

127 denom = np.cos(Deccen) - y * np.sin(Deccen) 

128 RA = RAcen + np.arctan2(x, denom) 

129 Dec = np.arctan2(np.sin(Deccen) + y * np.cos(Deccen), np.sqrt(x*x + denom*denom)) 

130 return RA, Dec 

131 

132 

133def match_hp_resolution(in_map, nside_out, UNSEEN2nan=True): 

134 """Utility to convert healpix map resolution if needed and change hp.UNSEEN values to 

135 np.nan. 

136 

137 Parameters 

138 ---------- 

139 in_map : np.array 

140 A valie healpix map 

141 nside_out : int 

142 The desired resolution to convert in_map to 

143 UNSEEN2nan : bool (True) 

144 If True, convert any hp.UNSEEN values to np.nan 

145 """ 

146 current_nside = hp.npix2nside(np.size(in_map)) 

147 if current_nside != nside_out: 

148 out_map = hp.ud_grade(in_map, nside_out=nside_out) 

149 else: 

150 out_map = in_map 

151 if UNSEEN2nan: 

152 out_map[np.where(out_map == hp.UNSEEN)] = np.nan 

153 return out_map 

154 

155 

156def raster_sort(x0, order=['x', 'y'], xbin=1.): 

157 """XXXX--depriciated, use tsp instead. 

158 

159 Do a sort to scan a grid up and down. Simple starting guess to traveling salesman. 

160 

161 Parameters 

162 ---------- 

163 x0 : array 

164 order : list 

165 Keys for the order x0 should be sorted in. 

166 xbin : float (1.) 

167 The binsize to round off the first coordinate into 

168 

169 returns 

170 ------- 

171 array sorted so that it rasters up and down. 

172 """ 

173 coords = x0.copy() 

174 bins = np.arange(coords[order[0]].min()-xbin/2., coords[order[0]].max()+3.*xbin/2., xbin) 

175 # digitize my bins 

176 coords[order[0]] = np.digitize(coords[order[0]], bins) 

177 order1 = np.argsort(coords, order=order) 

178 coords = coords[order1] 

179 places_to_invert = np.where(np.diff(coords[order[-1]]) < 0)[0] 

180 if np.size(places_to_invert) > 0: 

181 places_to_invert += 1 

182 indx = np.arange(coords.size) 

183 index_sorted = np.zeros(indx.size, dtype=int) 

184 index_sorted[0:places_to_invert[0]] = indx[0:places_to_invert[0]] 

185 

186 for i, inv_pt in enumerate(places_to_invert[:-1]): 

187 if i % 2 == 0: 

188 index_sorted[inv_pt:places_to_invert[i+1]] = indx[inv_pt:places_to_invert[i+1]][::-1] 

189 else: 

190 index_sorted[inv_pt:places_to_invert[i+1]] = indx[inv_pt:places_to_invert[i+1]] 

191 

192 if np.size(places_to_invert) % 2 != 0: 

193 index_sorted[places_to_invert[-1]:] = indx[places_to_invert[-1]:][::-1] 

194 else: 

195 index_sorted[places_to_invert[-1]:] = indx[places_to_invert[-1]:] 

196 return order1[index_sorted] 

197 else: 

198 return order1 

199 

200 

201class schema_converter(object): 

202 """ 

203 Record how to convert an observation array to the standard opsim schema 

204 """ 

205 def __init__(self): 

206 # Conversion dictionary, keys are opsim schema, values are observation dtype names 

207 self.convert_dict = {'observationId': 'ID', 'night': 'night', 

208 'observationStartMJD': 'mjd', 

209 'observationStartLST': 'lmst', 'numExposures': 'nexp', 

210 'visitTime': 'visittime', 'visitExposureTime': 'exptime', 

211 'proposalId': 'survey_id', 'fieldId': 'field_id', 

212 'fieldRA': 'RA', 'fieldDec': 'dec', 'altitude': 'alt', 'azimuth': 'az', 

213 'filter': 'filter', 'airmass': 'airmass', 'skyBrightness': 'skybrightness', 

214 'cloud': 'clouds', 'seeingFwhm500': 'FWHM_500', 

215 'seeingFwhmGeom': 'FWHM_geometric', 'seeingFwhmEff': 'FWHMeff', 

216 'fiveSigmaDepth': 'fivesigmadepth', 'slewTime': 'slewtime', 

217 'slewDistance': 'slewdist', 'paraAngle': 'pa', 'rotTelPos': 'rotTelPos', 

218 'rotSkyPos': 'rotSkyPos', 'moonRA': 'moonRA', 

219 'moonDec': 'moonDec', 'moonAlt': 'moonAlt', 'moonAz': 'moonAz', 

220 'moonDistance': 'moonDist', 'moonPhase': 'moonPhase', 

221 'sunAlt': 'sunAlt', 'sunAz': 'sunAz', 'solarElong': 'solarElong', 'note':'note'} 

222 # Column(s) not bothering to remap: 'observationStartTime': None, 

223 self.inv_map = {v: k for k, v in self.convert_dict.items()} 

224 # angles to converts 

225 self.angles_rad2deg = ['fieldRA', 'fieldDec', 'altitude', 'azimuth', 'slewDistance', 

226 'paraAngle', 'rotTelPos', 'rotSkyPos', 'moonRA', 'moonDec', 

227 'moonAlt', 'moonAz', 'moonDistance', 'sunAlt', 'sunAz', 'solarElong'] 

228 # Put LMST into degrees too 

229 self.angles_hours2deg = ['observationStartLST'] 

230 

231 def obs2opsim(self, obs_array, filename=None, info=None, delete_past=False): 

232 """convert an array of observations into a pandas dataframe with Opsim schema 

233 """ 

234 if delete_past: 

235 try: 

236 os.remove(filename) 

237 except OSError: 

238 pass 

239 

240 df = pd.DataFrame(obs_array) 

241 df = df.rename(index=str, columns=self.inv_map) 

242 for colname in self.angles_rad2deg: 

243 df[colname] = np.degrees(df[colname]) 

244 for colname in self.angles_hours2deg: 

245 df[colname] = df[colname] * 360./24. 

246 

247 if filename is not None: 

248 con = db.connect(filename) 

249 df.to_sql('SummaryAllProps', con, index=False) 

250 if info is not None: 

251 df = pd.DataFrame(info) 

252 df.to_sql('info', con) 

253 

254 def opsim2obs(self, filename): 

255 """convert an opsim schema dataframe into an observation array. 

256 """ 

257 

258 con = db.connect(filename) 

259 df = pd.read_sql('select * from SummaryAllProps;', con) 

260 for key in self.angles_rad2deg: 

261 df[key] = np.radians(df[key]) 

262 for key in self.angles_hours2deg: 

263 df[key] = df[key] * 24./360. 

264 

265 df = df.rename(index=str, columns=self.convert_dict) 

266 

267 blank = empty_observation() 

268 final_result = np.empty(df.shape[0], dtype=blank.dtype) 

269 # XXX-ugh, there has to be a better way. 

270 for i, key in enumerate(df.columns): 

271 if key in self.inv_map.keys(): 

272 final_result[key] = df[key].values 

273 

274 return final_result 

275 

276 

277def empty_observation(): 

278 """ 

279 Return a numpy array that could be a handy observation record 

280 

281 XXX: Should this really be "empty visit"? Should we have "visits" made 

282 up of multple "observations" to support multi-exposure time visits? 

283 

284 XXX-Could add a bool flag for "observed". Then easy to track all proposed 

285 observations. Could also add an mjd_min, mjd_max for when an observation should be observed. 

286 That way we could drop things into the queue for DD fields. 

287 

288 XXX--might be nice to add a generic "sched_note" str field, to record any metadata that 

289 would be useful to the scheduler once it's observed. and/or observationID. 

290 

291 Returns 

292 ------- 

293 numpy array 

294 

295 Notes 

296 ----- 

297 The numpy fields have the following structure 

298 RA : float 

299 The Right Acension of the observation (center of the field) (Radians) 

300 dec : float 

301 Declination of the observation (Radians) 

302 mjd : float 

303 Modified Julian Date at the start of the observation (time shutter opens) 

304 exptime : float 

305 Total exposure time of the visit (seconds) 

306 filter : str 

307 The filter used. Should be one of u, g, r, i, z, y. 

308 rotSkyPos : float 

309 The rotation angle of the camera relative to the sky E of N (Radians) 

310 nexp : int 

311 Number of exposures in the visit. 

312 airmass : float 

313 Airmass at the center of the field 

314 FWHMeff : float 

315 The effective seeing FWHM at the center of the field. (arcsec) 

316 skybrightness : float 

317 The surface brightness of the sky background at the center of the 

318 field. (mag/sq arcsec) 

319 night : int 

320 The night number of the observation (days) 

321 flush_by_mjd : float 

322 If we hit this MJD, we should flush the queue and refill it. 

323 """ 

324 

325 names = ['ID', 'RA', 'dec', 'mjd', 'flush_by_mjd', 'exptime', 'filter', 'rotSkyPos', 'nexp', 

326 'airmass', 'FWHM_500', 'FWHMeff', 'FWHM_geometric', 'skybrightness', 'night', 

327 'slewtime', 'visittime', 'slewdist', 'fivesigmadepth', 

328 'alt', 'az', 'pa', 'clouds', 'moonAlt', 'sunAlt', 'note', 

329 'field_id', 'survey_id', 'block_id', 

330 'lmst', 'rotTelPos', 'moonAz', 'sunAz', 'sunRA', 'sunDec', 'moonRA', 'moonDec', 

331 'moonDist', 'solarElong', 'moonPhase'] 

332 

333 types = [int, float, float, float, float, float, 'U1', float, int, 

334 float, float, float, float, float, int, 

335 float, float, float, float, 

336 float, float, float, float, float, float, 'U40', 

337 int, int, int, 

338 float, float, float, float, float, float, float, float, 

339 float, float, float] 

340 result = np.zeros(1, dtype=list(zip(names, types))) 

341 return result 

342 

343 

344def scheduled_observation(): 

345 """Make an array for pre-scheduling observations 

346 

347 mjd_tol : float 

348 The tolerance on how early an observation can execute (days). 

349 

350 """ 

351 

352 # Standard things from the usual observations 

353 names = ['ID', 'RA', 'dec', 'mjd', 'flush_by_mjd', 'exptime', 'filter', 'rotSkyPos', 'nexp', 

354 'note'] 

355 types = [int, float, float, float, float, float, 'U1', float, float, 'U40'] 

356 names += ['mjd_tol', 'dist_tol', 'alt_min', 'alt_max', 'HA_max', 'HA_min', 'observed'] 

357 types += [float, float, float, float, float, float, bool] 

358 result = np.zeros(1, dtype=list(zip(names, types))) 

359 return result 

360 

361 

362 

363def obs_to_fbsobs(obs): 

364 """ 

365 converts an Observation from the Driver (which is a normal python class) 

366 to an observation for the feature based scheduler (a numpy ndarray). 

367 """ 

368 

369 fbsobs = empty_observation() 

370 fbsobs['RA'] = obs.ra_rad 

371 fbsobs['dec'] = obs.dec_rad 

372 log.debug('Observation MJD: %.4f', obs.observation_start_mjd) 

373 fbsobs['mjd'] = obs.observation_start_mjd 

374 fbsobs['exptime'] = obs.exp_time 

375 fbsobs['filter'] = obs.filter 

376 fbsobs['rotSkyPos'] = obs.ang_rad 

377 fbsobs['nexp'] = obs.num_exp 

378 fbsobs['airmass'] = obs.airmass 

379 fbsobs['FWHMeff'] = obs.seeing_fwhm_eff 

380 fbsobs['FWHM_geometric'] = obs.seeing_fwhm_geom 

381 fbsobs['skybrightness'] = obs.sky_brightness 

382 fbsobs['night'] = obs.night 

383 fbsobs['slewtime'] = obs.slewtime 

384 fbsobs['fivesigmadepth'] = obs.five_sigma_depth 

385 fbsobs['alt'] = obs.alt_rad 

386 fbsobs['az'] = obs.az_rad 

387 fbsobs['clouds'] = obs.cloud 

388 fbsobs['moonAlt'] = obs.moon_alt 

389 fbsobs['sunAlt'] = obs.sun_alt 

390 fbsobs['note'] = obs.note 

391 fbsobs['field_id'] = obs.fieldid 

392 fbsobs['survey_id'] = obs.propid_list[0] 

393 

394 return fbsobs 

395 

396 

397def empty_scheduled_observation(): 

398 """ 

399 Same as empty observation, but with mjd_min, mjd_max columns 

400 """ 

401 start = empty_observation() 

402 names = start.dtype.names 

403 types = start.dtype.types 

404 names.extend(['mjd_min', 'mjd_max']) 

405 types.extend([float, float]) 

406 

407 result = np.zeros(1, dtype=list(zip(names, types))) 

408 return result 

409 

410 

411def read_fields(): 

412 """ 

413 Read in the Field coordinates 

414 Returns 

415 ------- 

416 numpy.array 

417 With RA and dec in radians. 

418 """ 

419 query = 'select fieldId, fieldRA, fieldDEC from Field;' 

420 fd = FieldsDatabase() 

421 fields = np.array(list(fd.get_field_set(query))) 

422 # order by field ID 

423 fields = fields[fields[:,0].argsort()] 

424 

425 names = ['RA', 'dec'] 

426 types = [float, float] 

427 result = np.zeros(np.size(fields[:, 1]), dtype=list(zip(names, types))) 

428 result['RA'] = np.radians(fields[:, 1]) 

429 result['dec'] = np.radians(fields[:, 2]) 

430 

431 return result 

432 

433 

434def hp_kd_tree(nside=None, leafsize=100, scale=1e5): 

435 """ 

436 Generate a KD-tree of healpixel locations 

437 

438 Parameters 

439 ---------- 

440 nside : int 

441 A valid healpix nside 

442 leafsize : int (100) 

443 Leafsize of the kdtree 

444 

445 Returns 

446 ------- 

447 tree : scipy kdtree 

448 """ 

449 if nside is None: 

450 nside = set_default_nside() 

451 

452 hpid = np.arange(hp.nside2npix(nside)) 

453 ra, dec = _hpid2RaDec(nside, hpid) 

454 return _buildTree(ra, dec, leafsize, scale=scale) 

455 

456 

457class hp_in_lsst_fov(object): 

458 """ 

459 Return the healpixels within a pointing. A very simple LSST camera model with 

460 no chip/raft gaps. 

461 """ 

462 def __init__(self, nside=None, fov_radius=1.75, scale=1e5): 

463 """ 

464 Parameters 

465 ---------- 

466 fov_radius : float (1.75) 

467 Radius of the filed of view in degrees 

468 """ 

469 if nside is None: 

470 nside = set_default_nside() 

471 

472 self.tree = hp_kd_tree(nside=nside, scale=scale) 

473 self.radius = np.round(xyz_angular_radius(fov_radius)*scale).astype(int) 

474 self.scale = scale 

475 

476 def __call__(self, ra, dec, **kwargs): 

477 """ 

478 Parameters 

479 ---------- 

480 ra : float 

481 RA in radians 

482 dec : float 

483 Dec in radians 

484 

485 Returns 

486 ------- 

487 indx : numpy array 

488 The healpixels that are within the FoV 

489 """ 

490 

491 x, y, z = _xyz_from_ra_dec(np.max(ra), np.max(dec)) 

492 x = np.round(x * self.scale).astype(int) 

493 y = np.round(y * self.scale).astype(int) 

494 z = np.round(z * self.scale).astype(int) 

495 

496 indices = self.tree.query_ball_point((x, y, z), self.radius) 

497 return np.array(indices) 

498 

499 

500class hp_in_comcam_fov(object): 

501 """ 

502 Return the healpixels within a ComCam pointing. Simple camera model 

503 with no chip gaps. 

504 """ 

505 def __init__(self, nside=None, side_length=0.7): 

506 """ 

507 Parameters 

508 ---------- 

509 side_length : float (0.7) 

510 The length of one side of the square field of view (degrees). 

511 """ 

512 if nside is None: 

513 nside = set_default_nside() 

514 self.nside = nside 

515 self.tree = hp_kd_tree(nside=nside) 

516 self.side_length = np.radians(side_length) 

517 self.inner_radius = xyz_angular_radius(side_length/2.) 

518 self.outter_radius = xyz_angular_radius(side_length/2.*np.sqrt(2.)) 

519 # The positions of the raft corners, unrotated 

520 self.corners_x = np.array([-self.side_length/2., -self.side_length/2., self.side_length/2., 

521 self.side_length/2.]) 

522 self.corners_y = np.array([self.side_length/2., -self.side_length/2., -self.side_length/2., 

523 self.side_length/2.]) 

524 

525 def __call__(self, ra, dec, rotSkyPos=0.): 

526 """ 

527 Parameters 

528 ---------- 

529 ra : float 

530 RA in radians 

531 dec : float 

532 Dec in radians 

533 rotSkyPos : float 

534 The rotation angle of the camera in radians 

535 Returns 

536 ------- 

537 indx : numpy array 

538 The healpixels that are within the FoV 

539 """ 

540 x, y, z = _xyz_from_ra_dec(np.max(ra), np.max(dec)) 

541 # Healpixels within the inner circle 

542 indices = self.tree.query_ball_point((x, y, z), self.inner_radius) 

543 # Healpixels withing the outer circle 

544 indices_all = np.array(self.tree.query_ball_point((x, y, z), self.outter_radius)) 

545 indices_to_check = indices_all[np.in1d(indices_all, indices, invert=True)] 

546 

547 cos_rot = np.cos(rotSkyPos) 

548 sin_rot = np.sin(rotSkyPos) 

549 x_rotated = self.corners_x*cos_rot - self.corners_y*sin_rot 

550 y_rotated = self.corners_x*sin_rot + self.corners_y*cos_rot 

551 

552 # Draw the square that we want to check if points are in. 

553 bbPath = mplPath.Path(np.array([[x_rotated[0], y_rotated[0]], 

554 [x_rotated[1], y_rotated[1]], 

555 [x_rotated[2], y_rotated[2]], 

556 [x_rotated[3], y_rotated[3]], 

557 [x_rotated[0], y_rotated[0]]])) 

558 

559 ra_to_check, dec_to_check = _hpid2RaDec(self.nside, indices_to_check) 

560 

561 # Project the indices to check to the tangent plane, see if they fall inside the polygon 

562 x, y = gnomonic_project_toxy(ra_to_check, dec_to_check, ra, dec) 

563 for i, xcheck in enumerate(x): 

564 # I wonder if I can do this all at once rather than a loop? 

565 if bbPath.contains_point((x[i], y[i])): 

566 indices.append(indices_to_check[i]) 

567 

568 return np.array(indices) 

569 

570 

571def run_info_table(observatory, extra_info=None): 

572 """ 

573 Make a little table for recording the information about a run 

574 """ 

575 

576 observatory_info = observatory.get_info() 

577 for key in extra_info: 

578 observatory_info.append([key, extra_info[key]]) 

579 observatory_info = np.array(observatory_info) 

580 

581 n_feature_entries = 4 

582 

583 names = ['Parameter', 'Value'] 

584 dtypes = ['|U200', '|U200'] 

585 result = np.zeros(observatory_info[:, 0].size + n_feature_entries, 

586 dtype=list(zip(names, dtypes))) 

587 

588 # Fill in info about the run 

589 result[0]['Parameter'] = 'Date, ymd' 

590 now = datetime.datetime.now() 

591 result[0]['Value'] = '%i, %i, %i' % (now.year, now.month, now.day) 

592 

593 result[1]['Parameter'] = 'hostname' 

594 result[1]['Value'] = socket.gethostname() 

595 

596 result[2]['Parameter'] = 'featureScheduler version' 

597 result[2]['Value'] = version.__version__ 

598 

599 result[3]['Parameter'] = 'featureScheduler fingerprint' 

600 result[3]['Value'] = version.__fingerprint__ 

601 

602 result[4:]['Parameter'] = observatory_info[:, 0] 

603 result[4:]['Value'] = observatory_info[:, 1] 

604 

605 return result 

606 

607 

608def inrange(inval, minimum=-1., maximum=1.): 

609 """ 

610 Make sure values are within min/max 

611 """ 

612 inval = np.array(inval) 

613 below = np.where(inval < minimum) 

614 inval[below] = minimum 

615 above = np.where(inval > maximum) 

616 inval[above] = maximum 

617 return inval 

618 

619 

620def warm_start(scheduler, observations, mjd_key='mjd'): 

621 """Replay a list of observations into the scheduler 

622 

623 Parameters 

624 ---------- 

625 scheduler : scheduler object 

626 

627 observations : np.array 

628 An array of observation (e.g., from sqlite2observations) 

629 """ 

630 

631 # Check that observations are in order 

632 observations.sort(order=mjd_key) 

633 for observation in observations: 

634 scheduler.add_observation(observation) 

635 

636 return scheduler 

637 

638 

639def season_calc(night, offset=0, modulo=None, max_season=None, season_length=365.25, floor=True): 

640 """ 

641 Compute what season a night is in with possible offset and modulo 

642 

643 using convention that night -365 to 0 is season -1. 

644 

645 Parameters 

646 ---------- 

647 night : int or array 

648 The night we want to convert to a season 

649 offset : float or array (0) 

650 Offset to be applied to night (days) 

651 modulo : int (None) 

652 If the season should be modulated (i.e., so we can get all even years) 

653 (seasons, years w/default season_length) 

654 max_season : int (None) 

655 For any season above this value (before modulo), set to -1 

656 season_length : float (365.25) 

657 How long to consider one season (nights) 

658 floor : bool (True) 

659 If true, take the floor of the season. Otherwise, returns season as a float 

660 """ 

661 if np.size(night) == 1: 

662 night = np.ravel(np.array([night])) 

663 result = night + offset 

664 result = result/season_length 

665 if floor: 

666 result = np.floor(result) 

667 if max_season is not None: 

668 over_indx = np.where(int_rounded(result) >= int_rounded(max_season)) 

669 

670 if modulo is not None: 

671 neg = np.where(int_rounded(result) < int_rounded(0)) 

672 result = result % modulo 

673 result[neg] = -1 

674 if max_season is not None: 

675 result[over_indx] = -1 

676 if floor: 

677 result = result.astype(int) 

678 return result 

679 

680 

681def create_season_offset(nside, sun_RA_rad): 

682 """ 

683 Make an offset map so seasons roll properly 

684 """ 

685 hpindx = np.arange(hp.nside2npix(nside)) 

686 ra, dec = _hpid2RaDec(nside, hpindx) 

687 offset = ra - sun_RA_rad + 2.*np.pi 

688 offset = offset % (np.pi*2) 

689 offset = offset * 365.25/(np.pi*2) 

690 offset = -offset - 365.25 

691 return offset 

692 

693 

694class TargetoO(object): 

695 """Class to hold information about a target of opportunity object 

696 

697 Parameters 

698 ---------- 

699 tooid : int 

700 Unique ID for the ToO. 

701 footprints : np.array 

702 np.array healpix maps. 1 for areas to observe, 0 for no observe. 

703 mjd_start : float 

704 The MJD the ToO starts 

705 duration : float 

706 Duration of the ToO (days). 

707 """ 

708 def __init__(self, tooid, footprint, mjd_start, duration): 

709 self.footprint = footprint 

710 self.duration = duration 

711 self.id = tooid 

712 self.mjd_start = mjd_start 

713 

714 

715class Sim_targetoO_server(object): 

716 """Wrapper to deliver a targetoO object at the right time 

717 """ 

718 

719 def __init__(self, targetoO_list): 

720 self.targetoO_list = targetoO_list 

721 self.mjd_starts = np.array([too.mjd_start for too in self.targetoO_list]) 

722 durations = np.array([too.duration for too in self.targetoO_list]) 

723 self.mjd_ends = self.mjd_starts + durations 

724 

725 def __call__(self, mjd): 

726 in_range = np.where((mjd > self.mjd_starts) & (mjd < self.mjd_ends))[0] 

727 result = None 

728 if in_range.size > 0: 

729 result = [self.targetoO_list[i] for i in in_range] 

730 return result