diff --git a/changes/1533.source_catalog.rst b/changes/1533.source_catalog.rst new file mode 100644 index 000000000..093ba66b2 --- /dev/null +++ b/changes/1533.source_catalog.rst @@ -0,0 +1 @@ +Use meta.source_catalog and meta.cal_step.source_catalog in source_catalog step. diff --git a/changes/1533.source_detection.rst b/changes/1533.source_detection.rst new file mode 100644 index 000000000..659a5b130 --- /dev/null +++ b/changes/1533.source_detection.rst @@ -0,0 +1 @@ +Remove SourceDetectionStep (use SourceCatalogStep). diff --git a/docs/roman/package_index.rst b/docs/roman/package_index.rst index 35ab907c2..bc03ed8ce 100644 --- a/docs/roman/package_index.rst +++ b/docs/roman/package_index.rst @@ -17,6 +17,5 @@ resample/index.rst skymatch/index.rst source_catalog/index.rst - source_detection/index.rst stpipe/index.rst tweakreg/index.rst diff --git a/docs/roman/pipeline_steps.rst b/docs/roman/pipeline_steps.rst index bb3cab974..93e1e7185 100644 --- a/docs/roman/pipeline_steps.rst +++ b/docs/roman/pipeline_steps.rst @@ -14,7 +14,6 @@ Pipeline Steps flatfield/index.rst photom/index.rst flux/index.rst - source_detection/index.rst tweakreg/index.rst skymatch/index.rst outlier_detection/index.rst diff --git a/docs/roman/source_catalog/main.rst b/docs/roman/source_catalog/main.rst index 7f0334c3e..689c8d1dc 100644 --- a/docs/roman/source_catalog/main.rst +++ b/docs/roman/source_catalog/main.rst @@ -65,7 +65,7 @@ PSF Fitting Star finding algorithms like `~photutils.detection.DAOStarFinder` provide approximate stellar centroids. More precise centroids may be inferred by -fitting model PSFs to the observations. Setting the SourceDetectionStep's +fitting model PSFs to the observations. Setting the SourceCatalogStep's option `fit_psf` to True will generate model Roman PSFs with `WebbPSF `_, and fit those models to each of the sources detected by diff --git a/docs/roman/source_detection/arguments.rst b/docs/roman/source_detection/arguments.rst deleted file mode 100644 index 7323cca7f..000000000 --- a/docs/roman/source_detection/arguments.rst +++ /dev/null @@ -1,61 +0,0 @@ -Arguments -========= - -The source detection fitting step has several arguments. These can be specified -by the user by passing them to the step in a Python session, or setting them -in a parameter file. - -* ``--kernel_fwhm``: A parameter for DAOStarFinder: size of Gaussian kernel in - pixels. By default the FWHM is assumed to be the diffraction - limited PSF, given the filter used for this observation. -* ``--sharplo``: A parameter for DAOStarFinder: lower bound for sharpness. - Default is 0.0. -* ``--sharphi``: A parameter for DAOStarFinder: upper bound for sharpness. - Default is 1.0. -* ``--roundlo``: A parameter for DAOStarFinder: lower bound for roundness. - Default is -1.0. A circular source will have a zero roundness. - A source extended in x or y will have a negative or positive - roundness, respectively. -* ``--roundhi``: A parameter for DAOStarFinder: upper bound for roundness. - Default is 1.0. A circular source will have a zero roundness. - A source extended in x or y will have a negative or positive - roundness, respectively. -* ``--peakmax``: A parameter for DAOStarFinder: upper limit on brightest pixel - in sources. Default is 1000.0. -* ``--max_sources``: The maximum number of sources in the output catalog, - choosing brightest. Default is None, which will return all - detected sources. -* ``--scalar_threshold``: If specified, the absolute detection threshold to be - used for the entire image. Units are assumed to be the - same as input data. One of `scalar_threshold`, - `calc_scalar_threshold` must be chosen. Default is - None. -* ``--calc_scalar_threshold``: If specified, a single scalar threshold will be - determined for the entire image. This is done by - calculating a 2D background image, and using that - to determine a single threshold value for the - entire image. One of `scalar_threshold` or - `calc_scalar_threshold` must be chosen. - must be chosen. Default is True. -* ``--snr_threshold``: If using `calc_threshold_img`, the SNR for the threshold - image. Default is 3.0. -* ``--bkg_estimator``: If using `calc_threshold_img`, choice of mean, median, or - mode. Default is median. -* ``--bkg_boxsize``: If using `calc_threshold_img` size of box in pixels for - 2D background / threshold images and if using - calc_threshold_2d, the size of the box used when detecting - sources. Default is 9. -* ``--bkg_sigma``: If using `calc_threshold_img`, n sigma for sigma clipping - for background calculation. Default is 2.0. -* ``--bkg_filter_size``: If using `calc_threshold_img` or `calc_threshold_2d`, - size of square gaussian kernel for background - calculation. Default is 3. -* ``--save_catalogs``: A True/False value that specifies whether to write - the optional output catalog. Default is False. -* ``--output_cat_filetype``: If `save_catalogs` is True, file type of output - catalog from choice of asdf and escv. Catalog - will be saved as a numpy array with four dimensions. - In order, these represent source ID, x centroid - position, y centroid position, and flux. -* ``--fit_psf``: If True, fit a PSF model to each detected source for more precise - source centroids and fluxes. diff --git a/docs/roman/source_detection/description.rst b/docs/roman/source_detection/description.rst deleted file mode 100644 index 21b37c150..000000000 --- a/docs/roman/source_detection/description.rst +++ /dev/null @@ -1,76 +0,0 @@ -Description -============ - -The source detection step produces catalogs of point-like sources for use by the -Tweakreg step for image alignment. It uses DAOStarFinder to detect point sources -in the image, with an option to subsequently fit PSF models to the detected -sources for more precise centroids and fluxes. - -Detecting Sources ------------------ - -Sources are detected using `~photutils.detection.DAOStarFinder` from -`photutils `_, which is an -implementation of the method `DAOFIND` from -`Stetson (1987) `_. -The algorithm can be provided limits on the source flux, radius, roundness, -sharpness, and background. - -PSF Fitting ------------ - -Star finding algorithms like `~photutils.detection.DAOStarFinder` provide -approximate stellar centroids. More precise centroids may be inferred by -fitting model PSFs to the observations. Setting the SourceDetectionStep's -option `fit_psf` to True will generate model Roman PSFs with -`WebbPSF `_, and fit -those models to each of the sources detected by -`~photutils.detection.DAOStarFinder`. More details are in :doc:`psf`. - -Outputs / Returns ------------------ - -By default, the resulting source catalog will be temporarily attached to the -output ImageModel in the `meta.source_catalog.tweakreg_catalog` attribute as -numpy array representing, in order, source ID, x centroid position, y centroid -position, and flux. This catalog will then be deleted from the model in the -Tweakreg step. - -Optionally, the catalog can be saved to disk in which case a -`meta.source_catalog.tweakreg_catalog_name` attribute will be added to the file -to point Tweakreg to the catalog on disk. To do this, set `save_catalogs` to -True. Output catalogs will be saved in the same directory as input files, and -are also 4D numpy arrays representing, in order, source ID, x centroid position, -y centroid position, and flux. Output catalogs can be in ASDF or ECSV format. - -NOTE: The intermediate resulting ImageModel from SourceDetectionStep can -only be saved if it does not contain an attached catalog - to do this, use the -`save_catalogs` option to separate the catalog from the file and save them -separately. - -Options for Thresholding ------------------------- - -The DAOStarFinder routine detects point-like sources in an image that are above -a certain, specified floating point threshold. This step provides several options -for calculating this threshold, either using one value for the entire image, -or by detecting sources in segments of the image and using a different appropriate -threshold for each (useful if background varies across the image). - -The first option is to set `scalar_threshold` - this will use the specified -threshold as the detection threshold for the entire image. - -The second option is to use `calc_threshold` - this will calculate a single -threshold value for the entire image based on the sigma-clipped average -(mean, median, or mode) background level of the whole image. - -Other Options -------------- - -Limiting maximum number of sources -++++++++++++++++++++++++++++++++++ - -By default, all detected sources will be returned in the final output catalog. -If you wish to limit the number of sources, this can be done with the -`max_sources` argument, which will sort the output catalog by flux and return -only the N brightest. diff --git a/docs/roman/source_detection/index.rst b/docs/roman/source_detection/index.rst deleted file mode 100644 index 2050333b9..000000000 --- a/docs/roman/source_detection/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -.. _source_detection_step: - -================ -Source Detection -================ - -.. toctree:: - :maxdepth: 2 - - description.rst - arguments.rst - psf.rst - -.. automodapi:: romancal.source_detection diff --git a/docs/roman/source_detection/psf.rst b/docs/roman/source_detection/psf.rst deleted file mode 100644 index 56f4af506..000000000 --- a/docs/roman/source_detection/psf.rst +++ /dev/null @@ -1,29 +0,0 @@ -PSF Fitting -=========== - -A few PSF fitting utilities are included to interface between observations -within Roman datamodels and methods within dependencies that generate and -fit PSF models to observations. - -Create PSF models ------------------ - -`~romancal.lib.psf.create_gridded_psf_model` computes a gridded PSF model for -a given detector using `~webbpsf.gridded_library.CreatePSFLibrary` from -`WebbPSF `_. The defaults are chosen to -balance more accurate PSF models with the cost of increased runtime. For -further reading on the WebbPSF approach to ePSFs, see the WebbPSF docs on -`Using PSF Grids `_. - -Fit model PSFs to an ImageModel -------------------------------- - -Once PSF models are generated, you can fit those models to observations -in an ImageModel with `~romancal.lib.psf.fit_psf_to_image_model` using -`photutils `_. -By default the fits are done with `~photutils.psf.PSFPhotometry`, and -crowded fields may benefit from using `~photutils.psf.IterativePSFPhotometry`. -For neighboring sources that are near one another on the detector, grouping -the sources and fitting their PSFs simultaneously improves the fit quality. -Initial guesses for target centroids can be given or source -detection can be performed with, e.g., `~photutils.detection.DAOStarFinder`. diff --git a/docs/roman/tweakreg/README.rst b/docs/roman/tweakreg/README.rst index 51915f135..bc568d5c6 100644 --- a/docs/roman/tweakreg/README.rst +++ b/docs/roman/tweakreg/README.rst @@ -7,7 +7,7 @@ Description Overview -------- This step uses the coordinates of point-like sources from an input catalog -(i.e. the result from `SourceDetectionStep` saved in the +(i.e. the result from `SourceCatalogStep` saved in the `meta.tweakreg_catalog` attribute) and compares them with the coordinates from a Gaia catalog to compute corrections to the WCS of the input images such that sky catalogs obtained from the image catalogs @@ -68,7 +68,7 @@ attributes to the catalog file names provided in either in the ASN table or Alignment --------- -The source catalog (either created by `SourceDetectionStep` or provided by the user) +The source catalog (either created by `SourceCatalogStep` or provided by the user) gets cross-matched and fit to an astrometric reference catalog (set by ``TweakRegStep.abs_refcat``) and the results are stored in ``model.meta.wcs_fit_results``. The pipeline initially supports fitting to any diff --git a/romancal/lib/suffix.py b/romancal/lib/suffix.py index 35a268af2..0eef48b74 100644 --- a/romancal/lib/suffix.py +++ b/romancal/lib/suffix.py @@ -54,7 +54,6 @@ "jump", "rampfit", "saturation", - "sourcedetection", "dark_current", "darkcurrent", "outlier_detection", @@ -91,8 +90,6 @@ "linearitystep", "dark_current", "jump", - "sourcedetection", - "sourcedetectionstep", "tweakregstep", "outlierdetectionstep", "skymatchstep", diff --git a/romancal/pipeline/exposure_pipeline.py b/romancal/pipeline/exposure_pipeline.py index 3dde01e25..e4dd40833 100644 --- a/romancal/pipeline/exposure_pipeline.py +++ b/romancal/pipeline/exposure_pipeline.py @@ -39,7 +39,7 @@ class ExposurePipeline(RomanPipeline): ramps to produce a 2-D slope product. Included steps are: dq_init, saturation, linearity, dark current, jump detection, ramp_fit, assign_wcs, flatfield (only applied to WFI imaging data), photom, - and source_detection. + and source_catalog. """ class_alias = "roman_elp" @@ -142,7 +142,7 @@ def process(self, input): "assign_wcs", "flat_field", "photom", - "source_detection", + "source_catalog", "dark", "refpix", "linearity", @@ -176,7 +176,7 @@ def process(self, input): log.info("Tweakreg step is being SKIPPED") result.meta.cal_step.flat_field = "SKIPPED" result.meta.cal_step.photom = "SKIPPED" - result.meta.cal_step.source_detection = "SKIPPED" + result.meta.cal_step.source_catalog = "SKIPPED" result.meta.cal_step.tweakreg = "SKIPPED" self.output_use_model = True @@ -215,7 +215,7 @@ def create_fully_saturated_zeroed_image(self, input_model): "assign_wcs", "flat_field", "photom", - "source_detection", + "source_catalog", "tweakreg", ]: fully_saturated_model.meta.cal_step[step_str] = "SKIPPED" diff --git a/romancal/regtest/test_multiband_catalog.py b/romancal/regtest/test_multiband_catalog.py index c408c820d..a5bc1dda0 100644 --- a/romancal/regtest/test_multiband_catalog.py +++ b/romancal/regtest/test_multiband_catalog.py @@ -28,7 +28,7 @@ def test_multiband_catalog(rtdata_module): # filter in it, so this is more of an existence proof for the multiband # catalogs than a detailed test. Using only a single catalog lets us # rely on the existing regtest files. - outputfn = "r0099101001001001001_r274dp63x31y81_prompt_cat.asdf" + outputfn = "r0099101001001001001_r274dp63x31y81_prompt_F158_mbcat_cat.asdf" rtdata.get_asn(f"WFI/image/{inputasnfn}") rtdata.output = outputfn rtdata.input = inputasnfn diff --git a/romancal/regtest/test_wfi_grism_pipeline.py b/romancal/regtest/test_wfi_grism_pipeline.py index abeb0ee72..16f4c6a62 100644 --- a/romancal/regtest/test_wfi_grism_pipeline.py +++ b/romancal/regtest/test_wfi_grism_pipeline.py @@ -98,7 +98,7 @@ def test_output_matches_truth(output_filename, truth_filename, ignore_asdf_paths # skipped ("flat_field", "SKIPPED"), ("photom", "SKIPPED"), - ("source_detection", "SKIPPED"), + ("source_catalog", "SKIPPED"), ("tweakreg", "SKIPPED"), ), ) diff --git a/romancal/regtest/test_wfi_image_pipeline.py b/romancal/regtest/test_wfi_image_pipeline.py index 4c75759a4..09e7c2fdc 100644 --- a/romancal/regtest/test_wfi_image_pipeline.py +++ b/romancal/regtest/test_wfi_image_pipeline.py @@ -299,6 +299,6 @@ def test_pipeline_suffix(rtdata, ignore_asdf_paths): assert model.meta.cal_step.assign_wcs == "COMPLETE" assert model.meta.cal_step.flat_field == "COMPLETE" assert model.meta.cal_step.photom == "COMPLETE" - assert model.meta.cal_step.source_detection == "COMPLETE" + assert model.meta.cal_step.source_catalog == "COMPLETE" assert model.meta.cal_step.tweakreg == "INCOMPLETE" assert model.meta.filename == output diff --git a/romancal/scripts/make_regtestdata.sh b/romancal/scripts/make_regtestdata.sh index dcf2afc3b..3b2e698d4 100644 --- a/romancal/scripts/make_regtestdata.sh +++ b/romancal/scripts/make_regtestdata.sh @@ -13,8 +13,25 @@ # r00r1601001001001001_0001_wfi01 - special 16 resultant file, imaging, only need cal file # r10r1601001001001001_0001_wfi01 - special 16 resultant file, spectroscopy, only need cal file -outdir=$1 -logfile=$outdir/make_regtestdata.log + +if [ $# -eq 0 ]; then + echo "Please provide an output directory as a command line argument" + exit 1 +fi + +if [ -z "${PATCH_TABLE_PATH}" ]; then + echo "Please set the PATCH_TABLE_PATH environment variable" + exit 1 +fi + +outdir="$1" + +logfile="$outdir/make_regtestdata.log" + +# stop on an error +# FIXME we can't do this because asn_from_list always returns an error +# see: https://github.com/spacetelescope/romancal/issues/1535 +#set -e # Redirect all output to the logfile exec > $logfile 2>&1 @@ -36,7 +53,7 @@ do cp ${fn}_*.asdf $outdir/roman-pipeline/dev/truth/WFI/$dirname/ # uncal file doesn't belong in truth. - rm $outdir/roman-pipeline/dev/truth/WFI/image/${fn}_uncal.asdf + rm $outdir/roman-pipeline/dev/truth/WFI/${dirname}/${fn}_uncal.asdf done @@ -61,15 +78,6 @@ strun roman_elp r0000101001001001001_0002_wfi10_uncal.asdf cp r0000101001001001001_0002_wfi10_cal.asdf $outdir/roman-pipeline/dev/WFI/image/ -# resample regtest; needs r0000101001001001001_000{1,2}_wfi01_cal.asdf -# builds the appropriate asn file and calls strun with it -echo "Creating regtest files for resample..." -asn_from_list r0000101001001001001_0001_wfi01_cal.asdf r0000101001001001001_0002_wfi01_cal.asdf r0000101001001001001_0003_wfi01_cal.asdf -o L3_mosaic_asn.json --product-name mosaic -strun romancal.step.ResampleStep L3_mosaic_asn.json --rotation=0 --output_file=mosaic.asdf -cp L3_mosaic_asn.json $outdir/roman-pipeline/dev/WFI/image/ -cp mosaic_resamplestep.asdf $outdir/roman-pipeline/dev/truth/WFI/image/ - - # CRDS test needs the "usual" r00001..._0001_wfi01 files. # It also needs a hacked r00001..._0001_wfi01 file, with the time changed. # this makes the hacked version. @@ -120,10 +128,8 @@ cp Test_linearity.asdf $outdir/roman-pipeline/dev/truth/WFI/image/ # we have a test that runs the flat field step directly on an _L1_ spectroscopic # file and verifies that it gets skipped. -# I don't really understand that but we can duplicate it for now. -# FIXME: is the uncal input for test_flat_field_grism_step() correct? basename="r0000201001001001001_0001_wfi01" -strun romancal.step.FlatFieldStep ${basename}_uncal.asdf +strun romancal.step.FlatFieldStep ${basename}_assignwcs.asdf cp ${basename}_flat.asdf $outdir/roman-pipeline/dev/truth/WFI/grism/ # make a version of a file with a different pointing @@ -191,12 +197,22 @@ strun romancal.step.SourceCatalogStep ${l3name}_coadd.asdf cp ${l3name}_cat.asdf $outdir/roman-pipeline/dev/truth/WFI/image/ +# L3 on skycell l3name="r0099101001001001001_r274dp63x31y81_prompt_F158" asn_from_list r0000101001001001001_0001_wfi01_cal.asdf r0000101001001001001_0002_wfi01_cal.asdf r0000101001001001001_0003_wfi01_cal.asdf -o L3_mosaic_asn.json --product-name $l3name --target r274dp63x31y81 +# The pipeline will silently do nothing and not return an error exit code if the output +# file already exists. +# see: https://github.com/spacetelescope/romancal/issues/1544 +# To work around this remove the expected output file +if [ -f "${l3name}_coadd.asdf" ]; then + rm "${l3name}_coadd.asdf" +fi strun roman_mos L3_mosaic_asn.json cp L3_mosaic_asn.json $outdir/roman-pipeline/dev/WFI/image/ cp ${l3name}_coadd.asdf $outdir/roman-pipeline/dev/WFI/image/ cp ${l3name}_coadd.asdf $outdir/roman-pipeline/dev/truth/WFI/image/ +strun romancal.step.ResampleStep L3_mosaic_asn.json --rotation=0 --output_file=mosaic.asdf +cp mosaic_resamplestep.asdf $outdir/roman-pipeline/dev/truth/WFI/image/ # L3 catalog strun romancal.step.SourceCatalogStep ${l3name}_coadd.asdf @@ -206,12 +222,6 @@ cp ${l3name}_cat.asdf $outdir/roman-pipeline/dev/truth/WFI/image/ strun romancal.step.SourceCatalogStep r0000101001001001001_0001_wfi01_cal.asdf cp r0000101001001001001_0001_wfi01_cat.asdf $outdir/roman-pipeline/dev/truth/WFI/image/ -# L3 on skycell -l3name="r0099101001001001001_r274dp63x31y81_prompt_F158" -asn_from_list --product-name=$l3name r0000101001001001001_0001_wfi01_cal.asdf r0000101001001001001_0002_wfi01_cal.asdf r0000101001001001001_0003_wfi01_cal.asdf -o L3_m1_asn.json -strun roman_mos L3_m1_asn.json -cp ${l3name}_coadd.asdf $outdir/roman-pipeline/dev/truth/WFI/image/ - # L3 skycell catalog strun romancal.step.SourceCatalogStep ${l3name}_coadd.asdf cp ${l3name}_cat.asdf $outdir/roman-pipeline/dev/truth/WFI/image/ @@ -219,6 +229,8 @@ cp ${l3name}_cat.asdf $outdir/roman-pipeline/dev/truth/WFI/image/ # multiband catalog asn_from_list --product-name=${l3name}_mbcat ${l3name}_coadd.asdf -o L3_skycell_mbcat_asn.json strun romancal.step.MultibandCatalogStep L3_skycell_mbcat_asn.json --deblend True +cp L3_skycell_mbcat_asn.json $outdir/roman-pipeline/dev/WFI/image/ +cp ${l3name}_mbcat_cat.asdf $outdir/roman-pipeline/dev/truth/WFI/image/ # tests passing suffix to the pipeline strun roman_elp r0000101001001001001_0001_wfi01_uncal.asdf --steps.tweakreg.skip=True --suffix=star diff --git a/romancal/source_catalog/source_catalog_step.py b/romancal/source_catalog/source_catalog_step.py index 1446a4e6e..1337f44ae 100644 --- a/romancal/source_catalog/source_catalog_step.py +++ b/romancal/source_catalog/source_catalog_step.py @@ -80,7 +80,7 @@ def process(self, step_input): ) if isinstance(model, ImageModel): - cat_model = datamodels.SourceCatalogModel + cat_model = datamodels.ImageSourceCatalogModel else: cat_model = datamodels.MosaicSourceCatalogModel source_catalog_model = maker_utils.mk_datamodel(cat_model) @@ -173,7 +173,7 @@ def save_base_results(self, segment_img, source_catalog_model): else source_catalog_model.meta.filename ) - if isinstance(source_catalog_model, datamodels.SourceCatalogModel): + if isinstance(source_catalog_model, datamodels.ImageSourceCatalogModel): seg_model = datamodels.SegmentationMapModel else: seg_model = datamodels.MosaicSegmentationMapModel @@ -202,7 +202,7 @@ def save_base_results(self, segment_img, source_catalog_model): def update_metadata(model, output_catalog_name): # update datamodel to point to the source catalog file destination - model.meta["source_detection"] = maker_utils.mk_source_detection( + model.meta["source_catalog"] = maker_utils.mk_source_catalog( tweakreg_catalog_name=output_catalog_name ) - model.meta.cal_step.source_detection = "COMPLETE" + model.meta.cal_step.source_catalog = "COMPLETE" diff --git a/romancal/source_catalog/tests/test_source_catalog.py b/romancal/source_catalog/tests/test_source_catalog.py index 60d3d9d0d..c414f9e2d 100644 --- a/romancal/source_catalog/tests/test_source_catalog.py +++ b/romancal/source_catalog/tests/test_source_catalog.py @@ -11,11 +11,11 @@ from roman_datamodels import datamodels as rdm from roman_datamodels.datamodels import ( ImageModel, + ImageSourceCatalogModel, MosaicModel, MosaicSegmentationMapModel, MosaicSourceCatalogModel, SegmentationMapModel, - SourceCatalogModel, ) from roman_datamodels.maker_utils import mk_level2_image, mk_level3_mosaic @@ -455,7 +455,7 @@ def test_do_psf_photometry_column_names(tmp_path, image_model, fit_psf): True, ImageModel, { - "cat": SourceCatalogModel, + "cat": ImageSourceCatalogModel, "segm": SegmentationMapModel, "sourcecatalog": ImageModel, }, @@ -466,9 +466,9 @@ def test_do_psf_photometry_column_names(tmp_path, image_model, fit_psf): 5, True, False, - SourceCatalogModel, + ImageSourceCatalogModel, { - "cat": SourceCatalogModel, + "cat": ImageSourceCatalogModel, "segm": SegmentationMapModel, }, ), @@ -480,7 +480,7 @@ def test_do_psf_photometry_column_names(tmp_path, image_model, fit_psf): True, ImageModel, { - "cat": SourceCatalogModel, + "cat": ImageSourceCatalogModel, "segm": SegmentationMapModel, }, ), @@ -490,9 +490,9 @@ def test_do_psf_photometry_column_names(tmp_path, image_model, fit_psf): 5, False, False, - SourceCatalogModel, + ImageSourceCatalogModel, { - "cat": SourceCatalogModel, + "cat": ImageSourceCatalogModel, "segm": SegmentationMapModel, }, ), @@ -659,7 +659,7 @@ def test_l3_source_catalog_keywords( ), ( False, - SourceCatalogModel, + ImageSourceCatalogModel, ), ), ) diff --git a/romancal/source_detection/__init__.py b/romancal/source_detection/__init__.py deleted file mode 100644 index 7b7af9b06..000000000 --- a/romancal/source_detection/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .source_detection_step import SourceDetectionStep - -__all__ = ["SourceDetectionStep"] diff --git a/romancal/source_detection/source_detection_step.py b/romancal/source_detection/source_detection_step.py deleted file mode 100644 index cab99993f..000000000 --- a/romancal/source_detection/source_detection_step.py +++ /dev/null @@ -1,296 +0,0 @@ -""" -Create a source catalog for tweakreg -""" - -import logging - -import astropy.units as u -import numpy as np -from asdf import AsdfFile -from astropy.stats import SigmaClip -from astropy.table import Table -from photutils.background import ( - Background2D, - MeanBackground, - MedianBackground, - ModeEstimatorBackground, -) -from photutils.detection import DAOStarFinder -from roman_datamodels import datamodels as rdm -from roman_datamodels import maker_utils -from roman_datamodels.dqflags import pixel - -from romancal.lib import psf -from romancal.stpipe import RomanStep - -log = logging.getLogger(__name__) -log.setLevel(logging.DEBUG) - -__all__ = ["SourceDetectionStep"] - - -class SourceDetectionStep(RomanStep): - """ - SourceDetectionStep: Detect point-like sources in image to create a catalog - for alignment in tweakreg. - """ - - class_alias = "source_detection" - - spec = """ - kernel_fwhm = float(default=None) # DAOStarFinder:Size of Gaussian kernel, - # in pixels. - sharplo = float(default=0.) # DAOStarFinder: Lower bound for sharpness. - # Typical values of sharpness range from 0 (flat) to 1 (delta function). - sharphi = float(default=1.0) # DAOStarFinder: Upper bound for sharpness. - roundlo = float(default=-1.0) # DAOStarFinder: Lower bound for roundness. - # A circular source will have a zero roundness. A source extended in x or - # y will have a negative or positive roundness, respectively. - roundhi = float(default=1.0) # DAOStarFinder: Upper bound for roundness. - peakmax = float(default=100000.0) # Upper limit on brightest pixel in sources. - max_sources = float(default=None) # Max number of sources, choosing brightest. - scalar_threshold = float(default=None) # Detection threshold, to - # be used for entire image. Assumed to be in same units as data, and is - # an absolute threshold over background. - calc_threshold = boolean(default=True) # Calculate a single absolute - # detection threshold from image based on background. - snr_threshold = float(default=3.0) # if calc_threshold_img, - # the SNR for the threshold image. - bkg_estimator = string(default='median') # if calc_threshold_img, - # choice of mean, median, or mode. - bkg_boxsize = integer(default=9) # if calc_threshold_img, - # size of box in pixels for 2D background. - bkg_sigma = float(default=2.0) # if calc_threshold_img, - # n sigma for sigma clipping bkgrnd. - bkg_filter_size = integer(default=3) # if calc_threshold_img, - # size of Gaussian kernel for background. - save_catalogs = boolean(default=False) # Save source catalog to file? - # Will overwrite an existing catalog of the same name. - output_cat_filetype = option('asdf', 'ecsv', default='asdf') # Used if - #save_catalogs=True - file type of output catalog. - fit_psf = boolean(default=True) # fit source PSFs for accurate astrometry - min_separation = integer(default=11) # don't find multiple sources closer - # than this number of pixels - """ - - def process(self, input): - if isinstance(input, rdm.DataModel): - input_model = input - else: - input_model = rdm.open(input) - - if input_model.meta.exposure.type != "WFI_IMAGE": - # Check to see if attempt to find sources in non-Image data - log.info("Skipping source detection for spectral exposure.") - input_model.meta.cal_step.source_detection = "SKIPPED" - return input_model - - # remove units from data in this step. - # DAOStarFinder requires unitless input - if hasattr(input_model.data, "unit"): - self.data = input_model.data.value - else: - self.data = input_model.data - - # mask DO_NOT_USE pixels - - self.coverage_mask = ((pixel.DO_NOT_USE) & input_model.dq).astype(bool) - - filt = input_model.meta.instrument["optical_element"] - - # if a pre-determined threshold value for detection for the whole - # image is provided, use this - if self.scalar_threshold is not None: - threshold = float(self.scalar_threshold) - log.info(f"Using a detection threshold of {threshold}.") - - # otherwise, if specified, calculate a scalar threshold from the - # image by calculating a 2D background image, using this to create - # a 2d threshold image, and using the median of the 2d threshold - # image as the scalar detection threshold for the whole image - elif self.calc_threshold: - log.info("Determining detection threshold from image.") - bkg = self._calc_2D_background() - threshold_img = bkg.background + self.snr_threshold * bkg.background_rms - threshold = np.median(threshold_img) - log.info(f"Calculated a detection threshold of {threshold} from image.") - else: - threshold = np.median(self.data) - - if self.kernel_fwhm is None: - # estimate the FWHM of the PSF using rough estimate - # from the diffraction limit: - central_wavelength = psf.filter_central_wavelengths[filt] * u.um - diffraction_limit = float(1.22 * central_wavelength / (2.4 * u.m)) - assume_pixel_scale = 0.11 * u.arcsec / u.pix - expect_psf_pix = np.sqrt( - ((diffraction_limit * u.rad).to(u.arcsec) / assume_pixel_scale) ** 2 - + 1 * u.pix**2 - ).to_value(u.pix) - self.kernel_fwhm = expect_psf_pix - - log.info("Detecting sources with DAOStarFinder, using entire image array.") - daofind = DAOStarFinder( - fwhm=self.kernel_fwhm, - threshold=threshold, - sharplo=self.sharplo, - sharphi=self.sharphi, - roundlo=self.roundlo, - roundhi=self.roundhi, - brightest=self.max_sources, - peakmax=self.peakmax, - min_separation=self.min_separation, - ) - - if self.scalar_threshold is not None: - # if absolute threshold is provided - sources = daofind(self.data, mask=self.coverage_mask) - - elif self.calc_threshold is not None: - # subtract background from data if calculating abs. threshold - sources = daofind(self.data - bkg.background, mask=self.coverage_mask) - else: - sources = daofind(self.data, mask=self.coverage_mask) - - # reduce table to minimal number of columns, just source ID, - # positions, and fluxes - columns = ["id", "xcentroid", "ycentroid", "flux"] - - if sources: - catalog = sources[columns] - log.info(f"Found {len(catalog)} sources.") - else: - # if no sources were detected, return an empty table - self.log.warning("No sources detected, returning empty catalog.") - catalog = Table( - names=columns, - dtype=(int, np.float64, np.float64, np.float64), - ) - - if self.fit_psf and len(sources): - # refine astrometry by fitting PSF models to the detected sources - log.info("Constructing a gridded PSF model.") - detector = input_model.meta.instrument["detector"].replace("WFI", "SCA") - gridded_psf_model, _ = psf.create_gridded_psf_model( - filt=filt, - detector=detector, - ) - - log.info( - "Fitting a PSF model to sources for improved astrometric precision." - ) - psf_photometry_table, photometry = psf.fit_psf_to_image_model( - image_model=input_model, - psf_model=gridded_psf_model, - x_init=catalog["xcentroid"], - y_init=catalog["ycentroid"], - exclude_out_of_bounds=True, - ) - - catalog_as_recarray = catalog.as_array() - - # create meta.source detection section in file - # if save_catalogs is True, this will be updated with the - # attribute 'tweakreg_catalog_name' to point to the location - # of the catalog on disk. If save_catalogs is false, this section - # will be updated to contain the catalog to pass to TweakReg - - # tweakreg_catalog_name will be saved to the final output file, - # while tweakreg_catalog is intended to be deleted by TweakRegStep - input_model.meta["source_detection"] = maker_utils.mk_source_detection() - - # if 'save_catalogs'= True, also save the output catalog to a file - # (format specified by output_cat_filetype) and add an attribute - # to the file that contains the path to this file - if self.save_catalogs: - cat_filename = input_model.meta.filename.replace(".asdf", "") - cat_filename += f"_tweakreg_catalog.{self.output_cat_filetype}" - log.info(f"Saving catalog to file: {cat_filename}.") - - if self.output_cat_filetype == "asdf": - tree = {"tweakreg_catalog": catalog_as_recarray} - ff = AsdfFile(tree) - ff.write_to(cat_filename) - else: - catalog.write(cat_filename, format="ascii.ecsv", overwrite=True) - - input_model.meta.source_detection["tweakreg_catalog_name"] = cat_filename - else: - if self.fit_psf: - # PSF photometry centroid results are stored in an astropy table - # in columns "x_fit" and "y_fit", which we'll rename for - # compatibility with tweakreg: - catalog = psf_photometry_table.copy() - - # rename the PSF photometry table's columns to match the - # expectated columns in tweakreg: - for old_name, new_name in zip( - ["x_fit", "y_fit"], ["xcentroid", "ycentroid"] - ): - catalog.rename_column(old_name, new_name) - - input_model.meta.source_detection["tweakreg_catalog"] = catalog - else: - # only attach catalog to file if its being passed to the next step - # and save_catalogs is false, since it is not in the schema - input_model.meta.source_detection["tweakreg_catalog"] = ( - catalog_as_recarray - ) - input_model.meta.cal_step["source_detection"] = "COMPLETE" - - # just pass input model to next step - catalog is stored in meta - return input_model - - def _calc_2D_background(self): - """Calculates a 2D background image. - - Calculates the background value for the input image in boxes specified by - self.bkg_box_size. A mean, median, or mode estimator may be used (set - by `bkg_estimator`). The pixels in each box will be sigma clipped, - using a sigma specified by `bkg_sigma`.""" - - filter_size = ( - self.bkg_filter_size, - self.bkg_filter_size, - ) # square size specified - box_size = np.asarray(self.bkg_boxsize).astype(int) # must be integer - - if self.bkg_estimator == "median": - bkg_estimator = MedianBackground() - elif self.bkg_estimator == "mean": - bkg_estimator = MeanBackground() - elif self.bkg_estimator == "mode": - bkg_estimator = ModeEstimatorBackground() - else: - raise ValueError("bkg_estimator must be one of 'mean', 'median', or 'mode'") - - sigma_clip = SigmaClip(self.bkg_sigma) - - try: - bkg_2D = Background2D( - self.data, - box_size, - filter_size=filter_size, - coverage_mask=self.coverage_mask, - sigma_clip=sigma_clip, - bkg_estimator=bkg_estimator, - ) - except ValueError: - # use the entire unmasked array - log.info( - "Background could not be estimated in meshes. " - "Using the entire unmasked array for background " - f"estimation: bkg_boxsize={self.data.shape}." - ) - - bkg_2D = Background2D( - self.data, - self.data.shape, - filter_size=filter_size, - coverage_mask=self.coverage_mask, - sigma_clip=sigma_clip, - bkg_estimator=bkg_estimator, - exclude_percentile=100.0, - ) - - return bkg_2D diff --git a/romancal/source_detection/tests/__init__.py b/romancal/source_detection/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/romancal/source_detection/tests/test_source_detection_step.py b/romancal/source_detection/tests/test_source_detection_step.py deleted file mode 100644 index 12300af12..000000000 --- a/romancal/source_detection/tests/test_source_detection_step.py +++ /dev/null @@ -1,67 +0,0 @@ -""" - Unit tests for the Roman source detection step code -""" - -from copy import deepcopy - -import numpy as np -import pytest -from astropy import units as u - -from romancal.lib.basic_utils import recarray_to_ndarray -from romancal.lib.tests.test_psf import add_sources, setup_inputs -from romancal.source_detection import SourceDetectionStep - -n_sources = 10 -image_model_shape = (100, 100) - - -class TestSourceDetection: - def setup_method(self): - self.image_model, self.webbpsf_config, self.psf_model = setup_inputs( - shape=image_model_shape, - ) - - @pytest.mark.webbpsf - def test_dao_vs_psf(self, seed=0): - rng = np.random.default_rng(seed) - image_model = deepcopy(self.image_model) - - n_models = 10 - flux_true = rng.normal(1e3, 100, n_models) - along_diag = np.arange(100, 950, 90) / 1000 * image_model_shape[0] - x_true = along_diag + rng.normal(scale=0.5, size=n_models) - y_true = along_diag + rng.normal(scale=0.5, size=n_models) - - add_sources(image_model, self.psf_model, x_true, y_true, flux_true) - - source_detect = SourceDetectionStep() - source_detect.scalar_threshold = 100 - source_detect.peakmax = None - source_detect.fit_psf = False - dao_result = source_detect.process(image_model) - idx, x_dao, y_dao, amp_dao = recarray_to_ndarray( - dao_result.meta.source_detection.tweakreg_catalog - ).T - - # check that all injected targets are found by DAO: - assert len(x_dao) == len(x_true) - - source_detect.fit_psf = True - psf_result = source_detect.process(image_model) - psf_catalog = psf_result.meta.source_detection.tweakreg_catalog - - extract_columns = ["xcentroid", "x_err", "ycentroid", "y_err", "flux_fit"] - x_psf, x_err, y_psf, y_err, amp_psf = psf_catalog[extract_columns].itercols() - - # check that typical PSF centroids are more accurate than DAO centroids: - assert np.median(np.abs(x_dao - x_true)) > np.median(np.abs(x_psf - x_true)) - - # check that the typical/worst PSF centroid is still within some tolerance: - pixel_scale = 0.11 * u.arcsec / u.pix - centroid_residuals = np.abs(x_psf - x_true) * u.pix * pixel_scale - assert np.max(centroid_residuals) < 11 * u.mas - assert np.median(centroid_residuals) < 3 * u.mas - - # check that typical residuals are consistent with their errors: - assert np.median(np.abs(x_psf - x_true) / x_err) < 2 diff --git a/romancal/step.py b/romancal/step.py index e365efb4d..34692c33e 100644 --- a/romancal/step.py +++ b/romancal/step.py @@ -19,7 +19,6 @@ from .saturation.saturation_step import SaturationStep from .skymatch.skymatch_step import SkyMatchStep from .source_catalog.source_catalog_step import SourceCatalogStep -from .source_detection.source_detection_step import SourceDetectionStep from .tweakreg.tweakreg_step import TweakRegStep __all__ = [ @@ -37,7 +36,6 @@ "ResampleStep", "SaturationStep", "SkyMatchStep", - "SourceDetectionStep", "SourceCatalogStep", "MultibandCatalogStep", "TweakRegStep", diff --git a/romancal/stpipe/integration.py b/romancal/stpipe/integration.py index 362464c81..e90f9921b 100644 --- a/romancal/stpipe/integration.py +++ b/romancal/stpipe/integration.py @@ -35,7 +35,6 @@ def get_steps(): ("romancal.step.SaturationStep", "saturation", False), ("romancal.step.AssignWcsStep", "assign_wcs", False), ("romancal.step.OutlierDetectionStep", "outlier_detection", False), - ("romancal.step.SourceDetectionStep", "source_detection", False), ("romancal.step.SkyMatchStep", "skymatch", False), ("romancal.step.TweakRegStep", "tweakreg", False), ("romancal.step.ResampleStep", "resample", False), diff --git a/romancal/tweakreg/tests/test_tweakreg.py b/romancal/tweakreg/tests/test_tweakreg.py index d3823e70c..288b860db 100644 --- a/romancal/tweakreg/tests/test_tweakreg.py +++ b/romancal/tweakreg/tests/test_tweakreg.py @@ -388,7 +388,7 @@ def create_base_image_source_catalog( t = Table(src_detector_coords, names=("x", "y")) if save_catalogs: t.write((tmp_path / output), format=catalog_format) - # mimic the same output format from SourceDetectionStep + # mimic the same output format from SourceCatalogStep t.add_column([i for i in range(len(t))], name="id", index=0) t.add_column([np.float64(i) for i in range(len(t))], name="flux") t.rename_columns(["x", "y"], ["xcentroid", "ycentroid"]) @@ -446,16 +446,16 @@ def add_tweakreg_catalog_attribute( save_catalogs=save_catalogs, ) - input_dm.meta["source_detection"] = maker_utils.mk_source_detection() + input_dm.meta["source_catalog"] = maker_utils.mk_source_catalog() if save_catalogs: - # SourceDetectionStep adds the catalog path+filename - input_dm.meta.source_detection["tweakreg_catalog_name"] = os.path.join( + # SourceCatalogStep adds the catalog path+filename + input_dm.meta.source_catalog["tweakreg_catalog_name"] = os.path.join( tmp_path, tweakreg_catalog_filename ) else: - # SourceDetectionStep attaches the catalog data as a structured array - input_dm.meta.source_detection["tweakreg_catalog"] = source_catalog + # SourceCatalogStep attaches the catalog data as a structured array + input_dm.meta.source_catalog["tweakreg_catalog"] = source_catalog @pytest.fixture @@ -507,7 +507,7 @@ def test_tweakreg_raises_attributeerror_on_missing_tweakreg_catalog(base_image): """ img = base_image() # remove attribute - del img.meta.source_detection["tweakreg_catalog_name"] + del img.meta.source_catalog["tweakreg_catalog_name"] with pytest.raises(AttributeError): trs.TweakRegStep.call([img]) @@ -1016,10 +1016,10 @@ def test_update_source_catalog_coordinates(tmp_path, base_image): tweakreg = trs.TweakRegStep() - # create SourceCatalogModel + # create ImageSourceCatalogModel source_catalog_model = setup_source_catalog_model(img) - # save SourceCatalogModel + # save ImageSourceCatalogModel tweakreg.save_model( source_catalog_model, output_file="img_1.asdf", @@ -1028,7 +1028,7 @@ def test_update_source_catalog_coordinates(tmp_path, base_image): ) # update tweakreg catalog name - img.meta.source_detection.tweakreg_catalog_name = "img_1_cat.asdf" + img.meta.source_catalog.tweakreg_catalog_name = "img_1_cat.asdf" # run TweakRegStep res = trs.TweakRegStep.call([img]) @@ -1036,9 +1036,9 @@ def test_update_source_catalog_coordinates(tmp_path, base_image): # tweak the current WCS using TweakRegStep and save the updated cat file with res: dm = res.borrow(0) - assert dm.meta.source_detection.tweakreg_catalog_name == "img_1_cat.asdf" + assert dm.meta.source_catalog.tweakreg_catalog_name == "img_1_cat.asdf" tweakreg.update_catalog_coordinates( - dm.meta.source_detection.tweakreg_catalog_name, dm.meta.wcs + dm.meta.source_catalog.tweakreg_catalog_name, dm.meta.wcs ) res.shelve(dm, 0) @@ -1074,10 +1074,10 @@ def test_source_catalog_coordinates_have_changed(tmp_path, base_image): tweakreg = trs.TweakRegStep() - # create SourceCatalogModel + # create ImageSourceCatalogModel source_catalog_model = setup_source_catalog_model(img) - # save SourceCatalogModel + # save ImageSourceCatalogModel tweakreg.save_model( source_catalog_model, output_file="img_1.asdf", @@ -1088,7 +1088,7 @@ def test_source_catalog_coordinates_have_changed(tmp_path, base_image): shutil.copy("img_1_cat.asdf", "img_1_cat_original.asdf") # update tweakreg catalog name - img.meta.source_detection.tweakreg_catalog_name = "img_1_cat.asdf" + img.meta.source_catalog.tweakreg_catalog_name = "img_1_cat.asdf" # run TweakRegStep res = trs.TweakRegStep.call([img]) @@ -1096,9 +1096,9 @@ def test_source_catalog_coordinates_have_changed(tmp_path, base_image): # tweak the current WCS using TweakRegStep and save the updated cat file with res: dm = res.borrow(0) - assert dm.meta.source_detection.tweakreg_catalog_name == "img_1_cat.asdf" + assert dm.meta.source_catalog.tweakreg_catalog_name == "img_1_cat.asdf" tweakreg.update_catalog_coordinates( - dm.meta.source_detection.tweakreg_catalog_name, dm.meta.wcs + dm.meta.source_catalog.tweakreg_catalog_name, dm.meta.wcs ) res.shelve(dm, 0) @@ -1171,7 +1171,7 @@ def setup_source_catalog_model(img): expected names, adds mock PSF coordinates, applies random shifts to the centroid and PSF coordinates, and calculates the world coordinates for the centroids. """ - cat_model = rdm.SourceCatalogModel + cat_model = rdm.ImageSourceCatalogModel source_catalog_model = maker_utils.mk_datamodel(cat_model) # this will be the output filename source_catalog_model.meta.filename = "img_1.asdf" @@ -1221,7 +1221,7 @@ def setup_source_catalog_model(img): source_catalog["ra_psf"].unit = u.deg source_catalog["dec_psf"].unit = u.deg - # add source catalog to SourceCatalogModel + # add source catalog to ImageSourceCatalogModel source_catalog_model.source_catalog = source_catalog return source_catalog_model diff --git a/romancal/tweakreg/tweakreg_step.py b/romancal/tweakreg/tweakreg_step.py index 41c13b155..974bcd59b 100644 --- a/romancal/tweakreg/tweakreg_step.py +++ b/romancal/tweakreg/tweakreg_step.py @@ -123,7 +123,7 @@ def process(self, input): # that setting a catalog via an association does not work. Is this # intended? If so, the container can be updated to not support that. model = images.borrow(i) - model.meta["source_detection"] = { + model.meta["source_catalog"] = { "tweakreg_catalog_name": catdict[filename], } images.shelve(model, i) @@ -151,20 +151,16 @@ def process(self, input): self.log.info("Skipping TweakReg for spectral exposure.") image_model.meta.cal_step.tweakreg = "SKIPPED" else: - source_detection = getattr( - image_model.meta, "source_detection", None - ) - if source_detection is None: + source_catalog = getattr(image_model.meta, "source_catalog", None) + if source_catalog is None: images.shelve(image_model, i, modify=False) raise AttributeError( - "Attribute 'meta.source_detection' is missing. " - "Please either run SourceDetectionStep or provide a custom source catalog." + "Attribute 'meta.source_catalog' is missing. " + "Please either run SourceCatalogStep or provide a custom source catalog." ) try: - catalog = self.get_tweakreg_catalog( - source_detection, image_model - ) + catalog = self.get_tweakreg_catalog(source_catalog, image_model) except AttributeError as e: self.log.error(f"Failed to retrieve tweakreg_catalog: {e}") images.shelve(image_model, i, modify=False) @@ -355,7 +351,7 @@ def read_catalog(self, catalog_name): catalog = Table.read(catalog_name, format=self.catalog_format) return catalog - def get_tweakreg_catalog(self, source_detection, image_model): + def get_tweakreg_catalog(self, source_catalog, image_model): """ Retrieve the tweakreg catalog from source detection. @@ -365,8 +361,8 @@ def get_tweakreg_catalog(self, source_detection, image_model): Parameters ---------- - source_detection : object - The source detection metadata containing catalog information. + source_catalog : object + The source catalog metadata containing catalog information. image_model : DataModel The image model associated with the source detection. @@ -380,17 +376,17 @@ def get_tweakreg_catalog(self, source_detection, image_model): AttributeError If the required catalog information is missing from the source detection. """ - if getattr(source_detection, "tweakreg_catalog", None): - tweakreg_catalog = Table(np.asarray(source_detection.tweakreg_catalog)) - del image_model.meta.source_detection["tweakreg_catalog"] + if getattr(source_catalog, "tweakreg_catalog", None): + tweakreg_catalog = Table(np.asarray(source_catalog.tweakreg_catalog)) + del image_model.meta.source_catalog["tweakreg_catalog"] return tweakreg_catalog - if getattr(source_detection, "tweakreg_catalog_name", None): - return self.read_catalog(source_detection.tweakreg_catalog_name) + if getattr(source_catalog, "tweakreg_catalog_name", None): + return self.read_catalog(source_catalog.tweakreg_catalog_name) raise AttributeError( - "Attribute 'meta.source_detection.tweakreg_catalog' is missing. " - "Please either run SourceDetectionStep or provide a custom source catalog." + "Attribute 'meta.source_catalog.tweakreg_catalog' is missing. " + "Please either run SourceCatalogStep or provide a custom source catalog." ) def do_relative_alignment(self, imcats):