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 

9import logging 

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

11from lsst.sims.featureScheduler import version 

12from lsst.sims.survey.fields import FieldsDatabase 

13 

14log = logging.getLogger(__name__) 

15 

16 

17class int_rounded(object): 

18 """ 

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

20 preventing machine precision issues cross-platforms 

21 

22 Parameters 

23 ---------- 

24 inval : number-like thing 

25 Some number that we want to compare 

26 scale : float (1e5) 

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

28 """ 

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

30 self.initial = inval 

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

32 self.scale = scale 

33 

34 def __eq__(self, other): 

35 return self.value == other.value 

36 

37 def __ne__(self, other): 

38 return self.value != other.value 

39 

40 def __lt__(self, other): 

41 return self.value < other.value 

42 

43 def __le__(self, other): 

44 return self.value <= other.value 

45 

46 def __gt__(self, other): 

47 return self.value > other.value 

48 

49 def __ge__(self, other): 

50 return self.value >= other.value 

51 

52 def __repr__(self): 

53 return str(self.initial) 

54 

55 def __add__(self, other): 

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

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

58 return result 

59 

60 def __sub__(self, other): 

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

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

63 return result 

64 

65 def __mul__(self, other): 

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

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

68 return result 

69 

70 def __div__(self, other): 

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

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

73 return result 

74 

75 

76def set_default_nside(nside=None): 

77 """ 

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

79 

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

81 

82 Parameters 

83 ---------- 

84 nside : int (None) 

85 A valid healpixel nside. 

86 """ 

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

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

89 nside = 32 

90 set_default_nside.nside = nside 

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

92 set_default_nside.nside = nside 

93 return set_default_nside.nside 

94 

95 

96def approx_altaz2pa(alt_rad, az_rad, latitude_rad): 

97 """ 

98 A fast calculation of parallactic angle 

99 XXX--could move this to lsst.sims.utils.approxCoordTransforms.py 

100 Parameters 

101 ---------- 

102 alt_rad : float 

103 Altitude (radians) 

104 az_rad : float 

105 Azimuth (radians) 

106 latitude_rad : float 

107 The latitude of the observatory (radians) 

108 """ 

109 

110 y = np.sin(-az_rad)*np.cos(latitude_rad) 

111 x = np.cos(alt_rad)*np.sin(latitude_rad) - np.sin(alt_rad)*np.cos(latitude_rad)*np.cos(-az_rad) 

112 pa = np.arctan2(y, x) 

113 # Make it run from 0-360 deg instead of of -180 to 180 

114 pa = pa % (2.*np.pi) 

115 return pa 

116 

117 

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

119 """ 

120 Like scipy.binned_statistic, but for unique int ids 

121 """ 

122 

123 uids = np.unique(ids) 

124 order = np.argsort(ids) 

125 

126 ordered_ids = ids[order] 

127 ordered_values = values[order] 

128 

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

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

131 

132 stat_results = [] 

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

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

135 

136 return uids, np.array(stat_results) 

137 

138 

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

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

141 Input radians. Grabbed from sims_selfcal""" 

142 # also used in Global Telescope Network website 

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

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

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

146 return x, y 

147 

148 

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

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

151 Returns Ra/Dec in radians.""" 

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

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

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

155 return RA, Dec 

156 

157 

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

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

160 np.nan. 

161 

162 Parameters 

163 ---------- 

164 in_map : np.array 

165 A valie healpix map 

166 nside_out : int 

167 The desired resolution to convert in_map to 

168 UNSEEN2nan : bool (True) 

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

170 """ 

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

172 if current_nside != nside_out: 

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

174 else: 

175 out_map = in_map 

176 if UNSEEN2nan: 

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

178 return out_map 

179 

180 

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

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

183 

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

185 

186 Parameters 

187 ---------- 

188 x0 : array 

189 order : list 

190 Keys for the order x0 should be sorted in. 

191 xbin : float (1.) 

192 The binsize to round off the first coordinate into 

193 

194 returns 

195 ------- 

196 array sorted so that it rasters up and down. 

197 """ 

198 coords = x0.copy() 

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

200 # digitize my bins 

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

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

203 coords = coords[order1] 

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

205 if np.size(places_to_invert) > 0: 

206 places_to_invert += 1 

207 indx = np.arange(coords.size) 

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

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

210 

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

212 if i % 2 == 0: 

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

214 else: 

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

216 

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

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

219 else: 

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

221 return order1[index_sorted] 

222 else: 

223 return order1 

224 

225 

226class schema_converter(object): 

227 """ 

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

229 """ 

230 def __init__(self): 

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

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

233 'observationStartMJD': 'mjd', 

234 'observationStartLST': 'lmst', 'numExposures': 'nexp', 

235 'visitTime': 'visittime', 'visitExposureTime': 'exptime', 

236 'proposalId': 'survey_id', 'fieldId': 'field_id', 

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

238 'filter': 'filter', 'airmass': 'airmass', 'skyBrightness': 'skybrightness', 

239 'cloud': 'clouds', 'seeingFwhm500': 'FWHM_500', 

240 'seeingFwhmGeom': 'FWHM_geometric', 'seeingFwhmEff': 'FWHMeff', 

241 'fiveSigmaDepth': 'fivesigmadepth', 'slewTime': 'slewtime', 

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

243 'rotSkyPos': 'rotSkyPos', 'moonRA': 'moonRA', 

244 'moonDec': 'moonDec', 'moonAlt': 'moonAlt', 'moonAz': 'moonAz', 

245 'moonDistance': 'moonDist', 'moonPhase': 'moonPhase', 

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

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

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

249 # angles to converts 

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

251 'paraAngle', 'rotTelPos', 'rotSkyPos', 'moonRA', 'moonDec', 

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

253 # Put LMST into degrees too 

254 self.angles_hours2deg = ['observationStartLST'] 

255 

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

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

258 """ 

259 if delete_past: 

260 try: 

261 os.remove(filename) 

262 except OSError: 

263 pass 

264 

265 df = pd.DataFrame(obs_array) 

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

267 for colname in self.angles_rad2deg: 

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

269 for colname in self.angles_hours2deg: 

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

271 

272 if filename is not None: 

273 con = db.connect(filename) 

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

275 if info is not None: 

276 df = pd.DataFrame(info) 

277 df.to_sql('info', con) 

278 

279 def opsim2obs(self, filename): 

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

281 """ 

282 

283 con = db.connect(filename) 

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

285 for key in self.angles_rad2deg: 

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

287 for key in self.angles_hours2deg: 

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

289 

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

291 

292 blank = empty_observation() 

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

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

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

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

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

298 

299 return final_result 

300 

301 

302def empty_observation(): 

303 """ 

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

305 

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

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

308 

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

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

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

312 

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

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

315 

316 Returns 

317 ------- 

318 numpy array 

319 

320 Notes 

321 ----- 

322 The numpy fields have the following structure 

323 RA : float 

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

325 dec : float 

326 Declination of the observation (Radians) 

327 mjd : float 

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

329 exptime : float 

330 Total exposure time of the visit (seconds) 

331 filter : str 

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

333 rotSkyPos : float 

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

335 nexp : int 

336 Number of exposures in the visit. 

337 airmass : float 

338 Airmass at the center of the field 

339 FWHMeff : float 

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

341 skybrightness : float 

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

343 field. (mag/sq arcsec) 

344 night : int 

345 The night number of the observation (days) 

346 flush_by_mjd : float 

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

348 """ 

349 

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

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

352 'slewtime', 'visittime', 'slewdist', 'fivesigmadepth', 

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

354 'field_id', 'survey_id', 'block_id', 

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

356 'moonDist', 'solarElong', 'moonPhase'] 

357 

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

359 float, float, float, float, float, int, 

360 float, float, float, float, 

361 float, float, float, float, float, float, 'U40', 

362 int, int, int, 

363 float, float, float, float, float, float, float, float, 

364 float, float, float] 

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

366 return result 

367 

368 

369def obs_to_fbsobs(obs): 

370 """ 

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

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

373 """ 

374 

375 fbsobs = empty_observation() 

376 fbsobs['RA'] = obs.ra_rad 

377 fbsobs['dec'] = obs.dec_rad 

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

379 fbsobs['mjd'] = obs.observation_start_mjd 

380 fbsobs['exptime'] = obs.exp_time 

381 fbsobs['filter'] = obs.filter 

382 fbsobs['rotSkyPos'] = obs.ang_rad 

383 fbsobs['nexp'] = obs.num_exp 

384 fbsobs['airmass'] = obs.airmass 

385 fbsobs['FWHMeff'] = obs.seeing_fwhm_eff 

386 fbsobs['FWHM_geometric'] = obs.seeing_fwhm_geom 

387 fbsobs['skybrightness'] = obs.sky_brightness 

388 fbsobs['night'] = obs.night 

389 fbsobs['slewtime'] = obs.slewtime 

390 fbsobs['fivesigmadepth'] = obs.five_sigma_depth 

391 fbsobs['alt'] = obs.alt_rad 

392 fbsobs['az'] = obs.az_rad 

393 fbsobs['clouds'] = obs.cloud 

394 fbsobs['moonAlt'] = obs.moon_alt 

395 fbsobs['sunAlt'] = obs.sun_alt 

396 fbsobs['note'] = obs.note 

397 fbsobs['field_id'] = obs.fieldid 

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

399 

400 return fbsobs 

401 

402 

403def empty_scheduled_observation(): 

404 """ 

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

406 """ 

407 start = empty_observation() 

408 names = start.dtype.names 

409 types = start.dtype.types 

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

411 types.extend([float, float]) 

412 

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

414 return result 

415 

416 

417def read_fields(): 

418 """ 

419 Read in the Field coordinates 

420 Returns 

421 ------- 

422 numpy.array 

423 With RA and dec in radians. 

424 """ 

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

426 fd = FieldsDatabase() 

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

428 # order by field ID 

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

430 

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

432 types = [float, float] 

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

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

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

436 

437 return result 

438 

439 

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

441 """ 

442 Generate a KD-tree of healpixel locations 

443 

444 Parameters 

445 ---------- 

446 nside : int 

447 A valid healpix nside 

448 leafsize : int (100) 

449 Leafsize of the kdtree 

450 

451 Returns 

452 ------- 

453 tree : scipy kdtree 

454 """ 

455 if nside is None: 

456 nside = set_default_nside() 

457 

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

459 ra, dec = _hpid2RaDec(nside, hpid) 

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

461 

462 

463class hp_in_lsst_fov(object): 

464 """ 

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

466 no chip/raft gaps. 

467 """ 

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

469 """ 

470 Parameters 

471 ---------- 

472 fov_radius : float (1.75) 

473 Radius of the filed of view in degrees 

474 """ 

475 if nside is None: 

476 nside = set_default_nside() 

477 

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

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

480 self.scale = scale 

481 

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

483 """ 

484 Parameters 

485 ---------- 

486 ra : float 

487 RA in radians 

488 dec : float 

489 Dec in radians 

490 

491 Returns 

492 ------- 

493 indx : numpy array 

494 The healpixels that are within the FoV 

495 """ 

496 

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

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

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

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

501 

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

503 return np.array(indices) 

504 

505 

506class hp_in_comcam_fov(object): 

507 """ 

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

509 with no chip gaps. 

510 """ 

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

512 """ 

513 Parameters 

514 ---------- 

515 side_length : float (0.7) 

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

517 """ 

518 if nside is None: 

519 nside = set_default_nside() 

520 self.nside = nside 

521 self.tree = hp_kd_tree(nside=nside) 

522 self.side_length = np.radians(side_length) 

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

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

525 # The positions of the raft corners, unrotated 

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

527 self.side_length/2.]) 

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

529 self.side_length/2.]) 

530 

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

532 """ 

533 Parameters 

534 ---------- 

535 ra : float 

536 RA in radians 

537 dec : float 

538 Dec in radians 

539 rotSkyPos : float 

540 The rotation angle of the camera in radians 

541 Returns 

542 ------- 

543 indx : numpy array 

544 The healpixels that are within the FoV 

545 """ 

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

547 # Healpixels within the inner circle 

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

549 # Healpixels withing the outer circle 

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

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

552 

553 cos_rot = np.cos(rotSkyPos) 

554 sin_rot = np.sin(rotSkyPos) 

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

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

557 

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

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

560 [x_rotated[1], y_rotated[1]], 

561 [x_rotated[2], y_rotated[2]], 

562 [x_rotated[3], y_rotated[3]], 

563 [x_rotated[0], y_rotated[0]]])) 

564 

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

566 

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

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

569 for i, xcheck in enumerate(x): 

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

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

572 indices.append(indices_to_check[i]) 

573 

574 return np.array(indices) 

575 

576 

577def run_info_table(observatory, extra_info=None): 

578 """ 

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

580 """ 

581 

582 observatory_info = observatory.get_info() 

583 for key in extra_info: 

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

585 observatory_info = np.array(observatory_info) 

586 

587 n_feature_entries = 4 

588 

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

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

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

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

593 

594 # Fill in info about the run 

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

596 now = datetime.datetime.now() 

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

598 

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

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

601 

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

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

604 

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

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

607 

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

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

610 

611 return result 

612 

613 

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

615 """ 

616 Make sure values are within min/max 

617 """ 

618 inval = np.array(inval) 

619 below = np.where(inval < minimum) 

620 inval[below] = minimum 

621 above = np.where(inval > maximum) 

622 inval[above] = maximum 

623 return inval 

624 

625 

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

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

628 

629 Parameters 

630 ---------- 

631 scheduler : scheduler object 

632 

633 observations : np.array 

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

635 """ 

636 

637 # Check that observations are in order 

638 observations.sort(order=mjd_key) 

639 for observation in observations: 

640 scheduler.add_observation(observation) 

641 

642 return scheduler 

643 

644 

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

646 """ 

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

648 

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

650 

651 Parameters 

652 ---------- 

653 night : int or array 

654 The night we want to convert to a season 

655 offset : float or array (0) 

656 Offset to be applied to night (days) 

657 modulo : int (None) 

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

659 (seasons, years w/default season_length) 

660 max_season : int (None) 

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

662 season_length : float (365.25) 

663 How long to consider one season (nights) 

664 floor : bool (True) 

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

666 """ 

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

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

669 result = night + offset 

670 result = result/season_length 

671 if floor: 

672 result = np.floor(result) 

673 if max_season is not None: 

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

675 

676 if modulo is not None: 

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

678 result = result % modulo 

679 result[neg] = -1 

680 if max_season is not None: 

681 result[over_indx] = -1 

682 if floor: 

683 result = result.astype(int) 

684 return result 

685 

686 

687def create_season_offset(nside, sun_RA_rad): 

688 """ 

689 Make an offset map so seasons roll properly 

690 """ 

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

692 ra, dec = _hpid2RaDec(nside, hpindx) 

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

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

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

696 offset = -offset - 365.25 

697 return offset 

698 

699 

700class TargetoO(object): 

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

702 

703 Parameters 

704 ---------- 

705 tooid : int 

706 Unique ID for the ToO. 

707 footprints : np.array 

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

709 mjd_start : float 

710 The MJD the ToO starts 

711 duration : float 

712 Duration of the ToO (days). 

713 """ 

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

715 self.footprint = footprint 

716 self.duration = duration 

717 self.id = tooid 

718 self.mjd_start = mjd_start 

719 

720 

721class Sim_targetoO_server(object): 

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

723 """ 

724 

725 def __init__(self, targetoO_list): 

726 self.targetoO_list = targetoO_list 

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

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

729 self.mjd_ends = self.mjd_starts + durations 

730 

731 def __call__(self, mjd): 

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

733 result = None 

734 if in_range.size > 0: 

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

736 return result