__all__ = [ 'PipelineImage', 'Pipeline' ]
# Imports STANDARD
import sys
import argparse
from functools import partial
import logging
from multiprocessing import Pool
import numpy as np
import nvtx
import pathlib
import re
import shutil
import tracemalloc
import uuid
# Imports ASTRO
from astropy.coordinates import SkyCoord
from astropy.table import Table
import astropy.units as u
from astropy.wcs.utils import skycoord_to_pixel
import fitsio
# Imports INTERNAL
import phrosty
from phrosty.imagesubtraction import sky_subtract, stampmaker
from sfft.SpaceSFFTFlow import SpaceSFFT_Flow
from snappl.dbclient import SNPITDBClient
from snappl.diaobject import DiaObject
from snappl.imagecollection import ImageCollection
from snappl.image import CompressedFITSImage
from snappl.lightcurve import Lightcurve
from snappl.provenance import Provenance
from snappl.psf import PSF
from snappl.config import Config
from snappl.logger import SNLogger
[docs]
class PipelineImage:
"""Holds a snappl.image.Image, with some other stuff the pipeline needs."""
def __init__( self, image, pipeline ):
"""Create a PipelineImage
Parameters:
-----------
image : snappl.image.Image
The image we're encapsulating. Pass either this or imagepath.
pipeline : phrosty.pipeline.Pipeline
The pipeline that owns this image.
"""
self.config = Config.get()
self.temp_dir = pipeline.temp_dir
self.keep_intermediate = self.config.value( 'photometry.phrosty.keep_intermediate' )
if self.keep_intermediate:
self.save_dir = pathlib.Path( self.config.value( 'system.paths.scratch_dir' ) )
elif not self.keep_intermediate:
self.save_dir = self.temp_dir
self.image = image
# if self.image.band != pipeline.band:
# raise ValueError( f"Image {self.image.path.name} has a band {self.image.band}, "
# f"which is different from the pipeline band {pipeline.band}" )
# Intermediate files
if self.keep_intermediate:
# Set to None. The path gets defined later on.
# They have to be defined here in __init__ so that they exist
# and are accessible in later functions.
self.skysub_img = {}
self.detmask_img = {}
self.input_sci_psf_path = {}
self.input_templ_psf_path = {}
self.aligned_templ_var_path = {}
self.aligned_templ_psf_path = {}
self.crossconv_sci_path = {}
self.crossconv_templ_path = {}
self.diff_path = {}
self.decorr_kernel_path = {}
# Always save and output these
self.decorr_psf_path = {}
self.decorr_zptimg_path = {}
self.decorr_diff_path = {}
self.zpt_stamp_path = {}
self.diff_var_path = {}
self.diff_var_stamp_path = {}
self.diff_stamp_path = {}
# Lauren added these for debugging...
self.aligned_templ_img_path = {}
self.aligned_templ_stamp_path = {}
self.diff_undecorr_img_path = {}
self.diff_undecorr_stamp_path = {}
# Held in memory
self.skyrms = None
self.psfobj = None
self.psf_data = None
# In case we fail...
self.fail_info = f'{image.band} {image.observation_id} {image.sca}'
self.failure_location = None
self.failure_pair = None
[docs]
def run_sky_subtract( self, mp=True ):
"""Run sky subtraction using Source Extractor.
Parameters
----------
mp : bool, optional
Toggle multiprocessing, by default True
Returns
-------
tuple
Tuple containing sky subtracted image, detection
mask array, and sky RMS value.
Output of phrosty.imagesubtraction.sky_subtract().
"""
try:
return sky_subtract( self.image, temp_dir=self.temp_dir )
except Exception as ex:
self.failure_location = 'skysub'
SNLogger.exception( ex )
raise
[docs]
def save_sky_subtract_info( self, info ):
"""Saves the sky-subtracted image, detection mask array,
and sky RMS values to attributes.
Parameters
----------
info : tuple
Output of self.run_sky_subtract(). See documentation
for phrosty.imagesubtraction.sky_subtract().
"""
SNLogger.debug( f"Saving sky_subtract info for path {info[0].path}" )
self.skysub_img = info[0]
self.detmask_img = info[1]
self.skyrms = info[2]
[docs]
def get_psf( self, ra, dec ):
"""Get the at the right spot on the image.
Parameters
----------
ra, dec : float
The coordinates in decimal degrees where we want the PSF.
Returns
-------
np.array
PSF stamp. If this function fails, None is returned.
"""
# TODO: right now snappl.psf.PSF.get_psf_object just
# passes the keyword arguments on to whatever makes
# the psf... and it's different for each type of
# PSF. We need to fix that... somehow....
try:
wcs = self.image.get_wcs()
x, y = wcs.world_to_pixel( ra, dec )
if self.psfobj is None:
psftype = self.config.value( 'photometry.phrosty.psf.type' )
psfparams = self.config.value( 'photometry.phrosty.psf.params' )
self.psfobj = PSF.get_psf_object( psftype, x=x, y=y,
band=self.image.band,
observation_id=self.image.observation_id,
sca=self.image.sca,
**psfparams )
stamp = self.psfobj.get_stamp( x, y )
if self.keep_intermediate:
outfile = self.save_dir / f"psf_{self.image.name}.fits"
fitsio.write( outfile, stamp, clobber=True )
return stamp
except Exception as ex:
self.failure_location = 'get_psf'
SNLogger.exception( ex )
raise
[docs]
def keep_psf_data( self, psf_data ):
"""Save PSF data to attribute.
Parameters
----------
psf_data : np.array
PSF stamp.
"""
self.psf_data = psf_data/np.sum(psf_data)
[docs]
def free( self, free_psf_data=False ):
"""Try to free memory. More might be done here."""
self.image.free()
self.skysub_img.free()
self.detmask_img.free()
if free_psf_data:
self.psf_data = None
[docs]
class Pipeline:
"""Phrosty's top-level pipeline"""
def __init__( self, diaobj, imgcol, band,
science_images=None,
template_images=None,
science_csv=None,
template_csv=None,
oid=None,
ltcv_prov_tag=None,
dbsave=False,
dbclient=None,
nprocs=1,
nwrite=5,
verbose=False,
memtrace=False,
catchfailures=False ):
"""Create the a pipeline object.
Parameters
----------
diaobj : DiaObject
The object we're building a lightcurve for
imgcol : ImageCollection
snappl.imagecollection.ImageCollection
band: str
One of R062, Z087, Y106, J129, H158, F184, K213
science_images: list of snappl.image.Image
The science images.
template_images: list of snappl.image.Image
The template images.
science_csv: Path or str
CSV file with the science images. The first line must be::
path observation_id sca mjd band
subsequent lines must have that information for all the
science images. path must be relative to ou24.images in
config. Pipeline will extract the images from this file
whose band matches the band of the pipeline (and ignore
the rest)
template_csv: Path or str
CSV file with template images. Same format as science_csv.
oid: str
Object ID. This is probably a temporary argument. It is only used
if diaobj is None, and is really only used to build a filepath
without "None" in it when saving the light curve parquet file.
ltcv_prov_tag: str
Provenance tag for light curve. Required to use SN PIT database.
dbsave: bool
Are we saving to the database?
Default False.
dbclient: snappl.dbclient.SNPITDBClient
nprocs: int, default 1
Number of cpus for the CPU multiprocessing segments of the pipeline.
(GPU segments will run a single process.)
nwrite: int, default 5
Number of asynchronous FITS writer processes.
verbose: bool, default True
Toggle verbose output.
memtrace: bool, default False
Toggle memory tracing.
catchfailures: bool, default False
Toggle collection of information for images that fail. If true, pipeline
will not crash if one image fails. If false, pipeline crashes if one image
in a given set fails (useful for debugging and tests).
"""
SNLogger.setLevel( logging.DEBUG if verbose else logging.INFO )
self.config = Config.get()
self.imgcol = imgcol
self.diaobj = diaobj
self.band = band
self.oid = oid
self.dia_out_dir = pathlib.Path( self.config.value( 'system.paths.dia_out_dir' ) )
self.scratch_dir = pathlib.Path( self.config.value( 'system.paths.scratch_dir' ) )
self.temp_dir_parent = pathlib.Path( self.config.value( 'system.paths.temp_dir' ) )
self.temp_dir = self.temp_dir_parent / str(uuid.uuid1())
self.temp_dir.mkdir()
self.ltcv_dir = pathlib.Path( self.config.value( 'system.paths.ltcv_dir' ) )
if ( science_images is None) == ( science_csv is None ):
raise ValueError( "Pass exactly one of science_images or science_csv" )
if science_csv is not None:
science_images = self._read_csv( science_csv )
if ( template_images is None ) == ( template_csv is None ):
raise ValueError( "Pass exactly one of template_images or template_csv" )
if template_csv is not None:
template_images = self._read_csv( template_csv )
if isinstance(science_images, list) or isinstance(science_images, tuple):
self.science_images = [ PipelineImage( i, self ) for i in science_images ]
else:
self.science_images = [ PipelineImage(science_images, self) ]
if isinstance(template_images, list) or isinstance(template_images, tuple):
self.template_images = [ PipelineImage( i, self ) for i in template_images ]
else:
self.template_images = [ PipelineImage(template_images, self) ]
# All of our failures.
self.catchfailures = catchfailures
self.failures = {'skysub': [],
'get_psf': [],
'align_and_preconvolve': [],
'find_decorrelation': [],
'subtract': [],
'variance': [],
'apply_decorrelation': [],
'make_stamps': []}
self.ltcv_prov_tag = ltcv_prov_tag
self.dbsave = dbsave
self.dbclient = dbclient
self.nprocs = nprocs
self.nwrite = nwrite
self.keep_intermediate = self.config.value( 'photometry.phrosty.keep_intermediate' )
self.remove_temp_dir = self.config.value( 'photometry.phrosty.remove_temp_dir' )
self.mem_trace = self.config.value( 'photometry.phrosty.mem_trace' )
# Debug LNA 20251202
# self.resid_img = None
# Debug LNA20260225
# self.aperture = None
def _read_csv( self, csvfile ):
"""Reads input csv files with columns:
'path observation_id sca mjd band'.
Parameters
----------
csvfile : str
Path to an input csv file.
Returns
-------
list of snappl.image.Image
Raises
------
ValueError
If the first line of the csv file doesn't match
'path observation_id sca mjd band', a ValueError is raised.
"""
imlist = []
with open( csvfile ) as ifp:
hdrline = ifp.readline()
if not re.search( r"^\s*path\s+observation_id\s+sca\s+mjd\s+band\s*$", hdrline ):
raise ValueError( f"First line of list file {csvfile} didn't match what was expected." )
for line in ifp:
path, observation_id, sca, _mjd, band = line.split()
if band == self.band:
# This should yell at us if the observation_id
# or sca doesn't match what is read from the path
imlist.append( self.imgcol.get_image( path=path,
observation_id=observation_id,
sca=sca,
band=band ) )
return imlist
[docs]
def sky_sub_all_images( self ):
"""Sky subtracts all snappl.image.Image objects in
self.science_images and self.template_images using
Source Extractor.
Contains its own error logging function, log_error().
"""
# Currently, this writes out a bunch of FITS files. Further refactoring needed
# to support more general image types.
all_imgs = self.science_images.copy() # shallow copy
all_imgs.extend( self.template_images )
def log_error( img, x ):
SNLogger.error( f"Sky subtraction failure on {img.image.path}: {x}" )
if self.catchfailures:
self.failures['skysub'].append(f'{self.image.band} \
{self.image.observation_id} \
{self.image.sca}')
if self.nprocs > 1:
with Pool( self.nprocs ) as pool:
for img in all_imgs:
pool.apply_async( img.run_sky_subtract, (), {},
callback=img.save_sky_subtract_info,
error_callback=partial(log_error, img) )
pool.close()
pool.join()
else:
for img in all_imgs:
img.save_sky_subtract_info( img.run_sky_subtract( mp=False ) )
[docs]
def get_psfs( self ):
"""Retrieve PSFs for all snappl.image.Image objects in
self.science_images and self.template_images.
Contains its own error logging function, log_error().
"""
all_imgs = self.science_images.copy() # shallow copy
all_imgs.extend( self.template_images )
def log_error( img, x ):
SNLogger.error( f"get_psf failure on {img.image.path}: {x}" )
img.failure_location = 'get_psf'
if self.nprocs > 1:
with Pool( self.nprocs ) as pool:
for img in all_imgs:
# callback_partial = partial( img.save_psf_path, all_imgs )
pool.apply_async( img.get_psf, (self.diaobj.ra, self.diaobj.dec), {},
callback=img.keep_psf_data,
error_callback=partial(log_error, img) )
pool.close()
pool.join()
for img in all_imgs:
if img.failure_location is not None:
self.failures[img.failure_location].append(img.fail_info)
else:
for img in all_imgs:
try:
img.keep_psf_data( img.get_psf(self.diaobj.ra, self.diaobj.dec) )
except OSError:
# OSError because if get_psf can't open a file, it does this
# (for example, if the file does not exist)
if self.catchfailures:
self.failures['get_psf'].append(img.fail_info)
[docs]
def align_and_pre_convolve(self, templ_image, sci_image ):
"""Align and pre convolve a single template/science pair.
Parameters
----------
sci_image: phrosty.PipelineImage
The science (new) image.
templ_image: phrosty.PipelineImage
The template (ref) image that will be subtracted from sci_image.
Returns
-------
sfftifier: SpaceSFFT_Flow
Use this object for futher SFFT work. Be sure to
dereference it to free the prodigious amount of memory it
allcoates.
"""
# SFFT needs FITS headers with a WCS and with NAXIS[12]
hdr_sci = sci_image.image.get_wcs().get_astropy_wcs().to_header( relax=True )
hdr_sci.insert( 0, ('NAXIS', 2) )
hdr_sci.insert( 'NAXIS', ('NAXIS1', sci_image.image.data.shape[1] ), after=True )
hdr_sci.insert( 'NAXIS1', ('NAXIS2', sci_image.image.data.shape[0] ), after=True )
data_sci = sci_image.skysub_img.data
noise_sci = sci_image.image.noise
var_sci = noise_sci ** 2
hdr_templ = templ_image.image.get_wcs().get_astropy_wcs().to_header( relax=True )
hdr_templ.insert( 0, ('NAXIS', 2) )
hdr_templ.insert( 'NAXIS', ('NAXIS1', templ_image.image.data.shape[1] ), after=True )
hdr_templ.insert( 'NAXIS1', ('NAXIS2', templ_image.image.data.shape[0] ), after=True )
data_templ = templ_image.skysub_img.data
noise_templ = templ_image.image.noise
var_templ = noise_templ ** 2
sci_psf = sci_image.psf_data
templ_psf = templ_image.psf_data
sci_detmask = sci_image.detmask_img.data
templ_detmask = templ_image.detmask_img.data
sfftifier = SpaceSFFT_Flow(
hdr_target=hdr_sci,
hdr_object=hdr_templ,
target_skyrms=sci_image.skyrms,
object_skyrms=templ_image.skyrms,
PixA_target=data_sci,
PixA_object=data_templ,
PixA_targetVar=var_sci,
PixA_objectVar=var_templ,
PixA_target_DMASK=sci_detmask,
PixA_object_DMASK=templ_detmask,
PSF_target=sci_psf,
PSF_object=templ_psf,
KerPolyOrder=Config.get().value('photometry.phrosty.kerpolyorder')
)
sfftifier.resample_image_mask_psf()
sfftifier.cross_convolve()
return sfftifier
[docs]
def phot_at_coords( self, img, psf, pxcoords=(50, 50), ap_r=3 ):
"""Do photometry at forced set of pixel coordinates.
Parameters
----------
img: snappl.image.Image
The image on which to do the photometry
psf: snappl.psf.PSF
The PSF.
pxcoords: tuple of (int, int)
The position on the image to do the photometry
ap_r: float
Radius of aperture.
Returns
-------
results: dict
Keys and values are:
* 'aperture_sum': flux in aperture of radius ap_r
* 'flux_fit': flux from PSF photometry
* 'flux_fit_err': uncertainty on flux_fit
* 'mag_fit': instrumental magnitude (i.e. no zeropoint) from flux_fit
* 'mag_fit_err': uncertainty on mag_fit
All values are floats.
"""
forcecoords = Table([[float(pxcoords[0])], [float(pxcoords[1])]], names=["x", "y"])
init = img.ap_phot( forcecoords, ap_r=ap_r )
init.rename_column( 'aperture_sum', 'flux_init' )
init.rename_column( 'xcenter', 'xcentroid' )
init.rename_column( 'ycenter', 'ycentroid' )
final = img.psf_phot( init_params=init,
psf=psf,
forced_phot=True
# return_resid_image=True
)
# Debug LNA 20250225
# self.aperture = img.apertures
# Debug LNA 20251202
# self.resid_img = resid_img
flux = final['flux_fit'][0]
flux_err = final['flux_err'][0]
mag = -2.5 * np.log10(final["flux_fit"][0])
mag_err = (2.5 / np.log(10)) * np.abs(final["flux_err"][0] / final["flux_fit"][0])
results_dict = {
'flux': flux,
'flux_err': flux_err,
'aperture_sum': init['flux_init'][0], # Has to be renamed and named back because photutils.
'mag': mag,
'mag_err': mag_err
}
return results_dict
[docs]
def make_phot_info_dict( self, sci_image, templ_image, ap_r=3 ):
""""
Do photometry on a difference image generated from sci_image
and templ_image. Collect the output in a dictionary.
Parmaeters
----------
sci_image: PipelineImage
science image wrapper
temp_image: PipelineImage
template image wrapper
ap_r: float, default 4
Radius of aperture to use in aperture photometry.
Returns
-------
results_dict: dict
Dictionary with keys sci_name, templ_name, success, ra,
dec, mjd, band, observation_id, sca, template_observation_id,
template_sca, zpt, aperture_sum, flux_fit, flux_fit_err,
mag_fit, and mag_fit_err.
"""
# Do photometry on stamp because it will read faster.
# (We hope. But CFS latency will kill you at 1 byte.)
SNLogger.debug( "...make_phot_info_dict reading stamp and psf" )
# Required results keys
req_results_keys = ['mjd', 'flux', 'flux_err', 'zpt', 'NEA', 'sky_rms',
'observation_id', 'sca', 'pix_x', 'pix_y']
phrosty_results_keys = ['science_name', 'template_name',
'science_id', 'template_id',
'template_observation_id', 'template_sca',
'aperture_sum', 'mag', 'mag_err', 'success']
results_keys = req_results_keys + phrosty_results_keys
results_dict = {key: np.nan for key in results_keys}
# Required results keys
results_dict['observation_id'] = str(sci_image.image.observation_id)
results_dict['sca'] = sci_image.image.sca
# phrosty results keys
results_dict['science_name'] = sci_image.image.name
results_dict['template_name'] = templ_image.image.name
results_dict['science_id'] = str(sci_image.image.id)
results_dict['template_id'] = str(templ_image.image.id)
results_dict['template_observation_id'] = str(templ_image.image.observation_id)
results_dict['template_sca'] = templ_image.image.sca
results_dict['success'] = False
try:
results_dict['mjd'] = sci_image.image.mjd
# Additional phrosty keys
diff_img = CompressedFITSImage( filepath=sci_image.diff_stamp_path[ templ_image.image.name ],
noisepath=sci_image.diff_var_stamp_path[ templ_image.image.name ]
)
psf_img = CompressedFITSImage( filepath=sci_image.decorr_psf_path[ templ_image.image.name ] )
# Make sure the files are there. Has side effect of loading the header and data.
diff_img.get_data( which='data', cache=True )
diff_img.get_data( which='noise', cache=True )
# The thing written to disk was actually variance, so fix that
diff_img.noise = np.sqrt( diff_img.noise )
psf_img.get_data( which='data', cache=True )
SNLogger.debug( "...make_phot_info_dict getting psf" )
coord = SkyCoord(ra=self.diaobj.ra * u.deg, dec=self.diaobj.dec * u.deg)
pxcoords = skycoord_to_pixel( coord, diff_img.get_wcs().get_astropy_wcs() )
psf = PSF.get_psf_object( 'OversampledImagePSF',
x=diff_img.data.shape[1]/2., y=diff_img.data.shape[1]/2.,
oversample_factor=1.,
data=psf_img.data )
SNLogger.debug( "...make_phot_info_dict doing photometry" )
results_dict.update( self.phot_at_coords( diff_img, psf, pxcoords=pxcoords, ap_r=ap_r) )
# Add additional info to the results dictionary so it can be merged into a nice file later.
SNLogger.debug( "...make_phot_info_dict getting zeropoint" )
results_dict['zpt'] = sci_image.image.zeropoint
results_dict['success'] = True
except Exception as e:
# results_dict['ap_zpt'] = np.nan
SNLogger.debug( f"...make_phot_info_dict failed for \
{sci_image.image.name} - {templ_image.image.name}. Reason: {e}" )
finally:
# Basically, make_lightcurve will never "fail". Instead, you will get a row of NaN
# with results_dict['success'] = False if something weird happened, here.
SNLogger.debug( "...make_phot_info_dict done." )
return results_dict
[docs]
def add_to_results_dict( self, one_pair ):
"""Record results from self.make_phot_info_dict() to the
aggregate dictionary for the entire light curve.
Parameters
----------
one_pair : dict
Dictionary output from self.make_phot_info_dict().
"""
for key, arr in self.results_dict.items():
arr.append( one_pair[ key ] )
if not one_pair['success']:
SNLogger.debug( "Failure in make_lightcurve!" )
SNLogger.debug( "Done adding to results dict" )
if self.mem_trace:
SNLogger.info( f"After adding to results dict for \
{one_pair['observation_id']} {one_pair['sca']}, memory usage = \
{tracemalloc.get_traced_memory()[1]/(1024**2):.2f} MB" )
[docs]
def save_stamp_paths( self, sci_image, templ_image, paths ):
"""Helper function for recording the stamp paths returned in
self.do_stamps.
Parameters
----------
sci_image : snappl.image.Image
Science image with supernova.
templ_image : snappl.image.Image
Template image without supernova.
paths : list of pathlib.Path
Output from self.do_stamps().
"""
try:
sci_image.zpt_stamp_path[ templ_image.image.name ] = paths[0]
sci_image.diff_stamp_path[ templ_image.image.name ] = paths[1]
sci_image.diff_var_stamp_path[ templ_image.image.name ] = paths[2]
# Lauren added these for debugging...
sci_image.aligned_templ_stamp_path[ templ_image.image.name ] = paths[3]
sci_image.diff_undecorr_stamp_path[ templ_image.image.name ] = paths[4]
except Exception as ex:
SNLogger.exception( f"Exception in self.save_stamp_paths: {ex}" )
if sci_image.failure_location is None:
sci_image.failure_location = 'make_stamps'
sci_image.failure_pair = templ_image.image.name
[docs]
def do_stamps( self, sci_image, templ_image ):
"""Make stamps from the zero point image, decorrelated
difference image, and variance image centered at the
location of the supernova.
Parameters
----------
sci_image : snappl.image.Image
Science image with supernova.
templ_image : snappl.image.Image
Template image without supernova.
Returns
-------
list of pathlib.Path
Paths to the stamps corresponding to the zero point image,
decorrelated difference image, and variance image centered
at the location of the supernova.
"""
try:
zptim = CompressedFITSImage( filepath=sci_image.decorr_zptimg_path[ templ_image.image.name ] )
zpt_stampname = stampmaker(
ra=self.diaobj.ra,
dec=self.diaobj.dec,
shape=np.array([100, 100]),
img=zptim,
savedir=self.dia_out_dir,
savename=f"stamp_{zptim.path.name}"
)
zpt_path = pathlib.Path( zpt_stampname )
zptim.free()
diffim = CompressedFITSImage( filepath=sci_image.decorr_diff_path[ templ_image.image.name ] )
diff_stampname = stampmaker(
ra=self.diaobj.ra,
dec=self.diaobj.dec,
shape=np.array([100, 100]),
img=diffim,
savedir=self.dia_out_dir,
savename=f"stamp_{diffim.path.name}"
)
diff_path = pathlib.Path( diff_stampname )
diffim.free()
diffvarim = CompressedFITSImage( filepath=sci_image.diff_var_path[ templ_image.image.name ] )
diffvar_stampname = stampmaker(
ra=self.diaobj.ra,
dec=self.diaobj.dec,
shape=np.array([100, 100]),
img=diffvarim,
savedir=self.dia_out_dir,
savename=f"stamp_{diffvarim.path.name}"
)
diffvar_path = pathlib.Path( diffvar_stampname )
diffvarim.free()
# Lauren added these for debugging...
alignedtemplim = CompressedFITSImage( filepath=sci_image.aligned_templ_img_path[ templ_image.image.name ] )
alignedtempl_stampname = stampmaker(
ra=self.diaobj.ra,
dec=self.diaobj.dec,
shape=np.array([100, 100]),
img=alignedtemplim,
savedir=self.dia_out_dir,
savename=f"stamp_{alignedtemplim.path.name}"
)
alignedtempl_path = pathlib.Path( alignedtempl_stampname )
alignedtemplim.free()
regdiffim = CompressedFITSImage( sci_image.diff_undecorr_img_path [ templ_image.image.name ] )
regdiffim_stampname = stampmaker(
ra=self.diaobj.ra,
dec=self.diaobj.dec,
shape=np.array([100, 100]),
img=regdiffim,
savedir=self.dia_out_dir,
savename=f"stamp_{regdiffim.path.name}"
)
regdiff_path = pathlib.Path( regdiffim_stampname )
regdiffim.free()
SNLogger.info(f"Decorrelated diff stamp path: {diff_path}")
SNLogger.info(f"Zpt image stamp path: {zpt_path}")
SNLogger.info(f"Decorrelated diff variance stamp path: {diffvar_path}")
SNLogger.info(f"Aligned template stamp path: {alignedtempl_path}")
SNLogger.info(f"Undecorrelated diff stamp path: {regdiff_path}")
return zpt_path, diff_path, diffvar_path, alignedtempl_path, regdiff_path
except Exception as e:
SNLogger.error( f"do_stamps failure for {sci_image.image.observation_id} \
{sci_image.image.sca} - \
{templ_image.image.observation_id} \
{templ_image.image.sca}" )
raise Exception(e)
[docs]
def make_lightcurve( self ):
"""Collect all results from photometry in one dictionary.
Write the output to a csv as a table.
Contains its own error logging function, log_error().
Returns
-------
pathlib.Path
Path to output csv file that contains a light curve.
"""
SNLogger.info( "Making lightcurve." )
self.metadata = {
'provenance_id': None,
'diaobject_id': self.diaobj.id,
'diaobject_position_id': None,
'iau_name': self.diaobj.iauname,
'band': self.band,
'ra': self.diaobj.ra,
'dec': self.diaobj.dec,
'ra_err': None,
'dec_err': None,
'ra_dec_covar': None,
f'local_surface_brightness_{self.band}': -999. # phrosty does not output this value!
# add it later!
}
self.results_dict = {
# Required keys in specified order
'mjd': [], # Days, float
'flux': [], # DN/s, float
'flux_err': [], # DN/s, float
'zpt': [], # AB mag of object is m = -2.5 * log(flux) + zpt, float
'NEA': [], # px^2, float
'sky_rms': [], # DN/s, float
'observation_id': [], # string, temporary name
'sca': [], # int
'pix_x': [], # x-position of SN on detector w/ 0-offset, float
'pix_y': [], # y-position of SN on detector w/ 0-offset, float
# Additional phrosty keys
'science_name': [],
'template_name': [],
'science_id': [],
'template_id': [],
'template_observation_id': [],
'template_sca': [],
'aperture_sum': [],
'mag': [],
'mag_err': [],
'success': []
}
def log_error( sci_image, templ_image, x ):
SNLogger.error( f"make_phot_info_dict failure for {sci_image.image.observation_id} \
{sci_image.image.sca} - \
{templ_image.image.observation_id} \
{templ_image.image.sca}: {x}" )
if self.nprocs > 1:
with Pool( self.nprocs ) as pool:
for sci_image in self.science_images:
for templ_image in self.template_images:
logerr_partial = partial(log_error, sci_image, templ_image)
pool.apply_async( self.make_phot_info_dict, (sci_image, templ_image), {},
self.add_to_results_dict,
error_callback=logerr_partial )
SNLogger.debug( f"pool.apply async done for \
{sci_image.image.observation_id} \
{sci_image.image.sca}" )
pool.close()
pool.join()
SNLogger.debug('Make phot info dict pool closed and joined.')
else:
for i, sci_image in enumerate( self.science_images ):
if sci_image.failure_location is None:
SNLogger.debug( f"Doing science image {i} of {len(self.science_images)}" )
for templ_image in self.template_images:
self.add_to_results_dict( self.make_phot_info_dict( sci_image, templ_image ) )
if self.dbsave:
SNLogger.debug('About to get image provenance.')
imgprov = Provenance.get_by_id( self.science_images[0].image.provenance_id, dbclient=self.dbclient )
SNLogger.debug('About to get object provenance.')
objprov = Provenance.get_by_id( self.diaobj.provenance_id, dbclient=self.dbclient )
phrosty_version = phrosty.__version__
major = int(phrosty_version.split('.')[0])
minor = int(phrosty_version.split('.')[1])
SNLogger.debug('About to make LC provenance.')
ltcvprov = Provenance( process='phrosty',
major=major,
minor=minor,
params=Config.get(),
keepkeys=[ 'photometry.phrosty' ],
omitkeys=None,
upstreams=[imgprov, objprov],
)
self.metadata['provenance_id'] = ltcvprov.id
SNLogger.debug('About to make LC object.')
lc_obj = Lightcurve(data=self.results_dict, meta=self.metadata)
lc_obj.diaobj = self.diaobj
lc_obj.provenance_object = ltcvprov
else:
lc_obj = Lightcurve(data=self.results_dict, meta=self.metadata)
if self.dbsave:
SNLogger.debug( "Saving results to database..." )
ltcvprov.save_to_db( tag=self.ltcv_prov_tag )
lc_obj.write()
lc_obj.save_to_db( dbclient=self.dbclient )
else:
SNLogger.debug( "Saving results using paths..." )
if self.diaobj.id is not None:
save_basename = str(self.diaobj.id)
else:
save_basename = str(self.oid)
filepath = pathlib.Path(f'data/{self.oid}/{save_basename}_{self.metadata["band"]}.pq')
results_savepath = f'{self.ltcv_dir}/{filepath}'
lc_obj.write( base_dir=self.ltcv_dir, filepath=filepath, overwrite=True)
SNLogger.info(f'Results saved to {results_savepath}.')
return results_savepath
[docs]
def write_fits_file( self, data, header, savepath ):
"""Helper function for writing fits files.
Parameters
----------
data : np.array
Image array.
header : _type_
FITS header.
savepath : str
Savepath for FITS file.
"""
# try:
if header is not None:
hdr_dict = dict(header.items())
else:
hdr_dict = None
fitsio.write( savepath, data, header=hdr_dict, clobber=True )
# except Exception as e:
# SNLogger.exception( f"Exception writing FITS image {savepath}: {e}" )
# raise
[docs]
def clear_contents( self, directory ):
"""Delete contents of a directory. Used to clear temporary
files.
Parameters
----------
directory : pathlib.Path
Path to directory to empty.
"""
for f in directory.iterdir():
try:
if f.is_dir():
shutil.rmtree( f )
else:
f.unlink()
except Exception as e:
print( f'Oops! Deleting {f} from {directory} did not work.\nReason: {e}' )
[docs]
def __call__( self, through_step=None ):
"""Run the pipeline.
Parameters
----------
through_step: str, default None
Which step to run thorough? Runs them all if not given.
Steps in order are:
* sky_subtract
* get_psfs
* align_and_preconvolve
* subtract
* find_decorrelation
* apply_decorrelation
* make_stamps
* make_lightcurve
Returns
-------
ltcvpath : pathlib.Path or None
The path to the output lightcurve file if make_lightcurve
was run, otherwise None.
"""
if self.mem_trace:
tracemalloc.start()
tracemalloc.reset_peak()
if through_step is None:
through_step = 'make_lightcurve'
steps = [ 'sky_subtract', 'get_psfs', 'align_and_preconvolve', 'subtract', 'find_decorrelation',
'apply_decorrelation', 'make_stamps', 'make_lightcurve' ]
stepdex = steps.index( through_step )
if stepdex < 0:
raise ValueError( f"Unknown step {through_step}" )
steps = steps[:stepdex+1]
if 'sky_subtract' in steps:
# After this step is done, all images (both science and template)
# will have the following fields set:
# .skysub_img : sub-subtracted image
# .detmask_img : deteciton mask image
# .skyrms : float, median of sky image calculated by SExtractor
SNLogger.info( "Running sky subtraction" )
with nvtx.annotate( "skysub", color=0xff8888 ):
self.sky_sub_all_images()
if self.mem_trace:
SNLogger.info( f"After sky_subtract, memory usage = {tracemalloc.get_traced_memory()[1]/(1024**2):.2f} MB" )
if 'get_psfs' in steps:
# After this step, all images (both science and template) will
# have the .psf_data field set with the image-resolution PSF stamp data
# (from a PSF.get_stamp() call.)
SNLogger.info( "Getting PSFs" )
with nvtx.annotate( "getpsfs", color=0xff8888 ):
self.get_psfs()
if self.mem_trace:
SNLogger.info( f"After get_psfs, memory usage = {tracemalloc.get_traced_memory()[1]/(1024**2):.2f} MB" )
# Create a process pool to write fits files
with Pool( self.nwrite ) as fits_writer_pool:
def log_fits_write_error( savepath, x ):
SNLogger.error( f"Exception writing FITS file {savepath}: {x}" )
# raise?
# Do the hardcore processing
for templ_image in self.template_images:
for sci_image in self.science_images:
SNLogger.info( f"Processing {sci_image.image.name} minus {templ_image.image.name}" )
sfftifier = None
i_failed_gpu = False # We haven't failed yet.
fail_info = {
'science': sci_image.fail_info,
'template': templ_image.fail_info
}
continuation_conditions = [
sci_image.fail_info in self.failures['skysub'],
sci_image.fail_info in self.failures['get_psf'],
templ_image.fail_info in self.failures['skysub'],
templ_image.fail_info in self.failures['get_psf']
]
if any(continuation_conditions):
i_failed_gpu = True
if 'align_and_preconvolve' in steps and not i_failed_gpu:
# After this step, sfftifier will be a SpaceSFFT_Flow object.
SNLogger.info( "...align_and_preconvolve" )
with nvtx.annotate( "align_and_pre_convolve", color=0x8888ff ):
try:
sfftifier = self.align_and_pre_convolve( templ_image=templ_image,
sci_image=sci_image
)
mess = f"{sci_image.image.name}-{templ_image.image.name}"
aligned_templ_path = self.dia_out_dir / f"aligned_{mess}"
sci_image.aligned_templ_img_path[ templ_image.image.name ] = aligned_templ_path
self.write_fits_file(sfftifier.op.asnumpy(sfftifier.PixA_resamp_object),
sfftifier.hdr_target, aligned_templ_path)
except Exception as e:
i_failed_gpu = True
SNLogger.debug(f'Failure in align_and_preconvolve! Reason: {e}')
if self.catchfailures:
self.failures['align_and_preconvolve'].append(fail_info)
if 'subtract' in steps and not i_failed_gpu:
# After this step is done, two more fields in sfftifier are set:
# Solution : Matching kernel parameterization (coefficients)
# PixA_DIFF : difference image
SNLogger.info( "...subtract" )
with nvtx.annotate( "subtraction", color=0x44ccff ):
try:
sfftifier.sfft_subtract()
mess = f"{sci_image.image.name}-{templ_image.image.name}"
undecorr_diff_path = self.dia_out_dir / f"diff_{mess}"
sci_image.diff_undecorr_img_path[ templ_image.image.name ] = undecorr_diff_path
self.write_fits_file(sfftifier.op.asnumpy(sfftifier.PixA_DIFF),
sfftifier.hdr_target, undecorr_diff_path)
except Exception as e:
i_failed_gpu = True
SNLogger.debug(f'Failure in subtraction! Failure is: {e}')
if self.catchfailures:
self.failures['subtract'].append(fail_info)
if 'find_decorrelation' in steps and not i_failed_gpu:
# This step does ...
# After it's done, the following fields of sfftifier are set:
# Solution : CPU copy of Solution
# FKDECO : result of PureCupy_Decorrelation_Calculator.PCDC
# In addition the two local varaibles diff_var and diff_var_path are set.
# diff_var : variance in difference image, on GPU
# diff_var_path : where we want to write diff_var in self.dia_out_dir
SNLogger.info( "...find_decorrelation" )
with nvtx.annotate( "find_decor", color=0xcc44ff ):
try:
sfftifier.find_decorrelation()
except Exception as e:
i_failed_gpu = True
SNLogger.debug(f'Failure in find_decorrelation! Reason: {e}')
if self.catchfailures:
self.failures['find_decorrelation'].append(fail_info)
SNLogger.info( "...generate variance image" )
with nvtx.annotate( "variance", color=0x44ccff ):
try:
diff_var = sfftifier.create_variance_image()
mess = f"{sci_image.image.name}-{templ_image.image.name}"
diff_var_path = self.dia_out_dir / f"diff_var_{mess}"
sci_image.diff_var_path[ templ_image.image.name ] = diff_var_path
self.write_fits_file(sfftifier.op.asnumpy(diff_var),
sfftifier.hdr_target, diff_var_path)
except Exception as e:
i_failed_gpu = True
SNLogger.debug(f'Failure in generate variance! Reason: {e}')
if self.catchfailures:
self.failures['variance'].append(fail_info)
if 'apply_decorrelation' in steps and not i_failed_gpu:
try:
mess = f"{sci_image.image.name}-{templ_image.image.name}"
decorr_psf_path = self.dia_out_dir / f"decorr_psf_{mess}"
decorr_zptimg_path = self.dia_out_dir / f"decorr_zptimg_{mess}"
decorr_diff_path = self.dia_out_dir / f"decorr_diff_{mess}"
images = [ sfftifier.PixA_DIFF,
sfftifier.PixA_Ctarget, sfftifier.PSF_Ctarget ]
savepaths = [ decorr_diff_path,
decorr_zptimg_path, decorr_psf_path ]
headers = [ sfftifier.hdr_target,
sfftifier.hdr_target, None ]
for i, (img, savepath, hdr) in enumerate(zip( images, savepaths, headers )):
with nvtx.annotate( "apply_decor", color=0xccccff ):
SNLogger.info( f"...apply_decor to {savepath}" )
decorimg = sfftifier.apply_decorrelation( img )
with nvtx.annotate( "submit writefits", color=0xff8888 ):
SNLogger.info( f"...writefits {savepath}" )
fits_writer_pool.apply_async( self.write_fits_file,
( sfftifier.op.asnumpy( decorimg ), hdr, savepath ), {},
error_callback=partial(log_fits_write_error,
savepath) )
sci_image.decorr_psf_path[ templ_image.image.name ] = decorr_psf_path
sci_image.decorr_zptimg_path[ templ_image.image.name ] = decorr_zptimg_path
sci_image.decorr_diff_path[ templ_image.image.name ] = decorr_diff_path
except Exception as e:
i_failed_gpu = True
SNLogger.debug( f"Failure in apply_decorrelation! Reason: {e}" )
if self.catchfailures:
self.failures['apply_decorrelation'].append(fail_info)
if self.keep_intermediate and not i_failed_gpu:
# Each key is the file prefix addition.
# Each list has [descriptive filetype, image file name, data, header].
# TODO: Include multiprocessing.
# In the future, we may want to write these things right after they happen
# instead of saving it all for the end of the SFFT stuff.
write_filepaths = {'aligned': [['img',
f'{templ_image.image.name}_-_{sci_image.image.name}',
sfftifier.op.asnumpy(sfftifier.PixA_resamp_object),
sfftifier.hdr_target],
['var',
f'{templ_image.image.name}_-_{sci_image.image.name}',
sfftifier.op.asnumpy(sfftifier.PixA_resamp_objectVar),
sfftifier.hdr_target],
['psf',
f'{templ_image.image.name}_-_{sci_image.image.name}',
sfftifier.op.asnumpy(sfftifier.PSF_target),
sfftifier.hdr_target],
['detmask',
f'{sci_image.image.name}_-_{templ_image.image.name}',
sfftifier.op.asnumpy(sfftifier.PixA_resamp_object_DMASK),
sfftifier.hdr_target]
],
'convolved': [['img',
f'{sci_image.image.name}_-_{templ_image.image.name}.fits',
sfftifier.op.asnumpy(sfftifier.PixA_Ctarget),
sfftifier.hdr_target],
['img',
f'{templ_image.image.name}_-_{sci_image.image.name}.fits',
sfftifier.op.asnumpy(sfftifier.PixA_Cresamp_object),
sfftifier.hdr_target]
],
'diff': [['img',
f'{sci_image.image.name}_-_{templ_image.image.name}.fits',
sfftifier.op.asnumpy(sfftifier.PixA_DIFF),
sfftifier.hdr_target]
],
'match_kernel': [['img',
f'{sci_image.image.name}_-_{templ_image.image.name}.fits',
sfftifier.op.asnumpy(sfftifier.MATCH_KERNEL),
sfftifier.hdr_target]
],
# LNA 20260131: There are issues with saving complex arrays using fitsio.
# But the imaginary part of the decorrelation kernel is real.
# Don't worry about it.
'decorr': [['kernel_real',
f'{sci_image.image.name}_-_{templ_image.image.name}.fits',
sfftifier.op.asnumpy(sfftifier.FKDECO).real,
sfftifier.hdr_target],
['kernel_imag',
f'{sci_image.image.name}_-_{templ_image.image.name}.fits',
sfftifier.op.asnumpy(sfftifier.FKDECO).imag,
sfftifier.hdr_target],
]
}
# Write the intermediate files
for key in write_filepaths.keys():
for (imgtype, name, data, header) in write_filepaths[key]:
savepath = self.scratch_dir / f'{key}_{imgtype}_{name}'
self.write_fits_file( data, header, savepath=savepath )
SNLogger.info( f"DONE processing {sci_image.image.name} minus {templ_image.image.name}" )
if self.mem_trace:
SNLogger.info( f"After preprocessing, subtracting, and postprocessing \
a science image, memory usage = \
{tracemalloc.get_traced_memory()[1]/(1024**2):.2f} MB" )
sci_image.free()
SNLogger.info( f"DONE with all science images for template {templ_image.image.name}" )
templ_image.free()
SNLogger.info( "Waiting for FITS writer processes to finish" )
with nvtx.annotate( "fits_write_wait", color=0xff8888 ):
fits_writer_pool.close()
fits_writer_pool.join()
SNLogger.info( "...FITS writer processes done." )
if 'make_stamps' in steps:
SNLogger.info( "Starting to make stamps..." )
with nvtx.annotate( "make stamps", color=0xff8888 ):
def log_stamp_err( sci_image, templ_image, x ):
SNLogger.error( f"do_stamps failure for {sci_image.image.observation_id} \
{sci_image.image.sca} - \
{templ_image.image.observation_id} \
{templ_image.image.sca}: {x} " )
partialstamp = partial(stampmaker, self.diaobj.ra, self.diaobj.dec, np.array([100, 100]))
# original sci path, savedir, savename
sci_orig_stamp_args = ( (si.image, self.dia_out_dir, f'stamp_{str(si.image.name)}', 'data')
for si in self.science_images)
# template path, savedir, savename
templstamp_args = ( (ti.image, self.dia_out_dir, f'stamp_{str(ti.image.name)}', 'data')
for ti in self.template_images )
if self.nwrite > 1:
# Save stamps for original template images.
with Pool( self.nwrite ) as templ_stamp_pool:
templ_stamp_pool.starmap_async( partialstamp, templstamp_args,
error_callback=log_stamp_err )
templ_stamp_pool.close()
templ_stamp_pool.join()
# Save stamps for original science images.
with Pool( self.nwrite ) as sci_orig_stamp_pool:
sci_orig_stamp_pool.starmap_async( partialstamp, sci_orig_stamp_args,
error_callback=log_stamp_err )
sci_orig_stamp_pool.close()
sci_orig_stamp_pool.join()
# Save stamps for decorrelated difference image, zero point image (decorrelated sky-subtracted
# science image), and variance image for decorrelated difference image
with Pool( self.nwrite ) as sci_stamp_pool:
for sci_image in self.science_images:
for templ_image in self.template_images:
pair = (sci_image, templ_image)
stamperr_partial = partial(log_stamp_err, sci_image, templ_image)
sci_stamp_pool.apply_async( self.do_stamps, pair, {},
callback = partial(self.save_stamp_paths,
sci_image, templ_image),
error_callback=stamperr_partial )
sci_stamp_pool.close()
sci_stamp_pool.join()
else:
# Make stamp of just the template image
for tsargs in templstamp_args:
try:
partialstamp(*tsargs)
except Exception:
self.failures['make_stamps'].append(tsargs[0].fail_info)
# Make stamp of just the science image
for sosargs in sci_orig_stamp_args:
try:
partialstamp(*sosargs)
except Exception:
self.failures['make_stamps'].append(sosargs[0].fail_info)
for sci_image in self.science_images:
for templ_image in self.template_images:
if templ_image.image.name in sci_image.decorr_diff_path.keys():
try:
stamp_paths = self.do_stamps( sci_image, templ_image)
self.save_stamp_paths( sci_image, templ_image, stamp_paths )
except Exception:
if self.catchfailures:
self.failures['make_stamps'].append({
'science': sci_image.fail_info,
'template': templ_image.fail_info
})
SNLogger.info('...finished making stamps.')
if self.mem_trace:
SNLogger.info( f"After make_stamps, memory usage = {tracemalloc.get_traced_memory()[1]/(1024**2):.2f} MB" )
lightcurve_path = None
if 'make_lightcurve' in steps:
with nvtx.annotate( "make_lightcurve", color=0xff8888 ):
lightcurve_path = self.make_lightcurve()
# Debugging stuff below for checking phot location...
# import matplotlib.pyplot as plt
# from astropy.visualization import ZScaleInterval
# check_image = self.science_images[0]
# check_templ = self.template_images[0]
# check_diff_path = check_image.diff_stamp_path[ check_templ.image.name ]
# check_diff = fitsio.FITS(check_diff_path)
# check_image_data = check_diff[0].read()
# fig, ax = plt.subplots(nrows=1, ncols=4, figsize=(15, 5))
# norm = ZScaleInterval().get_limits(check_image_data)
# ax[0].imshow(check_image_data, origin='lower', vmin=norm[0], vmax=norm[1])
# ax[1].imshow(check_image_data - self.resid_img, origin='lower', vmin=norm[0], vmax=norm[1])
# ax[2].imshow(check_image.psf_data, origin='lower')
# im = ax[3].imshow(self.resid_img, origin='lower', vmin=norm[0], vmax=norm[1])
# ax[0].set_title('Decorr diff')
# ax[1].set_title('Decorr PSF')
# ax[2].set_title('Original PSF')
# ax[3].set_title('Decorr diff - Decorr PSF')
# # ax[2].set_xlim(-25,75)
# # ax[2].set_ylim(-25,75)
# plt.tight_layout()
# plt.savefig('/home/psf_resids.pdf', format='pdf')
# fig, ax = plt.subplots(1,1)
# ax.imshow(check_image_data, origin='lower', vmin=norm[0], vmax=norm[1])
# self.aperture.plot(color='red', lw='2', ax=ax)
# plt.savefig('/home/aperture_stamp.pdf')
if self.mem_trace:
SNLogger.info( f"After make_lightcurve, memory usage = \
{tracemalloc.get_traced_memory()[1]/(1024**2):.2f} MB" )
if self.remove_temp_dir:
self.clear_contents( self.temp_dir )
if self.catchfailures:
if np.sum( [len(x) for x in self.failures.values()] ) > 0:
SNLogger.info( f"There were some failures here! They were:\n{self.failures}" )
else:
SNLogger.info( f"No failures here! Fail list:\n{self.failures}" )
if lightcurve_path is None:
SNLogger.info( "Light curves saved to database!" )
return lightcurve_path
# ======================================================================
def main():
# Run one arg pass just to get the config file, so we can augment
# the full arg parser later with config options
configparser = argparse.ArgumentParser( add_help=False )
configparser.add_argument( '-c', '--config-file', default=None,
help=( "Location of the .yaml config file; defaults to the value of the "
"SNPIT_CONFIG environment varaible." ) )
args, leftovers = configparser.parse_known_args()
try:
cfg = Config.get( args.config_file, setdefault=True )
except RuntimeError as e:
if str(e) == 'No default config defined yet; run Config.init(configfile)':
sys.stderr.write( "Error, no configuration file defined.\n"
"Either run phrosty with -c <configfile>\n"
"or set the SNPIT_CONFIG environment variable.\n" )
sys.exit(1)
else:
raise
parser = argparse.ArgumentParser()
# Put in the config_file argument, even though it will never be found, so it shows up in help
parser.add_argument( '-c', '--config-file', help="Location of the .yaml config file" )
# Running options
parser.add_argument( '-p', '--nprocs', type=int, default=1,
help="Number of process for multiprocessing steps (e.g. skysub)" )
parser.add_argument( '-w', '--nwrite', type=int, default=5,
help="Number of parallel FITS writing processes" )
parser.add_argument( '-v', '--verbose', action='store_true', default=False,
help="Show debug log info" )
parser.add_argument( '--through-step', default='make_lightcurve',
help="Stop after this step; one of (see above)" )
parser.add_argument( '--dbsave', action='store_true',
help="Toggle saving to the database." )
parser.add_argument( '--memtrace', action='store_true',
help="Toggle memory tracing with tracemalloc.")
parser.add_argument( '--catchfailures', action='store_true',
help="Toggle failure collection. If true, pipeline does not \
cancel if one image fails. If false, pipeline crashes if \
one image fails (useful for debugging)." )
# Object collections
parser.add_argument( '-oc', '--object-collection', default='snpitdb',
help='Collection of the object. Currently, "snpitdb", "ou2024", and "manual" supported.' )
parser.add_argument( '-os', '--object-subset', default=None,
help="Collection subset. Not used by all collections." )
# SN and observation information
parser.add_argument( '--oid', type=int, required=True,
help="Object ID. Meaning is collection-dependent." )
parser.add_argument( '-r', '--ra', type=float, default=None,
help="Object RA. By default, uses the one found for the object." )
parser.add_argument( '-d', '--dec', type=float, default=None,
help="Object Dec. By default, uses the one found for the object." )
parser.add_argument( '-b', '--band', type=str, required=True,
help="Band: R062, Z087, Y106, J129, H158, F184, or K213" )
# Required args for using SN PIT database
# DiaObj
parser.add_argument( '-did', '--diaobject-id', type=str, default=None,
help="ID for DiaObject. Required to use SN PIT database. \
Invalid if --image-collection is not snpitdb." )
parser.add_argument( '-dpt', '--diaobject-provenance-tag', type=str, default=None,
help="Provenance tag for DiaObject. Required to use SN PIT database. \
Invalid if --image-collection is not snpitdb." )
parser.add_argument( '-dp', '--diaobject-process', type=str, required=False, default=None,
help="Process for DiaObject. Required to use SN PIT database. \
Invalid if --image-collection is not snpitdb.")
parser.add_argument( '-dppt', '--diaobject-position-provenance-tag', type=str, default=None,
help="Provenance tag for the position of the DiaObject." )
parser.add_argument( '-dpp', '--diaobject-position-process', type=str, default=None,
help="The process where the DiaObject position originated." )
# Lightcurve
parser.add_argument( '-lpi', '--ltcv-provenance-id', type=str, default=None,
help="Provenance ID for lightcurve. Required to use SN PIT database. \
Invalid if --image-collection is not snpitdb." )
parser.add_argument( '-lpt', '--ltcv-provenance-tag', type=str, default=None,
help="Provenance tag for lightcurve. Required to use SN PIT database. \
Invalid if --image-collection is not snpitdb." )
parser.add_argument( '-lp', '--ltcv-process', type=str, default='phrosty',
help="Process for light curve. Required to use SN PIT database. \
Invalid if --image-collection is not snpitdb." )
# Image collection
parser.add_argument( '-ic', '--image-collection', default='snpitdb',
help="Collection of the images we're using. For SN PIT database, use snpitdb. \
Currently supported: ou2024, manual_fits, snpitdb (default)." )
parser.add_argument( '-is', '--image-subset', default=None,
help="Image collection subset. To use SN PIT database, must be None." )
# Image
parser.add_argument( '-ipt', '--image-provenance-tag', default=None,
help='Provenance tag for images. Required to use SN PIT database. \
Invalid if --image-collection is not snpitdb.' )
parser.add_argument( '-ip', '--image-process', default=None,
help='Image process. Required to use SN PIT database. \
Invalid if --image-collection is not snpitdb.' )
# Path-based options
parser.add_argument( '--base-path', type=str, default=None,
help='Base path for reading images. Required for "manual_fits" image collection.' )
parser.add_argument( '-t', '--template-images', type=str, default=None,
help="Path to file with, per line, ( path_to_image, observation_id, sca, mjd, band )" )
parser.add_argument( '-s', '--science-images', type=str, default=None,
help="Path to file with, per line, ( path_to_image, observation_id, sca, mjd, band )" )
cfg.augment_argparse( parser )
args = parser.parse_args( leftovers )
cfg.parse_args( args )
if args.base_path is None and args.image_collection == 'manual_fits':
SNLogger.error( 'Must provide --base-path if --image-collection is manual_fits.' )
raise ValueError( f'args.base_path is {args.base_path}.' )
if args.image_collection == 'snpitdb' and args.image_provenance_tag is None:
SNLogger.error( 'Must provide --image-provenance-tag if --image-collection is snpitdb.' )
raise ValueError( f'args.image_provenance_tag is {args.image_provenance_tag}.' )
dbclient = SNPITDBClient()
# Get the DiaObject, update the RA and Dec
if args.diaobject_id is None:
diaobjs = DiaObject.find_objects( collection=args.object_collection,
# subset=args.object_subset,
provenance_tag=args.diaobject_provenance_tag,
process=args.diaobject_process,
name=args.oid, ra=args.ra, dec=args.dec )
if len( diaobjs ) == 0:
raise ValueError( f"Could not find DiaObject with id={args.id}, ra={args.ra}, dec={args.dec}." )
if len( diaobjs ) > 1:
raise ValueError( f"Found multiple DiaObject with id={args.id}, ra={args.ra}, dec={args.dec}." )
diaobj = diaobjs[0]
if args.ra is not None:
if np.fabs( args.ra - diaobj.ra ) > 1. / 3600. / np.cos( diaobj.dec * np.pi / 180. ):
SNLogger.warning( f"Given RA {args.ra} is far from DiaObject nominal RA {diaobj.ra}" )
diaobj.ra = args.ra
if args.dec is not None:
if np.fabs( args.dec - diaobj.dec ) > 1. / 3600.:
SNLogger.warning( f"Given Dec {args.dec} is far from DiaObject nominal Dec {diaobj.dec}" )
diaobj.dec = args.dec
# Get the image collection
imgcol = ImageCollection.get_collection( collection=args.image_collection,
subset=args.image_subset,
provenance_tag=args.image_provenance_tag,
process=args.image_process,
base_path=args.base_path,
dbclient=dbclient
)
# if args.image_collection == 'snpitdb':
# _found_images = imgcol.find_images( ra=diaobj.ra,
# dec=diaobj.dec,
# band=args.band,
# dbclient=dbclient
# )
# else:
# _found_images = imgcol.find_images( ra=diaobj.ra,
# dec=diaobj.dec,
# band=args.band
# )
# fetched_prov = Provenance.get_provs_for_tag( tag=args.diaobject_provenance_tag,
# process=args.diaobject_process
# )
# Create and launch the pipeline
pipeline = Pipeline( diaobj, imgcol, args.band,
science_csv=args.science_images,
template_csv=args.template_images,
oid=args.oid,
ltcv_prov_tag=args.ltcv_provenance_tag,
dbsave=args.dbsave,
dbclient=dbclient,
nprocs=args.nprocs,
nwrite=args.nwrite,
verbose=args.verbose,
memtrace=args.memtrace,
catchfailures=args.catchfailures )
pipeline( args.through_step )
# ======================================================================
if __name__ == "__main__":
main()