Skip to content

Commit

Permalink
improve admin for aperture photometry results (#147)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Morcuende <dmorcuende@iaa.es>
  • Loading branch information
juanep97 and morcuended authored Oct 22, 2024
1 parent 858052a commit b7016d3
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 14 deletions.
34 changes: 32 additions & 2 deletions iop4admin/modeladmins/aperphotresult.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<int:pk>', 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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -97,4 +123,8 @@ def get_bkg_flux_counts_err(self, obj):
return "-"
else:
return f"{obj.bkg_flux_counts_err:.1f}"


@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"<img src='{url_img_preview}' width='64' height='64' />")
13 changes: 11 additions & 2 deletions iop4admin/modeladmins/photopolresult.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -86,4 +86,13 @@ def get_aperas(self, obj):

@admin.display(description='Status')
def get_flags(self, obj):
return ", ".join(list(obj.flag_labels))
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 href="{a_href}">{a_text}</a>')
93 changes: 92 additions & 1 deletion iop4lib/db/aperphotresult.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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})'
Expand All @@ -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
2 changes: 2 additions & 0 deletions iop4lib/db/masterbias.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions iop4lib/instruments/cafos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 3 additions & 1 deletion iop4lib/instruments/dipol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 17 additions & 3 deletions iop4lib/instruments/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),):

Expand All @@ -548,14 +550,18 @@ 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,
x_px=centroid_px_pos[0], y_px=centroid_px_pos[1],
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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit b7016d3

Please sign in to comment.