Skip to content

Commit

Permalink
begin refactor of photometry
Browse files Browse the repository at this point in the history
  • Loading branch information
emirkmo committed Mar 30, 2022
1 parent b6dc652 commit 1cadf43
Show file tree
Hide file tree
Showing 8 changed files with 683 additions and 65 deletions.
2 changes: 1 addition & 1 deletion flows/epsfbuilder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# -*- coding: utf-8 -*-
# flake8: noqa

from .epsfbuilder import FlowsEPSFBuilder
from .epsfbuilder import FlowsEPSFBuilder, verify_epsf
34 changes: 33 additions & 1 deletion flows/epsfbuilder/epsfbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
"""
import time
import numpy as np
from scipy.interpolate import griddata
from scipy.interpolate import griddata, UnivariateSpline
import photutils.psf
from typing import List, Tuple
import logging

logger = logging.getLogger(__name__)


class FlowsEPSFBuilder(photutils.psf.EPSFBuilder):
Expand Down Expand Up @@ -51,3 +55,31 @@ def __call__(self, *args, **kwargs):
epsf.fit_info = dict(n_iter=len(self._epsf), max_iters=self.maxiters, time=time.time() - t0, )

return epsf, stars


def verify_epsf(epsf: photutils.psf.EPSFBuilder) -> Tuple[bool, List[float]]:
fwhms = []
epsf_ok = True
for a in (0, 1):
# Collapse the PDF along this axis:
profile = epsf.data.sum(axis=a)
itop = profile.argmax()
poffset = profile[itop] / 2

# Run a spline through the points, but subtract half of the peak value, and find the roots:
# We have to use a cubic spline, since roots() is not supported for other splines
# for some reason
profile_intp = UnivariateSpline(np.arange(0, len(profile)), profile - poffset, k=3, s=0, ext=3)
lr = profile_intp.roots()

# Do some sanity checks on the ePSF:
# It should pass 50% exactly twice and have the maximum inside that region.
# I.e. it should be a single gaussian-like peak
if len(lr) != 2 or itop < lr[0] or itop > lr[1]:
logger.error(f"EPSF is not a single gaussian-like peak along axis {a}")
epsf_ok = False
else:
axis_fwhm = lr[1] - lr[0]
fwhms.append(axis_fwhm)

return epsf_ok, fwhms
69 changes: 69 additions & 0 deletions flows/fileio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import os
import logging
from typing import Optional, Protocol
from configparser import ConfigParser

logger = logging.getLogger(__name__)


class DirectoryProtocol(Protocol):
archive_local: str
output_folder: str

def set_output_dirs(self, target_name: str, fileid: int) -> None:
...

def image_path(self, image_path: str) -> str:
...


class Directories:
"""
Class for creating input and output directories, given a configparser instance.
Overwrite archive_local or output_folder to manually place elsewhere.
"""
archive_local: Optional[str] = None
output_folder: Optional[str] = None

def __init__(self, config: ConfigParser):
self.config = config

def set_output_dirs(self, target_name: str, fileid: int) -> None:
"""
The function is meant to be called from within a context where a
target_name and fileid are defined, so that the output_folder
can be created appropriately.
Parameters:
target_name (str): Target name.
fileid (int): The fileid of the file being processed
"""

# Checking for None allows manual declarations to not be overwritten.
if self.archive_local is None:
self.archive_local = self._set_archive()
if self.archive_local is None:
self.output_folder = self._set_output(target_name, fileid)

# Create output folder if necessary.
os.makedirs(self.output_folder, exist_ok=True)
logger.info("Placing output in '%s'", self.output_folder)

def _set_archive(self) -> str:
archive_local = self.config.get('photometry', 'archive_local', fallback=None)
if archive_local is not None and not os.path.isdir(archive_local):
raise FileNotFoundError("ARCHIVE is not available: " + archive_local)
logger.info(f"Using data from: {archive_local}.")
return archive_local

def _set_output(self, target_name: str, fileid: int) -> str:
"""
Directory for output, defaults to current
directory if config is invalid or empty.
"""
output_folder_root = self.config.get('photometry', 'output', fallback='.')
output_folder = os.path.join(output_folder_root, target_name, f'{fileid:05d}')
return output_folder

def image_path(self, image_path: str) -> str:
return os.path.join(self.archive_local, image_path)
33 changes: 33 additions & 0 deletions flows/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import logging

FILTERS = {
'up': 'u_mag',
'gp': 'g_mag',
'rp': 'r_mag',
'ip': 'i_mag',
'zp': 'z_mag',
'B': 'B_mag',
'V': 'V_mag',
'J': 'J_mag',
'H': 'H_mag',
'K': 'K_mag',
}

FALLBACK_FILTER = 'gp'
logger = logging.getLogger(__name__)


def get_reference_filter(photfilter: str) -> str:
"""
Translate photometric filter into table column.
Parameters:
photfilter (str): photometric filter corresponding to key of FILTERS
"""

_ref_filter = FILTERS.get(photfilter, None)
if _ref_filter is None:
logger.warning(f"Could not find filter {photfilter} in catalogs. "
f"Using default {FALLBACK_FILTER} filter.")
_ref_filter = FILTERS[FALLBACK_FILTER]
return _ref_filter
64 changes: 52 additions & 12 deletions flows/load_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from astropy.io import fits
from astropy.time import Time
from astropy.wcs import WCS, FITSFixedWarning
from typing import Tuple
from typing import Tuple, Union, Dict, Any, Optional
from tendrils import api

from dataclasses import dataclass # , field
Expand All @@ -20,15 +20,30 @@
logger = logging.getLogger(__name__) # Singleton logger instance


# --------------------------------------------------------------------------------------------------
@dataclass
class InstrumentDefaults:
"""
Default radius and FWHM for an instrument in arcseconds.
"""
radius: float = 10
fwhm: float = 6.0 # Best initial guess
fwhm_min: float = 3.5
fwhm_max: float = 18.0


@dataclass
class FlowsImage:
image: np.ndarray
header: typing.Dict
mask: np.ndarray = None
peakmax: float = None
exptime: float = None
clean: np.ma.MaskedArray = None
mask: Optional[np.ndarray] = None
peakmax: Optional[float] = None
exptime: Optional[float] = None
clean: Optional[np.ma.MaskedArray] = None
instrument_defaults: Optional[InstrumentDefaults] = None
site: Optional[Dict[str, Any]] = None
obstime: Optional[Time] = None
photfilter: Optional[str] = None
wcs: Optional[WCS] = None

def __post_init__(self):
self.shape = self.image.shape
Expand Down Expand Up @@ -169,23 +184,29 @@ class Instrument(AbstractInstrument):
def __init__(self, image: FlowsImage = None):
self.image = image

def get_site(self):
def get_site(self) -> Dict[str, Any]:
if self.siteid is not None:
return api.get_site(self.siteid)

def get_exptime(self):
def get_exptime(self) -> Union[float, int, str]:
exptime = self.image.header.get('EXPTIME', None)
if exptime is None:
raise ValueError("Image exposure time could not be extracted")
return exptime

def get_obstime(self):
def get_obstime(self) -> Time:
"""Default for JD, jd, utc."""
return Time(self.image.header['JD'], format='jd', scale='utc', location=self.image.site['EarthLocation'])

def get_photfilter(self):
return self.image.header['FILTER']

def set_instrument_defaults(self):
"""
Set default values for instrument.
"""
self.image.instrument_defaults = InstrumentDefaults()

def _get_clean_image(self):
self.image.peakmax = self.peakmax
self.image.site = self.get_site()
Expand All @@ -194,14 +215,16 @@ def _get_clean_image(self):
self.image.photfilter = self.get_photfilter()
self.image.create_masked_image()

def process_image(self, image: FlowsImage = None):
def process_image(self, image: FlowsImage = None) -> FlowsImage:
"""Process existing or new image."""
if image is not None:
self.image = image
if self.image is None:
raise AttributeError('No FlowsImage to be processed. Self.image was None')

self._get_clean_image()
self.set_instrument_defaults()
return self.image


class LCOGT(Instrument):
Expand Down Expand Up @@ -528,6 +551,21 @@ def get_obstime(self):
'PairTel': PairTel, 'TJO': TJO}


def correct_barycentric(obstime: Time, target_coord: coords.SkyCoord) -> Time:
"""
BARYCENTRIC CORRECTION OF TIME
Parameters:
obstime (astropy.time.Time): Midpoint observed image time.
target_coord (astropy.coords.SkyCoord): Coordinates of target in image.
Returns:
obstime (astropy.time.Time): Time corrected to barycenter with jpl ephemeris.
"""
ltt_bary = obstime.light_travel_time(target_coord, ephemeris='jpl')
return obstime.tdb + ltt_bary


def load_image(filename: str, target_coord: typing.Union[coords.SkyCoord, typing.Tuple[float, float]] = None):
"""
Load FITS image using FlowsImage class and Instrument Classes.
Expand Down Expand Up @@ -653,6 +691,8 @@ def load_image(filename: str, target_coord: typing.Union[coords.SkyCoord, typing
raise RuntimeError("Could not determine origin of image")

image = FlowsImage(image=np.asarray(hdul[ext].data, dtype='float64'), header=hdr, mask=mask)
instrument_name.process_image(image)
clean_image = instrument_name.process_image(image)

return instrument_name.image
if target_coord is not None:
clean_image.obstime = correct_barycentric(clean_image.obstime, target_coord)
return clean_image
Loading

0 comments on commit 1cadf43

Please sign in to comment.