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

1from builtins import zip 

2from builtins import object 

3import numpy as np 

4import numbers 

5from .SpatialBounds import SpatialBounds 

6from lsst.sims.utils import ModifiedJulianDate 

7from lsst.sims.utils import Site 

8 

9__all__ = ["ObservationMetaData"] 

10 

11 

12class ObservationMetaData(object): 

13 """Observation Metadata 

14 

15 This class contains any metadata for a query which is associated with 

16 a particular telescope pointing, including bounds in RA and DEC, and 

17 the time of the observation. 

18 

19 **Parameters** 

20 

21 All parameters are optional. It is possible to instantiate an 

22 ObservationMetaData that is empty of data. 

23 

24 * pointing[RA,Dec] float 

25 The coordinates of the pointing (in degrees; in the International 

26 Celestial Reference System) 

27 

28 * boundType characterizes the shape of the field of view. Current options 

29 are 'box, and 'circle' 

30 

31 * boundLength is the characteristic length scale of the field of view in degrees. 

32 

33 If boundType is 'box', boundLength can be a float(in which case boundLength is 

34 half the length of the side of each box) or boundLength can be a numpy array 

35 in which case the first argument is half the width of the RA side of the box 

36 and the second argument is half the Dec side of the box. 

37 

38 If boundType is 'circle,' this will be the radius of the circle. 

39 

40 The bound will be centered on the point (pointingRA, pointingDec), however, 

41 because objects are stored at their mean RA, Dec in the LSST databases 

42 (i.e. they are stored at values of RA, Dec which neglect proper motion), the 

43 bounds applied to database queries will be made slightly larger so that queries 

44 can be reasonably expected to return all of the objects within the desired field 

45 of view once those corrections have been applied. 

46 

47 * mjd : 

48 Either a float (in which case, it will be assumed to be in International 

49 Atomic Time), or an instantiation of the ModifiedJulianDate class representing 

50 the date of the observation 

51 

52 * bandpassName : a char (e.g. 'u') or list (e.g. ['u', 'g', 'z']) 

53 denoting the bandpasses used for this particular observation 

54 

55 * site: an instantiation of the lsst.sims.utils.Site class characterizing 

56 the site of the observatory. 

57 

58 * m5: float or list 

59 this should be the 5-sigma limiting magnitude in the bandpass or 

60 bandpasses specified in bandpassName. Ultimately, m5 will be stored 

61 in a dict keyed to the bandpassName (or Names) you passed in, i.e. 

62 you will be able to access m5 from outside of this class using, for 

63 example: 

64 

65 myObservationMetaData.m5['u'] 

66 

67 * skyBrightness: float the magnitude of the sky in the 

68 filter specified by bandpassName 

69 

70 * seeing float or list 

71 Analogous to m5, corresponds to the seeing in arcseconds in the bandpasses in 

72 bandpassName 

73 

74 * rotSkyPos float 

75 The orientation of the telescope in degrees. 

76 This is used by the Astrometry mixins in sims_coordUtils. 

77 

78 The convention for rotSkyPos is as follows: 

79 

80 rotSkyPos = 0 means north is in the +y direction on the focal plane and east is +x 

81 

82 rotSkyPos = 90 means north is +x and east is -y 

83 

84 rotSkyPos = -90 means north is -x and east is +y 

85 

86 rotSkyPos = 180 means north is -y and east is -x 

87 

88 This should be consistent with PhoSim conventions. 

89 

90 **Examples**:: 

91 >>> data = ObservationMetaData(boundType='box', pointingRA=5.0, pointingDec=15.0, 

92 boundLength=5.0) 

93 

94 """ 

95 def __init__(self, boundType=None, boundLength=None, 

96 mjd=None, pointingRA=None, pointingDec=None, rotSkyPos=None, 

97 bandpassName=None, site=Site(name='LSST'), m5=None, skyBrightness=None, 

98 seeing=None): 

99 

100 self._bounds = None 

101 self._boundType = boundType 

102 self._bandpass = bandpassName 

103 self._skyBrightness = skyBrightness 

104 self._site = site 

105 self._OpsimMetaData = None 

106 

107 if mjd is not None: 

108 if isinstance(mjd, numbers.Number): 

109 self._mjd = ModifiedJulianDate(TAI=mjd) 

110 elif isinstance(mjd, ModifiedJulianDate): 

111 self._mjd = mjd 

112 else: 

113 raise RuntimeError("You must pass either a float or a ModifiedJulianDate " 

114 "as the kwarg mjd to ObservationMetaData") 

115 else: 

116 self._mjd = None 

117 

118 if rotSkyPos is not None: 

119 self._rotSkyPos = np.radians(rotSkyPos) 

120 else: 

121 self._rotSkyPos = None 

122 

123 if pointingRA is not None: 

124 self._pointingRA = np.radians(pointingRA) 

125 else: 

126 self._pointingRA = None 

127 

128 if pointingDec is not None: 

129 self._pointingDec = np.radians(pointingDec) 

130 else: 

131 self._pointingDec = None 

132 

133 if boundLength is not None: 

134 self._boundLength = np.radians(boundLength) 

135 else: 

136 self._boundLength = None 

137 

138 self._m5 = self._assignDictKeyedToBandpass(m5, 'm5') 

139 

140 self._seeing = self._assignDictKeyedToBandpass(seeing, 'seeing') 

141 

142 if self._bounds is None: 

143 self._buildBounds() 

144 

145 @property 

146 def summary(self): 

147 mydict = {} 

148 mydict['site'] = self.site 

149 

150 mydict['boundType'] = self.boundType 

151 mydict['boundLength'] = self.boundLength 

152 mydict['pointingRA'] = self.pointingRA 

153 mydict['pointingDec'] = self.pointingDec 

154 mydict['rotSkyPos'] = self.rotSkyPos 

155 

156 if self.mjd is None: 

157 mydict['mjd'] = None 

158 else: 

159 mydict['mjd'] = self.mjd.TAI 

160 

161 mydict['bandpass'] = self.bandpass 

162 mydict['skyBrightness'] = self.skyBrightness 

163 # mydict['m5'] = self.m5 

164 

165 mydict['OpsimMetaData'] = self._OpsimMetaData 

166 

167 return mydict 

168 

169 def __ne__(self, other): 

170 return not self.__eq__(other) 

171 

172 def __eq__(self, other): 

173 

174 if self.bounds != other.bounds: 

175 return False 

176 

177 if self.pointingRA != other.pointingRA: 

178 return False 

179 

180 if self.pointingDec != other.pointingDec: 

181 return False 

182 

183 if self.rotSkyPos != other.rotSkyPos: 

184 return False 

185 

186 if self.bandpass != other.bandpass: 

187 return False 

188 

189 if self.seeing != other.seeing: 

190 return False 

191 

192 if self.m5 != other.m5: 

193 return False 

194 

195 if self.site != other.site: 

196 return False 

197 

198 if self.mjd != other.mjd: 

199 return False 

200 

201 if self.skyBrightness != other.skyBrightness: 

202 return False 

203 

204 if self.OpsimMetaData != other.OpsimMetaData: 

205 return False 

206 

207 return True 

208 

209 def _assignDictKeyedToBandpass(self, inputValue, inputName): 

210 """ 

211 This method sets up a dict of either m5 or seeing values (or any other quantity 

212 keyed to bandpassName). It reads in a list of values and associates them with 

213 the list of bandpass names in self._bandpass. 

214 

215 Note: this method assumes that self._bandpass has already been set. 

216 It will raise an exception of self._bandpass is None. 

217 

218 @param [in] inputValue is a single value or list of m5/seeing/etc. corresponding to 

219 the bandpasses stored in self._bandpass 

220 

221 @param [in] inputName is the name of the paramter stored in inputValue 

222 (for constructing helpful error message) 

223 

224 @param [out] returns a dict of inputValue values keed to self._bandpass 

225 """ 

226 

227 if inputValue is None: 

228 return None 

229 else: 

230 bandpassIsList = False 

231 inputIsList = False 

232 

233 if self._bandpass is None: 

234 raise RuntimeError('You cannot set %s if you have not set ' % inputName + 

235 'bandpass in ObservationMetaData') 

236 

237 if hasattr(self._bandpass, '__iter__') and not isinstance(self._bandpass, str): 

238 bandpassIsList = True 

239 

240 if hasattr(inputValue, '__iter__') and not isinstance(inputValue, str): 

241 inputIsList = True 

242 

243 if bandpassIsList and not inputIsList: 

244 raise RuntimeError('You passed a list of bandpass names' + 

245 'but did not pass a list of %s to ObservationMetaData' % inputName) 

246 

247 if inputIsList and not bandpassIsList: 

248 raise RuntimeError('You passed a list of %s ' % inputName + 

249 'but did not pass a list of bandpass names to ObservationMetaData') 

250 

251 if inputIsList: 

252 if len(inputValue) != len(self._bandpass): 

253 raise RuntimeError('The list of %s you passed to ObservationMetaData ' % inputName + 

254 'has a different length than the list of bandpass names you passed') 

255 

256 # now build the dict 

257 if bandpassIsList: 

258 if len(inputValue) != len(self._bandpass): 

259 raise RuntimeError('In ObservationMetaData you tried to assign bandpass ' + 

260 'and %s with lists of different length' % inputName) 

261 

262 outputDict = {} 

263 for b, m in zip(self._bandpass, inputValue): 

264 outputDict[b] = m 

265 else: 

266 outputDict = {self._bandpass: inputValue} 

267 

268 return outputDict 

269 

270 def _buildBounds(self): 

271 """ 

272 Set up the member variable self._bounds. 

273 

274 If self._boundType, self._boundLength, self._pointingRA, or 

275 self._pointingDec are None, nothing will happen. 

276 """ 

277 

278 if self._boundType is None: 

279 return 

280 

281 if self._boundLength is None: 

282 return 

283 

284 if self._pointingRA is None or self._pointingDec is None: 

285 return 

286 

287 self._bounds = SpatialBounds.getSpatialBounds(self._boundType, self._pointingRA, self._pointingDec, 

288 self._boundLength) 

289 

290 @property 

291 def pointingRA(self): 

292 """ 

293 The RA of the telescope pointing in degrees 

294 (in the International Celestial Reference System). 

295 """ 

296 if self._pointingRA is not None: 

297 return np.degrees(self._pointingRA) 

298 else: 

299 return None 

300 

301 @pointingRA.setter 

302 def pointingRA(self, value): 

303 self._pointingRA = np.radians(value) 

304 self._buildBounds() 

305 

306 @property 

307 def pointingDec(self): 

308 """ 

309 The Dec of the telescope pointing in degrees 

310 (in the International Celestial Reference System). 

311 """ 

312 if self._pointingDec is not None: 

313 return np.degrees(self._pointingDec) 

314 else: 

315 return None 

316 

317 @pointingDec.setter 

318 def pointingDec(self, value): 

319 self._pointingDec = np.radians(value) 

320 self._buildBounds() 

321 

322 @property 

323 def boundLength(self): 

324 """ 

325 Either a list or a float indicating the size of the field 

326 of view associated with this ObservationMetaData. 

327 

328 See the documentation in the SpatialBounds class for more 

329 details (specifically, the 'length' paramter). 

330 

331 In degrees (Yes: the documentation in SpatialBounds says that 

332 the length should be in radians. The present class converts 

333 from degrees to radians before passing to SpatialBounds). 

334 """ 

335 if self._boundLength is None: 

336 return None 

337 

338 return np.degrees(self._boundLength) 

339 

340 @boundLength.setter 

341 def boundLength(self, value): 

342 self._boundLength = np.radians(value) 

343 self._buildBounds() 

344 

345 @property 

346 def boundType(self): 

347 """ 

348 Tag indicating what sub-class of SpatialBounds should 

349 be instantiated for this ObservationMetaData. 

350 """ 

351 return self._boundType 

352 

353 @boundType.setter 

354 def boundType(self, value): 

355 self._boundType = value 

356 self._buildBounds() 

357 

358 @property 

359 def bounds(self): 

360 """ 

361 Instantiation of a sub-class of SpatialBounds. This 

362 is what actually construct the WHERE clause of the SQL 

363 query associated with this ObservationMetaData. 

364 """ 

365 return self._bounds 

366 

367 @property 

368 def rotSkyPos(self): 

369 """ 

370 The rotation of the telescope with respect to the sky in degrees. 

371 It is a parameter you should get from OpSim. 

372 """ 

373 if self._rotSkyPos is not None: 

374 return np.degrees(self._rotSkyPos) 

375 else: 

376 return None 

377 

378 @rotSkyPos.setter 

379 def rotSkyPos(self, value): 

380 self._rotSkyPos = np.radians(value) 

381 

382 @property 

383 def m5(self): 

384 """ 

385 A dict of m5 (the 5-sigma limiting magnitude) values 

386 associated with the bandpasses represented by this 

387 ObservationMetaData. 

388 """ 

389 return self._m5 

390 

391 @m5.setter 

392 def m5(self, value): 

393 self._m5 = self._assignDictKeyedToBandpass(value, 'm5') 

394 

395 @property 

396 def seeing(self): 

397 """ 

398 A dict of seeing values in arcseconds associated 

399 with the bandpasses represented by this ObservationMetaData 

400 """ 

401 return self._seeing 

402 

403 @seeing.setter 

404 def seeing(self, value): 

405 self._seeing = self._assignDictKeyedToBandpass(value, 'seeing') 

406 

407 @property 

408 def site(self): 

409 """ 

410 An instantiation of the Site class containing information about 

411 the telescope site. 

412 """ 

413 return self._site 

414 

415 @site.setter 

416 def site(self, value): 

417 self._site = value 

418 

419 @property 

420 def mjd(self): 

421 """ 

422 The MJD of the observation associated with this ObservationMetaData. 

423 """ 

424 return self._mjd 

425 

426 @mjd.setter 

427 def mjd(self, value): 

428 """ 

429 Either a float or a ModifiedJulianDate. If a float, this setter 

430 assumes that you are passing in International Atomic Time 

431 """ 

432 if isinstance(value, float): 

433 self._mjd = ModifiedJulianDate(TAI=value) 

434 elif isinstance(value, ModifiedJulianDate): 

435 self._mjd = value 

436 else: 

437 raise RuntimeError("You can only set mjd to either a float or a ModifiedJulianDate") 

438 

439 @property 

440 def bandpass(self): 

441 """ 

442 The bandpass associated with this ObservationMetaData. 

443 Can be a list. 

444 """ 

445 return self._bandpass 

446 

447 def setBandpassM5andSeeing(self, bandpassName=None, m5=None, seeing=None): 

448 """ 

449 Set the bandpasses and associated 5-sigma limiting magnitudes 

450 and seeing values for this ObservationMetaData. 

451 

452 @param [in] bandpassName is either a char or a list of chars denoting 

453 the name of the bandpass associated with this ObservationMetaData. 

454 

455 @param [in] m5 is the 5-sigma-limiting magnitude(s) associated 

456 with bandpassName 

457 

458 @param [in] seeing is the seeing(s) in arcseconds associated 

459 with bandpassName 

460 

461 Nothing is returned. This method just sets member variables of 

462 this ObservationMetaData. 

463 """ 

464 

465 self._bandpass = bandpassName 

466 self._m5 = self._assignDictKeyedToBandpass(m5, 'm5') 

467 self._seeing = self._assignDictKeyedToBandpass(seeing, 'seeing') 

468 

469 @property 

470 def skyBrightness(self): 

471 """ 

472 The sky brightness in mags per square arcsecond associated 

473 with this ObservationMetaData. 

474 """ 

475 return self._skyBrightness 

476 

477 @skyBrightness.setter 

478 def skyBrightness(self, value): 

479 self._skyBrightness = value 

480 

481 @property 

482 def OpsimMetaData(self): 

483 """ 

484 A dict of all of the columns taken from OpSim when constructing this 

485 ObservationMetaData 

486 """ 

487 return self._OpsimMetaData 

488 

489 @OpsimMetaData.setter 

490 def OpsimMetaData(self, value): 

491 if not isinstance(value, dict): 

492 raise RuntimeError('OpsimMetaData must be a dict') 

493 self._OpsimMetaData = value