def readStoredTable(tableFilePath): inpath = ut.absPath(tableFilePath) # open the file with open(inpath, 'rb') as infile: # verify header tags if stringFromFile(infile) != "SKIRT X" or intFromFile( infile) != 0x010203040A0BFEFF: raise ValueError( "File does not have SKIRT stored table format: {}".format( inpath)) # get the axes metadata and grids numAxes = intFromFile(infile) axisNames = [stringFromFile(infile) for i in range(numAxes)] axisUnits = [stringFromFile(infile) for i in range(numAxes)] axisScales = [stringFromFile(infile) for i in range(numAxes)] axisGrids = [ arrayFromFile(infile, (intFromFile(infile), )) for i in range(numAxes) ] # get the quantities metadata numQuantities = intFromFile(infile) quantityNames = [stringFromFile(infile) for i in range(numQuantities)] quantityUnits = [stringFromFile(infile) for i in range(numQuantities)] quantityScales = [stringFromFile(infile) for i in range(numQuantities)] # get the quantity values shapeValues = tuple([numQuantities] + [len(axisGrid) for axisGrid in axisGrids]) values = arrayFromFile(infile, shapeValues) # verify the trailing tag if stringFromFile(infile) != "STABEND": raise ValueError( "File does not have the proper trailing tag: {}".format( inpath)) # construct the dictionary that will be returned, adding basic metadata d = dict(axisNames=axisNames, axisUnits=axisUnits, axisScales=axisScales, quantityNames=quantityNames, quantityUnits=quantityUnits, quantityScales=quantityScales) # add axis grids for i in range(numAxes): d[axisNames[i]] = axisGrids[i] << sm.unit(axisUnits[i]) # add quantities information for i in range(numQuantities): d[quantityNames[i]] = values[i] << sm.unit(quantityUnits[i]) return d
def do( simDirPath: (str, "SKIRT simulation output directory"), prefix: (str, "SKIRT simulation prefix") = "", plot: (str, "type of plot: linmap, degmap, degavg, or cirmap") = "linmap", wave: (float, "wavelength of the frame to be plotted; 0 means all frames") = 0, bin: (int, "number of pixels in a bin, in both x and y directions") = 7, dex: (float, "number of decades to be included in the background intensity range (color bar)" ) = 5, ) -> "plot polarization maps for the output of one or more SKIRT simulations": import pts.simulation as sm import pts.visual as vis for sim in sm.createSimulations(simDirPath, prefix if len(prefix) > 0 else None): vis.plotPolarization( sim, plotLinMap=plot.lower().startswith("lin"), plotDegMap=plot.lower().startswith("degm"), plotDegAvg=plot.lower().startswith("dega"), plotCirMap=plot.lower().startswith("cir"), wavelength='all' if wave == 0 else wave << sm.unit("micron"), binSize=(bin, bin), decades=dex)
def makeConvolvedRGBImages(simulation, contributions, name="", *, fileType="total", decades=3, fmax=None, fmin=None, outDirPath=None): # get the (instrument, output file path) tuples to be handled instr_paths = sm.instrumentOutFilePaths(simulation, fileType+".fits") if len(instr_paths) > 0: name = "" if not name else "_" + name sbunit = sm.unit("MJy/sr") # construct an image object with integrated color channels for each output file # and keep track of the largest surface brightness value (in MJy/sr) in each of the images images = [] fmaxes = [] for instrument, filepath in instr_paths: logging.info("Convolving for RGB image {}{}".format(filepath.stem, name)) # get the data cube in its intrinsic units cube = sm.loadFits(filepath) # initialize an RGB frame dataRGB = np.zeros( (cube.shape[0], cube.shape[1], 3) ) << sbunit # add color for each filter for band,w0,w1,w2 in contributions: # convolve and convert to per frequency units data = band.convolve(instrument.wavelengths(), cube, flavor=sbunit, numWavelengths=10) dataRGB[:,:,0] += w0*data dataRGB[:,:,1] += w1*data dataRGB[:,:,2] += w2*data # construct the image object images.append(vis.RGBImage(dataRGB.value)) fmaxes.append(dataRGB.max()) # determine the appropriate pixel range for all output images fmax = max(fmaxes) if fmax is None else fmax << sbunit fmin = fmax/10**decades if fmin is None else fmin << sbunit # create an RGB file for each output file for (instrument, filepath), image in zip(instr_paths,images): image.setRange(fmin.value, fmax.value) image.applyLog() image.applyCurve() # determine output file path saveFilePath = ut.savePath(filepath.with_name(filepath.stem+name+".png"), ".png", outDirPath=outDirPath) image.saveTo(saveFilePath) logging.info("Created convolved RGB image file {}".format(saveFilePath)) # return the surface brightness range used for these images return fmin,fmax
def readStoredColumns(columnsFilePath): inpath = ut.absPath(columnsFilePath) # open the file with open(inpath, 'rb') as infile: # verify header tags if stringFromFile(infile) != "SKIRT X" or intFromFile(infile) != 0x010203040A0BFEFF \ or intFromFile(infile) != 0: raise ValueError( "File does not have SKIRT stored table format: {}".format( inpath)) # get the number of columns and rows numRows = intFromFile(infile) numColumns = intFromFile(infile) # get the column metadata columnNames = [stringFromFile(infile) for i in range(numColumns)] columnUnits = [stringFromFile(infile) for i in range(numColumns)] # get the data values values = arrayFromFile(infile, (numColumns, numRows)) # verify the trailing tag if stringFromFile(infile) != "SCOLEND": raise ValueError( "File does not have the proper trailing tag: {}".format( inpath)) # construct the dictionary that will be returned, adding basic metadata d = dict(columnNames=columnNames, columnUnits=columnUnits) # add data values for i in range(numColumns): d[columnNames[i]] = values[i] << sm.unit(columnUnits[i]) return d
def plotPolarization(simulation, *, plotLinMap=True, plotDegMap=False, plotDegAvg=False, plotCirMap=False, wavelength=None, binSize=(7, 7), degreeScale=None, decades=5, outDirPath=None, figSize=(8, 6), interactive=None): # loop over all applicable instruments for instrument, filepath in sm.instrumentOutFilePaths( simulation, "stokesQ.fits"): # form the simulation/instrument name insname = "{}_{}".format(instrument.prefix(), instrument.name()) # get the file paths for the frames/data cubes filepathI = instrument.outFilePaths("total.fits")[0] filepathQ = instrument.outFilePaths("stokesQ.fits")[0] filepathU = instrument.outFilePaths("stokesU.fits")[0] filepathV = instrument.outFilePaths("stokesV.fits")[0] # load datacubes with shape (nx, ny, nlambda) Is = sm.loadFits(filepathI) Qs = sm.loadFits(filepathQ) Us = sm.loadFits(filepathU) Vs = sm.loadFits(filepathV) # load the axes grids (assuming all files have the same axes) xgrid, ygrid, wavegrid = sm.getFitsAxes(filepathI) xmin = xgrid[0].value xmax = xgrid[-1].value ymin = ygrid[0].value ymax = ygrid[-1].value extent = (xmin, xmax, ymin, ymax) # determine binning configuration binX = binSize[0] orLenX = Is.shape[0] dropX = orLenX % binX startX = dropX // 2 binY = binSize[1] orLenY = Is.shape[1] dropY = orLenY % binY startY = dropY // 2 # construct arrays with central bin positions in pixel coordinates posX = np.arange(startX - 0.5 + binX / 2.0, orLenX - dropX + startX - 0.5, binX) posY = np.arange(startY - 0.5 + binY / 2.0, orLenY - dropY + startY - 0.5, binY) # determine the appropriate wavelength index or indices if wavelength is None: indices = [0] elif wavelength == 'all': indices = range(Is.shape[2]) else: if not isinstance(wavelength, (list, tuple)): wavelength = [wavelength] indices = instrument.wavelengthIndices(wavelength) # loop over all requested wavelength indices for index in indices: wave = wavegrid[index] wavename = "{:09.4f}".format(wave.to_value(sm.unit("micron"))) wavelatex = r"$\lambda={:.4g}$".format( wave.value) + sm.latexForUnit(wave) # extract the corresponding frame, and transpose to (y,x) style for compatibility with legacy code I = Is[:, :, index].T.value Q = Qs[:, :, index].T.value U = Us[:, :, index].T.value V = Vs[:, :, index].T.value # perform the actual binning binnedI = np.zeros((len(posY), len(posX))) binnedQ = np.zeros((len(posY), len(posX))) binnedU = np.zeros((len(posY), len(posX))) binnedV = np.zeros((len(posY), len(posX))) for x in range(len(posX)): for y in range(len(posY)): binnedI[y, x] = np.sum( I[startY + binY * y:startY + binY * (y + 1), startX + binX * x:startX + binX * (x + 1)]) binnedQ[y, x] = np.sum( Q[startY + binY * y:startY + binY * (y + 1), startX + binX * x:startX + binX * (x + 1)]) binnedU[y, x] = np.sum( U[startY + binY * y:startY + binY * (y + 1), startX + binX * x:startX + binX * (x + 1)]) binnedV[y, x] = np.sum( V[startY + binY * y:startY + binY * (y + 1), startX + binX * x:startX + binX * (x + 1)]) # ----------------------------------------------------------------- # plot a linear polarization map if plotLinMap: fig, ax = plt.subplots(ncols=1, nrows=1, figsize=figSize) # configure the axes ax.set_xlim(xmin, xmax) ax.set_ylim(ymin, ymax) ax.set_xlabel("x" + sm.latexForUnit(xgrid), fontsize='large') ax.set_ylabel("y" + sm.latexForUnit(ygrid), fontsize='large') # determine intensity range for the background image, ignoring pixels with outrageously high flux Ib = I.copy() highmask = Ib > 1e6 * np.nanmedian(np.unique(Ib)) vmax = np.nanmax(Ib[~highmask]) Ib[highmask] = vmax vmin = vmax / 10**decades # plot the background image and the corresponding color bar normalizer = matplotlib.colors.LogNorm(vmin, vmax) cmap = plt.get_cmap('PuRd') cmap.set_under('w') backPlot = ax.imshow(Ib, norm=normalizer, cmap=cmap, extent=extent, aspect='equal', interpolation='bicubic', origin='lower') cbarlabel = sm.latexForSpectralFlux(Is) + sm.latexForUnit( Is) + " @ " + wavelatex plt.colorbar(backPlot, ax=ax).set_label(cbarlabel, fontsize='large') # compute the linear polarization degree degreeLD = np.sqrt(binnedQ**2 + binnedU**2) degreeLD[degreeLD > 0] /= binnedI[degreeLD > 0] # determine a characteristic 'high' degree of polarization in the frame # (this has to be done before degreeLD contains 'np.NaN') charDegree = np.percentile(degreeLD, 99.0) if not 0 < charDegree < 1: charDegree = np.nanmax((np.nanmax(degreeLD), 0.0001)) # remove pixels with minuscule polarization degreeLD[degreeLD < charDegree / 50] = np.NaN # determine the scaling so that the longest arrows do not to overlap with neighboring arrows if degreeScale is None: degreeScale = _roundUp(charDegree) lengthScale = 2.2 * degreeScale * max( float(len(posX)) / figSize[0], float(len(posY)) / figSize[1]) key = "{:.3g}%".format(100 * degreeScale) # compute the polarization angle angle = 0.5 * np.arctan2( binnedU, binnedQ ) # angle from North through East while looking at the sky # create the polarization vector arrays xPolarization = -degreeLD * np.sin( angle ) #For angle = 0: North & x=0, For angle = 90deg: West & x=-1 yPolarization = degreeLD * np.cos( angle ) #For angle = 0: North & y=1, For angle = 90deg: West & y=0 # plot the vector field (scale positions to data coordinates) X, Y = np.meshgrid(xmin + posX * (xmax - xmin) / orLenX, ymin + posY * (ymax - ymin) / orLenY) quiverPlot = ax.quiver(X, Y, xPolarization, yPolarization, pivot='middle', units='inches', angles='xy', scale=lengthScale, scale_units='inches', headwidth=0, headlength=1, headaxislength=1, minlength=0.8, width=0.02) ax.quiverkey(quiverPlot, 0.85, 0.02, degreeScale, key, coordinates='axes', labelpos='E') # if not in interactive mode, save the figure; otherwise leave it open if not ut.interactive(interactive): saveFilePath = ut.savePath( filepath, ".pdf", outDirPath=outDirPath, outFileName="{}_{}_pollinmap.pdf".format( insname, wavename)) plt.savefig(saveFilePath, bbox_inches='tight', pad_inches=0.25) plt.close() logging.info("Created {}".format(saveFilePath)) # ----------------------------------------------------------------- # plot a linear polarization degree map if plotDegMap: fig, ax = plt.subplots(ncols=1, nrows=1, figsize=figSize) # configure the axes ax.set_xlim(xmin, xmax) ax.set_ylim(ymin, ymax) ax.set_xlabel("x" + sm.latexForUnit(xgrid), fontsize='large') ax.set_ylabel("y" + sm.latexForUnit(ygrid), fontsize='large') # calculate polarization degree for each pixel, in percent # set degree to zero for pixels with very low intensity cutmask = I < (np.nanmax( I[I < 1e6 * np.nanmedian(np.unique(I))]) / 10**decades) degreeHD = np.sqrt(Q**2 + U**2) degreeHD[~cutmask] /= I[~cutmask] degreeHD[cutmask] = 0 degreeHD *= 100 # plot the image and the corresponding color bar vmax = degreeScale if degreeScale is not None else np.percentile( degreeHD, 99) normalizer = matplotlib.colors.Normalize(vmin=0, vmax=vmax) backPlot = ax.imshow(degreeHD, norm=normalizer, cmap='plasma', extent=extent, aspect='equal', interpolation='bicubic', origin='lower') plt.colorbar(backPlot, ax=ax).set_label( "Linear polarization degree (%)" + " @ " + wavelatex, fontsize='large') # if not in interactive mode, save the figure; otherwise leave it open if not ut.interactive(interactive): saveFilePath = ut.savePath( filepath, ".pdf", outDirPath=outDirPath, outFileName="{}_{}_poldegmap.pdf".format( insname, wavename)) plt.savefig(saveFilePath, bbox_inches='tight', pad_inches=0.25) plt.close() logging.info("Created {}".format(saveFilePath)) # ----------------------------------------------------------------- # plot the y-axis averaged linear polarization degree if plotDegAvg: # construct the plot fig, ax = plt.subplots(ncols=1, nrows=1, figsize=figSize) degreeHD = np.sqrt( np.average(Q, axis=0)**2 + np.average(U, axis=0)**2) degreeHD /= np.average(I, axis=0) ax.plot(xgrid.value, degreeHD * 100) ax.set_xlim(xmin, xmax) ax.set_ylim(0, degreeScale) ax.set_title("{} {}".format(insname, wavelatex), fontsize='large') ax.set_xlabel("x" + sm.latexForUnit(xgrid), fontsize='large') ax.set_ylabel('Average linear polarization degree (%)', fontsize='large') # if not in interactive mode, save the figure; otherwise leave it open if not ut.interactive(interactive): saveFilePath = ut.savePath( filepathI, ".pdf", outDirPath=outDirPath, outFileName="{}_{}_poldegavg.pdf".format( insname, wavename)) plt.savefig(saveFilePath, bbox_inches='tight', pad_inches=0.25) plt.close() logging.info("Created {}".format(saveFilePath)) # ----------------------------------------------------------------- # plot a circular polarization map if plotCirMap: fig, ax = plt.subplots(ncols=1, nrows=1, figsize=figSize) # configure the axes ax.set_xlim(xmin, xmax) ax.set_ylim(ymin, ymax) ax.set_xlabel("x" + sm.latexForUnit(xgrid), fontsize='large') ax.set_ylabel("y" + sm.latexForUnit(ygrid), fontsize='large') # determine intensity range for the background image, ignoring pixels with outrageously high flux Ib = I.copy() highmask = Ib > 1e6 * np.nanmedian(np.unique(Ib)) vmax = np.nanmax(Ib[~highmask]) Ib[highmask] = vmax vmin = vmax / 10**decades # plot the background image and the corresponding color bar normalizer = matplotlib.colors.LogNorm(vmin, vmax) cmap = plt.get_cmap('PuRd') cmap.set_under('w') backPlot = ax.imshow(Ib, norm=normalizer, cmap=cmap, extent=extent, aspect='equal', interpolation='bicubic', origin='lower') cbarlabel = sm.latexForSpectralFlux(Is) + sm.latexForUnit( Is) + " @ " + wavelatex plt.colorbar(backPlot, ax=ax).set_label(cbarlabel, fontsize='large') # compute the circular polarization degree degreeLD = binnedV.copy() degreeLD[binnedI > 0] /= binnedI[binnedI > 0] # determine the scaling and add legend if degreeScale is None: degreeScale = _roundUp(np.percentile(np.abs(degreeLD), 99)) lengthScale = 0.7 / max(len(posX), len(posY)) _circArrow(ax, 0.84 - lengthScale / 2, 0.01 + lengthScale / 2, lengthScale) key = r'$+{} \%$'.format(100 * degreeScale) ax.text(0.85, 0.01 + lengthScale / 2, key, transform=ax.transAxes, ha='left', va='center') # actual plotting for x in range(len(posX)): for y in range(len(posY)): if np.isfinite(degreeLD[y, x]) and abs( degreeLD[y, x]) > degreeScale / 50: _circArrow( ax, posX[x] / orLenX, posY[y] / orLenY, degreeLD[y, x] / degreeScale * lengthScale) # if not in interactive mode, save the figure; otherwise leave it open if not ut.interactive(interactive): saveFilePath = ut.savePath( filepath, ".pdf", outDirPath=outDirPath, outFileName="{}_{}_polcirmap.pdf".format( insname, wavename)) plt.savefig(saveFilePath, bbox_inches='tight', pad_inches=0.25) plt.close() logging.info("Created {}".format(saveFilePath))
def do( simDirPath: (str, "SKIRT simulation output directory"), prefix: (str, "SKIRT simulation prefix") = "", type: (str, "type of SKIRT instrument output files to be handled") = "total", name: (str, "name segment that will be added to the image file names") = "", colors: (str, "three comma-separated wavelength values or broadband names defining the R,G,B colors" ) = "", ) -> "create RGB images for surface brightness maps generated by SKIRT instruments": import pts.band as bnd import pts.simulation as sm import pts.utils as ut import pts.visual as vis # get the simulations to be handled sims = sm.createSimulations(simDirPath, prefix if len(prefix) > 0 else None) # parse the colors and handle accordingly # no colors given if len(colors) == 0: if len(name) > 0: raise ut.UserError( "name argument is not supported when colors are not specified") for sim in sims: vis.makeRGBImages(sim, fileType=type) return # get segments segments = colors.split(',') if len(segments) != 3: raise ut.UserError( "colors argument must have three comma-separated segments") # try wavelengths try: wavelengths = [float(segment) for segment in segments] except ValueError: wavelengths = None if wavelengths is not None: tuples = {name: wavelengths << sm.unit("micron")} for sim in sims: vis.makeRGBImages(sim, wavelengthTuples=tuples, fileType=type) return # try bands try: bands = [bnd.BroadBand(segment) for segment in segments] except ValueError: bands = None if bands is not None: contributions = [(bands[0], 1, 0, 0), (bands[1], 0, 1, 0), (bands[1], 0, 0, 1)] for sim in sims: vis.makeConvolvedRGBImages(sim, contributions=contributions, fileType=type, name=name) return raise ut.UserError( "colors argument must specify three wavelengths in micron or three broadband names" )