Source code for corgidrp.fluxcal

# This module is written to do an absolute flux calibration observing a standard star having CALSPEC data.
import glob
import os
import json
import numpy as np
from astropy.io import fits, ascii
from astropy import wcs
from astropy.io import fits, ascii
from astropy.coordinates import SkyCoord
import corgidrp
from corgidrp.combine import combine_subexposures
from photutils.aperture import CircularAperture
from photutils.background import LocalBackground
from photutils.psf import fit_2dgaussian
from scipy import integrate
from corgidrp.astrom import centroid_with_roi
from urllib.request import Request, urlopen, urlretrieve

# Dictionary of anticipated bright and dim CASLPEC standard star names and corresponding fits names
[docs] calspec_names= { # bright standards '109 vir': '109vir_stis_005.fits', 'vega': 'alpha_lyr_stis_012.fits', 'eta uma': 'etauma_stis_008.fits', 'lam lep': 'lamlep_stis_008.fits', 'ksi2 ceti': 'ksi2ceti_stis_008.fits', # dim standards 'tyc 4433-1800-1': '1808347_stiswfc_006.fits', 'tyc 4205-1677-1': '1812095_stisnic_008.fits', 'tyc 4212-455-1': '1757132_stiswfc_006.fits', 'tyc 4209-1396-1': '1805292_stisnic_008.fits', 'tyc 4413-304-1': 'p041c_stisnic_010.fits', 'ucac3 313-62260': 'kf08t3_stisnic_005.fits', 'bps bs 17447-0067': '1802271_stiswfcnic_006.fits', 'tyc 4424-1286-1': '1732526_stisnic_009.fits', 'gsc 02581-02323': 'p330e_stiswfcnic_007.fits', 'tyc 4207-219-1': '1740346_stisnic_005.fits' }
[docs] calspec_url = 'https://archive.stsci.edu/hlsps/reference-atlases/cdbs/current_calspec/'
[docs] def get_calspec_file(star_name): """ look up the calspec fits file name in the names dict and look for the corresponding file in the .corgidrp/ calspec_data. If not available download the corresponding CALSPEC fits file and return the file path Args: star_name (str): Returns: str: file path str: fits file name """ calspec_dir = os.path.join(os.path.dirname(corgidrp.config_filepath), "calspec_data") names_file = os.path.join(calspec_dir, "calspec_names.json") calspec_names_ff = calspec_names if not os.path.exists(calspec_dir): os.mkdir(calspec_dir) with open(names_file, 'w') as f: json.dump(calspec_names_ff, f) else: if not os.path.exists(names_file): with open(names_file, 'w') as f: json.dump(calspec_names_ff, f) else: with open(names_file, 'r') as f: calspec_names_ff = json.load(f) if star_name.lower() not in calspec_names_ff.keys(): raise ValueError('{0} is not in list of anticipated standard stars \n {1},\n please check naming'.format(star_name, [*calspec_names_ff])) fits_name = calspec_names_ff.get(star_name.lower()) if fits_name in os.listdir(calspec_dir): file_name = os.path.join(calspec_dir, fits_name) name = fits_name else: basic_name = fits_name.split('stis')[0]+'stis' #to be flexible with the version of the calspec fits file, so essentially, the number in the name should not matter req = Request(calspec_url) list = urlopen(req).readlines() for line in list: str_line = str(line) if basic_name in str_line: name = str_line.split(".fits")[1].split(">")[-1] + ".fits" break fits_url = calspec_url + name try: file_name, headers = urlretrieve(fits_url, filename = os.path.join(calspec_dir, name)) except: raise Exception("cannot access CALSPEC archive web page and/or download {0}".format(name)) return file_name, name
[docs] def get_filter_name(image): """ return the name of the transmission curve csv file of used color filter Args: image (corgidrp.image): image of the observed calibration star Returns: str: filepath of the selected filter curve """ datadir = os.path.join(os.path.dirname(__file__), "data", "filter_curves") filters = os.path.join(datadir, "*.csv") filter = image.ext_hdr['CFAMNAME'] filter_names = os.listdir(datadir) filter_name = [name for name in filter_names if filter in name] if filter_name == []: raise ValueError("there is no filter available with name {0}".format(filter)) else: return os.path.join(datadir,filter_name[0])
[docs] def read_filter_curve(filter_filename): """ read the transmission curve csv file of the color filters Args: filter_filename (str): file name of the transmission curve data Returns: lambda_nm (np.array): wavelength in unit Angstroem transmission (np.array): transmission of the filter < 1 """ tab = ascii.read(filter_filename, format='csv', header_start = 3, data_start = 4) lambda_nm = tab['lambda_nm'].data #unit nm transmission = tab['%T'].data return lambda_nm * 10 , transmission/100.
[docs] def read_cal_spec(calspec_file, filter_wavelength): """ read the calspec flux density data interpolated on the wavelength grid of the transmission curve Args: calspec_file (str): file path to the CALSPEC fits file filter_wavelength (np.array): wavelength grid of the transmission curve in unit Angstroem Returns: np.array: flux density in Jy interpolated on the wavelength grid of the transmission curve in CALSPEC units erg/(s * cm^2 * AA) """ hdulist = fits.open(calspec_file) data = hdulist[1].data hdulist.close() w = data['WAVELENGTH'] #wavelength in Angstroem flux = data['FLUX'] flux = flux[(w<=filter_wavelength[-1]) & (w>=filter_wavelength[0])] #erg/(s*cm^2*AA) w = w[(w<=filter_wavelength[-1]) & (w>=filter_wavelength[0])] #interpolate on transmission curve wavelengths flux_inter = np.interp(filter_wavelength, w, flux) return flux_inter
[docs] def calculate_band_flux(filter_curve, calspec_flux, filter_wavelength): """ calculate the average band flux of a calspec source in the filter band, see convention A in Gordon et al. (2022) TBC if needed at all Args: filter_curve (np.array): filter transmission curve over the filter_wavelength calspec_flux (np.array): converted flux in units of erg/(s*cm^2*AA) of the calspec source in the filter band filter_wavelength (np.array): wavelengths in units Angstroem in the filter band Returns: float: average band flux of the calspec star in unit erg/(s*cm^2*AA) """ multi_flux = calspec_flux * filter_curve * filter_wavelength multi_band = filter_curve * filter_wavelength aver_flux = integrate.simpson(multi_flux, x=filter_wavelength)/integrate.simpson(multi_band, x=filter_wavelength) return aver_flux
[docs] def calculate_effective_lambda(filter_curve, calspec_flux, filter_wavelength): """ calculate the effective wavelength of a calspec source in the filter band, see convention A in Gordon et al. (2022) TBC if needed at all Args: filter_curve (np.array): filter transmission curve over the filter_wavelength calspec_flux (np.array): converted flux in units of the calspec source in the filter band filter_wavelength (np.array): wavelengths in units nm in the filter band Returns: float: effective wavelength in unit Angstroem """ multi_flux = calspec_flux * filter_curve * np.square(filter_wavelength) multi_band = calspec_flux * filter_curve * filter_wavelength eff_lambda = integrate.simpson(multi_flux, x=filter_wavelength)/integrate.simpson(multi_band, x=filter_wavelength) return eff_lambda
[docs] def calculate_pivot_lambda(filter_curve, filter_wavelength): """ calculate the reference pivot wavelength of the filter band, see convention B in Gordon et al. (2022) Args: filter_curve (np.array): filter transmission curve over the filter_wavelength filter_wavelength (np.array): wavelengths in unit Angstroem in the filter band Returns: float: pivot wavelength in unit Angstroem """ multi_flux = filter_curve * filter_wavelength multi_band = filter_curve / filter_wavelength piv_lambda = np.sqrt(integrate.simpson(multi_flux, x=filter_wavelength)/integrate.simpson(multi_band, x=filter_wavelength)) return piv_lambda
[docs] def calculate_flux_ref(filter_wavelength, calspec_flux, wave_ref): """ calculate the flux at the reference wavelength of the filter band Args: filter_wavelength (np.array): wavelengths in unit Angstroem in the filter band calspec_flux (np.array): converted flux in units of the calspec source in the filter band wave_ref (float): reference wavelength in unit Angstroem Returns: float: flux at reference wavelength in unit erg/(s*cm^2*AA) """ flux_ref = np.interp(wave_ref, filter_wavelength, calspec_flux) return flux_ref
[docs] def calculate_vega_mag(source_flux, filter_file): """ determine the apparent Vega magnitude of the source with known flux in CALSPEC units erg/(s * cm^2 *AA) in the used filter band. Args: source_flux (float): source flux usually determined by applying the FluxcalFactor filter_file (str): name of the file with the transmission data of the corresponding color filter Returns: float: the apparent VEGA magnitude """ wave, filter_trans = read_filter_curve(filter_file) # calculate the flux of VEGA and the source star from the user given CALSPEC file binned on the wavelength grid of the filter vega_filepath = get_calspec_file('Vega')[0] vega_sed = read_cal_spec(vega_filepath, wave) vega_flux = calculate_band_flux(filter_trans, vega_sed, wave) #calculate apparent vega magnitude vega_mag = -2.5 * np.log10(source_flux/vega_flux) return vega_mag
[docs] def compute_color_cor(filter_curve, filter_wavelength , flux_ref, wave_ref, flux_source): """ Compute the color correction factor K given the filter bandpass, reference spectrum (CALSPEC), and source spectrum model. To use this color correction, divide the flux density for a band by K. Such color corrections are needed to compute the correct flux density at the reference wavelength for a source with the flux_source spectral shape in the photometric convention that provides the flux density at a reference wavelength (convention B, see Gordon et al. 2022, The Astronomical Journal 163:267, for details). Thus the flux density value found by applying the calibration factor on the found detected electrons of an arbitrary source should be divided by K (for the appropriate filter and spectral shape) to produce the flux density at the reference wavelength of the filter. The color correction adjusts the calibration factor to align the reference spectral shape with the current source, which results in the correct flux density at the reference wavelength. Args: filter_curve (np.array): transmission of the filter bandpass filter_wavelength (np.array): the wavelengths of the filter bandpass, flux_ref, and flux_source in unit Angstroem flux_ref (np.array): reference flux density F(lambda) as a function of wavelength wave_ref (float): reference wavelength in unit Angstroem flux_source (np.array): source flux density F(lambda) as a function of wavelength in CALSPEC unit erg/(s * cm^2 * AA) Returns: float: color correction factor K """ # get the flux densities at the reference wavelength flux_source_lambda_ref = calculate_flux_ref(filter_wavelength, flux_source, wave_ref) flux_ref_lambda_ref = calculate_flux_ref(filter_wavelength, flux_ref, wave_ref) # compute the top and bottom integrals int_source = integrate.simpson(filter_wavelength * filter_curve * flux_source / flux_source_lambda_ref, x=filter_wavelength) int_ref = integrate.simpson(filter_wavelength * filter_curve * flux_ref / flux_ref_lambda_ref, x=filter_wavelength) return int_source / int_ref
[docs] def calculate_band_irradiance(filter_curve, calspec_flux, filter_wavelength): """ calculate the integrated band flux, irradiance of a calspec source in the filter band to determine the apparent magnitude Args: filter_curve (np.array): filter transmission curve over the filter_wavelength calspec_flux (np.array): converted flux in units of erg/(s*cm^2*AA) of the calspec source in the filter band filter_wavelength (np.array): wavelengths in units Angstroem in the filter band Returns: float: band irradiance of the calspec star in unit erg/(s*cm^2) """ multi_flux = calspec_flux * filter_curve irrad = integrate.simpson(multi_flux, x=filter_wavelength) return irrad
[docs] def aper_phot(image, encircled_radius, frac_enc_energy=1., method='subpixel', subpixels=5, background_sub=False, r_in=5, r_out=10, centering_method='xy', centroid_roi_radius=5, centering_initial_guess=None): """ Returns the flux in photo-electrons of a point source, either by placing an aperture using a centroiding method, or by using WCS information. Background subtraction can be done optionally using a user defined circular annulus. Parameters: image (corgidrp.data.Image): combined source exposure image encircled_radius (float): pixel radius of the circular aperture to sum the flux frac_enc_energy (float): fraction of encircled energy inside the encircled_radius of the PSF, inverse aperture correction, 0...1 method (str): {‘exact’, ‘center’, ‘subpixel’}, The method used to determine the overlap of the aperture on the pixel grid, default is 'exact'. For detailed description see https://photutils.readthedocs.io/en/stable/api/photutils.aperture.CircularAnnulus.html subpixels (int): For the 'subpixel' method, resample pixels by this factor in each dimension. That is, each pixel is divided into subpixels**2 subpixels. This keyword is ignored unless method='subpixel', default is 5 background_sub (boolean): background can be determine from a circular annulus (default: False). r_in (float): inner radius of circular annulus in pixels, (default: 5) r_out (float): outer radius of circular annulus in pixels, (default: 10) centering_method (str): 'xy' for centroiding or 'wcs' for WCS-based centering. centroid_roi_radius (int or float): Half-size of the box around the peak, in pixels. Adjust based on desired λ/D. centering_initial_guess (tuple): (Optional) (x,y) initial guess to perform centroiding. Returns: tuple: (flux, flux_err) or (flux, flux_err, back) if background_sub is True. """ if frac_enc_energy <= 0 or frac_enc_energy > 1: raise ValueError("frac_enc_energy {0} should be within 0 < fee <= 1".format(str(frac_enc_energy))) # Work on a copy so that background subtraction does not alter original data. dat = image.data.copy() # Determine the center position using either WCS or centroid method. if centering_method == 'wcs': ra = image.pri_hdr['RA'] dec = image.pri_hdr['DEC'] target_skycoord = SkyCoord(ra=ra, dec=dec, unit='deg') w = wcs.WCS(image.ext_hdr) pos = wcs.utils.skycoord_to_pixel(target_skycoord, w, origin=1) elif centering_method == 'xy': x_center, y_center = centroid_with_roi(image.data, centroid_roi_radius, centering_initial_guess) pos = (x_center, y_center) else: raise ValueError("Invalid centering_method. Choose 'xy' or 'wcs'.") # Optionally subtract the background. if background_sub: #This is essentially the median in a circular annulus bkg = LocalBackground(r_in, r_out) back = bkg(dat, pos[0], pos[1], mask=image.dq.astype(bool)) dat -= back # Create the circular aperture and compute photometry. aper = CircularAperture(pos, encircled_radius) aperture_sums, aperture_sums_errs = aper.do_photometry( dat, error=image.err[0], mask=image.dq.astype(bool), method=method, subpixels=subpixels ) flux = aperture_sums[0] / frac_enc_energy flux_err = aperture_sums_errs[0] / frac_enc_energy if background_sub: return flux, flux_err, back else: return flux, flux_err
[docs] def phot_by_gauss2d_fit(image, fwhm, fit_shape=None, background_sub=False, r_in=5, r_out=10, centering_method='xy', centroid_roi_radius=5, centering_initial_guess=None): """ Returns the flux in photo-electrons using a 2D Gaussian fit. Finds the star center either by placing an aperture using a centroiding method, or by using WCS information. Allows optional background subtraction and selection of centering method. Parameters: image (corgidrp.data.Image): the source image. fwhm (float): Estimated full-width at half maximum. fit_shape (int or tuple, optional): Fitting region shape. If a scalar, then it is assumed to be a square. If `None`, then the shape of the input 'data'. It must be an odd value and should be much bigger than fwhm. background_sub (bool): If True, subtract background. r_in (float): Inner annulus radius. r_out (float): Outer annulus radius. centering_method (str): 'xy' or 'wcs' centering. centroid_roi_radius (int or float): Half-size of the box around the peak, in pixels. Adjust based on desired λ/D. centering_initial_guess (tuple): (Optional) (x,y) initial guess to perform centroiding. Returns: tuple: (flux, flux_err) """ # Determine the star center via WCS or centroid method. if centering_method == 'wcs': ra = image.pri_hdr['RA'] dec = image.pri_hdr['DEC'] target_skycoord = SkyCoord(ra=ra, dec=dec, unit='deg') w = wcs.WCS(image.ext_hdr) pos = wcs.utils.skycoord_to_pixel(target_skycoord, w, origin=1) elif centering_method == 'xy': x_center, y_center = centroid_with_roi(image.data, centroid_roi_radius, centering_initial_guess) pos = (x_center, y_center) else: raise ValueError("Invalid centering_method. Choose 'xy' or 'wcs'.") # Work on a copy so that background subtraction does not alter original data. dat = image.data.copy() if background_sub: bkg = LocalBackground(r_in, r_out) back = bkg(dat, pos[0], pos[1], mask=image.dq.astype(bool)) dat -= back # fit_2dgaussian: error weighting raises exception if error is zero err = image.err[0].copy() err[err == 0] = np.finfo(np.float32).eps if fit_shape is None: fit_shape = image.data.shape[0] - 1 psf_phot = fit_2dgaussian(dat, xypos=pos, fwhm=fwhm, fit_shape=fit_shape, mask=image.dq.astype(bool), error=err) flux = psf_phot.results['flux_fit'][0] flux_err = psf_phot.results['flux_err'][0] if background_sub: return [flux, flux_err, back] else: return [flux, flux_err]
[docs] def calibrate_fluxcal_aper(dataset_or_image, calspec_file = None, flux_or_irr = 'flux', phot_kwargs=None): """ fills the FluxcalFactors calibration product values for one filter band, calculates the flux calibration factors by aperture photometry. The band flux values are divided by the found photoelectrons/s. Propagates also errors to flux calibration factor calfile. Background subtraction can be done optionally using a user defined circular annulus. The photometry parameters are controlled via the `phot_kwargs` dictionary. Defaults are provided below if these parameters are not defined. Accepted keywords: 'encircled_radius' (float): The radius of the circular aperture used for photometry. 'frac_enc_energy' (float): The fraction of the total flux expected to be enclosed within the aperture. Must be in the range (0, 1]. 'method' (str): The photometry method to use. For example, 'subpixel' indicates subpixel sampling for the aperture. 'subpixels' (int): The number of subpixels per pixel to use in the photometry calculation or improved resolution. 'background_sub' (bool): Flag indicating whether to subtract background using an annulus. 'r_in' (float): The inner radius of the annulus used for background estimation. 'r_out' (float): The outer radius of the annulus used for background estimation. 'centering_method' (str): The method for determining the star's center. Options include 'xy' for centroiding or 'wcs' for WCS-based centering. 'centroid_roi_radius' (int or float): Half-size of the box around the peak, in pixels. Adjust based on desired λ/D. 'centering_initial_guess' (tuple): (Optional) (x,y) initial guess to perform centroiding. Parameters: dataset_or_image (corgidrp.data.Dataset or corgidrp.data.Image): Image(s) to compute the calibration factor. Should already be normalized for exposure time. calspec_file (str, optional): file path to the calspec fits file of the observed star flux_or_irr (str, optional): Whether flux ('flux') or in-band irradiance ('irr) should be used. phot_kwargs (dict, optional): A dictionary of keyword arguments controlling the aperture photometry function. Returns: FluxcalFactor (corgidrp.data.FluxcalFactor): A calibration object containing the computed flux calibration factor in (TO DO: what units should this be in) """ d_or_i = dataset_or_image.copy() if isinstance(d_or_i, corgidrp.data.Dataset): #take the median of images in the dataset image = combine_subexposures(d_or_i, collapse = "median", num_frames_scaling=False)[0] dataset = d_or_i else: image = d_or_i dataset = corgidrp.data.Dataset([image]) if image.ext_hdr['BUNIT'] != "photoelectron/s": raise ValueError("input dataset must have unit photoelectron/s for the calibration, not {0}".format(image.ext_hdr['BUNIT'])) if phot_kwargs is None: phot_kwargs = { 'encircled_radius': 5, 'frac_enc_energy': 1.0, 'method': 'subpixel', 'subpixels': 5, 'background_sub': False, 'r_in': 5, 'r_out': 10, 'centering_method': 'xy', 'centroid_roi_radius': 5 } filter_name = image.ext_hdr["CFAMNAME"] filter_file = get_filter_name(image) # Read filter and CALSPEC data. wave, filter_trans = read_filter_curve(filter_file) if calspec_file is not None: calspec_filepath = calspec_file calspec_filename = os.path.basename(calspec_file) else: star_name = image.pri_hdr["TARGET"] calspec_filepath, calspec_filename = get_calspec_file(star_name) flux_ref = read_cal_spec(calspec_filepath, wave) if flux_or_irr == 'flux': flux = calculate_band_flux(filter_trans, flux_ref, wave) image.ext_hdr['BUNIT'] = 'erg/(s * cm^2 * AA)/(photoelectron/s)' image.err_hdr['BUNIT'] = 'erg/(s * cm^2 * AA)/(photoelectron/s)' elif flux_or_irr == 'irr': flux = calculate_band_irradiance(filter_trans, flux_ref, wave) image.ext_hdr['BUNIT'] = 'erg/(s * cm^2)/(photoelectron/s)' image.err_hdr['BUNIT'] = 'erg/(s * cm^2)/(photoelectron/s)' else: raise ValueError("Invalid flux method. Choose 'flux' or 'irr'.") result = aper_phot(image, **phot_kwargs) if phot_kwargs.get('background_sub', False): ap_sum, ap_sum_err, back = result else: ap_sum, ap_sum_err = result fluxcal_fac = flux / ap_sum fluxcal_fac_err = flux / ap_sum**2 * ap_sum_err fluxcal_obj = corgidrp.data.FluxcalFactor( fluxcal_fac, err=fluxcal_fac_err, pri_hdr=image.pri_hdr, ext_hdr=image.ext_hdr, input_dataset=dataset ) # If background subtraction was performed, set the LOCBACK keyword. if phot_kwargs.get('background_sub', False): # Here, "back" is the third value returned from phot_by_gauss2d_fit. fluxcal_obj.ext_hdr['LOCBACK'] = back # Append to or create a HISTORY entry in the header. history_entry = "Flux calibration factor was determined by aperture photometry using SED file {0}".format(calspec_filename) fluxcal_obj.ext_hdr.add_history(history_entry) return fluxcal_obj
[docs] def calibrate_pol_fluxcal_aper(dataset_or_image, image_center_x=512, image_center_y=512, separation_diameter_arcsec=7.5, alignment_angle=None, calspec_file = None, flux_or_irr = 'flux', phot_kwargs=None): """ Same overall process as calibrate_fluxcal_aper, adapted for polarimetric images from WP1 or WP2 with two apertures instead of one. fills the FluxcalFactors calibration product values for one filter band, calculates the flux calibration factors by aperture photometry. The band flux values are divided by the found photoelectrons/s. Propagates also errors to flux calibration factor calfile. Background subtraction can be done optionally using a user defined circular annulus. The photometry parameters are controlled via the `phot_kwargs` dictionary. Defaults are provided below if these parameters are not defined. Accepted keywords: 'encircled_radius' (float): The radius of the circular aperture used for photometry. 'frac_enc_energy' (float): The fraction of the total flux expected to be enclosed within the aperture. Must be in the range (0, 1]. 'method' (str): The photometry method to use. For example, 'subpixel' indicates subpixel sampling for the aperture. 'subpixels' (int): The number of subpixels per pixel to use in the photometry calculation or improved resolution. 'background_sub' (bool): Flag indicating whether to subtract background using an annulus. 'r_in' (float): The inner radius of the annulus used for background estimation. 'r_out' (float): The outer radius of the annulus used for background estimation. 'centroid_roi_radius' (int or float): Half-size of the box around the peak, in pixels. Adjust based on desired λ/D. Parameters: dataset_or_image (corgidrp.data.Dataset or corgidrp.data.Image): Image(s) to compute the calibration factor. Should already be normalized for exposure time. Images must be from polarimetric observations taken with WP1 or WP2 in the DPAM. image_center_x (int): X pixel coordinate of where the two wollaston spots are centered around image_center_y (int): Y pixel coordinate of where the two wollaston spots are centered around separation_diameter_arcsec (optional, float): Distance between the centers of the two polarized images on the detector in arcsec, default for Roman CGI is 7.5" alignment_angle (optional, float): the angle in degrees of how the two polarized images are aligned with respect to the horizontal, defaults to 0 for WP1 and 45 for WP2 calspec_file (str, optional): file path to the calspec fits file of the observed star flux_or_irr (str, optional): Whether flux ('flux') or in-band irradiance ('irr) should be used. phot_kwargs (dict, optional): A dictionary of keyword arguments controlling the aperture photometry function. Returns: FluxcalFactor (corgidrp.data.FluxcalFactor): A calibration object containing the computed flux calibration factor in (TO DO: what units should this be in) """ d_or_i = dataset_or_image.copy() if isinstance(d_or_i, corgidrp.data.Dataset): #take the mean of images in the dataset image = combine_subexposures(d_or_i, collapse = "median", num_frames_scaling=False)[0] dataset = d_or_i else: image = d_or_i dataset = corgidrp.data.Dataset([image]) if image.ext_hdr['BUNIT'] != "photoelectron/s": raise ValueError("input dataset must have unit photoelectron/s for the calibration, not {0}".format(image.ext_hdr['BUNIT'])) #estimate the centers of the wollaston spots based on relative position from image center #polarized images separated 7.5" or 344 pix on the detector by default (1"=0.0218 pix) #WP1 output is aligned horizontally across the image center by default #WP2 output is algined diagonally across the image center by default image_center = (image_center_x, image_center_y) dpamname = image.ext_hdr['DPAMNAME'] if dpamname not in ['POL0', 'POL45']: raise ValueError('input dataset must be a polarimetric observation') if alignment_angle is None: if dpamname == 'POL0': alignment_angle = 0 else: alignment_angle = 45 angle_rad = alignment_angle * (np.pi / 180) displacement_x = int(round((separation_diameter_arcsec * np.cos(angle_rad)) / (2 * 0.0218))) displacement_y = int(round((separation_diameter_arcsec * np.sin(angle_rad)) / (2 * 0.0218))) #estimate where the centers are based on alignment angle and separation centering_initial_guess_beam_1 = (image_center[0] - displacement_x, image_center[1] + displacement_y) centering_initial_guess_beam_2 = (image_center[0] + displacement_x, image_center[1] - displacement_y) #ensure xy centering method is used with estimated centers for aperture photometry if phot_kwargs is None: phot_kwargs = { 'encircled_radius': 5, 'frac_enc_energy': 1.0, 'method': 'subpixel', 'subpixels': 5, 'background_sub': False, 'r_in': 5, 'r_out': 10, 'centroid_roi_radius': 5, } #update parameters to ensure centering is performed correctly phot_kwargs_beam_1 = phot_kwargs.copy() phot_kwargs_beam_2 = phot_kwargs.copy() phot_kwargs_beam_1.update({ 'centering_method': 'xy', 'centering_initial_guess': centering_initial_guess_beam_1 }) phot_kwargs_beam_2.update({ 'centering_method': 'xy', 'centering_initial_guess': centering_initial_guess_beam_2 }) result_beam_1, result_beam_2 = measure_aper_flux_pol( image, image_center_x=image_center_x, image_center_y=image_center_y, separation_diameter_arcsec=separation_diameter_arcsec, alignment_angle=alignment_angle, phot_kwargs=phot_kwargs ) #Optionally subtract a local background if phot_kwargs.get('background_sub', False): ap_sum_beam_1, ap_sum_err_beam_1, back_beam_1 = result_beam_1 ap_sum_beam_2, ap_sum_err_beam_2, back_beam_2 = result_beam_2 else: ap_sum_beam_1, ap_sum_err_beam_1 = result_beam_1 ap_sum_beam_2, ap_sum_err_beam_2 = result_beam_2 filter_file = get_filter_name(image) # Read filter and CALSPEC data. wave, filter_trans = read_filter_curve(filter_file) if calspec_file is not None: calspec_filepath = calspec_file calspec_filename = os.path.basename(calspec_file) else: star_name = image.pri_hdr["TARGET"] calspec_filepath, calspec_filename = get_calspec_file(star_name) flux_ref = read_cal_spec(calspec_filepath, wave) if flux_or_irr == 'flux': flux = calculate_band_flux(filter_trans, flux_ref, wave) image.ext_hdr['BUNIT'] = 'erg/(s * cm^2 * AA)/(photoelectron/s)' image.err_hdr['BUNIT'] = 'erg/(s * cm^2 * AA)/(photoelectron/s)' elif flux_or_irr == 'irr': flux = calculate_band_irradiance(filter_trans, flux_ref, wave) image.ext_hdr['BUNIT'] = 'erg/(s * cm^2)/(photoelectron/s)' image.err_hdr['BUNIT'] = 'erg/(s * cm^2)/(photoelectron/s)' else: raise ValueError("Invalid flux method. Choose 'flux' or 'irr'.") fluxcal_fac = flux / (ap_sum_beam_1 + ap_sum_beam_2) fluxcal_fac_err = flux / (ap_sum_beam_1 + ap_sum_beam_2)**2 * np.sqrt((ap_sum_err_beam_1**2) + (ap_sum_err_beam_2**2)) fluxcal_obj = corgidrp.data.FluxcalFactor( fluxcal_fac, err=fluxcal_fac_err, pri_hdr=image.pri_hdr, ext_hdr=image.ext_hdr, input_dataset=dataset ) # If local background subtraction was performed, set the LOCBACK keyword. if phot_kwargs.get('background_sub', False): #average medium background photoelectrons from both beams back = 0.5 * (back_beam_1 + back_beam_2) # Here, "back" is the third value returned from phot_by_gauss2d_fit. fluxcal_obj.ext_hdr['LOCBACK'] = back # Append to or create a HISTORY entry in the header. history_entry = "Flux calibration factor was determined by aperture photometry using SED file {0}".format(calspec_filename) fluxcal_obj.ext_hdr.add_history(history_entry) return fluxcal_obj
[docs] def measure_aper_flux_pol(image, image_center_x=512, image_center_y=512, separation_diameter_arcsec=7.5, alignment_angle=None, phot_kwargs=None, pixel_scale = 0.0218): """ Measure the individual flux from both polarized images in a polarimetric observation using aperture photometry. Parameters: image (corgidrp.data.Image): the source image. image_center_x (int): X pixel coordinate of where the two wollaston spots are centered around image_center_y (int): Y pixel coordinate of where the two wollaston spots are centered around separation_diameter_arcsec (optional, float): Distance between the centers of the two polarized images on the detector in arcsec, default for Roman CGI is 7.5" alignment_angle (optional, float): the angle in degrees of how the two polarized images are aligned with respect to the horizontal, defaults to 0 for WP1 and 45 for WP2 phot_kwargs (dict, optional): A dictionary of keyword arguments controlling the aperture photometry function. See calibrate_pol_fluxcal_aper for accepted keywords. pixel_scale (float): pixel scale in arcsec/pixel, default for Roman CGI is 0.0218"/pix Returns: tuple: (result_beam_1, result_beam_2) where each result is either (flux, flux_err) or (flux, flux_err, back) """ if image.ext_hdr['BUNIT'] != "photoelectron/s": raise ValueError("input dataset must have unit photoelectron/s, not {0}".format(image.ext_hdr['BUNIT'])) #estimate the centers of the wollaston spots based on relative position from image center #polarized images separated 7.5" or 344 pix on the detector by default (1"=0.0218 pix) #WP1 output is aligned horizontally across the image center by default #WP2 output is algined diagonally across the image center by default image_center = (image_center_x, image_center_y) dpamname = image.ext_hdr['DPAMNAME'] if dpamname not in ['POL0', 'POL45']: raise ValueError('input dataset must be a polarimetric observation') if alignment_angle is None: if dpamname == 'POL0': alignment_angle = 0 else: alignment_angle = 45 angle_rad = alignment_angle * (np.pi / 180) displacement_x = int(round((separation_diameter_arcsec * np.cos(angle_rad)) / (2 * pixel_scale))) displacement_y = int(round((separation_diameter_arcsec * np.sin(angle_rad)) / (2 * pixel_scale))) #estimate where the centers are based on alignment angle and separation centering_initial_guess_beam_1 = (image_center[0] - displacement_x, image_center[1] + displacement_y) centering_initial_guess_beam_2 = (image_center[0] + displacement_x, image_center[1] - displacement_y) #ensure xy centering method is used with estimated centers for aperture photometry if phot_kwargs is None: phot_kwargs = { 'encircled_radius': 5, 'frac_enc_energy': 1.0, 'method': 'subpixel', 'subpixels': 5, 'background_sub': False, 'r_in': 5, 'r_out': 10, 'centroid_roi_radius': 5, } #update parameters to ensure centering is performed correctly phot_kwargs_beam_1 = phot_kwargs.copy() phot_kwargs_beam_2 = phot_kwargs.copy() phot_kwargs_beam_1.update({ 'centering_method': 'xy', 'centering_initial_guess': centering_initial_guess_beam_1 }) phot_kwargs_beam_2.update({ 'centering_method': 'xy', 'centering_initial_guess': centering_initial_guess_beam_2 }) #calculate flux from both apertures result_beam_1 = aper_phot(image, **phot_kwargs_beam_1) result_beam_2 = aper_phot(image, **phot_kwargs_beam_2) return result_beam_1, result_beam_2
[docs] def calibrate_fluxcal_gauss2d(dataset_or_image, calspec_file = None, flux_or_irr = 'flux', phot_kwargs=None): """ fills the FluxcalFactors calibration product values for one filter band, calculates the flux calibration factors by fitting a 2D Gaussian. The band flux values are divided by the found photoelectrons/s. Propagates also errors to flux calibration factor calfile. Background subtraction can be done optionally using a user defined circular annulus. All photometry settings are provided via the phot_kwargs dictionary. Defaults are provided below if these parameters are not defined. Accepted keywords: 'fwhm' (float): The expected full width at half maximum. 'fit_shape' (int or tuple): Fitting region shape. 'background_sub' (bool): Flag indicating whether to subtract background using an annulus. 'r_in' (float): The inner radius of the annulus used for background estimation. 'r_out' (float): The outer radius of the annulus used for background estimation. 'centering_method' (str): The method for determining the star's center. Options include 'xy' for centroiding or 'wcs' for WCS-based centering. 'centroid_roi_radius' (int or float): Half-size of the box around the peak, in pixels. Adjust based on desired λ/D. 'centering_initial_guess' (tuple): (Optional) (x,y) initial guess to perform centroiding. Parameters: dataset_or_image (corgidrp.data.Dataset or corgidrp.data.Image): Image(s) to compute the calibration factor. Should already be normalized for exposure time. calspec_file (str, optional): file path to the calspec fits file of the observed star flux_or_irr (str, optional): Whether flux ('flux') or in-band irradiance ('irr) should be used. phot_kwargs (dict, optional): A dictionary of keyword arguments controlling the Gaussian photometry function. Returns: FluxcalFactor (corgidrp.data.FluxcalFactor): A calibration object containing the computed flux calibration factor in (TO DO: what units should this be in) """ d_or_i = dataset_or_image.copy() if isinstance(d_or_i, corgidrp.data.Dataset): #take the mean of images in the dataset image = combine_subexposures(d_or_i, collapse = "median", num_frames_scaling=False)[0] dataset = d_or_i else: image = d_or_i dataset = corgidrp.data.Dataset([image]) if image.ext_hdr['BUNIT'] != "photoelectron/s": raise ValueError("input dataset must have unit photoelectron/s for the calibration, not {0}".format(image.ext_hdr['BUNIT'])) if phot_kwargs is None: phot_kwargs = { 'fwhm': 3, 'fit_shape': None, 'background_sub': False, 'r_in': 5, 'r_out': 10, 'centering_method': 'xy', 'centroid_roi_radius': 5 } filter_file = get_filter_name(image) wave, filter_trans = read_filter_curve(filter_file) if calspec_file is not None: calspec_filepath = calspec_file calspec_filename = os.path.basename(calspec_file) else: star_name = image.pri_hdr["TARGET"] calspec_filepath, calspec_filename = get_calspec_file(star_name) flux_ref = read_cal_spec(calspec_filepath, wave) if flux_or_irr == 'flux': flux = calculate_band_flux(filter_trans, flux_ref, wave) image.ext_hdr['BUNIT'] = 'erg/(s * cm^2 * AA)/(photoelectron/s)' image.err_hdr['BUNIT'] = 'erg/(s * cm^2 * AA)/(photoelectron/s)' elif flux_or_irr == 'irr': flux = calculate_band_irradiance(filter_trans, flux_ref, wave) image.ext_hdr['BUNIT'] = 'erg/(s * cm^2)/(photoelectron/s)' image.err_hdr['BUNIT'] = 'erg/(s * cm^2)/(photoelectron/s)' else: raise ValueError("Invalid flux method. Choose 'flux' or 'irr'.") if phot_kwargs.get('background_sub', False): gauss_sum, gauss_sum_err, back = phot_by_gauss2d_fit(image, **phot_kwargs) else: gauss_sum, gauss_sum_err = phot_by_gauss2d_fit(image, **phot_kwargs) fluxcal_fac = flux / gauss_sum fluxcal_fac_err = flux / gauss_sum**2 * gauss_sum_err fluxcal_obj = corgidrp.data.FluxcalFactor( fluxcal_fac, err=fluxcal_fac_err, pri_hdr=image.pri_hdr, ext_hdr=image.ext_hdr, input_dataset=dataset ) # If background subtraction was performed, set the LOCBACK keyword. if phot_kwargs.get('background_sub', False): # Here, "back" is the third value returned from phot_by_gauss2d_fit. fluxcal_obj.ext_hdr['LOCBACK'] = back # Append to or create a HISTORY entry in the header. history_entry = "Flux calibration factor was determined by a Gaussian 2D fit photometry using SED file {0}".format(calspec_filename) fluxcal_obj.ext_hdr.add_history(history_entry) return fluxcal_obj