from builtins import zip
from builtins import range
import numpy as np
from .baseStacker import BaseStacker
__all__ = ['wrapRADec', 'wrapRA', 'inHexagon', 'polygonCoords',
'RandomDitherFieldPerVisitStacker', 'RandomDitherFieldPerNightStacker',
'RandomDitherPerNightStacker',
'SpiralDitherFieldPerVisitStacker', 'SpiralDitherFieldPerNightStacker',
'SpiralDitherPerNightStacker',
'HexDitherFieldPerVisitStacker', 'HexDitherFieldPerNightStacker',
'HexDitherPerNightStacker', 'DefaultDitherStacker']
# Stacker naming scheme:
# [Pattern]Dither[Field]Per[Timescale].
# Timescale indicates how often the dither offset is changed.
# The presence of 'Field' indicates that a new offset is chosen per field, on the indicated timescale.
# The absence of 'Field' indicates that all visits within the indicated timescale use the same dither offset.
# Original dither stackers (Random, Spiral, Hex) written by Lynne Jones (lynnej@uw.edu)
# Additional dither stackers written by Humna Awan (humna.awan@rutgers.edu), with addition of
# constraining dither offsets to be within an inscribed hexagon (code modifications for use here by LJ).
[docs]def wrapRADec(ra, dec):
"""
Wrap RA into 0-2pi and Dec into +/0 pi/2.
Parameters
----------
ra : numpy.ndarray
RA in radians
dec : numpy.ndarray
Dec in radians
Returns
-------
numpy.ndarray, numpy.ndarray
Wrapped RA/Dec values, in radians.
"""
# Wrap dec.
low = np.where(dec < -np.pi / 2.0)[0]
dec[low] = -1 * (np.pi + dec[low])
ra[low] = ra[low] - np.pi
high = np.where(dec > np.pi / 2.0)[0]
dec[high] = np.pi - dec[high]
ra[high] = ra[high] - np.pi
# Wrap RA.
ra = ra % (2.0 * np.pi)
return ra, dec
[docs]def wrapRA(ra):
"""
Wrap only RA values into 0-2pi (using mod).
Parameters
----------
ra : numpy.ndarray
RA in radians
Returns
-------
numpy.ndarray
Wrapped RA values, in radians.
"""
ra = ra % (2.0 * np.pi)
return ra
[docs]def inHexagon(xOff, yOff, maxDither):
"""
Identify dither offsets which fall within the inscribed hexagon.
Parameters
----------
xOff : numpy.ndarray
The x values of the dither offsets.
yoff : numpy.ndarray
The y values of the dither offsets.
maxDither : float
The maximum dither offset.
Returns
-------
numpy.ndarray
Indexes of the offsets which are within the hexagon inscribed inside the 'maxDither' radius circle.
"""
# Set up the hexagon limits.
# y = mx + b, 2h is the height.
m = np.sqrt(3.0)
b = m * maxDither
h = m / 2.0 * maxDither
# Identify offsets inside hexagon.
inside = np.where((yOff < m * xOff + b) &
(yOff > m * xOff - b) &
(yOff < -m * xOff + b) &
(yOff > -m * xOff - b) &
(yOff < h) & (yOff > -h))[0]
return inside
[docs]def polygonCoords(nside, radius, rotationAngle):
"""
Find the x,y coords of a polygon.
This is useful for plotting dither points and showing they lie within
a given shape.
Parameters
----------
nside : int
The number of sides of the polygon
radius : float
The radius within which to plot the polygon
rotationAngle : float
The angle to rotate the polygon to.
Returns
-------
[float, float]
List of x/y coordinates of the points describing the polygon.
"""
eachAngle = 2 * np.pi / float(nside)
xCoords = np.zeros(nside, float)
yCoords = np.zeros(nside, float)
for i in range(0, nside):
xCoords[i] = np.sin(eachAngle * i + rotationAngle) * radius
yCoords[i] = np.cos(eachAngle * i + rotationAngle) * radius
return list(zip(xCoords, yCoords))
[docs]class RandomDitherFieldPerVisitStacker(BaseStacker):
"""
Randomly dither the RA and Dec pointings up to maxDither degrees from center,
with a different offset for each field, for each visit.
Parameters
----------
raCol : str, optional
The name of the RA column in the data.
Default 'fieldRA'.
decCol : str, optional
The name of the Dec column in the data.
Default 'fieldDec'.
maxDither : float, optional
The radius of the maximum dither offset, in degrees.
Default 1.75 degrees.
inHex : bool, optional
If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle.
If False, offsets can lie anywhere out to the edges of the maxDither circle.
Default True.
randomSeed : int, optional
If set, then used as the random seed for the numpy random number generation for the dither offsets.
Default None.
"""
def __init__(self, raCol='fieldRA', decCol='fieldDec', maxDither=1.75,
inHex=True, randomSeed=None):
"""
@ MaxDither in degrees
"""
# Instantiate the RandomDither object and set internal variables.
self.raCol = raCol
self.decCol = decCol
# Convert maxDither from degrees (internal units for ra/dec are radians)
self.maxDither = np.radians(maxDither)
self.inHex = inHex
self.randomSeed = randomSeed
# self.units used for plot labels
self.units = ['rad', 'rad']
# Values required for framework operation: this specifies the names of the new columns.
self.colsAdded = ['randomDitherFieldPerVisitRa', 'randomDitherFieldPerVisitDec']
# Values required for framework operation: this specifies the data columns required from the database.
self.colsReq = [self.raCol, self.decCol]
def _generateRandomOffsets(self, noffsets):
xOut = np.array([], float)
yOut = np.array([], float)
maxTries = 100
tries = 0
while (len(xOut) < noffsets) and (tries < maxTries):
dithersRad = np.sqrt(np.random.rand(noffsets * 2)) * self.maxDither
dithersTheta = np.random.rand(noffsets * 2) * np.pi * 2.0
xOff = dithersRad * np.cos(dithersTheta)
yOff = dithersRad * np.sin(dithersTheta)
if self.inHex:
# Constrain dither offsets to be within hexagon.
idx = inHexagon(xOff, yOff, self.maxDither)
xOff = xOff[idx]
yOff = yOff[idx]
xOut = np.concatenate([xOut, xOff])
yOut = np.concatenate([yOut, yOff])
tries += 1
if len(xOut) < noffsets:
raise ValueError('Could not find enough random points within the hexagon in %d tries. '
'Try another random seed?' % (maxTries))
self.xOff = xOut[0:noffsets]
self.yOff = yOut[0:noffsets]
def _run(self, simData):
# Generate random numbers for dither, using defined seed value if desired.
if self.randomSeed is not None:
np.random.seed(self.randomSeed)
# Generate the random dither values.
noffsets = len(simData[self.raCol])
self._generateRandomOffsets(noffsets)
# Add to RA and dec values.
ra = np.radians(simData[self.raCol])
dec = np.radians(simData[self.decCol])
simData['randomDitherFieldPerVisitRa'] = (ra + self.xOff / np.cos(dec))
simData['randomDitherFieldPerVisitDec'] = dec + self.yOff
# Wrap back into expected range.
simData['randomDitherFieldPerVisitRa'], simData['randomDitherFieldPerVisitDec'] = \
wrapRADec(simData['randomDitherFieldPerVisitRa'], simData['randomDitherFieldPerVisitDec'])
# Convert to degrees
for col in self.colsAdded:
simData[col] = np.degrees(simData[col])
return simData
[docs]class RandomDitherFieldPerNightStacker(RandomDitherFieldPerVisitStacker):
"""
Randomly dither the RA and Dec pointings up to maxDither degrees from center,
one dither offset per new night of observation of a field.
e.g. visits within the same night, to the same field, have the same offset.
Parameters
----------
raCol : str, optional
The name of the RA column in the data.
Default 'fieldRA'.
decCol : str, optional
The name of the Dec column in the data.
Default 'fieldDec'.
fieldIdCol : str, optional
The name of the fieldId column in the data.
Used to identify fields which should be identified as the 'same'.
Default 'fieldId'.
nightCol : str, optional
The name of the night column in the data.
Default 'night'.
maxDither : float, optional
The radius of the maximum dither offset, in degrees.
Default 1.75 degrees.
inHex : bool, optional
If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle.
If False, offsets can lie anywhere out to the edges of the maxDither circle.
Default True.
randomSeed : int, optional
If set, then used as the random seed for the numpy random number generation for the dither offsets.
Default None.
"""
def __init__(self, raCol='fieldRA', decCol='fieldDec', fieldIdCol='fieldId', nightCol='night',
maxDither=1.75, inHex=True, randomSeed=None):
"""
@ MaxDither in degrees
"""
# Instantiate the RandomDither object and set internal variables.
super(RandomDitherFieldPerNightStacker, self).__init__(raCol=raCol, decCol=decCol,
maxDither=maxDither, inHex=inHex,
randomSeed=randomSeed)
self.nightCol = nightCol
self.fieldIdCol = fieldIdCol
# Values required for framework operation: this specifies the names of the new columns.
self.colsAdded = ['randomDitherFieldPerNightRa', 'randomDitherFieldPerNightDec']
# Values required for framework operation: this specifies the data columns required from the database.
self.colsReq = [self.raCol, self.decCol, self.nightCol, self.fieldIdCol]
def _run(self, simData):
# Generate random numbers for dither, using defined seed value if desired.
if self.randomSeed is not None:
np.random.seed(self.randomSeed)
# Generate the random dither values, one per night per field.
fields = np.unique(simData[self.fieldIdCol])
nights = np.unique(simData[self.nightCol])
self._generateRandomOffsets(len(fields) * len(nights))
ra = np.radians(simData[self.raCol])
dec = np.radians(simData[self.decCol])
# counter to ensure new random numbers are chosen every time
delta = 0
for fieldid in np.unique(simData[self.fieldIdCol]):
# Identify observations of this field.
match = np.where(simData[self.fieldIdCol] == fieldid)[0]
# Apply dithers, increasing each night.
nights = simData[self.nightCol][match]
vertexIdxs = np.searchsorted(np.unique(nights), nights)
vertexIdxs = vertexIdxs % len(self.xOff)
# ensure that the same xOff/yOff entries are not chosen
delta = delta + len(vertexIdxs)
simData['randomDitherFieldPerNightRa'][match] = (ra[match] +
self.xOff[vertexIdxs] /
np.cos(dec[match]))
simData['randomDitherFieldPerNightDec'][match] = (dec[match] +
self.yOff[vertexIdxs])
# Wrap into expected range.
simData['randomDitherFieldPerNightRa'], simData['randomDitherFieldPerNightDec'] = \
wrapRADec(simData['randomDitherFieldPerNightRa'], simData['randomDitherFieldPerNightDec'])
for col in self.colsAdded:
simData[col] = np.degrees(simData[col])
return simData
[docs]class RandomDitherPerNightStacker(RandomDitherFieldPerVisitStacker):
"""
Randomly dither the RA and Dec pointings up to maxDither degrees from center,
one dither offset per night.
All fields observed within the same night get the same offset.
Parameters
----------
raCol : str, optional
The name of the RA column in the data.
Default 'fieldRA'.
decCol : str, optional
The name of the Dec column in the data.
Default 'fieldDec'.
nightCol : str, optional
The name of the night column in the data.
Default 'night'.
maxDither : float, optional
The radius of the maximum dither offset, in degrees.
Default 1.75 degrees.
inHex : bool, optional
If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle.
If False, offsets can lie anywhere out to the edges of the maxDither circle.
Default True.
randomSeed : int, optional
If set, then used as the random seed for the numpy random number generation for the dither offsets.
Default None.
"""
def __init__(self, raCol='fieldRA', decCol='fieldDec', nightCol='night',
maxDither=1.75, inHex=True, randomSeed=None):
"""
@ MaxDither in degrees
"""
# Instantiate the RandomDither object and set internal variables.
super(RandomDitherPerNightStacker, self).__init__(raCol=raCol, decCol=decCol,
maxDither=maxDither, inHex=inHex,
randomSeed=randomSeed)
self.nightCol = nightCol
# Values required for framework operation: this specifies the names of the new columns.
self.colsAdded = ['randomDitherPerNightRa', 'randomDitherPerNightDec']
# Values required for framework operation: this specifies the data columns required from the database.
self.colsReq = [self.raCol, self.decCol, self.nightCol]
def _run(self, simData):
# Generate random numbers for dither, using defined seed value if desired.
if self.randomSeed is not None:
np.random.seed(self.randomSeed)
# Generate the random dither values, one per night.
nights = np.unique(simData[self.nightCol])
self._generateRandomOffsets(len(nights))
ra = np.radians(simData[self.raCol])
dec = np.radians(simData[self.decCol])
# Add to RA and dec values.
for n, x, y in zip(nights, self.xOff, self.yOff):
match = np.where(simData[self.nightCol] == n)[0]
simData['randomDitherPerNightRa'][match] = (ra[match] +
x / np.cos(dec[match]))
simData['randomDitherPerNightDec'][match] = dec[match] + y
# Wrap RA/Dec into expected range.
simData['randomDitherPerNightRa'], simData['randomDitherPerNightDec'] = \
wrapRADec(simData['randomDitherPerNightRa'], simData['randomDitherPerNightDec'])
for col in self.colsAdded:
simData[col] = np.degrees(simData[col])
return simData
[docs]class SpiralDitherFieldPerVisitStacker(BaseStacker):
"""
Offset along an equidistant spiral with numPoints, out to a maximum radius of maxDither.
Each visit to a field receives a new, sequential offset.
Parameters
----------
raCol : str, optional
The name of the RA column in the data.
Default 'fieldRA'.
decCol : str, optional
The name of the Dec column in the data.
Default 'fieldDec'.
fieldIdCol : str, optional
The name of the fieldId column in the data.
Used to identify fields which should be identified as the 'same'.
Default 'fieldId'.
numPoints : int, optional
The number of points in the spiral.
Default 60.
maxDither : float, optional
The radius of the maximum dither offset, in degrees.
Default 1.75 degrees.
nCoils : int, optional
The number of coils the spiral should have.
Default 5.
inHex : bool, optional
If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle.
If False, offsets can lie anywhere out to the edges of the maxDither circle.
Default True.
"""
def __init__(self, raCol='fieldRA', decCol='fieldDec', fieldIdCol='fieldId',
numPoints=60, maxDither=1.75, nCoils=5, inHex=True):
"""
@ MaxDither in degrees
"""
self.raCol = raCol
self.decCol = decCol
self.fieldIdCol = fieldIdCol
# Convert maxDither from degrees (internal units for ra/dec are radians)
self.numPoints = numPoints
self.nCoils = nCoils
self.maxDither = np.radians(maxDither)
self.inHex = inHex
# self.units used for plot labels
self.units = ['rad', 'rad']
# Values required for framework operation: this specifies the names of the new columns.
self.colsAdded = ['spiralDitherFieldPerVisitRa', 'spiralDitherFieldPerVisitDec']
# Values required for framework operation: this specifies the data columns required from the database.
self.colsReq = [self.raCol, self.decCol, self.fieldIdCol]
def _generateSpiralOffsets(self):
# First generate a full archimedean spiral ..
theta = np.arange(0.0001, self.nCoils * np.pi * 2., 0.001)
a = self.maxDither/theta.max()
if self.inHex:
a = 0.85 * a
r = theta * a
# Then pick out equidistant points along the spiral.
arc = a / 2.0 * (theta * np.sqrt(1 + theta**2) + np.log(theta + np.sqrt(1 + theta**2)))
stepsize = arc.max()/float(self.numPoints)
arcpts = np.arange(0, arc.max(), stepsize)
arcpts = arcpts[0:self.numPoints]
rpts = np.zeros(self.numPoints, float)
thetapts = np.zeros(self.numPoints, float)
for i, ap in enumerate(arcpts):
diff = np.abs(arc - ap)
match = np.where(diff == diff.min())[0]
rpts[i] = r[match]
thetapts[i] = theta[match]
# Translate these r/theta points into x/y (ra/dec) offsets.
self.xOff = rpts * np.cos(thetapts)
self.yOff = rpts * np.sin(thetapts)
def _run(self, simData):
# Generate the spiral offset vertices.
self._generateSpiralOffsets()
# Now apply to observations.
ra = np.radians(simData[self.raCol])
dec = np.radians(simData[self.decCol])
for fieldid in np.unique(simData[self.fieldIdCol]):
match = np.where(simData[self.fieldIdCol] == fieldid)[0]
# Apply sequential dithers, increasing with each visit.
vertexIdxs = np.arange(0, len(match), 1)
vertexIdxs = vertexIdxs % self.numPoints
simData['spiralDitherFieldPerVisitRa'][match] = (ra[match] +
self.xOff[vertexIdxs] /
np.cos(dec[match]))
simData['spiralDitherFieldPerVisitDec'][match] = (dec[match] +
self.yOff[vertexIdxs])
# Wrap into expected range.
simData['spiralDitherFieldPerVisitRa'], simData['spiralDitherFieldPerVisitDec'] = \
wrapRADec(simData['spiralDitherFieldPerVisitRa'], simData['spiralDitherFieldPerVisitDec'])
for col in self.colsAdded:
simData[col] = np.degrees(simData[col])
return simData
[docs]class SpiralDitherFieldPerNightStacker(SpiralDitherFieldPerVisitStacker):
"""
Offset along an equidistant spiral with numPoints, out to a maximum radius of maxDither.
Each field steps along a sequential series of offsets, each night it is observed.
Parameters
----------
raCol : str, optional
The name of the RA column in the data.
Default 'fieldRA'.
decCol : str, optional
The name of the Dec column in the data.
Default 'fieldDec'.
fieldIdCol : str, optional
The name of the fieldId column in the data.
Used to identify fields which should be identified as the 'same'.
Default 'fieldId'.
nightCol : str, optional
The name of the night column in the data.
Default 'night'.
numPoints : int, optional
The number of points in the spiral.
Default 60.
maxDither : float, optional
The radius of the maximum dither offset, in degrees.
Default 1.75 degrees.
nCoils : int, optional
The number of coils the spiral should have.
Default 5.
inHex : bool, optional
If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle.
If False, offsets can lie anywhere out to the edges of the maxDither circle.
Default True.
"""
def __init__(self, raCol='fieldRA', decCol='fieldDec', fieldIdCol='fieldId', nightCol='night',
numPoints=60, maxDither=1.75, nCoils=5, inHex=True):
"""
@ MaxDither in degrees
"""
super(SpiralDitherFieldPerNightStacker, self).__init__(raCol=raCol, decCol=decCol,
fieldIdCol=fieldIdCol,
numPoints=numPoints, maxDither=maxDither,
nCoils=nCoils, inHex=inHex)
self.nightCol = nightCol
# Values required for framework operation: this specifies the names of the new columns.
self.colsAdded = ['spiralDitherFieldPerNightRa', 'spiralDitherFieldPerNightDec']
# Values required for framework operation: this specifies the data columns required from the database.
self.colsReq.append(self.nightCol)
def _run(self, simData):
self._generateSpiralOffsets()
ra = np.radians(simData[self.raCol])
dec = np.radians(simData[self.decCol])
for fieldid in np.unique(simData[self.fieldIdCol]):
# Identify observations of this field.
match = np.where(simData[self.fieldIdCol] == fieldid)[0]
# Apply a sequential dither, increasing each night.
nights = simData[self.nightCol][match]
vertexIdxs = np.searchsorted(np.unique(nights), nights)
vertexIdxs = vertexIdxs % self.numPoints
simData['spiralDitherFieldPerNightRa'][match] = (ra[match] +
self.xOff[vertexIdxs] /
np.cos(dec[match]))
simData['spiralDitherFieldPerNightDec'][match] = (dec[match] +
self.yOff[vertexIdxs])
# Wrap into expected range.
simData['spiralDitherFieldPerNightRa'], simData['spiralDitherFieldPerNightDec'] = \
wrapRADec(simData['spiralDitherFieldPerNightRa'], simData['spiralDitherFieldPerNightDec'])
for col in self.colsAdded:
simData[col] = np.degrees(simData[col])
return simData
[docs]class SpiralDitherPerNightStacker(SpiralDitherFieldPerVisitStacker):
"""
Offset along an equidistant spiral with numPoints, out to a maximum radius of maxDither.
All fields observed in the same night receive the same sequential offset, changing per night.
Parameters
----------
raCol : str, optional
The name of the RA column in the data.
Default 'fieldRA'.
decCol : str, optional
The name of the Dec column in the data.
Default 'fieldDec'.
fieldIdCol : str, optional
The name of the fieldId column in the data.
Used to identify fields which should be identified as the 'same'.
Default 'fieldId'.
nightCol : str, optional
The name of the night column in the data.
Default 'night'.
numPoints : int, optional
The number of points in the spiral.
Default 60.
maxDither : float, optional
The radius of the maximum dither offset, in degrees.
Default 1.75 degrees.
nCoils : int, optional
The number of coils the spiral should have.
Default 5.
inHex : bool, optional
If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle.
If False, offsets can lie anywhere out to the edges of the maxDither circle.
Default True.
"""
def __init__(self, raCol='fieldRA', decCol='fieldDec', fieldIdCol='fieldId', nightCol='night',
numPoints=60, maxDither=1.75, nCoils=5, inHex=True):
"""
@ MaxDither in degrees
"""
super(SpiralDitherPerNightStacker, self).__init__(raCol=raCol, decCol=decCol, fieldIdCol=fieldIdCol,
numPoints=numPoints, maxDither=maxDither,
nCoils=nCoils, inHex=inHex)
self.nightCol = nightCol
# Values required for framework operation: this specifies the names of the new columns.
self.colsAdded = ['spiralDitherPerNightRa', 'spiralDitherPerNightDec']
# Values required for framework operation: this specifies the data columns required from the database.
self.colsReq.append(self.nightCol)
def _run(self, simData):
self._generateSpiralOffsets()
nights = np.unique(simData[self.nightCol])
ra = np.radians(simData[self.raCol])
dec = np.radians(simData[self.decCol])
# Add to RA and dec values.
vertexIdxs = np.searchsorted(nights, simData[self.nightCol])
vertexIdxs = vertexIdxs % self.numPoints
simData['spiralDitherPerNightRa'] = (ra +
self.xOff[vertexIdxs] / np.cos(dec))
simData['spiralDitherPerNightDec'] = dec + self.yOff[vertexIdxs]
# Wrap RA/Dec into expected range.
simData['spiralDitherPerNightRa'], simData['spiralDitherPerNightDec'] = \
wrapRADec(simData['spiralDitherPerNightRa'], simData['spiralDitherPerNightDec'])
for col in self.colsAdded:
simData[col] = np.degrees(simData[col])
return simData
[docs]class HexDitherFieldPerVisitStacker(BaseStacker):
"""
Use offsets from the hexagonal grid of 'hexdither', but visit each vertex sequentially.
Sequential offset for each visit.
Parameters
----------
raCol : str, optional
The name of the RA column in the data.
Default 'fieldRA'.
decCol : str, optional
The name of the Dec column in the data.
Default 'fieldDec'.
fieldIdCol : str, optional
The name of the fieldId column in the data.
Used to identify fields which should be identified as the 'same'.
Default 'fieldId'.
maxDither : float, optional
The radius of the maximum dither offset, in degrees.
Default 1.75 degrees.
inHex : bool, optional
If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle.
If False, offsets can lie anywhere out to the edges of the maxDither circle.
Default True.
"""
def __init__(self, raCol='fieldRA', decCol='fieldDec', fieldIdCol='fieldId', maxDither=1.75, inHex=True):
"""
@ MaxDither in degrees
"""
self.raCol = raCol
self.decCol = decCol
self.fieldIdCol = fieldIdCol
self.maxDither = np.radians(maxDither)
self.inHex = inHex
# self.units used for plot labels
self.units = ['rad', 'rad']
# Values required for framework operation: this specifies the names of the new columns.
self.colsAdded = ['hexDitherFieldPerVisitRa', 'hexDitherFieldPerVisitDec']
# Values required for framework operation: this specifies the data columns required from the database.
self.colsReq = [self.raCol, self.decCol, self.fieldIdCol]
def _generateHexOffsets(self):
# Set up basics of dither pattern.
dith_level = 4
nrows = 2**dith_level
halfrows = int(nrows / 2.)
# Calculate size of each offset
dith_size_x = self.maxDither * 2.0 / float(nrows)
dith_size_y = np.sqrt(3) * self.maxDither / float(nrows) # sqrt 3 comes from hexagon
if self.inHex:
dith_size_x = 0.95 * dith_size_x
dith_size_y = 0.95 * dith_size_y
# Calculate the row identification number, going from 0 at center
nid_row = np.arange(-halfrows, halfrows + 1, 1)
# and calculate the number of vertices in each row.
vert_in_row = np.arange(-halfrows, halfrows + 1, 1)
# First calculate how many vertices we will create in each row.
total_vert = 0
for i in range(-halfrows, halfrows + 1, 1):
vert_in_row[i] = (nrows+1) - abs(nid_row[i])
total_vert += vert_in_row[i]
self.numPoints = total_vert
self.xOff = []
self.yOff = []
# Calculate offsets over hexagonal grid.
for i in range(0, nrows+1, 1):
for j in range(0, vert_in_row[i], 1):
self.xOff.append(dith_size_x * (j - (vert_in_row[i] - 1) / 2.0))
self.yOff.append(dith_size_y * nid_row[i])
self.xOff = np.array(self.xOff)
self.yOff = np.array(self.yOff)
def _run(self, simData):
self._generateHexOffsets()
ra = np.radians(simData[self.raCol])
dec = np.radians(simData[self.decCol])
for fieldid in np.unique(simData[self.fieldIdCol]):
# Identify observations of this field.
match = np.where(simData[self.fieldIdCol] == fieldid)[0]
# Apply sequential dithers, increasing with each visit.
vertexIdxs = np.arange(0, len(match), 1)
vertexIdxs = vertexIdxs % self.numPoints
simData['hexDitherFieldPerVisitRa'][match] = (ra[match] +
self.xOff[vertexIdxs] /
np.cos(dec[match]))
simData['hexDitherFieldPerVisitDec'][match] = dec[match] + self.yOff[vertexIdxs]
# Wrap into expected range.
simData['hexDitherFieldPerVisitRa'], simData['hexDitherFieldPerVisitDec'] = \
wrapRADec(simData['hexDitherFieldPerVisitRa'], simData['hexDitherFieldPerVisitDec'])
for col in self.colsAdded:
simData[col] = np.degrees(simData[col])
return simData
[docs]class HexDitherFieldPerNightStacker(HexDitherFieldPerVisitStacker):
"""
Use offsets from the hexagonal grid of 'hexdither', but visit each vertex sequentially.
Sequential offset for each night of visits.
Parameters
----------
raCol : str, optional
The name of the RA column in the data.
Default 'fieldRA'.
decCol : str, optional
The name of the Dec column in the data.
Default 'fieldDec'.
fieldIdCol : str, optional
The name of the fieldId column in the data.
Used to identify fields which should be identified as the 'same'.
Default 'fieldId'.
nightCol : str, optional
The name of the night column in the data.
Default 'night'.
maxDither : float, optional
The radius of the maximum dither offset, in degrees.
Default 1.75 degrees.
inHex : bool, optional
If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle.
If False, offsets can lie anywhere out to the edges of the maxDither circle.
Default True.
"""
def __init__(self, raCol='fieldRA', decCol='fieldDec', fieldIdCol='fieldIdCol', nightCol='night',
maxDither=1.75, inHex=True):
"""
@ MaxDither in degrees
"""
super(HexDitherFieldPerNightStacker, self).__init__(raCol=raCol, decCol=decCol,
maxDither=maxDither, inHex=inHex)
self.nightCol = nightCol
# Values required for framework operation: this specifies the names of the new columns.
self.colsAdded = ['hexDitherFieldPerNightRa', 'hexDitherFieldPerNightDec']
# Values required for framework operation: this specifies the data columns required from the database.
self.colsReq.append(self.nightCol)
def _run(self, simData):
self._generateHexOffsets()
ra = np.radians(simData[self.raCol])
dec = np.radians(simData[self.decCol])
for fieldid in np.unique(simData[self.fieldIdCol]):
# Identify observations of this field.
match = np.where(simData[self.fieldIdCol] == fieldid)[0]
# Apply a sequential dither, increasing each night.
vertexIdxs = np.arange(0, len(match), 1)
nights = simData[self.nightCol][match]
vertexIdxs = np.searchsorted(np.unique(nights), nights)
vertexIdxs = vertexIdxs % self.numPoints
simData['hexDitherFieldPerNightRa'][match] = (ra[match] +
self.xOff[vertexIdxs] /
np.cos(dec[match]))
simData['hexDitherFieldPerNightDec'][match] = (dec[match] +
self.yOff[vertexIdxs])
# Wrap into expected range.
simData['hexDitherFieldPerNightRa'], simData['hexDitherFieldPerNightDec'] = \
wrapRADec(simData['hexDitherFieldPerNightRa'], simData['hexDitherFieldPerNightDec'])
for col in self.colsAdded:
simData[col] = np.degrees(simData[col])
return simData
[docs]class HexDitherPerNightStacker(HexDitherFieldPerVisitStacker):
"""
Use offsets from the hexagonal grid of 'hexdither', but visit each vertex sequentially.
Sequential offset per night for all fields.
Parameters
----------
raCol : str, optional
The name of the RA column in the data.
Default 'fieldRA'.
decCol : str, optional
The name of the Dec column in the data.
Default 'fieldDec'.
fieldIdCol : str, optional
The name of the fieldId column in the data.
Used to identify fields which should be identified as the 'same'.
Default 'fieldId'.
nightCol : str, optional
The name of the night column in the data.
Default 'night'.
maxDither : float, optional
The radius of the maximum dither offset, in degrees.
Default 1.75 degrees.
inHex : bool, optional
If True, offsets are constrained to lie within a hexagon inscribed within the maxDither circle.
If False, offsets can lie anywhere out to the edges of the maxDither circle.
Default True.
"""
def __init__(self, raCol='fieldRA', decCol='fieldDec', fieldIdCol='fieldId',
nightCol='night', maxDither=1.75, inHex=True):
"""
@ MaxDither in degrees
"""
super(HexDitherPerNightStacker, self).__init__(raCol=raCol, decCol=decCol, fieldIdCol=fieldIdCol,
maxDither=maxDither, inHex=inHex)
self.nightCol = nightCol
# Values required for framework operation: this specifies the data columns required from the database.
self.colsReq.append(self.nightCol)
self.addedRA = 'hexDitherPerNightRa'
self.addedDec = 'hexDitherPerNightDec'
# Values required for framework operation: this specifies the names of the new columns.
self.colsAdded = [self.addedRA, self.addedDec]
def _run(self, simData):
# Generate the spiral dither values
self._generateHexOffsets()
nights = np.unique(simData[self.nightCol])
ra = np.radians(simData[self.raCol])
dec = np.radians(simData[self.decCol])
# Add to RA and dec values.
vertexID = 0
for n in nights:
match = np.where(simData[self.nightCol] == n)[0]
vertexID = vertexID % self.numPoints
simData[self.addedRA][match] = (ra[match] + self.xOff[vertexID] / np.cos(dec[match]))
simData[self.addedDec][match] = dec[match] + self.yOff[vertexID]
vertexID += 1
# Wrap RA/Dec into expected range.
simData[self.addedRA], simData[self.addedDec] = \
wrapRADec(simData[self.addedRA], simData[self.addedDec])
for col in self.colsAdded:
simData[col] = np.degrees(simData[col])
return simData
[docs]class DefaultDitherStacker(HexDitherPerNightStacker):
"""
Make a default dither pattern to stack on ditheredRA and ditheredDec
"""
def __init__(self, raCol='fieldRA', decCol='fieldDec', fieldIdCol='fieldId',
nightCol='night', maxDither=1.75, inHex=True):
super(DefaultDitherStacker, self).__init__(raCol=raCol, decCol=decCol, fieldIdCol=fieldIdCol,
maxDither=maxDither, inHex=inHex)
self.addedRA = 'ditheredRA'
self.addedDec = 'ditheredDec'
# Values required for framework operation: this specifies the names of the new columns.
self.colsAdded = [self.addedRA, self.addedDec]