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 range 

3import numpy as np 

4from .baseStacker import BaseStacker 

5import warnings 

6 

7__all__ = ['setupDitherStackers', 'wrapRADec', 'wrapRA', 'inHexagon', 'polygonCoords', 

8 'BaseDitherStacker', 

9 'RandomDitherFieldPerVisitStacker', 'RandomDitherFieldPerNightStacker', 

10 'RandomDitherPerNightStacker', 

11 'SpiralDitherFieldPerVisitStacker', 'SpiralDitherFieldPerNightStacker', 

12 'SpiralDitherPerNightStacker', 

13 'HexDitherFieldPerVisitStacker', 'HexDitherFieldPerNightStacker', 

14 'HexDitherPerNightStacker', 

15 'RandomRotDitherPerFilterChangeStacker'] 

16 

17# Stacker naming scheme: 

18# [Pattern]Dither[Field]Per[Timescale]. 

19# Timescale indicates how often the dither offset is changed. 

20# The presence of 'Field' indicates that a new offset is chosen per field, on the indicated timescale. 

21# The absence of 'Field' indicates that all visits within the indicated timescale use the same dither offset. 

22 

23 

24# Original dither stackers (Random, Spiral, Hex) written by Lynne Jones (lynnej@uw.edu) 

25# Additional dither stackers written by Humna Awan (humna.awan@rutgers.edu), with addition of 

26# constraining dither offsets to be within an inscribed hexagon (code modifications for use here by LJ). 

27 

28def setupDitherStackers(raCol, decCol, degrees, **kwargs): 

29 b = BaseStacker() 

30 stackerList = [] 

31 if raCol in b.sourceDict: 

32 stackerList.append(b.sourceDict[raCol](degrees=degrees, **kwargs)) 

33 if decCol in b.sourceDict: 

34 if b.sourceDict[raCol] != b.sourceDict[decCol]: 

35 stackerList.append(b.sourceDict[decCol](degrees=degrees, **kwargs)) 

36 return stackerList 

37 

38 

39def wrapRADec(ra, dec): 

40 """ 

41 Wrap RA into 0-2pi and Dec into +/0 pi/2. 

42 

43 Parameters 

44 ---------- 

45 ra : numpy.ndarray 

46 RA in radians 

47 dec : numpy.ndarray 

48 Dec in radians 

49 

50 Returns 

51 ------- 

52 numpy.ndarray, numpy.ndarray 

53 Wrapped RA/Dec values, in radians. 

54 """ 

55 # Wrap dec. 

56 low = np.where(dec < -np.pi / 2.0)[0] 

57 dec[low] = -1 * (np.pi + dec[low]) 

58 ra[low] = ra[low] - np.pi 

59 high = np.where(dec > np.pi / 2.0)[0] 

60 dec[high] = np.pi - dec[high] 

61 ra[high] = ra[high] - np.pi 

62 # Wrap RA. 

63 ra = ra % (2.0 * np.pi) 

64 return ra, dec 

65 

66 

67def wrapRA(ra): 

68 """ 

69 Wrap only RA values into 0-2pi (using mod). 

70 

71 Parameters 

72 ---------- 

73 ra : numpy.ndarray 

74 RA in radians 

75 

76 Returns 

77 ------- 

78 numpy.ndarray 

79 Wrapped RA values, in radians. 

80 """ 

81 ra = ra % (2.0 * np.pi) 

82 return ra 

83 

84 

85def inHexagon(xOff, yOff, maxDither): 

86 """ 

87 Identify dither offsets which fall within the inscribed hexagon. 

88 

89 Parameters 

90 ---------- 

91 xOff : numpy.ndarray 

92 The x values of the dither offsets. 

93 yoff : numpy.ndarray 

94 The y values of the dither offsets. 

95 maxDither : float 

96 The maximum dither offset. 

97 

98 Returns 

99 ------- 

100 numpy.ndarray 

101 Indexes of the offsets which are within the hexagon inscribed inside the 'maxDither' radius circle. 

102 """ 

103 # Set up the hexagon limits. 

104 # y = mx + b, 2h is the height. 

105 m = np.sqrt(3.0) 

106 b = m * maxDither 

107 h = m / 2.0 * maxDither 

108 # Identify offsets inside hexagon. 

109 inside = np.where((yOff < m * xOff + b) & 

110 (yOff > m * xOff - b) & 

111 (yOff < -m * xOff + b) & 

112 (yOff > -m * xOff - b) & 

113 (yOff < h) & (yOff > -h))[0] 

114 return inside 

115 

116 

117def polygonCoords(nside, radius, rotationAngle): 

118 """ 

119 Find the x,y coords of a polygon. 

120 

121 This is useful for plotting dither points and showing they lie within 

122 a given shape. 

123 

124 Parameters 

125 ---------- 

126 nside : int 

127 The number of sides of the polygon 

128 radius : float 

129 The radius within which to plot the polygon 

130 rotationAngle : float 

131 The angle to rotate the polygon to. 

132 

133 Returns 

134 ------- 

135 [float, float] 

136 List of x/y coordinates of the points describing the polygon. 

137 """ 

138 eachAngle = 2 * np.pi / float(nside) 

139 xCoords = np.zeros(nside, float) 

140 yCoords = np.zeros(nside, float) 

141 for i in range(0, nside): 

142 xCoords[i] = np.sin(eachAngle * i + rotationAngle) * radius 

143 yCoords[i] = np.cos(eachAngle * i + rotationAngle) * radius 

144 return list(zip(xCoords, yCoords)) 

145 

146 

147class BaseDitherStacker(BaseStacker): 

148 """Base class for dither stackers. 

149 

150 The base class just adds an easy way to define a stacker as one of the 'dither' types of stackers. 

151 These run first, before any other stackers. 

152 

153 Parameters 

154 ---------- 

155 raCol : str, optional 

156 The name of the RA column in the data. 

157 Default 'fieldRA'. 

158 decCol : str, optional 

159 The name of the Dec column in the data. 

160 Default 'fieldDec'. 

161 degrees : bool, optional 

162 Flag whether RA/Dec should be treated as (and kept as) degrees. 

163 maxDither : float, optional 

164 The radius of the maximum dither offset, in degrees. 

165 Default 1.75 degrees. 

166 inHex : bool, optional 

167 If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle. 

168 If False, offsets can lie anywhere out to the edges of the maxDither circle. 

169 Default True. 

170 """ 

171 colsAdded = [] 

172 

173 def __init__(self, raCol='fieldRA', decCol='fieldDec', degrees=True, 

174 maxDither=1.75, inHex=True): 

175 # Instantiate the RandomDither object and set internal variables. 

176 self.raCol = raCol 

177 self.decCol = decCol 

178 self.degrees = degrees 

179 # Convert maxDither to radians for internal use. 

180 self.maxDither = np.radians(maxDither) 

181 self.inHex = inHex 

182 # self.units used for plot labels 

183 if self.degrees: 183 ↛ 186line 183 didn't jump to line 186, because the condition on line 183 was never false

184 self.units = ['deg', 'deg'] 

185 else: 

186 self.units = ['rad', 'rad'] 

187 # Values required for framework operation: this specifies the data columns required from the database. 

188 self.colsReq = [self.raCol, self.decCol] 

189 

190 

191class RandomDitherFieldPerVisitStacker(BaseDitherStacker): 

192 """ 

193 Randomly dither the RA and Dec pointings up to maxDither degrees from center, 

194 with a different offset for each field, for each visit. 

195 

196 Parameters 

197 ---------- 

198 raCol : str, optional 

199 The name of the RA column in the data. 

200 Default 'fieldRA'. 

201 decCol : str, optional 

202 The name of the Dec column in the data. 

203 Default 'fieldDec'. 

204 degrees : bool, optional 

205 Flag whether RA/Dec should be treated as (and kept as) degrees. 

206 maxDither : float, optional 

207 The radius of the maximum dither offset, in degrees. 

208 Default 1.75 degrees. 

209 inHex : bool, optional 

210 If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle. 

211 If False, offsets can lie anywhere out to the edges of the maxDither circle. 

212 Default True. 

213 randomSeed : int or None, optional 

214 If set, then used as the random seed for the numpy random number generation for the dither offsets. 

215 Default None. 

216 """ 

217 # Values required for framework operation: this specifies the name of the new columns. 

218 colsAdded = ['randomDitherFieldPerVisitRa', 'randomDitherFieldPerVisitDec'] 

219 

220 def __init__(self, raCol='fieldRA', decCol='fieldDec', degrees=True, maxDither=1.75, 

221 inHex=True, randomSeed=None): 

222 """ 

223 @ MaxDither in degrees 

224 """ 

225 super().__init__(raCol=raCol, decCol=decCol, degrees=degrees, maxDither=maxDither, inHex=inHex) 

226 self.randomSeed = randomSeed 

227 

228 def _generateRandomOffsets(self, noffsets): 

229 xOut = np.array([], float) 

230 yOut = np.array([], float) 

231 maxTries = 100 

232 tries = 0 

233 while (len(xOut) < noffsets) and (tries < maxTries): 

234 dithersRad = np.sqrt(self._rng.rand(noffsets * 2)) * self.maxDither 

235 dithersTheta = self._rng.rand(noffsets * 2) * np.pi * 2.0 

236 xOff = dithersRad * np.cos(dithersTheta) 

237 yOff = dithersRad * np.sin(dithersTheta) 

238 if self.inHex: 

239 # Constrain dither offsets to be within hexagon. 

240 idx = inHexagon(xOff, yOff, self.maxDither) 

241 xOff = xOff[idx] 

242 yOff = yOff[idx] 

243 xOut = np.concatenate([xOut, xOff]) 

244 yOut = np.concatenate([yOut, yOff]) 

245 tries += 1 

246 if len(xOut) < noffsets: 

247 raise ValueError('Could not find enough random points within the hexagon in %d tries. ' 

248 'Try another random seed?' % (maxTries)) 

249 self.xOff = xOut[0:noffsets] 

250 self.yOff = yOut[0:noffsets] 

251 

252 def _run(self, simData, cols_present=False): 

253 if cols_present: 

254 # Column already present in data; assume it is correct and does not need recalculating. 

255 return simData 

256 # Generate random numbers for dither, using defined seed value if desired. 

257 if not hasattr(self, '_rng'): 

258 if self.randomSeed is not None: 

259 self._rng = np.random.RandomState(self.randomSeed) 

260 else: 

261 self._rng = np.random.RandomState(2178813) 

262 

263 # Generate the random dither values. 

264 noffsets = len(simData[self.raCol]) 

265 self._generateRandomOffsets(noffsets) 

266 # Add to RA and dec values. 

267 if self.degrees: 

268 ra = np.radians(simData[self.raCol]) 

269 dec = np.radians(simData[self.decCol]) 

270 else: 

271 ra = simData[self.raCol] 

272 dec = simData[self.decCol] 

273 simData['randomDitherFieldPerVisitRa'] = (ra + self.xOff / np.cos(dec)) 

274 simData['randomDitherFieldPerVisitDec'] = dec + self.yOff 

275 # Wrap back into expected range. 

276 simData['randomDitherFieldPerVisitRa'], simData['randomDitherFieldPerVisitDec'] = \ 

277 wrapRADec(simData['randomDitherFieldPerVisitRa'], simData['randomDitherFieldPerVisitDec']) 

278 # Convert to degrees 

279 if self.degrees: 

280 for col in self.colsAdded: 

281 simData[col] = np.degrees(simData[col]) 

282 return simData 

283 

284 

285class RandomDitherFieldPerNightStacker(RandomDitherFieldPerVisitStacker): 

286 """ 

287 Randomly dither the RA and Dec pointings up to maxDither degrees from center, 

288 one dither offset per new night of observation of a field. 

289 e.g. visits within the same night, to the same field, have the same offset. 

290 

291 Parameters 

292 ---------- 

293 raCol : str, optional 

294 The name of the RA column in the data. 

295 Default 'fieldRA'. 

296 decCol : str, optional 

297 The name of the Dec column in the data. 

298 Default 'fieldDec'. 

299 degrees : bool, optional 

300 Flag whether RA/Dec should be treated as (and kept as) degrees. 

301 fieldIdCol : str, optional 

302 The name of the fieldId column in the data. 

303 Used to identify fields which should be identified as the 'same'. 

304 Default 'fieldId'. 

305 nightCol : str, optional 

306 The name of the night column in the data. 

307 Default 'night'. 

308 maxDither : float, optional 

309 The radius of the maximum dither offset, in degrees. 

310 Default 1.75 degrees. 

311 inHex : bool, optional 

312 If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle. 

313 If False, offsets can lie anywhere out to the edges of the maxDither circle. 

314 Default True. 

315 randomSeed : int or None, optional 

316 If set, then used as the random seed for the numpy random number generation for the dither offsets. 

317 Default None. 

318 """ 

319 # Values required for framework operation: this specifies the names of the new columns. 

320 colsAdded = ['randomDitherFieldPerNightRa', 'randomDitherFieldPerNightDec'] 

321 

322 def __init__(self, raCol='fieldRA', decCol='fieldDec', degrees=True, fieldIdCol='fieldId', 

323 nightCol='night', maxDither=1.75, inHex=True, randomSeed=None): 

324 """ 

325 @ MaxDither in degrees 

326 """ 

327 # Instantiate the RandomDither object and set internal variables. 

328 super().__init__(raCol=raCol, decCol=decCol, degrees=degrees, 

329 maxDither=maxDither, inHex=inHex, randomSeed=randomSeed) 

330 self.nightCol = nightCol 

331 self.fieldIdCol = fieldIdCol 

332 # Values required for framework operation: this specifies the data columns required from the database. 

333 self.colsReq = [self.raCol, self.decCol, self.nightCol, self.fieldIdCol] 

334 

335 def _run(self, simData, cols_present=False): 

336 if cols_present: 

337 return simData 

338 # Generate random numbers for dither, using defined seed value if desired. 

339 if not hasattr(self, '_rng'): 

340 if self.randomSeed is not None: 

341 self._rng = np.random.RandomState(self.randomSeed) 

342 else: 

343 self._rng = np.random.RandomState(872453) 

344 

345 # Generate the random dither values, one per night per field. 

346 fields = np.unique(simData[self.fieldIdCol]) 

347 nights = np.unique(simData[self.nightCol]) 

348 self._generateRandomOffsets(len(fields) * len(nights)) 

349 if self.degrees: 

350 ra = np.radians(simData[self.raCol]) 

351 dec = np.radians(simData[self.decCol]) 

352 else: 

353 ra = simData[self.raCol] 

354 dec = simData[self.decCol] 

355 # counter to ensure new random numbers are chosen every time 

356 delta = 0 

357 for fieldid in np.unique(simData[self.fieldIdCol]): 

358 # Identify observations of this field. 

359 match = np.where(simData[self.fieldIdCol] == fieldid)[0] 

360 # Apply dithers, increasing each night. 

361 nights = simData[self.nightCol][match] 

362 vertexIdxs = np.searchsorted(np.unique(nights), nights) 

363 vertexIdxs = vertexIdxs % len(self.xOff) 

364 # ensure that the same xOff/yOff entries are not chosen 

365 delta = delta + len(vertexIdxs) 

366 simData['randomDitherFieldPerNightRa'][match] = (ra[match] + 

367 self.xOff[vertexIdxs] / 

368 np.cos(dec[match])) 

369 simData['randomDitherFieldPerNightDec'][match] = (dec[match] + 

370 self.yOff[vertexIdxs]) 

371 # Wrap into expected range. 

372 simData['randomDitherFieldPerNightRa'], simData['randomDitherFieldPerNightDec'] = \ 

373 wrapRADec(simData['randomDitherFieldPerNightRa'], simData['randomDitherFieldPerNightDec']) 

374 if self.degrees: 

375 for col in self.colsAdded: 

376 simData[col] = np.degrees(simData[col]) 

377 return simData 

378 

379 

380class RandomDitherPerNightStacker(RandomDitherFieldPerVisitStacker): 

381 """ 

382 Randomly dither the RA and Dec pointings up to maxDither degrees from center, 

383 one dither offset per night. 

384 All fields observed within the same night get the same offset. 

385 

386 Parameters 

387 ---------- 

388 raCol : str, optional 

389 The name of the RA column in the data. 

390 Default 'fieldRA'. 

391 decCol : str, optional 

392 The name of the Dec column in the data. 

393 Default 'fieldDec'. 

394 degrees : bool, optional 

395 Flag whether RA/Dec should be treated as (and kept as) degrees. 

396 nightCol : str, optional 

397 The name of the night column in the data. 

398 Default 'night'. 

399 maxDither : float, optional 

400 The radius of the maximum dither offset, in degrees. 

401 Default 1.75 degrees. 

402 inHex : bool, optional 

403 If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle. 

404 If False, offsets can lie anywhere out to the edges of the maxDither circle. 

405 Default True. 

406 randomSeed : int or None, optional 

407 If set, then used as the random seed for the numpy random number generation for the dither offsets. 

408 Default None. 

409 """ 

410 # Values required for framework operation: this specifies the names of the new columns. 

411 colsAdded = ['randomDitherPerNightRa', 'randomDitherPerNightDec'] 

412 

413 def __init__(self, raCol='fieldRA', decCol='fieldDec', degrees=True, nightCol='night', 

414 maxDither=1.75, inHex=True, randomSeed=None): 

415 """ 

416 @ MaxDither in degrees 

417 """ 

418 # Instantiate the RandomDither object and set internal variables. 

419 super().__init__(raCol=raCol, decCol=decCol, degrees=degrees, 

420 maxDither=maxDither, inHex=inHex, randomSeed=randomSeed) 

421 self.nightCol = nightCol 

422 # Values required for framework operation: this specifies the data columns required from the database. 

423 self.colsReq = [self.raCol, self.decCol, self.nightCol] 

424 

425 def _run(self, simData, cols_present=False): 

426 if cols_present: 

427 return simData 

428 # Generate random numbers for dither, using defined seed value if desired. 

429 if not hasattr(self, '_rng'): 

430 if self.randomSeed is not None: 

431 self._rng = np.random.RandomState(self.randomSeed) 

432 else: 

433 self._rng = np.random.RandomState(66334) 

434 

435 # Generate the random dither values, one per night. 

436 nights = np.unique(simData[self.nightCol]) 

437 self._generateRandomOffsets(len(nights)) 

438 if self.degrees: 

439 ra = np.radians(simData[self.raCol]) 

440 dec = np.radians(simData[self.decCol]) 

441 else: 

442 ra = simData[self.raCol] 

443 dec = simData[self.decCol] 

444 # Add to RA and dec values. 

445 for n, x, y in zip(nights, self.xOff, self.yOff): 

446 match = np.where(simData[self.nightCol] == n)[0] 

447 simData['randomDitherPerNightRa'][match] = (ra[match] + 

448 x / np.cos(dec[match])) 

449 simData['randomDitherPerNightDec'][match] = dec[match] + y 

450 # Wrap RA/Dec into expected range. 

451 simData['randomDitherPerNightRa'], simData['randomDitherPerNightDec'] = \ 

452 wrapRADec(simData['randomDitherPerNightRa'], simData['randomDitherPerNightDec']) 

453 if self.degrees: 

454 for col in self.colsAdded: 

455 simData[col] = np.degrees(simData[col]) 

456 return simData 

457 

458 

459class SpiralDitherFieldPerVisitStacker(BaseDitherStacker): 

460 """ 

461 Offset along an equidistant spiral with numPoints, out to a maximum radius of maxDither. 

462 Each visit to a field receives a new, sequential offset. 

463 

464 Parameters 

465 ---------- 

466 raCol : str, optional 

467 The name of the RA column in the data. 

468 Default 'fieldRA'. 

469 decCol : str, optional 

470 The name of the Dec column in the data. 

471 Default 'fieldDec'. 

472 degrees : bool, optional 

473 Flag whether RA/Dec should be treated as (and kept as) degrees. 

474 fieldIdCol : str, optional 

475 The name of the fieldId column in the data. 

476 Used to identify fields which should be identified as the 'same'. 

477 Default 'fieldId'. 

478 numPoints : int, optional 

479 The number of points in the spiral. 

480 Default 60. 

481 maxDither : float, optional 

482 The radius of the maximum dither offset, in degrees. 

483 Default 1.75 degrees. 

484 nCoils : int, optional 

485 The number of coils the spiral should have. 

486 Default 5. 

487 inHex : bool, optional 

488 If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle. 

489 If False, offsets can lie anywhere out to the edges of the maxDither circle. 

490 Default True. 

491 """ 

492 # Values required for framework operation: this specifies the names of the new columns. 

493 colsAdded = ['spiralDitherFieldPerVisitRa', 'spiralDitherFieldPerVisitDec'] 

494 

495 def __init__(self, raCol='fieldRA', decCol='fieldDec', degrees=True, fieldIdCol='fieldId', 

496 numPoints=60, maxDither=1.75, nCoils=5, inHex=True): 

497 """ 

498 @ MaxDither in degrees 

499 """ 

500 super().__init__(raCol=raCol, decCol=decCol, degrees=degrees, maxDither=maxDither, inHex=inHex) 

501 self.fieldIdCol = fieldIdCol 

502 # Convert maxDither from degrees (internal units for ra/dec are radians) 

503 self.numPoints = numPoints 

504 self.nCoils = nCoils 

505 # Values required for framework operation: this specifies the data columns required from the database. 

506 self.colsReq = [self.raCol, self.decCol, self.fieldIdCol] 

507 

508 def _generateSpiralOffsets(self): 

509 # First generate a full archimedean spiral .. 

510 theta = np.arange(0.0001, self.nCoils * np.pi * 2., 0.001) 

511 a = self.maxDither/theta.max() 

512 if self.inHex: 

513 a = 0.85 * a 

514 r = theta * a 

515 # Then pick out equidistant points along the spiral. 

516 arc = a / 2.0 * (theta * np.sqrt(1 + theta**2) + np.log(theta + np.sqrt(1 + theta**2))) 

517 stepsize = arc.max()/float(self.numPoints) 

518 arcpts = np.arange(0, arc.max(), stepsize) 

519 arcpts = arcpts[0:self.numPoints] 

520 rpts = np.zeros(self.numPoints, float) 

521 thetapts = np.zeros(self.numPoints, float) 

522 for i, ap in enumerate(arcpts): 

523 diff = np.abs(arc - ap) 

524 match = np.where(diff == diff.min())[0] 

525 rpts[i] = r[match] 

526 thetapts[i] = theta[match] 

527 # Translate these r/theta points into x/y (ra/dec) offsets. 

528 self.xOff = rpts * np.cos(thetapts) 

529 self.yOff = rpts * np.sin(thetapts) 

530 

531 def _run(self, simData, cols_present=False): 

532 if cols_present: 

533 return simData 

534 # Generate the spiral offset vertices. 

535 self._generateSpiralOffsets() 

536 # Now apply to observations. 

537 if self.degrees: 

538 ra = np.radians(simData[self.raCol]) 

539 dec = np.radians(simData[self.decCol]) 

540 else: 

541 ra = simData[self.raCol] 

542 dec = simData[self.decCol] 

543 for fieldid in np.unique(simData[self.fieldIdCol]): 

544 match = np.where(simData[self.fieldIdCol] == fieldid)[0] 

545 # Apply sequential dithers, increasing with each visit. 

546 vertexIdxs = np.arange(0, len(match), 1) 

547 vertexIdxs = vertexIdxs % self.numPoints 

548 simData['spiralDitherFieldPerVisitRa'][match] = (ra[match] + 

549 self.xOff[vertexIdxs] / 

550 np.cos(dec[match])) 

551 simData['spiralDitherFieldPerVisitDec'][match] = (dec[match] + 

552 self.yOff[vertexIdxs]) 

553 # Wrap into expected range. 

554 simData['spiralDitherFieldPerVisitRa'], simData['spiralDitherFieldPerVisitDec'] = \ 

555 wrapRADec(simData['spiralDitherFieldPerVisitRa'], simData['spiralDitherFieldPerVisitDec']) 

556 if self.degrees: 

557 for col in self.colsAdded: 

558 simData[col] = np.degrees(simData[col]) 

559 return simData 

560 

561 

562class SpiralDitherFieldPerNightStacker(SpiralDitherFieldPerVisitStacker): 

563 """ 

564 Offset along an equidistant spiral with numPoints, out to a maximum radius of maxDither. 

565 Each field steps along a sequential series of offsets, each night it is observed. 

566 

567 Parameters 

568 ---------- 

569 raCol : str, optional 

570 The name of the RA column in the data. 

571 Default 'fieldRA'. 

572 decCol : str, optional 

573 The name of the Dec column in the data. 

574 Default 'fieldDec'. 

575 degrees : bool, optional 

576 Flag whether RA/Dec should be treated as (and kept as) degrees. 

577 fieldIdCol : str, optional 

578 The name of the fieldId column in the data. 

579 Used to identify fields which should be identified as the 'same'. 

580 Default 'fieldId'. 

581 nightCol : str, optional 

582 The name of the night column in the data. 

583 Default 'night'. 

584 numPoints : int, optional 

585 The number of points in the spiral. 

586 Default 60. 

587 maxDither : float, optional 

588 The radius of the maximum dither offset, in degrees. 

589 Default 1.75 degrees. 

590 nCoils : int, optional 

591 The number of coils the spiral should have. 

592 Default 5. 

593 inHex : bool, optional 

594 If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle. 

595 If False, offsets can lie anywhere out to the edges of the maxDither circle. 

596 Default True. 

597 """ 

598 # Values required for framework operation: this specifies the names of the new columns. 

599 colsAdded = ['spiralDitherFieldPerNightRa', 'spiralDitherFieldPerNightDec'] 

600 

601 def __init__(self, raCol='fieldRA', decCol='fieldDec', degrees=True, fieldIdCol='fieldId', 

602 nightCol='night', numPoints=60, maxDither=1.75, nCoils=5, inHex=True): 

603 """ 

604 @ MaxDither in degrees 

605 """ 

606 super().__init__(raCol=raCol, decCol=decCol, degrees=degrees, fieldIdCol=fieldIdCol, 

607 numPoints=numPoints, maxDither=maxDither, nCoils=nCoils, inHex=inHex) 

608 self.nightCol = nightCol 

609 # Values required for framework operation: this specifies the data columns required from the database. 

610 self.colsReq.append(self.nightCol) 

611 

612 def _run(self, simData, cols_present=False): 

613 if cols_present: 

614 return simData 

615 self._generateSpiralOffsets() 

616 if self.degrees: 

617 ra = np.radians(simData[self.raCol]) 

618 dec = np.radians(simData[self.decCol]) 

619 else: 

620 ra = simData[self.raCol] 

621 dec = simData[self.decCol] 

622 for fieldid in np.unique(simData[self.fieldIdCol]): 

623 # Identify observations of this field. 

624 match = np.where(simData[self.fieldIdCol] == fieldid)[0] 

625 # Apply a sequential dither, increasing each night. 

626 nights = simData[self.nightCol][match] 

627 vertexIdxs = np.searchsorted(np.unique(nights), nights) 

628 vertexIdxs = vertexIdxs % self.numPoints 

629 simData['spiralDitherFieldPerNightRa'][match] = (ra[match] + 

630 self.xOff[vertexIdxs] / 

631 np.cos(dec[match])) 

632 simData['spiralDitherFieldPerNightDec'][match] = (dec[match] + 

633 self.yOff[vertexIdxs]) 

634 # Wrap into expected range. 

635 simData['spiralDitherFieldPerNightRa'], simData['spiralDitherFieldPerNightDec'] = \ 

636 wrapRADec(simData['spiralDitherFieldPerNightRa'], simData['spiralDitherFieldPerNightDec']) 

637 if self.degrees: 

638 for col in self.colsAdded: 

639 simData[col] = np.degrees(simData[col]) 

640 return simData 

641 

642 

643class SpiralDitherPerNightStacker(SpiralDitherFieldPerVisitStacker): 

644 """ 

645 Offset along an equidistant spiral with numPoints, out to a maximum radius of maxDither. 

646 All fields observed in the same night receive the same sequential offset, changing per night. 

647 

648 Parameters 

649 ---------- 

650 raCol : str, optional 

651 The name of the RA column in the data. 

652 Default 'fieldRA'. 

653 decCol : str, optional 

654 The name of the Dec column in the data. 

655 Default 'fieldDec'. 

656 degrees : bool, optional 

657 Flag whether RA/Dec should be treated as (and kept as) degrees. 

658 fieldIdCol : str, optional 

659 The name of the fieldId column in the data. 

660 Used to identify fields which should be identified as the 'same'. 

661 Default 'fieldId'. 

662 nightCol : str, optional 

663 The name of the night column in the data. 

664 Default 'night'. 

665 numPoints : int, optional 

666 The number of points in the spiral. 

667 Default 60. 

668 maxDither : float, optional 

669 The radius of the maximum dither offset, in degrees. 

670 Default 1.75 degrees. 

671 nCoils : int, optional 

672 The number of coils the spiral should have. 

673 Default 5. 

674 inHex : bool, optional 

675 If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle. 

676 If False, offsets can lie anywhere out to the edges of the maxDither circle. 

677 Default True. 

678 """ 

679 # Values required for framework operation: this specifies the names of the new columns. 

680 colsAdded = ['spiralDitherPerNightRa', 'spiralDitherPerNightDec'] 

681 

682 def __init__(self, raCol='fieldRA', decCol='fieldDec', degrees=True, fieldIdCol='fieldId', 

683 nightCol='night', numPoints=60, maxDither=1.75, nCoils=5, inHex=True): 

684 """ 

685 @ MaxDither in degrees 

686 """ 

687 super().__init__(raCol=raCol, decCol=decCol, degrees=degrees, fieldIdCol=fieldIdCol, 

688 numPoints=numPoints, maxDither=maxDither, nCoils=nCoils, inHex=inHex) 

689 self.nightCol = nightCol 

690 # Values required for framework operation: this specifies the data columns required from the database. 

691 self.colsReq.append(self.nightCol) 

692 

693 def _run(self, simData, cols_present=False): 

694 if cols_present: 

695 return simData 

696 self._generateSpiralOffsets() 

697 nights = np.unique(simData[self.nightCol]) 

698 if self.degrees: 

699 ra = np.radians(simData[self.raCol]) 

700 dec = np.radians(simData[self.decCol]) 

701 else: 

702 ra = simData[self.raCol] 

703 dec = simData[self.decCol] 

704 # Add to RA and dec values. 

705 vertexIdxs = np.searchsorted(nights, simData[self.nightCol]) 

706 vertexIdxs = vertexIdxs % self.numPoints 

707 simData['spiralDitherPerNightRa'] = (ra + 

708 self.xOff[vertexIdxs] / np.cos(dec)) 

709 simData['spiralDitherPerNightDec'] = dec + self.yOff[vertexIdxs] 

710 # Wrap RA/Dec into expected range. 

711 simData['spiralDitherPerNightRa'], simData['spiralDitherPerNightDec'] = \ 

712 wrapRADec(simData['spiralDitherPerNightRa'], simData['spiralDitherPerNightDec']) 

713 if self.degrees: 

714 for col in self.colsAdded: 

715 simData[col] = np.degrees(simData[col]) 

716 return simData 

717 

718 

719class HexDitherFieldPerVisitStacker(BaseDitherStacker): 

720 """ 

721 Use offsets from the hexagonal grid of 'hexdither', but visit each vertex sequentially. 

722 Sequential offset for each visit. 

723 

724 Parameters 

725 ---------- 

726 raCol : str, optional 

727 The name of the RA column in the data. 

728 Default 'fieldRA'. 

729 decCol : str, optional 

730 The name of the Dec column in the data. 

731 Default 'fieldDec'. 

732 degrees : bool, optional 

733 Flag whether RA/Dec should be treated as (and kept as) degrees. 

734 fieldIdCol : str, optional 

735 The name of the fieldId column in the data. 

736 Used to identify fields which should be identified as the 'same'. 

737 Default 'fieldId'. 

738 maxDither : float, optional 

739 The radius of the maximum dither offset, in degrees. 

740 Default 1.75 degrees. 

741 inHex : bool, optional 

742 If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle. 

743 If False, offsets can lie anywhere out to the edges of the maxDither circle. 

744 Default True. 

745 """ 

746 # Values required for framework operation: this specifies the names of the new columns. 

747 colsAdded = ['hexDitherFieldPerVisitRa', 'hexDitherFieldPerVisitDec'] 

748 

749 def __init__(self, raCol='fieldRA', decCol='fieldDec', degrees=True, 

750 fieldIdCol='fieldId', maxDither=1.75, inHex=True): 

751 """ 

752 @ MaxDither in degrees 

753 """ 

754 super().__init__(raCol=raCol, decCol=decCol, degrees=degrees, maxDither=maxDither, inHex=inHex) 

755 self.fieldIdCol = fieldIdCol 

756 # Values required for framework operation: this specifies the data columns required from the database. 

757 self.colsReq = [self.raCol, self.decCol, self.fieldIdCol] 

758 

759 def _generateHexOffsets(self): 

760 # Set up basics of dither pattern. 

761 dith_level = 4 

762 nrows = 2**dith_level 

763 halfrows = int(nrows / 2.) 

764 # Calculate size of each offset 

765 dith_size_x = self.maxDither * 2.0 / float(nrows) 

766 dith_size_y = np.sqrt(3) * self.maxDither / float(nrows) # sqrt 3 comes from hexagon 

767 if self.inHex: 

768 dith_size_x = 0.95 * dith_size_x 

769 dith_size_y = 0.95 * dith_size_y 

770 # Calculate the row identification number, going from 0 at center 

771 nid_row = np.arange(-halfrows, halfrows + 1, 1) 

772 # and calculate the number of vertices in each row. 

773 vert_in_row = np.arange(-halfrows, halfrows + 1, 1) 

774 # First calculate how many vertices we will create in each row. 

775 total_vert = 0 

776 for i in range(-halfrows, halfrows + 1, 1): 

777 vert_in_row[i] = (nrows+1) - abs(nid_row[i]) 

778 total_vert += vert_in_row[i] 

779 self.numPoints = total_vert 

780 self.xOff = [] 

781 self.yOff = [] 

782 # Calculate offsets over hexagonal grid. 

783 for i in range(0, nrows+1, 1): 

784 for j in range(0, vert_in_row[i], 1): 

785 self.xOff.append(dith_size_x * (j - (vert_in_row[i] - 1) / 2.0)) 

786 self.yOff.append(dith_size_y * nid_row[i]) 

787 self.xOff = np.array(self.xOff) 

788 self.yOff = np.array(self.yOff) 

789 

790 def _run(self, simData, cols_present=False): 

791 if cols_present: 

792 return simData 

793 self._generateHexOffsets() 

794 if self.degrees: 

795 ra = np.radians(simData[self.raCol]) 

796 dec = np.radians(simData[self.decCol]) 

797 else: 

798 ra = simData[self.raCol] 

799 dec = simData[self.decCol] 

800 for fieldid in np.unique(simData[self.fieldIdCol]): 

801 # Identify observations of this field. 

802 match = np.where(simData[self.fieldIdCol] == fieldid)[0] 

803 # Apply sequential dithers, increasing with each visit. 

804 vertexIdxs = np.arange(0, len(match), 1) 

805 vertexIdxs = vertexIdxs % self.numPoints 

806 simData['hexDitherFieldPerVisitRa'][match] = (ra[match] + 

807 self.xOff[vertexIdxs] / 

808 np.cos(dec[match])) 

809 simData['hexDitherFieldPerVisitDec'][match] = dec[match] + self.yOff[vertexIdxs] 

810 # Wrap into expected range. 

811 simData['hexDitherFieldPerVisitRa'], simData['hexDitherFieldPerVisitDec'] = \ 

812 wrapRADec(simData['hexDitherFieldPerVisitRa'], simData['hexDitherFieldPerVisitDec']) 

813 if self.degrees: 

814 for col in self.colsAdded: 

815 simData[col] = np.degrees(simData[col]) 

816 return simData 

817 

818 

819class HexDitherFieldPerNightStacker(HexDitherFieldPerVisitStacker): 

820 """ 

821 Use offsets from the hexagonal grid of 'hexdither', but visit each vertex sequentially. 

822 Sequential offset for each night of visits. 

823 

824 Parameters 

825 ---------- 

826 raCol : str, optional 

827 The name of the RA column in the data. 

828 Default 'fieldRA'. 

829 decCol : str, optional 

830 The name of the Dec column in the data. 

831 Default 'fieldDec'. 

832 degrees : bool, optional 

833 Flag whether RA/Dec should be treated as (and kept as) degrees. 

834 fieldIdCol : str, optional 

835 The name of the fieldId column in the data. 

836 Used to identify fields which should be identified as the 'same'. 

837 Default 'fieldId'. 

838 nightCol : str, optional 

839 The name of the night column in the data. 

840 Default 'night'. 

841 maxDither : float, optional 

842 The radius of the maximum dither offset, in degrees. 

843 Default 1.75 degrees. 

844 inHex : bool, optional 

845 If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle. 

846 If False, offsets can lie anywhere out to the edges of the maxDither circle. 

847 Default True. 

848 """ 

849 # Values required for framework operation: this specifies the names of the new columns. 

850 colsAdded = ['hexDitherFieldPerNightRa', 'hexDitherFieldPerNightDec'] 

851 

852 def __init__(self, raCol='fieldRA', decCol='fieldDec', degrees=True, 

853 fieldIdCol='fieldId', nightCol='night', 

854 maxDither=1.75, inHex=True): 

855 """ 

856 @ MaxDither in degrees 

857 """ 

858 super().__init__(raCol=raCol, decCol=decCol, fieldIdCol=fieldIdCol, 

859 degrees=degrees, maxDither=maxDither, inHex=inHex) 

860 self.nightCol = nightCol 

861 # Values required for framework operation: this specifies the data columns required from the database. 

862 self.colsReq.append(self.nightCol) 

863 

864 def _run(self, simData, cols_present=False): 

865 if cols_present: 

866 return simData 

867 self._generateHexOffsets() 

868 if self.degrees: 

869 ra = np.radians(simData[self.raCol]) 

870 dec = np.radians(simData[self.decCol]) 

871 else: 

872 ra = simData[self.raCol] 

873 dec = simData[self.decCol] 

874 for fieldid in np.unique(simData[self.fieldIdCol]): 

875 # Identify observations of this field. 

876 match = np.where(simData[self.fieldIdCol] == fieldid)[0] 

877 # Apply a sequential dither, increasing each night. 

878 vertexIdxs = np.arange(0, len(match), 1) 

879 nights = simData[self.nightCol][match] 

880 vertexIdxs = np.searchsorted(np.unique(nights), nights) 

881 vertexIdxs = vertexIdxs % self.numPoints 

882 simData['hexDitherFieldPerNightRa'][match] = (ra[match] + 

883 self.xOff[vertexIdxs] / 

884 np.cos(dec[match])) 

885 simData['hexDitherFieldPerNightDec'][match] = (dec[match] + 

886 self.yOff[vertexIdxs]) 

887 # Wrap into expected range. 

888 simData['hexDitherFieldPerNightRa'], simData['hexDitherFieldPerNightDec'] = \ 

889 wrapRADec(simData['hexDitherFieldPerNightRa'], simData['hexDitherFieldPerNightDec']) 

890 if self.degrees: 

891 for col in self.colsAdded: 

892 simData[col] = np.degrees(simData[col]) 

893 return simData 

894 

895 

896class HexDitherPerNightStacker(HexDitherFieldPerVisitStacker): 

897 """ 

898 Use offsets from the hexagonal grid of 'hexdither', but visit each vertex sequentially. 

899 Sequential offset per night for all fields. 

900 

901 Parameters 

902 ---------- 

903 raCol : str, optional 

904 The name of the RA column in the data. 

905 Default 'fieldRA'. 

906 decCol : str, optional 

907 The name of the Dec column in the data. 

908 Default 'fieldDec'. 

909 degrees : bool, optional 

910 Flag whether RA/Dec should be treated as (and kept as) degrees. 

911 fieldIdCol : str, optional 

912 The name of the fieldId column in the data. 

913 Used to identify fields which should be identified as the 'same'. 

914 Default 'fieldId'. 

915 nightCol : str, optional 

916 The name of the night column in the data. 

917 Default 'night'. 

918 maxDither : float, optional 

919 The radius of the maximum dither offset, in degrees. 

920 Default 1.75 degrees. 

921 inHex : bool, optional 

922 If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle. 

923 If False, offsets can lie anywhere out to the edges of the maxDither circle. 

924 Default True. 

925 """ 

926 # Values required for framework operation: this specifies the names of the new columns. 

927 colsAdded = ['hexDitherPerNightRa', 'hexDitherPerNightDec'] 

928 

929 def __init__(self, raCol='fieldRA', decCol='fieldDec', degrees=True, fieldIdCol='fieldId', 

930 nightCol='night', maxDither=1.75, inHex=True): 

931 """ 

932 @ MaxDither in degrees 

933 """ 

934 super().__init__(raCol=raCol, decCol=decCol, degrees=degrees, 

935 fieldIdCol=fieldIdCol, maxDither=maxDither, inHex=inHex) 

936 self.nightCol = nightCol 

937 # Values required for framework operation: this specifies the data columns required from the database. 

938 self.colsReq.append(self.nightCol) 

939 self.addedRA = self.colsAdded[0] 

940 self.addedDec = self.colsAdded[1] 

941 

942 def _run(self, simData, cols_present=False): 

943 if cols_present: 

944 return simData 

945 # Generate the spiral dither values 

946 self._generateHexOffsets() 

947 nights = np.unique(simData[self.nightCol]) 

948 if self.degrees: 

949 ra = np.radians(simData[self.raCol]) 

950 dec = np.radians(simData[self.decCol]) 

951 else: 

952 ra = simData[self.raCol] 

953 dec = simData[self.decCol] 

954 # Add to RA and dec values. 

955 vertexID = 0 

956 for n in nights: 

957 match = np.where(simData[self.nightCol] == n)[0] 

958 vertexID = vertexID % self.numPoints 

959 simData[self.addedRA][match] = (ra[match] + self.xOff[vertexID] / np.cos(dec[match])) 

960 simData[self.addedDec][match] = dec[match] + self.yOff[vertexID] 

961 vertexID += 1 

962 # Wrap RA/Dec into expected range. 

963 simData[self.addedRA], simData[self.addedDec] = \ 

964 wrapRADec(simData[self.addedRA], simData[self.addedDec]) 

965 if self.degrees: 

966 for col in self.colsAdded: 

967 simData[col] = np.degrees(simData[col]) 

968 return simData 

969 

970 

971class RandomRotDitherPerFilterChangeStacker(BaseDitherStacker): 

972 """ 

973 Randomly dither the physical angle of the telescope rotator wrt the mount, 

974 after every filter change. Visits (in between filter changes) that cannot 

975 all be assigned an offset without surpassing the rotator limit are not 

976 dithered. 

977 

978 Parameters 

979 ---------- 

980 rotTelCol : str, optional 

981 The name of the column in the data specifying the physical angle 

982 of the telescope rotator wrt. the mount. 

983 Default: 'rotTelPos'. 

984 filterCol : str, optional 

985 The name of the filter column in the data. 

986 Default: 'filter'. 

987 degrees : boolean, optional 

988 True if angles in the database are in degrees (default). 

989 If True, returned dithered values are in degrees also. 

990 If False, angles assumed to be in radians and returned in radians. 

991 maxDither : float, optional 

992 Abs(maximum) rotational dither, in degrees. The dithers then will be 

993 between -maxDither to maxDither. 

994 Default: 90 degrees. 

995 maxRotAngle : float, optional 

996 Maximum rotator angle possible for the camera (degrees). Default 90 degrees. 

997 minRotAngle : float, optional 

998 Minimum rotator angle possible for the camera (degrees). Default -90 degrees. 

999 randomSeed: int, optional 

1000 If set, then used as the random seed for the numpy random number 

1001 generation for the dither offsets. 

1002 Default: None. 

1003 debug: bool, optinal 

1004 If True, will print intermediate steps and plots histograms of 

1005 rotTelPos for cases when no dither is applied. 

1006 Default: False 

1007 """ 

1008 # Values required for framework operation: this specifies the names of the new columns. 

1009 colsAdded = ['randomDitherPerFilterChangeRotTelPos'] 

1010 

1011 def __init__(self, rotTelCol= 'rotTelPos', filterCol= 'filter', degrees=True, 

1012 maxDither=90., maxRotAngle=90, minRotAngle=-90, randomSeed=None, 

1013 debug=False): 

1014 # Instantiate the RandomDither object and set internal variables. 

1015 self.rotTelCol = rotTelCol 

1016 self.filterCol = filterCol 

1017 self.degrees = degrees 

1018 self.maxDither = maxDither 

1019 self.maxRotAngle = maxRotAngle 

1020 self.minRotAngle = minRotAngle 

1021 self.randomSeed = randomSeed 

1022 # self.units used for plot labels 

1023 if self.degrees: 1023 ↛ 1026line 1023 didn't jump to line 1026, because the condition on line 1023 was never false

1024 self.units = ['deg'] 

1025 else: 

1026 self.units = ['rad'] 

1027 # Convert user-specified values into radians as well. 

1028 self.maxDither = np.radians(self.maxDither) 

1029 self.maxRotAngle = np.radians(self.maxRotAngle) 

1030 self.minRotAngle = np.radians(self.minRotAngle) 

1031 self.debug = debug 

1032 

1033 # Values required for framework operation: specify the data columns required from the database. 

1034 self.colsReq = [self.rotTelCol, self.filterCol] 

1035 

1036 def _run(self, simData, cols_present=False): 

1037 if self.debug: import matplotlib.pyplot as plt 

1038 

1039 # Just go ahead and return if the columns were already in place. 

1040 if cols_present: 

1041 return simData 

1042 

1043 # Generate random numbers for dither, using defined seed value if desired. 

1044 # Note that we must define the random state for np.random, to ensure consistency in the build system. 

1045 if not hasattr(self, '_rng'): 

1046 if self.randomSeed is not None: 

1047 self._rng = np.random.RandomState(self.randomSeed) 

1048 else: 

1049 self._rng = np.random.RandomState(544320) 

1050 

1051 if len(np.where(simData[self.rotTelCol]>self.maxRotAngle)[0]) > 0: 

1052 warnings.warn('Input data does not respect the specified maxRotAngle constraint: ' 

1053 '(Re)Setting maxRotAngle to max value in the input data: %s' 

1054 % max(simData[self.rotTelCol])) 

1055 self.maxRotAngle = max(simData[self.rotTelCol]) 

1056 if len(np.where(simData[self.rotTelCol]<self.minRotAngle)[0]) > 0: 

1057 warnings.warn('Input data does not respect the specified minRotAngle constraint: ' 

1058 '(Re)Setting minRotAngle to min value in the input data: %s' 

1059 % min(simData[self.rotTelCol])) 

1060 self.minRotAngle = min(simData[self.rotTelCol]) 

1061 

1062 # Identify points where the filter changes. 

1063 changeIdxs = np.where(simData[self.filterCol][1:] != simData[self.filterCol][:-1])[0] 

1064 

1065 # Add the random offsets to the RotTelPos values. 

1066 rotDither = self.colsAdded[0] 

1067 

1068 if len(changeIdxs) == 0: 

1069 # There are no filter changes, so nothing to dither. Just use original values. 

1070 simData[rotDither] = simData[self.rotTelCol] 

1071 else: 

1072 # For each filter change, generate a series of random values for the offsets, 

1073 # between +/- self.maxDither. These are potential values for the rotational offset. 

1074 # The offset actually used will be confined to ensure that rotTelPos for all visits in 

1075 # that set of observations (between filter changes) fall within 

1076 # the specified min/maxRotAngle -- without truncating the rotTelPos values. 

1077 

1078 # Generate more offsets than needed - either 2x filter changes or 2500, whichever is bigger. 

1079 # 2500 is an arbitrary number. 

1080 maxNum = max(len(changeIdxs) * 2, 2500) 

1081 

1082 rotOffset = np.zeros(len(simData), float) 

1083 # Some sets of visits will not be assigned dithers: it was too hard to find an offset. 

1084 n_problematic_ones = 0 

1085 

1086 # Loop over the filter change indexes (current filter change, next filter change) to identify 

1087 # sets of visits that should have the same offset. 

1088 for (c, cn) in zip(changeIdxs, changeIdxs[1:]): 

1089 randomOffsets = self._rng.rand(maxNum + 1) * 2.0 * self.maxDither - self.maxDither 

1090 i = 0 

1091 potential_offset = randomOffsets[i] 

1092 # Calculate new rotTelPos values, if we used this offset. 

1093 new_rotTel = simData[self.rotTelCol][c+1:cn+1] + potential_offset 

1094 # Does it work? Do all values fall within minRotAngle / maxRotAngle? 

1095 goodToGo = (new_rotTel >= self.minRotAngle).all() and (new_rotTel <= self.maxRotAngle).all() 

1096 while ((not goodToGo) and (i < maxNum)): 

1097 # break if find a good offset or hit maxNum tries. 

1098 i += 1 

1099 potential_offset = randomOffsets[i] 

1100 new_rotTel = simData[self.rotTelCol][c+1:cn+1] + potential_offset 

1101 goodToGo = (new_rotTel >= self.minRotAngle).all() and \ 

1102 (new_rotTel <= self.maxRotAngle).all() 

1103 

1104 if not goodToGo: # i.e. no good offset was found after maxNum tries 

1105 n_problematic_ones += 1 

1106 rotOffset[c+1:cn+1] = 0. # no dither 

1107 else: 

1108 rotOffset[c+1:cn+1] = randomOffsets[i] # assign the chosen offset 

1109 

1110 # Handle the last set of observations (after the last filter change to the end of the survey). 

1111 randomOffsets = self._rng.rand(maxNum + 1) * 2.0 * self.maxDither - self.maxDither 

1112 i = 0 

1113 potential_offset = randomOffsets[i] 

1114 new_rotTel = simData[self.rotTelCol][changeIdxs[-1]+1:] + potential_offset 

1115 goodToGo = (new_rotTel >= self.minRotAngle).all() and (new_rotTel <= self.maxRotAngle).all() 

1116 while ((not goodToGo) and (i < maxNum)): 

1117 # break if find a good offset or cant (after maxNum tries) 

1118 i += 1 

1119 potential_offset = randomOffsets[i] 

1120 new_rotTel = simData[self.rotTelCol][changeIdxs[-1]+1:] + potential_offset 

1121 goodToGo = (new_rotTel >= self.minRotAngle).all() and \ 

1122 (new_rotTel <= self.maxRotAngle).all() 

1123 

1124 if not goodToGo: # i.e. no good offset was found after maxNum tries 

1125 n_problematic_ones += 1 

1126 rotOffset[c+1:cn+1] = 0. 

1127 else: 

1128 rotOffset[changeIdxs[-1]+1:] = potential_offset 

1129 

1130 # Assign the dithers 

1131 simData[rotDither] = simData[self.rotTelCol] + rotOffset 

1132 

1133 # Final check to make sure things are okay 

1134 goodToGo = (simData[rotDither] >= self.minRotAngle).all() and \ 

1135 (simData[rotDither] <= self.maxRotAngle).all() 

1136 if not goodToGo: 

1137 message = 'Rotational offsets are not working properly:\n' 

1138 message += ' dithered rotTelPos: %s\n' % (simData[rotDither]) 

1139 message += ' minRotAngle: %s ; maxRotAngle: %s' % (self.minRotAngle, self.maxRotAngle) 

1140 raise ValueError(message) 

1141 else: 

1142 return simData