Coverage for python/lsst/sims/maf/stackers/ditherStackers.py : 18%

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
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']
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.
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).
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
39def wrapRADec(ra, dec):
40 """
41 Wrap RA into 0-2pi and Dec into +/0 pi/2.
43 Parameters
44 ----------
45 ra : numpy.ndarray
46 RA in radians
47 dec : numpy.ndarray
48 Dec in radians
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
67def wrapRA(ra):
68 """
69 Wrap only RA values into 0-2pi (using mod).
71 Parameters
72 ----------
73 ra : numpy.ndarray
74 RA in radians
76 Returns
77 -------
78 numpy.ndarray
79 Wrapped RA values, in radians.
80 """
81 ra = ra % (2.0 * np.pi)
82 return ra
85def inHexagon(xOff, yOff, maxDither):
86 """
87 Identify dither offsets which fall within the inscribed hexagon.
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.
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
117def polygonCoords(nside, radius, rotationAngle):
118 """
119 Find the x,y coords of a polygon.
121 This is useful for plotting dither points and showing they lie within
122 a given shape.
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.
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))
147class BaseDitherStacker(BaseStacker):
148 """Base class for dither stackers.
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.
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 = []
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]
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.
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']
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
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]
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)
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
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.
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']
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]
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)
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
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.
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']
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]
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)
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
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.
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']
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]
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)
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
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.
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']
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)
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
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.
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']
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)
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
719class HexDitherFieldPerVisitStacker(BaseDitherStacker):
720 """
721 Use offsets from the hexagonal grid of 'hexdither', but visit each vertex sequentially.
722 Sequential offset for each visit.
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']
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]
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)
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
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.
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']
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)
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
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.
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']
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]
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
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.
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']
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
1033 # Values required for framework operation: specify the data columns required from the database.
1034 self.colsReq = [self.rotTelCol, self.filterCol]
1036 def _run(self, simData, cols_present=False):
1037 if self.debug: import matplotlib.pyplot as plt
1039 # Just go ahead and return if the columns were already in place.
1040 if cols_present:
1041 return simData
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)
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])
1062 # Identify points where the filter changes.
1063 changeIdxs = np.where(simData[self.filterCol][1:] != simData[self.filterCol][:-1])[0]
1065 # Add the random offsets to the RotTelPos values.
1066 rotDither = self.colsAdded[0]
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.
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)
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
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()
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
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()
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
1130 # Assign the dithers
1131 simData[rotDither] = simData[self.rotTelCol] + rotOffset
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