def plot_spectrum(self): """One plot showing the flux of the spectrum for this lenslet. Plot shows Flux (in counts) vs. Wavelength. ``%(Partials)s/Instrument-%(num)04d-Flux%(ext)s`` **Variables which are used**: :var tfl: flux of each pixel in counts :var twl: wavelength of each pixel in meters """ assert self.traced self.log.debug(npArrayInfo(self.twl*1e6,"Wavlength")) self.log.debug(npArrayInfo(self.tfl,"Flux")) plt.clf() plt.semilogy(self.twl*1e6,self.tfl,"b.") xmin,xmax,ymin,ymax = plt.axis() if ymin < 1e-4: ymin = 1e-4 plt.axis((xmin,xmax,ymin,ymax)) plt.title("Generated, Fluxed Spectra (%d)" % self.num) plt.xlabel("Wavelength ($\mu m$)") plt.ylabel("Flux (Electrons)") plt.savefig("%(Partials)s/Instrument-%(num)04d-Flux%(ext)s" % dict(num=self.num, ext=self.config["Plots"]["format"],**self.config["Dirs"])) plt.clf()
def plot_trace(self): """Two plots showing the trace data for this lenslet. 1. Delta Lambda per Pixel vs. Wavelenth ``%(Partials)s/Instrument-%(num)04d-DeltaWL%(ext)s`` 2. Sampling Resolution vs. Wavelength ``%(Partials)s/Instrument-%(num)04d-Resolution%(ext)s`` **Variables which are used**: :var tfl: flux of each pixel in counts :var twl: wavelength of each pixel in meters :var tdw: delta wavelength covered by each pixel in meters :var trs: sampling resolution of each pixel """ assert self.traced plt.clf() plt.plot(self.twl*1e6,self.tdw*1e6,"g.") plt.title("$\Delta\lambda$ for each pixel (%d)" % self.num) plt.xlabel("Wavelength ($\mu m$)") plt.ylabel("$\Delta\lambda$ per pixel") plt.savefig("%(Partials)s/Instrument-%(num)04d-DeltaWL%(ext)s" % dict(num=self.num, ext=self.config["Plots"]["format"],**self.config["Dirs"])) plt.clf() self.log.debug(npArrayInfo(self.trs,"Trace RS")) plt.semilogy(self.twl*1e6,self.trs,"g.") plt.title("$R = \\frac{\lambda}{\Delta\lambda}$ for each pixel (%d)" % self.num) plt.xlabel("Wavelength ($\mu m$)") plt.ylabel("Resolution $R = \\frac{\lambda}{\Delta\lambda}$ per pixel") plt.savefig("%(Partials)s/Instrument-%(num)04d-Resolution%(ext)s" % dict(num=self.num, ext=self.config["Plots"]["format"],**self.config["Dirs"])) plt.clf()
def place_trace(self,get_conv): """Place the trace on the subimage. First a blank image is created using the ``subshape`` variable as a template. Then, iterating through each point, we get the convolved PSF and telescope image for that point (called the "convolution"). The convolution can vary by wavelenght, and can have other variables which are sytem dependent. The convolution is multiplied by the flux value for that point. The corner of the convolution is then calculated (the top-left and bottom right corners are actually calculated) so that the image can be insterted as a 'flattened' array. Each image is inserted by addition, adding on to the image already in place. **Variables which are used**: :var txs: x-subimage-indicies of each illuminated oversampled pixel in o-px :var tys: y-subimage-indicies of each illuminated oversampled pixel in o-px :var tfl: flux of each pixel in counts :var twl: wavelength of each pixel in meters :var subshape: shape of subimage to contain spectrum :var subcorner: corner of subimage to contain spectrum in px **Variables which are set**: :var frame: A frame labeled "Raw Spectrum" with the oversampled spectrum. """ img = np.zeros(self.subshape) for x,y,wl,flux in zip(self.txs,self.tys,self.twl,self.tfl): if self.config["Instrument"]["Tel"]["ellipse"]: a = self.fa(wl) b = self.fb(wl) rot = self.falpha(wl) conv = get_conv(wl,a,b,rot) else: conv = get_conv(wl) tiny_image = conv * flux tl_corner = [ x - tiny_image.shape[0]/2.0, y - tiny_image.shape[0]/2.0 ] br_corner = [ x + tiny_image.shape[0]/2.0, y + tiny_image.shape[0]/2.0 ] img[tl_corner[0]:br_corner[0],tl_corner[1]:br_corner[1]] += tiny_image self.log.debug(npArrayInfo(img,"DenseSubImage")) self["Raw Spectrum"] = img frame = self.frame() frame.lensletNumber = self.num frame.corner = self.subcorner frame.configHash = hash(str(self.config.extract()))
def get_trace(self,spectrum): """Returns a trace of this spectrum. The trace will contain x and y over-dense pixel positions, flux values for each of those illuminated pixels, instantaneous resolution at each pixel, and wavelength of each pixel. The trace also determines the corners of the spectrum, and saves those corner positions with ample padding in the image. To perform the trace, we first get the oversampled point positions in o-px from the ``dxs`` and ``dys`` variables. These points are saved as both ``x,y`` and ``xorig,yorig``, for later use. ``xorig,yorig`` are stored to save an unmodified copy of the points. The ``xint,yint`` variables are used to store the integer (in px) positions of each ``x,y`` pair, essentially, thier containing camera pixel. ``x,y`` are then zeroed, such that (0,0) is the upper-right corner of the spectrum to be inserted. We then calculate the size of the subimage (in ``xdist,ydist``) and adjust this size so that it is an integer number of camera pixels (px) across. This makes the binning much easier later. The pixels ``x,y`` are then padded to provide space for the PSF to be applied on all sides of the single-pixel spectrum. We then find the corner of the of the subimage. First, the corner will generally be extracted as the position with a minimum in the ``y`` direction and a maximum in the ``x`` direction. We first find the corner's position in integer camrea-px space (i.e. from ``xint,yint``, stored as ``corner``), and the corner's position in integer o-px space (i.e. from ``xorig,yorig``, stored as ``realcorner``). Converting the camera's position in integer camera-px space, we take the difference between the two corners as the ``offset``. This is the shift we must insert into ``x,y`` in order to ensure that the corner of our subimage will line up with the corner of a binned pixel. Next we add padding distances into the ``corner`` position. Finally, we use the offset to move the ``x,y`` positions to account for aligning the corner of the subimage with the corner of a full camera pixel. I will make a diagram to explain all of this shortly. Next, we add the padding values into ``xdist,ydist`` to get the full size of the subimage in o-px. We are now ready to extract flux values from our spectrum. This is done using the ``dwl`` values as the wavelength values to sample at. Using the ``dwl`` values, we also calculate an effective sampling resolution, which is used to resample the spectra. Feeding both of these, we compute the flux of the spectrum at each pixel position. This computation is not described in this function, but in a separate location in the documentation. After computing the flux at each pixel, we save the ``x,y`` indicies of each pixel, the flux for that pixel, the wavelength of that pixel, and the shape and corner of the subimage for later use. **Variables which are used**: :var dxs: x-camera-positions of each illuminated oversampled pixel in px :var dys: y-camera-positions of each illuminated oversampled pixel in px :var dwl: wavelength of each illuminated oversampled pixel in meters **Variables which are set**: :var txs: x-subimage-indicies of each illuminated oversampled pixel in o-px :var tys: y-subimage-indicies of each illuminated oversampled pixel in o-px :var tfl: flux of each pixel in counts :var twl: wavelength of each pixel in meters :var tdw: delta wavelength covered by each pixel in meters :var trs: sampling resolution of each pixel :var subshape: shape of subimage to contain spectrum :var subcorner: corner of subimage to contain spectrum in px :var spectrum: the spectrum object used for flux :var traced: bool True """ if self.traced: return self.traced # Variables taken from the dispersion calculation points = np.array([self.dxs,self.dys]).T deltawl = np.diff(self.dwl) # Take our points out. Note from the above that we multiply by the density in order to do this xorig,yorig = (points * self.config["Instrument"]["density"])[:-1].T.astype(np.int) x,y = (points * self.config["Instrument"]["density"])[:-1].T.astype(np.int) # Get the way in which those points correspond to actual pixels. # As such, this array of points should have duplicates xint,yint = points.T.astype(np.int) # Zero-adjust our x and y points. They will go into a fake subimage anyways, so we don't care # for now where they would be on the real image x -= np.min(x) y -= np.min(y) # Get the approximate size of our spectra xdist = np.max(x)-np.min(x) ydist = np.max(y)-np.min(y) # Convert this size into an integer number of pixels for our subimage. This makes it # *much* easier to register our sub-image to the master, larger pixel image xdist += (self.config["Instrument"]["density"] - xdist % self.config["Instrument"]["density"]) ydist += (self.config["Instrument"]["density"] - ydist % self.config["Instrument"]["density"]) # Move our x and y coordinates to the middle of our sub image by applying padding below each one. x += self.config["Instrument"]["padding"] * self.config["Instrument"]["density"] y += self.config["Instrument"]["padding"] * self.config["Instrument"]["density"] # Find the first (by the flatten method) corner of the subimage, # useful for placing the sub-image into the full image. corner = np.array([ xint[np.argmax(x)], yint[np.argmin(y)]]) self.log.debug("Corner Position in Integer Space: %s" % corner) corner *= self.config["Instrument"]["density"] realcorner = np.array([ xorig[np.argmax(x)], yorig[np.argmin(y)]]) offset = corner - realcorner corner /= self.config["Instrument"]["density"] self.log.debug("Corner Position Offset in Dense Space: %s" % (offset)) if self.log.getEffectiveLevel() <= logging.DEBUG: with open("%(Partials)s/Instrument-Offsets.dat" % self.config["Dirs"],'a') as handle: np.savetxt(handle,offset) corner -= np.array([-self.config["Instrument"]["padding"],self.config["Instrument"]["padding"]]) x += offset[0] y += offset[1] # Create our sub-image, using the x and y width of the spectrum, plus 2 padding widths. # Padding is specified in full-size pixels to ensure that the final image is an integer # number of full-size pixels across. xsize = xdist+2*self.config["Instrument"]["padding"]*self.config["Instrument"]["density"] ysize = ydist+2*self.config["Instrument"]["padding"]*self.config["Instrument"]["density"] # Calculate the resolution inherent to the pixels asked for WLS = self.dwl DWL = np.diff(WLS) WLS = WLS[:-1] RS = WLS/DWL # Call and evaluate the spectrum self.log.debug(npArrayInfo(WLS,"Calling Wavelength")) self.log.debug(npArrayInfo(RS,"Calling Resolution")) wl,flux = spectrum(wavelengths=WLS,resolution=RS) self.log.debug(npArrayInfo(flux,"Final Flux")) self.log.debug(npArrayInfo(RS,"Saving Resolution")) self.txs = x self.tys = y self.tfl = flux self.twl = WLS self.tdw = DWL self.trs = RS self.subshape = (xsize,ysize) self.subcorner = corner self.spectrum = spectrum self.traced = True return self.traced
def find_dispersion(self): """Find the dispersion (dense, pixel aligned wavelength values) for this lenslet. To calculate dispersion, we first create an interpolation from (wavelength) -> (xpix) and (wavelength) -> (ypix). Then, using a large array of possible wavelengths (100x the number of oversampled pixel positions) we create arrays of possible overdense x and y pixel positions. These arrays are then truncated to contain only integer (overdense) pixel positions. We then calculate the arc-distance to each of these pixel positions from the start (lowest wavelength) of the spectrum. Taking only unique points along the arc-distance, we find a list of all of the unique x and y pixel positions which are illuminated by the spectrum in the over-dense sample space. This array, along with thier corresponding wavelengths and arc-distances, are stored for later use. **Variables which are used**: :var xcs: x-camera positions (center of image) in mm :var ycs: y-camera positions (center of image) in mm :var ls: wavelengths :var xpixs: x-camera positions (center of image) in px (integer) :var ypixs: y-camera positions (center of image) in px (integer) **Variables which are set**: :var dxs: x-camera-positions of each illuminated oversampled pixel in px (integer o-px) :var dys: y-camera-positions of each illuminated oversampled pixel in px (integer o-px) :var dwl: wavelength of each illuminated oversampled pixel in meters :var drs: arc-distance along spectrum in mm :var dis: array of ``[dxs,dys,dwl,drs]`` :var dispersion: boolean True """ assert self.valid(), "Lenslet must contain valid data." if self.dispersion: return self.dispersion if self.config["Instrument"]["Tel"]["ellipse"]: # Find ellipse major and minor axis from given data. self.a = np.sqrt((self.xcs - self.xas)**2.0 + (self.ycs-self.yas)**2.0) * self.config["Instrument"]["convert"]["mmtopx"] * self.config["Instrument"]["density"] self.b = np.sqrt((self.xcs - self.xbs)**2.0 + (self.ycs-self.ybs)**2.0) * self.config["Instrument"]["convert"]["mmtopx"] * self.config["Instrument"]["density"] top = self.xcs - self.xas bot = self.ycs - self.yas bot[np.logical_and(top == 0,bot == 0)] = 1.0 self.alpha = np.arctan(top/bot) self.fa = np.poly1d(np.polyfit(self.ls, self.a, self.config["Instrument"]["Tel"]["dispfitorder"])) self.fb = np.poly1d(np.polyfit(self.ls, self.b, self.config["Instrument"]["Tel"]["dispfitorder"])) self.falpha = np.poly1d(np.polyfit(self.ls, self.alpha, self.config["Instrument"]["Tel"]["dispfitorder"])) # Interpolation to convert from wavelength to pixels. # The accuracy of this interpolation is not important. # Rather, it is used to find the pixels where the light will fall # and is fed an array that is very dense, used on this dense interpolation # and then binned back onto pixels. Thus it will be used to get a list # of all illuminated pixels. fx = np.poly1d(np.polyfit(self.ls, self.xpixs, self.config["Instrument"]["dispfitorder"])) fy = np.poly1d(np.polyfit(self.ls, self.ypixs, self.config["Instrument"]["dispfitorder"])) # Find the starting and ending position of the spectra startix = np.argmin(self.ls) endix = np.argmax(self.ls) start = np.array([self.xcs[startix],self.ycs[startix]]) end = np.array([self.xcs[endix],self.ycs[endix]]) # Get the total length of the spectra distance = np.sqrt(np.sum(end-start)**2) # This should have been checked in the validity function. if distance == 0: raise SEDLimits # Find the length in units of (int) pixels npix = (distance * self.config["Instrument"]["convert"]["mmtopx"]).astype(np.int) * self.config["Instrument"]["density"] # Create a data array one hundred times as dense as the number of pixels # This is the super dense array which will use the above interpolation superDense_lam = np.linspace(np.min(self.ls),np.max(self.ls),npix*100) # Interpolate along our really dense set of wavelengths to find all possible # illuminated pixel positions in this spectrum superDense_pts = np.array([fx(superDense_lam),fy(superDense_lam)]) # Measure the distance along our really dense set of points superDense_interval = np.sqrt(np.sum(np.power(np.diff(superDense_pts,axis=1),2),axis=0)) superDense_distance = np.cumsum(superDense_interval) # Adjust the density of our points. This rounds all values to only full pixel values. superDense_pts = np.round(superDense_pts * self.config["Instrument"]["density"]) / self.config["Instrument"]["density"] superDense_int = (superDense_pts * self.config["Instrument"]["density"]).astype(np.int) # We can identify unique points using the points when the integer position ratchets up or down. unique_x,unique_y = np.diff(superDense_int).astype(np.bool) # We want unique index to include points where either 'y' or 'x' ratchets up or down unique_idx = np.logical_or(unique_x,unique_y) # Remove any duplicate points. This does not do so in order, so we must # sort the array of unique points afterwards... unique_pts = superDense_pts[:,1:][:,unique_idx] # An array of distances to the origin of this spectrum, can be used to find wavelength # of light at each distance distance = superDense_distance[unique_idx] * self.config["Instrument"]["convert"]["pxtomm"] # Re sort everything by distnace along the trace. # Strictly, this shouldn't be necessary if all of the above functions preserved order. sorted_idx = np.argsort(distance) # Pull out sorted valuses distance = distance[sorted_idx] points = unique_pts[:,sorted_idx].T self.log.debug(npArrayInfo(points,"Points")) # Pull out the original wavelengths wl_orig = superDense_lam[unique_idx][sorted_idx] wl = wl_orig self.log.debug(npArrayInfo(wl,"Wavelengths")) # We are getting some odd behavior, where the dispersion function seems to not cover the whole # arc length and instead covers only part of it. This causes much of our arc to leave the desired # and available wavelength boundaries. As such, I'm disabling the more accurate dispersion mode. # Convert to wavelength space along the dispersion spline. # wl = self.spline(distance) xs,ys = points.T self.dxs = xs self.dys = ys self.dwl = wl self.drs = distance self.dis = np.array([xs,ys,wl,distance]) self.dispersion = True return self.dispersion
def valid(self,strict=True): """Returns true if this is a valid lenslet, false if it fails any of the tests. :param strict: In not strict mode, validator will let many lenslets which are not well-formed through the system. :returns: bool Checks performed: - Wavelengths and Points all have the same number of entries. - We have at least three entries. - No entry's pixel position is exactly 0. - The dispersion distance along the x-axis is less than 30 pixels. - The distance between the start and end of the spectrum is non-zero. - The lenslet positions are not within some configured tolerance of the edge of the spectrum. - Warning about the units for wavelengths. """ if self.checked: return self.passed self.checked = True self.passed = False # Data consistency if len(self.points) != len(self.ps) or len(self.points) != len(self.ls) or len(self.points) != len(self.pixs): self.log.warning("Lenslet %d failed: The data had inconsistent points" % self.num) return self.passed # Data utility if len(self.points) < 3: self.log.debug("Lenslet %d failed: There were fewer than three data points" % self.num) if strict: return self.passed if np.any(self.pixs.flatten == 0): self.log.debug("Lenslet %d failed: Some (x,y) were exactly zero" % self.num) if strict: return self.passed # X distance calculation (all spectra should be roughly constant in x, as they are fairly well aligned) # NOTE: There really isn't a whole lot to this requriement dist = 30 if np.any(np.abs(np.diff(self.xpixs)) > dist): self.log.debug("Lenslet %d failed: x distance was more than %d" % (self.num,dist)) if strict: return self.passed # The spectrum should span some finite distance startix = np.argmin(self.ls) endix = np.argmax(self.ls) start = np.array([self.xcs[startix],self.ycs[startix]]) end = np.array([self.xcs[endix],self.ycs[endix]]) # Get the total length of the spectra self.distance = np.sqrt(np.sum(end-start)**2) if self.distance == 0: self.log.debug("Lenslet %d failed: The points have no separating distance" % self.num) return self.passed # Find the xs and ys that are not within 0.1 mm of the edge of the detector... padding = self.config["Instrument"]["image"]["pad"]["mm"] if not ((self.xcs > 0.1) & (self.xcs < self.config["Instrument"]["image"]["size"]["mm"]-padding) & (self.ycs > padding) & (self.ycs < self.config["Instrument"]["image"]["size"]["mm"]-padding)).any(): self.log.debug("Lenslet %d failed: The points are too close to the image edge" % self.num) if strict: return self.passed self.passed = True # Warnings about our data go here. if np.any(self.ls < 1e-12) or np.any(self.ls > 1e-3): self.log.warning("The wavelengths provided for lenslet %d appear as if they aren't SI units." % self.num) self.log.debug(npArrayInfo(self.ls,"Lenslet %d Wavelengths" % self.num)) return self.passed