1+ import os
2+ import warnings
13from datetime import datetime
24
35import bqplot
46import numpy as np
57from astropy import units as u
8+ from astropy .modeling .fitting import LevMarLSQFitter
9+ from astropy .modeling import Parameter
10+ from astropy .modeling .models import Gaussian1D
611from astropy .table import QTable
712from astropy .time import Time
813from ipywidgets import widget_serialization
@@ -41,6 +46,8 @@ class SimpleAperturePhotometry(TemplateMixin, DatasetSelectMixin):
4146 current_plot_type = Unicode ().tag (sync = True )
4247 plot_available = Bool (False ).tag (sync = True )
4348 radial_plot = Any ('' ).tag (sync = True , ** widget_serialization )
49+ fit_radial_profile = Bool (False ).tag (sync = True )
50+ fit_results = List ().tag (sync = True )
4451
4552 def __init__ (self , * args , ** kwargs ):
4653 super ().__init__ (* args , ** kwargs )
@@ -63,6 +70,7 @@ def __init__(self, *args, **kwargs):
6370 self ._fig = bqplot .Figure ()
6471 self .plot_types = ["Curve of Growth" , "Radial Profile" , "Radial Profile (Raw)" ]
6572 self .current_plot_type = self .plot_types [0 ]
73+ self ._fitted_model_name = 'phot_radial_profile'
6674
6775 def reset_results (self ):
6876 self .result_available = False
@@ -218,6 +226,11 @@ def vue_do_aper_phot(self, *args, **kwargs):
218226 data = self ._selected_data
219227 reg = self ._selected_subset
220228
229+ # Reset last fitted model
230+ fit_model = None
231+ if self ._fitted_model_name in self .app .fitted_models :
232+ del self .app .fitted_models [self ._fitted_model_name ]
233+
221234 try :
222235 comp = data .get_component (data .main_components [0 ])
223236 try :
@@ -319,38 +332,68 @@ def vue_do_aper_phot(self, *args, **kwargs):
319332 line_y_sc = bqplot .LinearScale ()
320333
321334 if self .current_plot_type == "Curve of Growth" :
322- self ._fig .title = 'Curve of growth from Subset center '
335+ self ._fig .title = 'Curve of growth from source centroid '
323336 x_arr , sum_arr , x_label , y_label = _curve_of_growth (
324- comp_data , aperture , phot_table ['sum' ][0 ], wcs = data . coords ,
325- background = bg , pixarea_fac = pixarea_fac )
337+ comp_data , phot_aperstats . centroid , aperture , phot_table ['sum' ][0 ],
338+ wcs = data . coords , background = bg , pixarea_fac = pixarea_fac )
326339 self ._fig .axes = [bqplot .Axis (scale = line_x_sc , label = x_label ),
327340 bqplot .Axis (scale = line_y_sc , orientation = 'vertical' ,
328341 label = y_label )]
329342 bqplot_line = bqplot .Lines (x = x_arr , y = sum_arr , marker = 'circle' ,
330343 scales = {'x' : line_x_sc , 'y' : line_y_sc },
331344 marker_size = 32 , colors = 'gray' )
345+ bqplot_marks = [bqplot_line ]
332346
333347 else : # Radial profile
334348 self ._fig .axes = [bqplot .Axis (scale = line_x_sc , label = 'pix' ),
335349 bqplot .Axis (scale = line_y_sc , orientation = 'vertical' ,
336350 label = comp .units or 'Value' )]
337351
338352 if self .current_plot_type == "Radial Profile" :
339- self ._fig .title = 'Radial profile from Subset center '
353+ self ._fig .title = 'Radial profile from source centroid '
340354 x_data , y_data = _radial_profile (
341- phot_aperstats .data_cutout , phot_aperstats .bbox , aperture , raw = False )
355+ phot_aperstats .data_cutout , phot_aperstats .bbox , phot_aperstats .centroid ,
356+ raw = False )
342357 bqplot_line = bqplot .Lines (x = x_data , y = y_data , marker = 'circle' ,
343358 scales = {'x' : line_x_sc , 'y' : line_y_sc },
344359 marker_size = 32 , colors = 'gray' )
345360 else : # Radial Profile (Raw)
346- self ._fig .title = 'Raw radial profile from Subset center'
347- radial_r , radial_img = _radial_profile (
348- phot_aperstats .data_cutout , phot_aperstats .bbox , aperture , raw = True )
349- bqplot_line = bqplot .Scatter (x = radial_r , y = radial_img , marker = 'circle' ,
361+ self ._fig .title = 'Raw radial profile from source centroid'
362+ x_data , y_data = _radial_profile (
363+ phot_aperstats .data_cutout , phot_aperstats .bbox , phot_aperstats .centroid ,
364+ raw = True )
365+ bqplot_line = bqplot .Scatter (x = x_data , y = y_data , marker = 'circle' ,
350366 scales = {'x' : line_x_sc , 'y' : line_y_sc },
351367 default_size = 1 , colors = 'gray' )
352368
353- self ._fig .marks = [bqplot_line ]
369+ # Fit Gaussian1D to radial profile data.
370+ # mean is fixed at 0 because we recentered to centroid.
371+ if self .fit_radial_profile :
372+ fitter = LevMarLSQFitter ()
373+ y_max = y_data .max ()
374+ std = 0.5 * (phot_table ['semimajor_sigma' ][0 ] +
375+ phot_table ['semiminor_sigma' ][0 ])
376+ if isinstance (std , u .Quantity ):
377+ std = std .value
378+ gs = Gaussian1D (amplitude = y_max , mean = 0 , stddev = std ,
379+ fixed = {'mean' : True , 'amplitude' : True },
380+ bounds = {'amplitude' : (y_max * 0.5 , y_max )})
381+ with warnings .catch_warnings (record = True ) as warns :
382+ fit_model = fitter (gs , x_data , y_data )
383+ if len (warns ) > 0 :
384+ msg = os .linesep .join ([str (w .message ) for w in warns ])
385+ self .hub .broadcast (SnackbarMessage (
386+ f"Radial profile fitting: { msg } " , color = 'warning' , sender = self ))
387+ y_fit = fit_model (x_data )
388+ self .app .fitted_models [self ._fitted_model_name ] = fit_model
389+ bqplot_fit = bqplot .Lines (x = x_data , y = y_fit , marker = None ,
390+ scales = {'x' : line_x_sc , 'y' : line_y_sc },
391+ colors = 'magenta' , line_style = 'dashed' )
392+ bqplot_marks = [bqplot_line , bqplot_fit ]
393+ else :
394+ bqplot_marks = [bqplot_line ]
395+
396+ self ._fig .marks = bqplot_marks
354397
355398 except Exception as e : # pragma: no cover
356399 self .reset_results ()
@@ -379,7 +422,18 @@ def vue_do_aper_phot(self, *args, **kwargs):
379422 f'{ x :.4e} ({ phot_table ["aperture_sum_counts_err" ][0 ]:.4e} )' })
380423 else :
381424 tmp .append ({'function' : key , 'result' : str (x )})
425+
426+ # Also display fit results
427+ fit_tmp = []
428+ if fit_model is not None and isinstance (fit_model , Gaussian1D ):
429+ for param in ('fwhm' , 'amplitude' ): # mean is fixed at 0
430+ p_val = getattr (fit_model , param )
431+ if isinstance (p_val , Parameter ):
432+ p_val = p_val .value
433+ fit_tmp .append ({'function' : param , 'result' : f'{ p_val :.4e} ' })
434+
382435 self .results = tmp
436+ self .fit_results = fit_tmp
383437 self .result_available = True
384438 self .radial_plot = self ._fig
385439 self .bqplot_figs_resize = [self ._fig ]
@@ -389,7 +443,7 @@ def vue_do_aper_phot(self, *args, **kwargs):
389443# NOTE: These are hidden because the APIs are for internal use only
390444# but we need them as a separate functions for unit testing.
391445
392- def _radial_profile (radial_cutout , reg_bb , aperture , raw = False ):
446+ def _radial_profile (radial_cutout , reg_bb , centroid , raw = False ):
393447 """Calculate radial profile.
394448
395449 Parameters
@@ -400,23 +454,24 @@ def _radial_profile(radial_cutout, reg_bb, aperture, raw=False):
400454 reg_bb : obj
401455 Bounding box from ``ApertureStats``.
402456
403- aperture : obj
404- ``photutils `` aperture object .
457+ centroid : tuple of int
458+ ``ApertureStats `` centroid or desired center in ``(x, y)`` .
405459
406460 raw : bool
407461 If `True`, returns raw data points for scatter plot.
408462 Otherwise, use ``imexam`` algorithm for a clean plot.
409463
410464 """
411465 reg_ogrid = np .ogrid [reg_bb .iymin :reg_bb .iymax , reg_bb .ixmin :reg_bb .ixmax ]
412- radial_dx = reg_ogrid [1 ] - aperture . positions [0 ]
413- radial_dy = reg_ogrid [0 ] - aperture . positions [1 ]
466+ radial_dx = reg_ogrid [1 ] - centroid [0 ]
467+ radial_dy = reg_ogrid [0 ] - centroid [1 ]
414468 radial_r = np .hypot (radial_dx , radial_dy )[~ radial_cutout .mask ].ravel () # pix
415469 radial_img = radial_cutout .compressed () # data unit
416470
417471 if raw :
418- x_arr = radial_r
419- y_arr = radial_img
472+ i_arr = np .argsort (radial_r )
473+ x_arr = radial_r [i_arr ]
474+ y_arr = radial_img [i_arr ]
420475 else :
421476 # This algorithm is from the imexam package,
422477 # see licenses/IMEXAM_LICENSE.txt for more details
@@ -427,7 +482,7 @@ def _radial_profile(radial_cutout, reg_bb, aperture, raw=False):
427482 return x_arr , y_arr
428483
429484
430- def _curve_of_growth (data , aperture , final_sum , wcs = None , background = 0 , n_datapoints = 10 ,
485+ def _curve_of_growth (data , centroid , aperture , final_sum , wcs = None , background = 0 , n_datapoints = 10 ,
431486 pixarea_fac = None ):
432487 """Calculate curve of growth for aperture photometry.
433488
@@ -436,8 +491,14 @@ def _curve_of_growth(data, aperture, final_sum, wcs=None, background=0, n_datapo
436491 data : ndarray or `~astropy.units.Quantity`
437492 Data for the calculation.
438493
494+ centroid : tuple of int
495+ ``ApertureStats`` centroid or desired center in ``(x, y)``.
496+
439497 aperture : obj
440- ``photutils`` aperture object.
498+ ``photutils`` aperture to use, except its center will be
499+ changed to the given ``centroid``. This is because the aperture
500+ might be hand-drawn and a more accurate centroid has been
501+ recalculated separately.
441502
442503 final_sum : float or `~astropy.units.Quantity`
443504 Aperture sum that is already calculated in the
@@ -477,20 +538,20 @@ def _curve_of_growth(data, aperture, final_sum, wcs=None, background=0, n_datapo
477538 if isinstance (aperture , CircularAperture ):
478539 x_label = 'Radius (pix)'
479540 x_arr = np .linspace (0 , aperture .r , num = n_datapoints )[1 :]
480- aper_list = [CircularAperture (aperture . positions , cur_r ) for cur_r in x_arr [:- 1 ]]
541+ aper_list = [CircularAperture (centroid , cur_r ) for cur_r in x_arr [:- 1 ]]
481542 elif isinstance (aperture , EllipticalAperture ):
482543 x_label = 'Semimajor axis (pix)'
483544 x_arr = np .linspace (0 , aperture .a , num = n_datapoints )[1 :]
484545 a_arr = x_arr [:- 1 ]
485546 b_arr = aperture .b * a_arr / aperture .a
486- aper_list = [EllipticalAperture (aperture . positions , cur_a , cur_b , theta = aperture .theta )
547+ aper_list = [EllipticalAperture (centroid , cur_a , cur_b , theta = aperture .theta )
487548 for (cur_a , cur_b ) in zip (a_arr , b_arr )]
488549 elif isinstance (aperture , RectangularAperture ):
489550 x_label = 'Width (pix)'
490551 x_arr = np .linspace (0 , aperture .w , num = n_datapoints )[1 :]
491552 w_arr = x_arr [:- 1 ]
492553 h_arr = aperture .h * w_arr / aperture .w
493- aper_list = [RectangularAperture (aperture . positions , cur_w , cur_h , theta = aperture .theta )
554+ aper_list = [RectangularAperture (centroid , cur_w , cur_h , theta = aperture .theta )
494555 for (cur_w , cur_h ) in zip (w_arr , h_arr )]
495556 else :
496557 raise TypeError (f'Unsupported aperture: { aperture } ' )
0 commit comments