diff --git a/iop4admin/modeladmins/aperphotresult.py b/iop4admin/modeladmins/aperphotresult.py index 3f1d217a..20261891 100644 --- a/iop4admin/modeladmins/aperphotresult.py +++ b/iop4admin/modeladmins/aperphotresult.py @@ -17,12 +17,26 @@ class AdminAperPhotResult(admin.ModelAdmin): model = AperPhotResult - list_display = ['id', 'get_telescope', 'get_instrument', 'get_datetime', 'get_src_name', 'get_src_type', 'get_fwhm', 'get_aperpix', 'get_reducedfit', 'get_obsmode', 'pairs', 'get_rotangle', 'get_src_type', 'get_flux_counts', 'get_flux_counts_err', 'get_bkg_flux_counts', 'get_bkg_flux_counts_err', 'modified'] + list_display = ['id', 'get_telescope', 'get_instrument', 'get_datetime', 'get_src_name', 'get_src_type', 'get_fwhm', 'get_aperpix', 'get_r_in', 'get_r_out', 'get_reducedfit', 'get_obsmode', 'pairs', 'get_rotangle', 'get_src_type', 'get_flux_counts', 'get_flux_counts_err', 'get_bkg_flux_counts', 'get_bkg_flux_counts_err', 'get_image_preview', 'modified'] readonly_fields = [field.name for field in AperPhotResult._meta.fields] search_fields = ['id', 'reducedfit__instrument', 'astrosource__name', 'astrosource__srctype', 'reducedfit__id'] list_filter = ['reducedfit__instrument', 'astrosource__srctype', 'reducedfit__epoch__telescope', 'reducedfit__obsmode'] + def get_urls(self): + urls = super().get_urls() + my_urls = [ + path('preview/', self.admin_site.admin_view(self.view_preview), name=f"iop4api_{self.model._meta.model_name}_preview"), + ] + return my_urls + urls + + def view_preview(self, request, pk): + + if ((fit := self.model.objects.filter(id=pk).first()) is None): + return HttpResponseNotFound() + + imgbytes = fit.get_img() + return HttpResponse(imgbytes, content_type="image/png") @admin.display(description="TELESCOPE") def get_telescope(self, obj): @@ -70,6 +84,18 @@ def get_aperpix(self, obj): return "-" return f"{obj.aperpix:.1f}" + @admin.display(description="r_in") + def get_r_in(self, obj): + if obj.r_in is None: + return "-" + return f"{obj.r_in:.1f}" + + @admin.display(description="r_out") + def get_r_out(self, obj): + if obj.r_out is None: + return "-" + return f"{obj.r_out:.1f}" + @admin.display(description="flux_counts") def get_flux_counts(self, obj): if obj.flux_counts is None: @@ -97,4 +123,8 @@ def get_bkg_flux_counts_err(self, obj): return "-" else: return f"{obj.bkg_flux_counts_err:.1f}" - \ No newline at end of file + + @admin.display(description="img") + def get_image_preview(self, obj, allow_tags=True): + url_img_preview = reverse(f"iop4admin:iop4api_{self.model._meta.model_name}_preview", args=[obj.id]) + return format_html(rf"") \ No newline at end of file diff --git a/iop4admin/modeladmins/photopolresult.py b/iop4admin/modeladmins/photopolresult.py index 0efd55c2..83a39ad9 100644 --- a/iop4admin/modeladmins/photopolresult.py +++ b/iop4admin/modeladmins/photopolresult.py @@ -15,7 +15,7 @@ class AdminPhotoPolResult(admin.ModelAdmin): model = PhotoPolResult - list_display = ['id', 'get_telescope', 'get_juliandate', 'get_datetime', 'get_src_name', 'get_src_type', 'get_reducedfits', 'obsmode', 'band', 'exptime', 'get_mag', 'get_mag_err', 'get_p', 'get_p_err', 'get_chi', 'get_chi_err', 'get_aperpix', 'get_aperas', 'get_flags', 'modified'] + list_display = ['id', 'get_telescope', 'get_juliandate', 'get_datetime', 'get_src_name', 'get_src_type', 'get_reducedfits', 'obsmode', 'band', 'exptime', 'get_mag', 'get_mag_err', 'get_p', 'get_p_err', 'get_chi', 'get_chi_err', 'get_aperpix', 'get_aperas', 'get_flags', 'get_aperphots', 'modified'] readonly_fields = [field.name for field in PhotoPolResult._meta.fields] search_fields = ['id', 'astrosource__name', 'astrosource__srctype', 'epoch__night'] ordering = ['-juliandate'] @@ -86,4 +86,13 @@ def get_aperas(self, obj): @admin.display(description='Status') def get_flags(self, obj): - return ", ".join(list(obj.flag_labels)) \ No newline at end of file + return ", ".join(list(obj.flag_labels)) + + @admin.display(description="AperPhots") + def get_aperphots(self, obj): + self.allow_tags = True + + ids_str_L = [str(apres.id) for apres in obj.aperphotresults.all()] + a_href = reverse('iop4admin:%s_%s_changelist' % (AperPhotResult._meta.app_label, AperPhotResult._meta.model_name)) + "?id__in=%s" % ",".join(ids_str_L) + a_text = ", ".join(ids_str_L) + return mark_safe(f'{a_text}') \ No newline at end of file diff --git a/iop4lib/db/aperphotresult.py b/iop4lib/db/aperphotresult.py index c216dd5e..8f95621c 100644 --- a/iop4lib/db/aperphotresult.py +++ b/iop4lib/db/aperphotresult.py @@ -6,9 +6,19 @@ from django.db import models # other imports +import os +import io +import numpy as np +import matplotlib as mplt +import matplotlib.pyplot as plt +from astropy.visualization.mpl_normalize import ImageNormalize +from astropy.visualization import LogStretch +from astropy.nddata import Cutout2D +from photutils.aperture import CircularAperture, CircularAnnulus # iop4lib imports from ..enums import * +from iop4lib.instruments.instrument import Instrument # logging import logging @@ -53,7 +63,7 @@ class Meta: models.UniqueConstraint(fields=['reducedfit', 'astrosource', 'aperpix', 'r_in', 'r_out', 'pairs'], name='unique_aperphotresult') ] - # repr and str + # repr and str def __repr__(self): return f'{self.__class__.__name__}.objects.get(id={self.id!r})' @@ -75,3 +85,84 @@ def create(cls, reducedfit, astrosource, aperpix, pairs, **kwargs): result.save() + return result + + @property + def filedpropdir(self): + return os.path.join(iop4conf.datadir, "aperphotresults", str(self.id)) + + def get_img(self, force_rebuild=True, **kwargs): + """ + Build an image preview (png) of the aperture and annulus over the source. + + If called with default arguments (no kwargs) it will try to load from the disk, + except if called with force_rebuild. + + When called with default arguments (no kwargs), if rebuilt, it will save the image to disk. + """ + + wcs = self.reducedfit.wcs1 if self.pairs == 'O' else self.reducedfit.wcs2 + + if self.reducedfit.has_pairs: + cutout_size = np.ceil(2.2*np.linalg.norm(Instrument.by_name(self.reducedfit.instrument).disp_sign_mean)) + else: + cutout_size = np.ceil(1.3*self.r_out) + + cutout = Cutout2D(self.reducedfit.mdata, (self.x_px, self.y_px), (cutout_size, cutout_size), wcs) + + width = kwargs.get('width', 256) + height = kwargs.get('height', 256) + normtype = kwargs.get('norm', "log") + vmin = kwargs.get('vmin', np.quantile(cutout.data.compressed(), 0.3)) + vmax = kwargs.get('vmax', np.quantile(cutout.data.compressed(), 0.99)) + a = kwargs.get('a', 10) + + fpath = os.path.join(self.filedpropdir, "img_preview_image.png") + + if len(kwargs) == 0 and not force_rebuild: + if os.path.isfile(fpath) and os.path.getmtime(self.filepath) < os.path.getmtime(fpath): + with open(fpath, 'rb') as f: + return f.read() + + cmap = plt.cm.gray.copy() + cmap.set_bad(color='red') + cmap.set_under(color='black') + cmap.set_over(color='white') + + if normtype == "log": + norm = ImageNormalize(cutout.data.compressed(), vmin=vmin, vmax=vmax, stretch=LogStretch(a=a)) + elif normtype == "logstretch": + norm = ImageNormalize(stretch=LogStretch(a=a)) + + + buf = io.BytesIO() + + fig = mplt.figure.Figure(figsize=(width/100, height/100), dpi=iop4conf.mplt_default_dpi) + ax = fig.subplots() + + wcs_px_pos = self.astrosource.coord.to_pixel(cutout.wcs) + xy_px_pos = cutout.to_cutout_position((self.x_px, self.y_px)) + ap = CircularAperture(xy_px_pos, r=self.aperpix) + annulus = CircularAnnulus(xy_px_pos, r_in=self.r_in, r_out=self.r_out) + + ax.imshow(cutout.data, cmap=cmap, origin='lower', norm=norm) + ax.plot(wcs_px_pos[0], wcs_px_pos[1], 'rx', label='WCS') + ax.plot(xy_px_pos[0], xy_px_pos[1], 'bo', label='Photometry') + ap.plot(ax, color='blue', lw=2, alpha=1) + annulus.plot(ax, color='green', lw=2, alpha=1) + + ax.axis('off') + fig.savefig(buf, format='png', bbox_inches='tight', pad_inches=0) + fig.clf() + + buf.seek(0) + imgbytes = buf.read() + + # if it was rebuilt, save it to disk if it is the default image settings. + if len(kwargs) == 0: + if not os.path.exists(self.filedpropdir): + os.makedirs(self.filedpropdir) + with open(fpath, 'wb') as f: + f.write(imgbytes) + + return imgbytes \ No newline at end of file diff --git a/iop4lib/db/masterbias.py b/iop4lib/db/masterbias.py index 8132aa6e..efdc02f0 100644 --- a/iop4lib/db/masterbias.py +++ b/iop4lib/db/masterbias.py @@ -164,6 +164,8 @@ def create(cls, if auto_merge_to_db: mb.save() + return mb + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.auto_merge_to_db = True diff --git a/iop4lib/instruments/cafos.py b/iop4lib/instruments/cafos.py index 4857bc28..64b3cdc5 100644 --- a/iop4lib/instruments/cafos.py +++ b/iop4lib/instruments/cafos.py @@ -383,11 +383,16 @@ def compute_relative_polarimetry(cls, polarimetry_group): # save the results result = PhotoPolResult.create(reducedfits=polarimetry_group, - astrosource=astrosource, - reduction=REDUCTIONMETHODS.RELPOL, - mag_inst=mag_inst, mag_inst_err=mag_inst_err, mag_zp=mag_zp, mag_zp_err=mag_zp_err, - flux_counts=flux_mean, p=P, p_err=dP, chi=Theta, chi_err=dTheta, - aperpix=aperpix) + astrosource=astrosource, + reduction=REDUCTIONMETHODS.RELPOL, + mag_inst=mag_inst, mag_inst_err=mag_inst_err, + mag_zp=mag_zp, mag_zp_err=mag_zp_err, + flux_counts=flux_mean, + p=P, p_err=dP, + chi=Theta, chi_err=dTheta, + aperpix=aperpix) + + result.aperphotresults.set(qs, clear=True) photopolresult_L.append(result) diff --git a/iop4lib/instruments/dipol.py b/iop4lib/instruments/dipol.py index acd16270..268d0b09 100644 --- a/iop4lib/instruments/dipol.py +++ b/iop4lib/instruments/dipol.py @@ -1188,7 +1188,9 @@ def _get_p_and_chi(Qr, Ur, dQr, dUr): p=P, p_err=dP, chi=chi, chi_err=dchi, _q_nocorr=Qr_uncorr, _u_nocorr=Ur_uncorr, _p_nocorr=P_uncorr, _chi_nocorr=chi_uncorr, aperpix=aperpix) - + + result.aperphotresults.set(aperphotresults, clear=True) + photopolresult_L.append(result) # 3. Save results diff --git a/iop4lib/instruments/instrument.py b/iop4lib/instruments/instrument.py index 376db483..5c5b74d3 100644 --- a/iop4lib/instruments/instrument.py +++ b/iop4lib/instruments/instrument.py @@ -526,6 +526,8 @@ def compute_aperture_photometry(cls, redf, aperpix, r_in, r_out): error = calc_total_error(img, bkg.background_rms, cls.gain_e_adu) + apres_L = list() + for astrosource in redf.sources_in_field.all(): for pairs, wcs in (('O', redf.wcs1), ('E', redf.wcs2)) if redf.has_pairs else (('O',redf.wcs),): @@ -548,7 +550,7 @@ def compute_aperture_photometry(cls, redf, aperpix, r_in, r_out): flux_counts = ap_stats.sum - annulus_stats.mean*ap_stats.sum_aper_area.value # TODO: check if i should use mean! flux_counts_err = ap_stats.sum_err - AperPhotResult.create(reducedfit=redf, + apres = AperPhotResult.create(reducedfit=redf, astrosource=astrosource, aperpix=aperpix, r_in=r_in, r_out=r_out, @@ -556,6 +558,10 @@ def compute_aperture_photometry(cls, redf, aperpix, r_in, r_out): pairs=pairs, bkg_flux_counts=bkg_flux_counts, bkg_flux_counts_err=bkg_flux_counts_err, flux_counts=flux_counts, flux_counts_err=flux_counts_err) + + apres_L.append(apres) + + return apres_L @classmethod def compute_relative_photometry(cls, redf: 'ReducedFit') -> None: @@ -590,9 +596,15 @@ def compute_relative_photometry(cls, redf: 'ReducedFit') -> None: for astrosource in redf.sources_in_field.all(): - result = PhotoPolResult.create(reducedfits=[redf], astrosource=astrosource, reduction=REDUCTIONMETHODS.RELPHOT) + qs_aperphotresult = AperPhotResult.objects.filter(reducedfit=redf, astrosource=astrosource, aperpix=aperpix, pairs="O") - aperphotresult = AperPhotResult.objects.get(reducedfit=redf, astrosource=astrosource, aperpix=aperpix, pairs="O") + if not qs_aperphotresult.exists(): + logger.error(f"{redf}: no aperture photometry for source {astrosource.name} found, skipping relative photometry.") + continue + + aperphotresult = qs_aperphotresult.first() + + result = PhotoPolResult.create(reducedfits=[redf], astrosource=astrosource, reduction=REDUCTIONMETHODS.RELPHOT) result.aperpix = aperpix result.bkg_flux_counts = aperphotresult.bkg_flux_counts @@ -630,6 +642,8 @@ def compute_relative_photometry(cls, redf: 'ReducedFit') -> None: result.mag_zp = None result.mag_zp_err = None + result.aperphotresults.set([aperphotresult], clear=True) + result.save() photopolresult_L.append(result)