From 070f94a53c5981dad563c33f5cdf851f652d6e72 Mon Sep 17 00:00:00 2001 From: bensharkey Date: Fri, 15 May 2026 16:38:17 -0400 Subject: [PATCH 1/4] Adding API routes for a default aperture photometry step, including centroided astrometry route --- catch_analysis_tools/app/api/openapi.yaml | 321 +++++++++++++ .../app/services/photometry.py | 451 ++++++------------ .../services/subtract_median_background.py | 52 ++ catch_analysis_tools/background.py | 58 ++- catch_analysis_tools/photometry.py | 87 ++-- 5 files changed, 589 insertions(+), 380 deletions(-) create mode 100644 catch_analysis_tools/app/services/subtract_median_background.py diff --git a/catch_analysis_tools/app/api/openapi.yaml b/catch_analysis_tools/app/api/openapi.yaml index 221423c..3ad3969 100644 --- a/catch_analysis_tools/app/api/openapi.yaml +++ b/catch_analysis_tools/app/api/openapi.yaml @@ -239,3 +239,324 @@ paths: "500": description: Internal server error + /subtract_median_background: + post: + tags: + - Background Estimation + operationId: catch_analysis_tools.app.services.subtract_median_background.perform_median_subtraction + summary: "Compute the median background with source masking, saving the background-subtracted image as a new FITS file." + parameters: + - name: url + in: query + description: "Cutout URL from CATCH query" + example: "https://sbnsurveys.astro.umd.edu/api/images/urn:nasa:pds:gbo.ast.atlas.survey.258:59185:01a59185o0401o.fits?ra=13.46483&dec=15.80546&size=0.16deg&format=fits" + required: true + schema: + type: string + responses: + "200": + description: Background-subtracted image saved as a new FITS file. + content: + application/json: + schema: + type: object + description: Path to the background-subtracted FITS file. + properties: + background_subtracted_image_path: + description: Path to the background-subtracted FITS file. + type: string + /get_world_coordinates: + post: + tags: + - Target Search + operationId: catch_analysis_tools.app.services.photometry.get_world_coordinates + summary: "Retrieve the RA and Dec in decimal degrees from an (x,y) pixel location, using a supplied WCS file." + parameters: + - name: WCS_file + in: query + description: "WCS file, can be CATCH-generated query URL, a .fits image, or a .wcs file." + example: "https://sbnsurveys.astro.umd.edu/api/images/urn:nasa:pds:gbo.ast.atlas.survey.258:59185:01a59185o0401o.fits?ra=13.46483&dec=15.80546&size=0.16deg&format=fits" + required: true + schema: + type: string + - name: x + in: query + description: "Pixel x location." + example: 154 + required: true + schema: + type: number + - name: y + in: query + description: "Pixel y location." + example: 154 + required: true + schema: + type: number + requestBody: + required: true + description: Parameters for pixel-to-world coordinate transformation. + content: + application/json: + schema: + type: object + required: + - WCS_file + - x + - y + properties: + WCS_file: + type: string + description: "Cutout URL from CATCH query" + example: "https://sbnsurveys.astro.umd.edu/api/images/urn:nasa:pds:gbo.ast.atlas.survey.258:59185:01a59185o0401o.fits?ra=13.46483&dec=15.80546&size=0.16deg&format=fits" + x: + type: number + description: "Pixel x location." + example: 154 + y: + type: number + description: "Pixel y location." + example: 154 + responses: + "200": + description: Results of pixel-to-world transformation. + content: + application/json: + schema: + type: object + description: Output from photometry.get_world_coordinates(). Contains input location and output WCS values. + properties: + x: + description: x pixel coordinate + type: number + y: + description: y pixel coordinate + type: number + ra: + description: RA value of point in decimal degrees + type: number + dec: + description: Dec value of point in decimal degrees + type: number + /get_pixel_coordinates: + post: + tags: + - Target Search + operationId: catch_analysis_tools.app.services.photometry.get_pixel_coordinates + summary: "Retrieve the pixel (x,y) coordinates from RA and Dec world coordinates, using a supplied WCS file." + parameters: + - name: WCS_file + in: query + description: "WCS file, can be CATCH-generated query URL, a .fits image, or a .wcs file." + example: "https://sbnsurveys.astro.umd.edu/api/images/urn:nasa:pds:gbo.ast.atlas.survey.258:59185:01a59185o0401o.fits?ra=13.46483&dec=15.80546&size=0.16deg&format=fits" + required: true + schema: + type: string + - name: ra + in: query + description: "RA location in decimal degrees." + example: 13.46483 + required: true + schema: + type: number + - name: dec + in: query + description: "Dec location in decimal degrees." + example: 15.80546 + required: true + schema: + type: number + responses: + "200": + description: Results of world-to-pixel transformation. + content: + application/json: + schema: + type: object + description: Output from photometry.get_pixel_coordinates(). Contains input location and output pixel coordinates. + properties: + x: + description: x pixel coordinate + type: number + y: + description: y pixel coordinate + type: number + ra: + description: RA value of point in decimal degrees + type: number + dec: + description: Dec value of point in decimal degrees + type: number + /target_photometry: + post: + tags: + - Photometry + operationId: catch_analysis_tools.app.services.photometry.target_extraction + summary: "Perform aperture photometry on a target using specified target and background aperture configurations." + requestBody: + required: true + description: Image and aperture parameters for photometry extraction. + content: + application/json: + schema: + type: object + required: + - file + - target_aperture_params + - background_aperture_params + properties: + file: + type: string + description: "URL or file path of FITS image from CATCH-generated query." + example: "https://sbnsurveys.astro.umd.edu/api/images/urn:nasa:pds:gbo.ast.atlas.survey.258:59185:01a59185o0401o.fits?ra=13.46483&dec=15.80546&size=0.16deg&format=fits" + target_aperture_params: + type: object + description: "Configuration for the target aperture." + required: + - shape + - position + - size + properties: + shape: + type: string + description: "Shape of aperture (Circular, Rectangular or Circular_Annulus)." + example: "Circular" + position: + type: array + description: "Pixel coordinate pair [x, y] for the aperture center." + items: + type: number + minItems: 2 + maxItems: 2 + example: [154, 154] + size: + type: number + description: "Aperture size (radius in pixels) for circular apertures." + example: 2 + inner_r: + type: number + description: "Inner radius for annulus aperture (pixels)." + example: 0 + outer_r: + type: number + description: "Outer radius for annulus aperture (pixels)." + example: 0 + background_aperture_params: + type: object + description: "Configuration for the background aperture." + required: + - shape + - position + - size + properties: + shape: + type: string + description: "Shape of aperture (Circular, Rectangular or Circular_Annulus)." + example: "Circular_Annulus" + position: + type: array + description: "Pixel coordinate pair [x, y] for the aperture center." + items: + type: number + minItems: 2 + maxItems: 2 + example: [154, 154] + size: + type: number + description: "Aperture size (radius in pixels) for circular apertures." + example: 5 + inner_r: + type: number + description: "Inner radius for annulus aperture (pixels)." + example: 5 + outer_r: + type: number + description: "Outer radius for annulus aperture (pixels)." + example: 20 + responses: + "200": + description: Photometry results including flux measurements and visualization. + content: + application/json: + schema: + type: object + description: Output from photometry extraction with aperture flux and uncertainties. + properties: + aperture_flux: + description: Target flux measured within the target aperture. + type: number + aperture_fluxerr: + description: Uncertainty in the aperture flux measurement. + type: number + aperture_figure: + description: File path to the output plot showing apertures on the cutout image. + type: string + /centroid: + post: + tags: + - Target Search + operationId: catch_analysis_tools.app.services.photometry.centroid + summary: "Find nearest target centroid to a user-specified point, with user-specified search radius." + parameters: + - name: file + in: query + description: "Filename of image taken from CATCH-generated query URL." + example: "https://sbnsurveys.astro.umd.edu/api/images/urn:nasa:pds:gbo.ast.atlas.survey.258:59185:01a59185o0401o.fits?ra=13.46483&dec=15.80546&size=0.16deg&format=fits" + required: true + schema: + type: string + - name: target_x + in: query + description: "Target pixel x value, to be used as initial guess for the object." + example: "155" + required: true + schema: + type: number + - name: target_y + in: query + description: "Target pixel y value, to be used as initial guess for the object." + example: "155" + required: true + schema: + type: number + - name: search_radius + in: query + description: "Radius in pixels of search area (centered on (target_x,target_y) ) for centroiding." + example: "7" + required: true + schema: + type: number + responses: + "200": + description: Calibrated magnitude array from chosen aperture photometry settings, and output plot showing the aperture on the cutout + content: + application/json: + schema: + type: array + description: Magnitude from source flux with asymmetric uncertainties. + items: + type: object + description: Output from photometry.centroid_location(). Array containing search input parameters (initial guess, search radius) and final centroided location. + properties: + init_guess_x: + description: x pixel coordinate of guess + type: number + init_guess_y: + description: y pixel coordinate of guess + type: number + search_radius: + description: Radius in pixels for source search + type: number + cent_x: + description: x pixel coordinate of centroided source + type: number + cent_y: + description: y pixel coordinate of centroided source + type: number + type: object + description: Cutout image showing selected target centroid in the cutout image. + properties: + centroid_figure: + description: Location of centroid plot. + type: string + diff --git a/catch_analysis_tools/app/services/photometry.py b/catch_analysis_tools/app/services/photometry.py index 7ea5d7c..3cf76db 100644 --- a/catch_analysis_tools/app/services/photometry.py +++ b/catch_analysis_tools/app/services/photometry.py @@ -1,350 +1,199 @@ -import numpy as np -from matplotlib import pyplot as plt -from astropy.io import fits -from photutils.segmentation import detect_sources, detect_threshold, make_2dgaussian_kernel, SourceFinder, SourceCatalog, make_2dgaussian_kernel -from astropy.stats import sigma_clipped_stats, SigmaClip -from photutils.background import SExtractorBackground as SourceExtractorBackground -from photutils.background import Background2D, MedianBackground -from photutils.utils import circular_footprint -from astropy.convolution import convolve -from astropy.coordinates import SkyCoord, ICRS +import os +from catch_analysis_tools.photometry import get_image,subpixel_centroid,create_user_aperture,define_aperture,do_aperture_photometry,source_instr_mag,calibrated_mag +from catch_analysis_tools.background import calc_bkg from astropy.wcs import WCS -from photutils.aperture import aperture_photometry, CircularAnnulus, CircularAperture -from photutils.centroids import centroid_quadratic, centroid_sources, centroid_com, centroid_2dg +import matplotlib +matplotlib.use('Agg') +from matplotlib import pyplot as plt +from astropy.visualization import ZScaleInterval +from astropy.visualization.mpl_normalize import ImageNormalize +import numpy as np from astropy import units as u +from astropy.coordinates import SkyCoord, ICRS +from astropy.io import fits +from catch_analysis_tools.background import global_subtraction -# here are functions for grabbing the data, doing background subtractions and manipulating source extractions -def get_image(url): - """Access a cutout via a call to a CATCH URL - - - Parameters - ---------- - url : string - CATCH-generated URL from a query. - - - - Returns - ------- - data : array_like - This is a 2D array containing the image data returned by the CATCH query - header : - FITS header class, from astropy.io.fits.Header - +def get_world_coordinates(WCS_file,x,y): + """ + Accepts an astropy-readable WCS object (a .wcs or fits file ) and outputs the world coordinates of an (x,y) pixel point in decimal degrees. + """ + #WCS_file = body['WCS_file'] + #x = body['x'] + #y = body['y'] + world_coords = WCS(fits.open(WCS_file)[0].header) + loc = world_coords.pixel_to_world(x,y) + print(loc) + transform_results = { + "x":x, + "y":y, + "ra":loc.ra.deg, + "dec":loc.dec.deg + } + return transform_results +def get_pixel_coordinates(WCS_file,ra,dec): """ + Accepts an astropy-readable WCS object (a .wcs or fits file) and outputs the (x,y) pixel coordinates of an (ra,dec) point in decimal degrees. + """ + #WCS_file = body['WCS_file'] + #ra = body['ra'] + #dec = body['dec'] + world_coords = WCS(fits.open(WCS_file)[0].header) + sky_loc = SkyCoord(ICRS(ra=ra*u.deg, dec=dec*u.deg)) + loc = world_coords.world_to_pixel(sky_loc) + x = loc[0].item() + y = loc[1].item() + + transform_results = { + "x":x, + "y":y, + "ra":ra, + "dec":dec + } + return transform_results - fits_hdu = fits.open(url) - data = fits_hdu[0].data - header = fits_hdu[0].header - return data, header +def centroid(file,target_x,target_y,search_radius): + """ + Searches for source nearby to expected ephemeris location in image, assumes that astrometry solution has been rerun and that both the cutout .fits file and the redone .wcs file exist. + -def id_good_sources(data,bkg): - - """Uses a segmentation image to identify reliable sources in NON-BACKGROUND-SUBTRACTED image that can be snapped to. - - Coincidentally, computes baseline photometry that could be used as a quality comparison user results, - though this flux isn't always a good comparison as it often underestimates the source size - - Parameters ---------- - data : array_like - 2D image array to be background subtracted - - bkg : - background object returned from get_background() or global_subtraction() - - Returns - ------- - cat : - Astropy Table class, from SourceCatalog output giving source locations, fluxes - - - - """ - - source_threshold = bkg.background_median + 1.5 * bkg.background_rms - - - kernel = make_2dgaussian_kernel(3.0, size=5) # FWHM = 3.0 - convolved_data = convolve(data, kernel) - finder = SourceFinder(npixels=5, progress_bar=False) - segment_map = finder(convolved_data, source_threshold) - - - vmax = np.percentile(np.ndarray.flatten(data),99) - vmin = np.percentile(np.ndarray.flatten(data),1) - - # make a plot to show the background subtracted frame and the resulting segment map - #fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 12.5)) - #ax1.imshow(data, origin='lower', cmap='Greys_r', vmin=vmin,vmax=vmax) - #ax1.set_title('Original Data') - - #ax2.imshow(segment_map, origin='lower', cmap=segment_map.cmap, - # interpolation='nearest') - #ax2.set_title('Segmentation Image') + file : string + Base filename (without .fits or .wcs extension) taken from CATCH-generated query URL. - - cat = SourceCatalog(data, segment_map, convolved_data=convolved_data) - - return cat - -def create_user_aperture(position,size): - """Simple placeholder function for making user-selected circular apertures + target_x : float + Target x pixel location, to be used as initial guess for the object. + target_y : float + Target y pixel ephemeris location, to be used as initial guess for the object. - Parameters - ---------- - Position : array_like - [x,y] location of aperture center + search_radius : float + Radius in pixels of search area (centered on (x,y) ) for centroiding - Size : Int - Radius of aperture, given in pixels Returns ------- - aperture : - Photutils circular aperture object with the specified size, location parameters - - + search_results : + array_like + + + figname: + string + Output plot showing the default aperture + annulus extraction onto the cutout image. + """ - """ - aperture = CircularAperture(position, r=size) - return aperture - -def subpixel_centroid(user_point,data,radius): - - """Takes in a user defined point and returns the location of the brightest pixel within radius pixels - - - Parameters - ---------- - user_point : array_like - [x,y] location of desired point, to be snapped from + #tmp_wcs = WCS(file+'.wcs') + img, header = get_image(file) - data : array_like - image to be searched for sources to be snapped to + #target_pix = get_WCS_pixel(tmp_wcs,target_ra,target_dec) + #target_x = target_pix[0].item() + #target_y = target_pix[1].item() - radius : Int - number of pixels within which objects can be snapped to from user_point - - Returns - ------- - target_position: array_like - [x,y] location of the source that is closest to user_point found in data + cent_pix = subpixel_centroid([target_x,target_y],img,search_radius) + #cent_loc = get_pixel_WCS(tmp_wcs,cent_pix[0],cent_pix[1]) + search_results = { + "init_guess_x":target_x, + "init_guess_y":target_y, + "search_radius":search_radius, + "cent_x":cent_pix[0], + "cent_y":cent_pix[1], + } + print(search_results) + interval = ZScaleInterval() + norm = ImageNormalize(img, interval=ZScaleInterval()) + + + plt.figure(figsize=(8,8)) + plt.imshow(img,norm=norm,cmap='gray_r') + plt.scatter(img.shape[0]/2,img.shape[1]/2,s=100,marker='x',label='Image Center') + plt.scatter(target_x,target_y,s=100,marker='+',label='Initial Guess') + plt.scatter(cent_pix[0],cent_pix[1],s=50,marker='.',c='yellow',label='Centroid Location') + plt.xlim(target_x-20,target_x+20) + plt.ylim(target_y-20,target_y+20) + plt.legend() - """ - - footprint = circular_footprint(radius) - x, y = centroid_sources(data, user_point[0], user_point[1], footprint=footprint, - centroid_func=centroid_2dg) - - target_position = np.array([x[0],y[0]]) - return target_position -def do_aperture_photometry(data,source_aperture,bkg_median, bkg_var, bkg_aperture): - """ Takes in an image, a source aperture, and outputs from the calc_annulus_bkg function - - Returns the source flux (background subtracted, per-pixel background median) and the - uncertainty as defined at the quoted link - - method='center' means pixels are either in or out, no interpolation to a perfect circle - (in other words, areas will be in whole pixels) - - Parameters - ---------- - data : array_like - image data to be used for photometry - - source_aperture : - Photutils aperture object containing desired source - - bkg_median : float - median value of pixel background, - - bkg_var : float - variance of pixel background, ideally from output of calc_annulus_background - bkg_aperture : - Photutils aperture object containing background, ideally as annulus_aperture output from calc_annulus_background + file_base = os.path.splitext(file)[0] + figname = 'centroid.png' + plt.savefig(figname) + plt.close() + centroid_results = { + "search_results": search_results, + "centroid_figure": figname + } + return centroid_results - Returns - ------- - source_sum : float - background subtracted flux of the targeted source - - source_err : float - error on source flux +#todo: create function that takes target location, background location and outputs aperture objects +# this function can be fed the outputs of centroid_location +def target_extraction(body): """ - - aperture_mask = source_aperture.to_mask(method='center') - aperture_data = aperture_mask.multiply(data) - aperture_sum = np.nansum(aperture_data) - - background = bkg_median * source_aperture.area - source_sum = aperture_sum-background - + Searches for source nearby to expected ephemeris location in image, assumes that astrometry solution has been rerun and that both the cutout .fits file and the redone .wcs file exist. - # Using uncertainty as derived by https://wise2.ipac.caltech.edu/staff/fmasci/ApPhotUncert.pdf - # Setting the gain g=1, N_i = 1. Assumes data has already been converted to e- - ### TODO: FIND THE GAINS FOR EACH SURVEY - term1 = source_sum - term2 = (source_aperture.area + (np.pi/2) * (np.square(source_aperture.area)/bkg_aperture.area) )*bkg_var - source_err = np.sqrt(term1 + term2) - return source_sum, source_err - -def load_thumbnail(url): - """Access a cutout via a call to a CATCH URL, returning WCSobject - - Parameters ---------- - url : string - CATCH-generated URL from a query. - - - - Returns - ------- - data : array_like - This is a 2D array containing the image data returned by the CATCH query - - header : - FITS header class, from astropy.io.fits.Header - - img_WCS: - Astropy WCS object - + body : dict + Request body containing file, target_aperture_params, and background_aperture_params. """ - fits_hdu = fits.open(url) - data = fits_hdu[0].data - header = fits_hdu[0].header - img_WCS = WCS(header) - return data, header, img_WCS - -def get_pixel_WCS(img_WCS,pixel): - """Retrieve WCS location of a pixel given its (x,y) position - - Parameters - ---------- - - img_WCS: - Astropy WCS object + file = body['file'] + target_aperture_params = body['target_aperture_params'] + background_aperture_params = body['background_aperture_params'] - pixel: array_like - [x,y] pixel location to get WCS location of + #tmp_wcs = WCS(filebase+'.wcs') + img, header = get_image(file) - Returns - ------- - loc: - Astropy SkyCoord location of desired pixel - """ - - loc = img_WCS.pixel_to_world(pixel[0],pixel[1]) - return loc -def get_WCS_pixel(img_WCS,ra_dec): - """Retrieve pixel location at a given (RA, Dec) sky coordinate position + data_sub, twoD_bkg = global_subtraction(img) + file_base = os.path.splitext(file)[0] + #targ_loc = get_WCS_pixel(tmp_wcs,target_ra,target_dec) - Parameters - ---------- - - img_WCS: - Astropy WCS object + #centroid_results = centroid_location(filebase,target_x,target_y,search_radius) + #targ_cent = np.array([centroid_results["search_results"]["cent_x"],centroid_results["search_results"]["cent_y"]]) - ra_dec: array_like - [Right Ascension, Declination] location to retrieve pixel location of + target_aperture = define_aperture(target_aperture_params) + background_aperture = define_aperture(background_aperture_params) - Returns - ------- + interval = ZScaleInterval() + norm = ImageNormalize(data_sub, interval=ZScaleInterval()) - loc: array_like - Pixel location of specified (RA, Dec) - """ + bkg, bkg_var = calc_bkg(data_sub,background_aperture,'median',None) + + target_flux, target_fluxerr = do_aperture_photometry(img,target_aperture,background_aperture) + #instr_mag = source_instr_mag(aperture_flux,aperture_fluxerr,1) + #cal_mag = calibrated_mag(instr_mag,header['zp'],header['zp_std']) # temporarily using the original Atlas header values - sky_loc = SkyCoord(ICRS(ra=ra_dec[0]*u.deg, dec=ra_dec[1]*u.deg)) - loc = img_WCS.world_to_pixel(sky_loc) - return loc + #cal_mag_array = calibrated_mag(instr_mag,header['magzpt'],header['zprms']) -def source_instr_mag(ap_flux,ap_fluxerr,exposure_time): + # could put code to filter out frames where targ_loc and targ_cent vary by more than a couple pixels here + # (would mean star hit) + + plt.figure(figsize=(8,8)) + plt.imshow(data_sub,norm=norm,cmap='gray_r') + target_aperture.plot(color='blue', lw=1.5, alpha=0.5) + background_aperture.plot(color='yellow',lw=1.5) + #plt.scatter(img.shape[0]/2,img.shape[1]/2,s=100,marker='x') + plt.scatter(target_aperture_params["position"][0],target_aperture_params["position"][1],s=100,marker='+') + plt.scatter(background_aperture_params["position"][0],background_aperture_params["position"][1],s=50,marker='.',c='yellow') + plt.xlim(target_aperture_params["position"][0]-20,target_aperture_params["position"][0]+20) + plt.ylim(target_aperture_params["position"][1]-20,target_aperture_params["position"][1]+20) - """ Quick function to return instrumental magnitudes from a source flux - Does not force magnitude uncertainties to be symmetric - To be used by calibrated_mag() function - - - Parameters - ---------- - - ap_flux: float - Flux (in counts) of source - - ap_fluxerr: float - Flux error (in counts) - - exposure_time: float - integration time of the frame (s) - - Returns - ------- - - instr_mag_array: array_like - array containing source instrumental magnitude and uncertainties, - as [Magnitude, Upper Magnitude Uncertainty, Lower Magnitude Uncertainty] - """ - instr_mag = -2.5*np.log10(ap_flux/exposure_time) - instr_mag_hi = -2.5*np.log10((ap_flux-ap_fluxerr)/exposure_time) - instr_mag_lo = -2.5*np.log10((ap_flux+ap_fluxerr)/exposure_time) - - instr_mag_hi_uncert = instr_mag_hi - instr_mag - instr_mag_lo_uncert = instr_mag_lo - instr_mag - - inst_mag_array = { - "instr_mag": float(instr_mag), - "instr_mag_hi_uncert": float(instr_mag_hi_uncert), - "instr_mag_lo_uncert": float(instr_mag_lo_uncert) + figname = 'aperture_extraction.png' + plt.savefig(figname) + plt.close() + extraction_results = { + "aperture_flux": target_flux, + "aperture_fluxerr": target_fluxerr, + "aperture_figure": figname } - return inst_mag_array - -def calibrated_mag(instr_mag_array,zero_point,zero_point_uncert): - """ Takes in the array from source_instr_mag, converts to derived magnitude, - propagating uncertainties from both - - - Parameters - ---------- - instr_mag_array: array_like - From source_instr_mag() output: array containing source instrumental magnitude and uncertainties, - as [Magnitude, Upper Magnitude Uncertainty, Lower Magnitude Uncertainty] - zero_point: float - zero point magnitude of image (ideally taken from metadata in header given by astrometry solution) - - zero_point_uncert: float - zero point uncertainty (mags) of image (ideally taken from metadata in header given by astrometry solution) - - Returns - ------- - - calib_mag_array: array_like - array containing source CALIBRATED magnitude and uncertainties, - as [Magnitude, Upper Magnitude Uncertainty, Lower Magnitude Uncertainty] - """ - - calib_mag = zero_point+instr_mag_array[0] - calib_mag_hi = np.sqrt(np.square(zero_point_uncert) + np.square(instr_mag_array[1])) - calib_mag_lo = np.sqrt(np.square(zero_point_uncert) + np.square(instr_mag_array[2])) - - calib_mag_array = np.array([calib_mag,calib_mag_hi,calib_mag_lo]) - return calib_mag_array + return extraction_results diff --git a/catch_analysis_tools/app/services/subtract_median_background.py b/catch_analysis_tools/app/services/subtract_median_background.py new file mode 100644 index 0000000..d5b9d63 --- /dev/null +++ b/catch_analysis_tools/app/services/subtract_median_background.py @@ -0,0 +1,52 @@ +import os +import requests +from astropy.io import fits +from catch_analysis_tools.background import global_subtraction +from catch_analysis_tools.photometry import get_image + +def download_file(url): + response = requests.get(url) + if "content-disposition" in response.headers: + content_disposition = response.headers["content-disposition"] + filename = content_disposition.split("filename=")[1] + else: + filename = url.split("/")[-1] + cleaned_filename = filename.replace(" ", "") + cleaned_filename = cleaned_filename.replace("\"", "") + with open(cleaned_filename, mode="wb") as file: + file.write(response.content) + print(cleaned_filename) + print(f"Downloaded file {filename} as {cleaned_filename}") + + return os.path.splitext(cleaned_filename)[0] + +def perform_median_subtraction(url): + """Takes a cutout URL from CATCH, opens the file, performs a median background subtraction + after masking sources. Returns background subtracted image. + + Parameters: + ---------- + url: str + URL to CATCH cutout image + + Returns: + ------- + subt_fname: str + Filename of background subtracted image saved locally + """ + + # Download file and get file base name + file_base = download_file(url) + # Get image data + data, header = get_image(file_base+'.fits') + + # Perform global background subtraction + data_sub, bkg = global_subtraction(data) + header['BKG_MED'] = bkg.background_median + + # Save background subtracted image + subtract_fname = file_base+'_subtracted.fits' + + fits.writeto(subtract_fname, data_sub, header, overwrite=True) + + return subtract_fname diff --git a/catch_analysis_tools/background.py b/catch_analysis_tools/background.py index 7f3e0d9..340022b 100644 --- a/catch_analysis_tools/background.py +++ b/catch_analysis_tools/background.py @@ -77,13 +77,11 @@ def global_subtraction(data): return data_sub, bkg -def calc_annulus_bkg(data,position,inner_r,outer_r): +def calc_bkg(data,background_aperture,method,sigma_clip): - """ Takes in an image array and position + inner/outer radii for a circular annulus - Computes and returns background median, variance from pixels within this annulus - - Currently exports the annulus object, too, although this might not be necessary - + """ Takes in an image array and aperture for background estimate. Determines background + estimator per specified method (mean or median) and variance of pixels, with optional + sigma clipping. Parameters @@ -102,33 +100,45 @@ def calc_annulus_bkg(data,position,inner_r,outer_r): Returns ------- - bkg_median : float - median value of pixels within the annulus + bkg_estimator : float + estimate of true background level in background aperture, per "method" bkg_var : float - variance of pixels within the annulus, as square of range of values from 50-16th percentiles - - annulus_aperture : - Photutils annulus aperture object with the specified size, location parameters - + Variance of pixels within the background aperture. If sigma clipped, square of + clipped stddev. Otherwise, as square of range of values from 50-16th percentiles """ - annulus_aperture = CircularAnnulus(position, r_in=inner_r, r_out=outer_r) - annulus_mask = annulus_aperture.to_mask(method='center') - annulus_data = annulus_mask.multiply(data) - annulus_data_1d = annulus_data[annulus_mask.data > 0] + background_mask = background_aperture.to_mask(method='center') + + background_data = background_mask.multiply(data) + background_data_1d = background_data[background_mask.data > 0] + #Todo: Add flexibility for the stats for computing background. Include rectangular aperture. + if sigma_clip is not None: + bkg_mean,bkg_median,bkg_stddev = sigma_clipped_stats(background_data_1d, sigma=sigma_clip, maxiters=10) + bkg_var = bkg_stddev**2 + else: + bkg_median = np.nanmedian(background_data_1d) + bkg_mean = np.nanmean(background_data_1d) + # robust approximation via https://wise2.ipac.caltech.edu/staff/fmasci/ApPhotUncert.pdf + bkg_var = np.square(np.percentile(background_data_1d,50)-np.percentile(background_data_1d,16)) + bkg_stddev = np.sqrt(bkg_var) - bkg_median = np.nanmedian(annulus_data_1d) - # robust approximation via https://wise2.ipac.caltech.edu/staff/fmasci/ApPhotUncert.pdf - bkg_var = np.square(np.percentile(annulus_data_1d,50)-np.percentile(annulus_data_1d,16)) + if method=='mean': + bkg_estimator = bkg_mean + elif method=='median': + bkg_estimator = bkg_median + else: + raise ValueError("Method must be 'mean' or 'median'") + + #plt.figure(figsize=(8,8)) # optional code to check the standard deviation estimate of the background - plt.axvspan(bkg_median-np.sqrt(bkg_var),bkg_median+np.sqrt(bkg_var),alpha=0.5) - plt.axvline(bkg_median) - plt.hist(annulus_data_1d) + #plt.axvspan(bkg_estimator-bkg_stddev,bkg_estimator+bkg_stddev,alpha=0.5) + #plt.axvline(bkg_estimator,color='red') + #plt.hist(annulus_data_1d) - return bkg_median, bkg_var, annulus_aperture + return bkg_estimator, bkg_var diff --git a/catch_analysis_tools/photometry.py b/catch_analysis_tools/photometry.py index 54a9309..2cbca3d 100644 --- a/catch_analysis_tools/photometry.py +++ b/catch_analysis_tools/photometry.py @@ -9,8 +9,8 @@ from astropy.convolution import convolve from astropy.coordinates import SkyCoord, ICRS from astropy.wcs import WCS -from photutils.aperture import aperture_photometry, CircularAnnulus, CircularAperture -from photutils.centroids import centroid_quadratic, centroid_sources, centroid_com +from photutils.aperture import aperture_photometry, CircularAnnulus, CircularAperture, RectangularAperture +from photutils.centroids import centroid_quadratic, centroid_sources from astropy import units as u # here are functions for grabbing the data, doing background subtractions and manipulating source extractions @@ -115,6 +115,20 @@ def create_user_aperture(position,size): aperture = CircularAperture(position, r=size) return aperture +def define_aperture(aperture_params): + # Defines an aperture from user supplied parameters, used both to create target and background apertires + if aperture_params["shape"]=="Circular": + aperture = CircularAperture(aperture_params["position"], r=aperture_params["size"]) + elif aperture_params["shape"]=="Circular_Annulus": + aperture = CircularAnnulus(aperture_params["position"], r_in=aperture_params["inner_r"], r_out=aperture_params["outer_r"]) + elif aperture_params["shape"]=="Rectangular": + aperture = RectangularAperture(aperture_params["position"], aperture_params["w"], aperture_params["h"], theta=aperture_params["theta"]) + else: + aperture = 0 + return aperture + + + def subpixel_centroid(user_point,data,radius): """Takes in a user defined point and returns the location of the brightest pixel within radius pixels @@ -141,12 +155,12 @@ def subpixel_centroid(user_point,data,radius): footprint = circular_footprint(radius) x, y = centroid_sources(data, user_point[0], user_point[1], footprint=footprint, - centroid_func=centroid_com) + centroid_func=centroid_quadratic) target_position = np.array([x[0],y[0]]) return target_position -def do_aperture_photometry(data,source_aperture,bkg_median, bkg_var, bkg_aperture): +def do_aperture_photometry(data,source_aperture, bkg_aperture): """ Takes in an image, a source aperture, and outputs from the calc_annulus_bkg function Returns the source flux (background subtracted, per-pixel background median) and the @@ -186,16 +200,21 @@ def do_aperture_photometry(data,source_aperture,bkg_median, bkg_var, bkg_apertur aperture_mask = source_aperture.to_mask(method='center') aperture_data = aperture_mask.multiply(data) aperture_sum = np.nansum(aperture_data) - - background = bkg_median * source_aperture.area + aperture_area = np.nansum(aperture_mask) + + bkg_mask = bkg_aperture.to_mask(method='center') + bkg_area = np.nansum(bkg_mask) + bkg_data = bkg_mask.multiply(data) + bkg_median = np.nanmedian(bkg_data) + bkg_var = np.nanvar(bkg_data[bkg_data != 0]) + background = bkg_median * aperture_area source_sum = aperture_sum-background - # Using uncertainty as derived by https://wise2.ipac.caltech.edu/staff/fmasci/ApPhotUncert.pdf # Setting the gain g=1, N_i = 1. Assumes data has already been converted to e- ### TODO: FIND THE GAINS FOR EACH SURVEY term1 = source_sum - term2 = (source_aperture.area + (np.pi/2) * (np.square(source_aperture.area)/bkg_aperture.area) )*bkg_var + term2 = (aperture_area + (np.pi/2) * (np.square(aperture_area)/bkg_area) )*bkg_var source_err = np.sqrt(term1 + term2) return source_sum, source_err @@ -228,51 +247,6 @@ def load_thumbnail(url): header = fits_hdu[0].header img_WCS = WCS(header) return data, header, img_WCS - -def get_pixel_WCS(img_WCS,pixel): - """Retrieve WCS location of a pixel given its (x,y) position - - Parameters - ---------- - - img_WCS: - Astropy WCS object - - pixel: array_like - [x,y] pixel location to get WCS location of - - - Returns - ------- - loc: - Astropy SkyCoord location of desired pixel - """ - - loc = img_WCS.pixel_to_world(pixel[0],pixel[1]) - return loc - -def get_WCS_pixel(img_WCS,ra_dec): - """Retrieve pixel location at a given (RA, Dec) sky coordinate position - - Parameters - ---------- - - img_WCS: - Astropy WCS object - - ra_dec: array_like - [Right Ascension, Declination] location to retrieve pixel location of - - Returns - ------- - - loc: array_like - Pixel location of specified (RA, Dec) - """ - - sky_loc = SkyCoord(ICRS(ra=ra_dec[0]*u.deg, dec=ra_dec[1]*u.deg)) - loc = img_WCS.world_to_pixel(sky_loc) - return loc def source_instr_mag(ap_flux,ap_fluxerr,exposure_time): @@ -341,7 +315,10 @@ def calibrated_mag(instr_mag_array,zero_point,zero_point_uncert): calib_mag = zero_point+instr_mag_array[0] calib_mag_hi = np.sqrt(np.square(zero_point_uncert) + np.square(instr_mag_array[1])) calib_mag_lo = np.sqrt(np.square(zero_point_uncert) + np.square(instr_mag_array[2])) - - calib_mag_array = np.array([calib_mag,calib_mag_hi,calib_mag_lo]) + calib_mag_array = { + "cal_mag": calib_mag, + "cal_mag_hi_uncert": calib_mag_hi, + "cal_mag_lo_uncert": calib_mag_lo + } return calib_mag_array From 9ecb18a1b2f8e84104d9d948267ceaeb5baee7f5 Mon Sep 17 00:00:00 2001 From: bensharkey Date: Wed, 20 May 2026 15:26:02 -0400 Subject: [PATCH 2/4] Responding to review comments --- catch_analysis_tools/app/api/openapi.yaml | 12 ++--- .../app/services/photometry.py | 49 ++++--------------- .../services/subtract_median_background.py | 15 +++++- catch_analysis_tools/photometry.py | 4 +- 4 files changed, 31 insertions(+), 49 deletions(-) diff --git a/catch_analysis_tools/app/api/openapi.yaml b/catch_analysis_tools/app/api/openapi.yaml index 3ad3969..268899b 100644 --- a/catch_analysis_tools/app/api/openapi.yaml +++ b/catch_analysis_tools/app/api/openapi.yaml @@ -281,14 +281,14 @@ paths: type: string - name: x in: query - description: "Pixel x location." + description: "Pixel x location (0-indexed)." example: 154 required: true schema: type: number - name: y in: query - description: "Pixel y location." + description: "Pixel y location (0-indexed)." example: 154 required: true schema: @@ -376,10 +376,10 @@ paths: description: Output from photometry.get_pixel_coordinates(). Contains input location and output pixel coordinates. properties: x: - description: x pixel coordinate + description: x pixel coordinate (0-indexed) type: number y: - description: y pixel coordinate + description: y pixel coordinate (0-indexed) type: number ra: description: RA value of point in decimal degrees @@ -423,7 +423,7 @@ paths: example: "Circular" position: type: array - description: "Pixel coordinate pair [x, y] for the aperture center." + description: "Zero-indexed pixel coordinate pair [x, y] for the aperture center." items: type: number minItems: 2 @@ -455,7 +455,7 @@ paths: example: "Circular_Annulus" position: type: array - description: "Pixel coordinate pair [x, y] for the aperture center." + description: "Zero-indexed pixel coordinate pair [x, y] for the aperture center." items: type: number minItems: 2 diff --git a/catch_analysis_tools/app/services/photometry.py b/catch_analysis_tools/app/services/photometry.py index 3cf76db..62d1de2 100644 --- a/catch_analysis_tools/app/services/photometry.py +++ b/catch_analysis_tools/app/services/photometry.py @@ -1,25 +1,19 @@ import os -from catch_analysis_tools.photometry import get_image,subpixel_centroid,create_user_aperture,define_aperture,do_aperture_photometry,source_instr_mag,calibrated_mag -from catch_analysis_tools.background import calc_bkg +from ...photometry import get_image,subpixel_centroid,define_aperture,do_aperture_photometry from astropy.wcs import WCS import matplotlib matplotlib.use('Agg') from matplotlib import pyplot as plt from astropy.visualization import ZScaleInterval from astropy.visualization.mpl_normalize import ImageNormalize -import numpy as np from astropy import units as u from astropy.coordinates import SkyCoord, ICRS from astropy.io import fits -from catch_analysis_tools.background import global_subtraction def get_world_coordinates(WCS_file,x,y): """ - Accepts an astropy-readable WCS object (a .wcs or fits file ) and outputs the world coordinates of an (x,y) pixel point in decimal degrees. + Accepts an astropy-readable WCS object (a .wcs or fits file ) and outputs the world coordinates of a 0-indexed (x,y) pixel point in decimal degrees. """ - #WCS_file = body['WCS_file'] - #x = body['x'] - #y = body['y'] world_coords = WCS(fits.open(WCS_file)[0].header) loc = world_coords.pixel_to_world(x,y) print(loc) @@ -33,11 +27,8 @@ def get_world_coordinates(WCS_file,x,y): def get_pixel_coordinates(WCS_file,ra,dec): """ - Accepts an astropy-readable WCS object (a .wcs or fits file) and outputs the (x,y) pixel coordinates of an (ra,dec) point in decimal degrees. + Accepts an astropy-readable WCS object (a .wcs or fits file) and outputs the 0-indexed (x,y) pixel coordinates of an (ra,dec) point in decimal degrees. """ - #WCS_file = body['WCS_file'] - #ra = body['ra'] - #dec = body['dec'] world_coords = WCS(fits.open(WCS_file)[0].header) sky_loc = SkyCoord(ICRS(ra=ra*u.deg, dec=dec*u.deg)) loc = world_coords.world_to_pixel(sky_loc) @@ -83,16 +74,9 @@ def centroid(file,target_x,target_y,search_radius): Output plot showing the default aperture + annulus extraction onto the cutout image. """ - - #tmp_wcs = WCS(file+'.wcs') img, header = get_image(file) - #target_pix = get_WCS_pixel(tmp_wcs,target_ra,target_dec) - #target_x = target_pix[0].item() - #target_y = target_pix[1].item() - cent_pix = subpixel_centroid([target_x,target_y],img,search_radius) - #cent_loc = get_pixel_WCS(tmp_wcs,cent_pix[0],cent_pix[1]) search_results = { "init_guess_x":target_x, @@ -101,8 +85,7 @@ def centroid(file,target_x,target_y,search_radius): "cent_x":cent_pix[0], "cent_y":cent_pix[1], } - print(search_results) - interval = ZScaleInterval() + norm = ImageNormalize(img, interval=ZScaleInterval()) @@ -117,7 +100,7 @@ def centroid(file,target_x,target_y,search_radius): - file_base = os.path.splitext(file)[0] + figname = 'centroid.png' plt.savefig(figname) plt.close() @@ -145,40 +128,26 @@ def target_extraction(body): target_aperture_params = body['target_aperture_params'] background_aperture_params = body['background_aperture_params'] - - #tmp_wcs = WCS(filebase+'.wcs') img, header = get_image(file) + - data_sub, twoD_bkg = global_subtraction(img) - file_base = os.path.splitext(file)[0] - #targ_loc = get_WCS_pixel(tmp_wcs,target_ra,target_dec) - - #centroid_results = centroid_location(filebase,target_x,target_y,search_radius) - #targ_cent = np.array([centroid_results["search_results"]["cent_x"],centroid_results["search_results"]["cent_y"]]) + target_aperture = define_aperture(target_aperture_params) background_aperture = define_aperture(background_aperture_params) - interval = ZScaleInterval() - norm = ImageNormalize(data_sub, interval=ZScaleInterval()) - - bkg, bkg_var = calc_bkg(data_sub,background_aperture,'median',None) + norm = ImageNormalize(img, interval=ZScaleInterval()) target_flux, target_fluxerr = do_aperture_photometry(img,target_aperture,background_aperture) - #instr_mag = source_instr_mag(aperture_flux,aperture_fluxerr,1) - #cal_mag = calibrated_mag(instr_mag,header['zp'],header['zp_std']) # temporarily using the original Atlas header values - - #cal_mag_array = calibrated_mag(instr_mag,header['magzpt'],header['zprms']) # could put code to filter out frames where targ_loc and targ_cent vary by more than a couple pixels here # (would mean star hit) plt.figure(figsize=(8,8)) - plt.imshow(data_sub,norm=norm,cmap='gray_r') + plt.imshow(img,norm=norm,cmap='gray_r') target_aperture.plot(color='blue', lw=1.5, alpha=0.5) background_aperture.plot(color='yellow',lw=1.5) - #plt.scatter(img.shape[0]/2,img.shape[1]/2,s=100,marker='x') plt.scatter(target_aperture_params["position"][0],target_aperture_params["position"][1],s=100,marker='+') plt.scatter(background_aperture_params["position"][0],background_aperture_params["position"][1],s=50,marker='.',c='yellow') plt.xlim(target_aperture_params["position"][0]-20,target_aperture_params["position"][0]+20) diff --git a/catch_analysis_tools/app/services/subtract_median_background.py b/catch_analysis_tools/app/services/subtract_median_background.py index d5b9d63..801420f 100644 --- a/catch_analysis_tools/app/services/subtract_median_background.py +++ b/catch_analysis_tools/app/services/subtract_median_background.py @@ -1,10 +1,21 @@ import os import requests from astropy.io import fits -from catch_analysis_tools.background import global_subtraction -from catch_analysis_tools.photometry import get_image +from ...background import global_subtraction +from ...photometry import get_image def download_file(url): + """ + Downloads a file from a given URL and saves it locally, returning the base filename (without extension). + Parameters: + ---------- + url: str + URL to the file to be downloaded + Returns: + ------- + file_base: str + Base filename (without extension) of the downloaded file + """ response = requests.get(url) if "content-disposition" in response.headers: content_disposition = response.headers["content-disposition"] diff --git a/catch_analysis_tools/photometry.py b/catch_analysis_tools/photometry.py index 2cbca3d..0ede353 100644 --- a/catch_analysis_tools/photometry.py +++ b/catch_analysis_tools/photometry.py @@ -116,7 +116,9 @@ def create_user_aperture(position,size): return aperture def define_aperture(aperture_params): - # Defines an aperture from user supplied parameters, used both to create target and background apertires + """ + Defines an aperture from user supplied parameters, used both to create target and background apertures + """ if aperture_params["shape"]=="Circular": aperture = CircularAperture(aperture_params["position"], r=aperture_params["size"]) elif aperture_params["shape"]=="Circular_Annulus": From cb065a7b0d77a1850f7a8de6baca342f246e8992 Mon Sep 17 00:00:00 2001 From: bensharkey Date: Thu, 28 May 2026 13:36:17 -0400 Subject: [PATCH 3/4] Fixing tests, adding exception handling for WCS transformations --- .../app/services/photometry.py | 24 +++++++++++-- catch_analysis_tools/tests/photometry_test.py | 36 ++++++++++--------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/catch_analysis_tools/app/services/photometry.py b/catch_analysis_tools/app/services/photometry.py index 62d1de2..28905dd 100644 --- a/catch_analysis_tools/app/services/photometry.py +++ b/catch_analysis_tools/app/services/photometry.py @@ -14,7 +14,17 @@ def get_world_coordinates(WCS_file,x,y): """ Accepts an astropy-readable WCS object (a .wcs or fits file ) and outputs the world coordinates of a 0-indexed (x,y) pixel point in decimal degrees. """ - world_coords = WCS(fits.open(WCS_file)[0].header) + try: + world_coords = WCS(fits.open(WCS_file)[0].header) + except Exception as e: + try: + world_coords = WCS(WCS_file) + except Exception as e: + try: + world_coords = WCS_file + except Exception as e: + raise ValueError("Could not read WCS file. Please ensure that the file is either a .wcs file or a .fits file with a valid WCS solution in the header.") + loc = world_coords.pixel_to_world(x,y) print(loc) transform_results = { @@ -29,7 +39,17 @@ def get_pixel_coordinates(WCS_file,ra,dec): """ Accepts an astropy-readable WCS object (a .wcs or fits file) and outputs the 0-indexed (x,y) pixel coordinates of an (ra,dec) point in decimal degrees. """ - world_coords = WCS(fits.open(WCS_file)[0].header) + try: + world_coords = WCS(fits.open(WCS_file)[0].header) + except Exception as e: + try: + world_coords = WCS(WCS_file) + except Exception as e: + try: + world_coords = WCS_file + except Exception as e: + raise ValueError("Could not read WCS file. Please ensure that the file is either a .wcs file or a .fits file with a valid WCS solution in the header.") + sky_loc = SkyCoord(ICRS(ra=ra*u.deg, dec=dec*u.deg)) loc = world_coords.world_to_pixel(sky_loc) x = loc[0].item() diff --git a/catch_analysis_tools/tests/photometry_test.py b/catch_analysis_tools/tests/photometry_test.py index e1b88d0..e0c572d 100644 --- a/catch_analysis_tools/tests/photometry_test.py +++ b/catch_analysis_tools/tests/photometry_test.py @@ -7,7 +7,7 @@ from ..photometry import * from ..background import * - +from ..app.services.photometry import * @pytest.mark.remote_data def test_image(): test_url = 'https://sbnsurveys.astro.umd.edu/api/images/urn%3Anasa%3Apds%3Agbo.ast.neat.survey%3Adata_tricam%3Ap20020121_obsdata_20020121132624c?format=fits&size=10.00arcmin&ra=177.51011&dec=15.25013' @@ -28,38 +28,40 @@ def test_create_aperture(): aperture = create_user_aperture((11,54),12) assert approx(aperture.area) == 452.3893421169302 +def test_define_aperture(): + aperture = define_aperture(({"shape":"Circular","position":[11,54],"size":12})) + assert approx(aperture.area) == 452.3893421169302 + def test_subpixel_centroid(): data = np.zeros((100,100)) data[40:50,40:50] = 5+data[40:50,40:50] - #data[45,45] = 100 + data[45,45] = 100 source = subpixel_centroid([41,48],data,20) - plt.imshow(data) - print(source) - assert approx(source) == np.array([44.5, 44.5]) + assert approx(source) == np.array([45., 45.]) def test_do_aperture_photometry(): import photutils.datasets noise = photutils.datasets.make_noise_image((100,100), distribution='gaussian', mean=0, stddev=1, seed=1) - noise[50,50] = 100 # give us a "source" with flux of 100 to recover - bkg_median, bkg_var, bkg_aperture = calc_annulus_bkg(noise,(50,50),5,10) - source_aperture = create_user_aperture((50,50),5) - source_sum, source_err = do_aperture_photometry(noise,source_aperture,bkg_median,bkg_var,bkg_aperture) - assert approx([source_sum, source_err]) == [92.28235162232964, 15.19308011549977] + noise[50,50] = noise[50,50]+ 100 # give us a "source" with flux of 100 to recover + source_aperture = define_aperture({"shape":"Circular","position":[50,50],"size":5}) + bkg_aperture = define_aperture({"shape":"Circular_Annulus","position":[50,50],"size":5,"inner_r":5,"outer_r":10}) + source_sum, source_err = do_aperture_photometry(noise,source_aperture,bkg_aperture) + assert approx([source_sum, source_err]) == [89.2212216869083, 14.261818958632185] -def test_get_pixel_WCS(): +def test_get_world_coordinates(): shape = (100, 100) wcs = make_wcs(shape) - skycoord = get_pixel_WCS(wcs,[42, 57]) - assert approx([skycoord.ra.value,skycoord.dec.value]) == [197.89278975, -1.36561284] + skycoord = get_world_coordinates(wcs,42, 57) + assert approx([skycoord['ra'],skycoord['dec']]) == [197.89278975, -1.36561284] -def test_get_WCS_pixel(): +def test_get_pixel_coordinates(): shape = (100, 100) wcs = make_wcs(shape) - x, y = get_WCS_pixel(wcs,[197.89278975, -1.36561284]) + pixcoord = get_pixel_coordinates(wcs,197.89278975, -1.36561284) - assert all(np.round([x, y]) == [42.0, 57.0]) + assert approx([np.round(pixcoord['x']),np.round(pixcoord['y'])]) == [42.0,57.0] def test_source_instr_mag(): mag = source_instr_mag(10,1,1) @@ -68,5 +70,5 @@ def test_source_instr_mag(): def test_calibrated_mag(): calib_mag = calibrated_mag(source_instr_mag(10,1,1),22,0.5) print (calib_mag) - assert approx(calib_mag[0] + calib_mag[1]) == 20.01291902 + assert approx(calib_mag["cal_mag"] + calib_mag["cal_mag_hi_uncert"]) == 20.01291902 From a546750e56bcd856c6b61d3b8f2303849b890a1e Mon Sep 17 00:00:00 2001 From: bensharkey Date: Thu, 28 May 2026 13:44:14 -0400 Subject: [PATCH 4/4] Fixing final background test --- catch_analysis_tools/tests/background_test.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/catch_analysis_tools/tests/background_test.py b/catch_analysis_tools/tests/background_test.py index e3b3295..9053985 100644 --- a/catch_analysis_tools/tests/background_test.py +++ b/catch_analysis_tools/tests/background_test.py @@ -1,7 +1,7 @@ import numpy as np import pytest from pytest import approx - +from ..photometry import define_aperture from ..background import * def test_global_subtraction(): @@ -21,8 +21,9 @@ def test_get_background(): bkg = get_background(data) assert np.mean(bkg.background) == 1.0 -def test_calc_annulus_bkg(): +def test_calc_bkg(): import photutils.datasets noise = photutils.datasets.make_noise_image((100,100), distribution='gaussian', mean=5, stddev=1, seed=1) - bkg_median, bkg_var, annulus_aperture = calc_annulus_bkg(noise,(50,50),1,40) + bkg_aperture = define_aperture({"shape":"Circular_Annulus","position":[50,50],"size":5,"inner_r":1,"outer_r":40}) + bkg_median, bkg_var= calc_bkg(noise,bkg_aperture,"median",sigma_clip=None) assert approx([bkg_median,bkg_var]) == [4.990637730701752, 0.9217670741324576]