Skip to content

Commit 228b2f6

Browse files
authored
Move source catalog reader to lib (#9994)
1 parent c93743f commit 228b2f6

7 files changed

Lines changed: 224 additions & 143 deletions

File tree

jwst/assign_wcs/util.py

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from astropy.constants import c
88
from astropy.coordinates import SkyCoord
99
from astropy.modeling import models as astmodels
10-
from astropy.table import QTable
1110
from gwcs import WCS
1211
from gwcs import utils as gwutils
1312
from gwcs.wcstools import grid_from_bounding_box
@@ -16,7 +15,7 @@
1615
from stdatamodels.jwst.transforms.models import GrismObject
1716
from stpipe.exceptions import StpipeExitException
1817

19-
from jwst.lib.catalog_utils import SkyObject
18+
from jwst.lib.catalog_utils import SkyObject, read_source_catalog
2019

2120
log = logging.getLogger(__name__)
2221

@@ -32,7 +31,6 @@
3231
"calc_rotation_matrix",
3332
"wrap_ra",
3433
"update_fits_wcsinfo",
35-
"read_source_catalog",
3634
]
3735

3836

@@ -219,47 +217,6 @@ def not_implemented_mode(input_model, ref, slit_y_range=None): # noqa: ARG001
219217
log.critical(message)
220218

221219

222-
def read_source_catalog(catalog_name):
223-
"""
224-
Read a source catalog from file or validate an existing QTable.
225-
226-
Parameters
227-
----------
228-
catalog_name : str or `~astropy.table.QTable`
229-
Either the filename of a source catalog in ECSV format, or an
230-
existing Astropy QTable containing the catalog data.
231-
232-
Returns
233-
-------
234-
catalog : `~astropy.table.QTable`
235-
The source catalog as a table.
236-
237-
Raises
238-
------
239-
ValueError
240-
If an empty filename string is provided.
241-
FileNotFoundError
242-
If the specified catalog file cannot be found.
243-
TypeError
244-
If the input is neither a string filename nor a QTable instance.
245-
"""
246-
if isinstance(catalog_name, str):
247-
if len(catalog_name) == 0:
248-
err_text = "Empty catalog filename"
249-
log.error(err_text)
250-
raise ValueError(err_text)
251-
try:
252-
return QTable.read(catalog_name, format="ascii.ecsv")
253-
except FileNotFoundError as e:
254-
log.error(f"Could not find catalog file: {e}")
255-
raise FileNotFoundError(f"Could not find catalog: {e}") from None
256-
elif isinstance(catalog_name, QTable):
257-
return catalog_name
258-
err_text = "Need to input string name of catalog or astropy.table.table.QTable instance"
259-
log.error(err_text)
260-
raise TypeError(err_text)
261-
262-
263220
def get_object_info(catalog_name=None):
264221
"""
265222
Return a list of SkyObjects from the direct image.

jwst/extract_2d/grisms.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from stdatamodels.jwst.transforms.models import IdealToV2V3
1717

1818
from jwst.assign_wcs import util
19+
from jwst.lib.catalog_utils import read_source_catalog
1920

2021
log = logging.getLogger(__name__)
2122

@@ -750,7 +751,7 @@ def radec_to_source_ids(catalog, source_ids=None, source_ra=None, source_dec=Non
750751
source_ids : np.ndarray or None
751752
List of unique source IDs to extract.
752753
"""
753-
catalog = util.read_source_catalog(catalog)
754+
catalog = read_source_catalog(catalog)
754755
catalog_coord = catalog["sky_centroid"]
755756
if source_ids is None:
756757
source_ids = []

jwst/lib/catalog_utils.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,56 @@
11
"""Utilities for naming source catalogs."""
22

3+
import logging
34
import re
45
from collections import namedtuple
56
from pathlib import Path
67

7-
__all__ = ["replace_suffix_ext", "SkyObject"]
8+
from astropy.table import QTable
9+
10+
log = logging.getLogger(__name__)
11+
12+
__all__ = ["replace_suffix_ext", "SkyObject", "read_source_catalog"]
13+
14+
15+
def read_source_catalog(catalog_name):
16+
"""
17+
Read a source catalog from file or validate an existing QTable.
18+
19+
Parameters
20+
----------
21+
catalog_name : str or `~astropy.table.QTable`
22+
Either the filename of a source catalog in ECSV format, or an
23+
existing Astropy QTable containing the catalog data.
24+
25+
Returns
26+
-------
27+
catalog : `~astropy.table.QTable`
28+
The source catalog as a table.
29+
30+
Raises
31+
------
32+
ValueError
33+
If an empty filename string is provided.
34+
FileNotFoundError
35+
If the specified catalog file cannot be found.
36+
TypeError
37+
If the input is neither a string filename nor a QTable instance.
38+
"""
39+
if isinstance(catalog_name, str):
40+
if len(catalog_name) == 0:
41+
err_text = "Empty catalog filename"
42+
log.error(err_text)
43+
raise ValueError(err_text)
44+
try:
45+
return QTable.read(catalog_name, format="ascii.ecsv")
46+
except FileNotFoundError as e:
47+
log.error(f"Could not find catalog file: {e}")
48+
raise FileNotFoundError(f"Could not find catalog: {e}") from None
49+
elif isinstance(catalog_name, QTable):
50+
return catalog_name
51+
err_text = "Need to input string name of catalog or astropy.table.table.QTable instance"
52+
log.error(err_text)
53+
raise TypeError(err_text)
854

955

1056
def replace_suffix_ext(filename, old_suffix_list, new_suffix, output_ext="ecsv", output_dir=None):
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import pytest
2+
from astropy.utils.diff import report_diff_values
3+
4+
from jwst.lib.catalog_utils import read_source_catalog
5+
from jwst.source_catalog import SourceCatalogStep
6+
from jwst.source_catalog.tests.helpers import make_nircam_model
7+
8+
9+
@pytest.fixture
10+
def nircam_model():
11+
return make_nircam_model()
12+
13+
14+
def test_read_source_catalog(nircam_model, tmp_path):
15+
"""Ensure read_source_catalog can read a real source catalog file."""
16+
17+
step = SourceCatalogStep(
18+
snr_threshold=0.5, npixels=10, bkg_boxsize=50, kernel_fwhm=2.0, save_results=False
19+
)
20+
cat = step.run(nircam_model)
21+
22+
# in memory
23+
cat_in_memory = read_source_catalog(cat)
24+
assert len(cat_in_memory) > 0
25+
26+
# from file
27+
catpath = tmp_path / "test.ecsv"
28+
catstr = str(catpath)
29+
cat.write(catpath, format="ascii.ecsv", overwrite=True)
30+
cat_from_str = read_source_catalog(catstr)
31+
identical = report_diff_values(cat_in_memory, cat_from_str)
32+
assert identical, f"Catalogs read in memory and from file str differ: {identical}"
33+
34+
35+
def test_read_source_catalog_invalid_input():
36+
"""Test that read_source_catalog raises appropriate errors for invalid inputs."""
37+
38+
# Test empty string
39+
with pytest.raises(ValueError, match="Empty catalog filename"):
40+
read_source_catalog("")
41+
42+
# Test file not found
43+
with pytest.raises(FileNotFoundError, match="Could not find catalog"):
44+
read_source_catalog("nonexistent_catalog.ecsv")
45+
46+
# Test invalid type
47+
with pytest.raises(
48+
TypeError, match="Need to input string name of catalog or astropy.table.table.QTable"
49+
):
50+
read_source_catalog(12345)
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import numpy as np
2+
import stdatamodels.jwst.datamodels as dm
3+
from photutils.datasets import make_gwcs
4+
5+
6+
def make_nircam_model():
7+
"""
8+
Create a NIRCam ImageModel with synthetic data for testing.
9+
10+
Returns
11+
-------
12+
model : dm.ImageModel
13+
NIRCam image model with data, error, and weight arrays.
14+
"""
15+
rng = np.random.default_rng(seed=123)
16+
data = rng.normal(0, 0.5, size=(101, 101))
17+
data[20:80, 10:20] = 1.4
18+
data[20:30, 20:45] = 1.4
19+
data[20:80, 55:65] = 7.2
20+
data[70:80, 65:87] = 7.2
21+
data[45:55, 65:87] = 7.2
22+
data[20:30, 65:87] = 7.2
23+
data[55:75, 82:92] = 7.2
24+
data[25:45, 82:92] = 7.2
25+
26+
wht = np.ones(data.shape)
27+
wht[0:10, :] = 0.0
28+
err = np.abs(data) / 10.0
29+
model = dm.ImageModel(data, wht=wht, err=err)
30+
model.meta.bunit_data = "MJy/sr"
31+
model.meta.bunit_err = "MJy/sr"
32+
model.meta.photometry.pixelarea_steradians = 1.0e-13
33+
model.meta.wcs = make_gwcs(data.shape)
34+
model.meta.wcsinfo = {
35+
"ctype1": "RA---TAN",
36+
"ctype2": "DEC--TAN",
37+
"dec_ref": 11.99875540218638,
38+
"ra_ref": 22.02351763251896,
39+
"roll_ref": 0.005076934167039675,
40+
"v2_ref": 86.039011,
41+
"v3_ref": -493.385704,
42+
"v3yangle": -0.07385127,
43+
"vparity": -1,
44+
"wcsaxes": 2,
45+
"crpix1": 50,
46+
"crpix2": 50,
47+
}
48+
model.meta.instrument = {
49+
"channel": "LONG",
50+
"detector": "NRCALONG",
51+
"filter": "F444W",
52+
"lamp_mode": "NONE",
53+
"module": "A",
54+
"name": "NIRCAM",
55+
"pupil": "CLEAR",
56+
}
57+
model.meta.exposure.type = "NRC_IMAGE"
58+
model.meta.observation.date = "2021-01-01"
59+
model.meta.observation.time = "00:00:00"
60+
61+
return model
62+
63+
64+
def make_nircam_model_without_apcorr():
65+
"""
66+
Create a NIRCam ImageModel without aperture correction.
67+
68+
Returns
69+
-------
70+
model : dm.ImageModel
71+
NIRCam image model with data, error, and weight arrays.
72+
"""
73+
rng = np.random.default_rng(seed=123)
74+
data = rng.normal(0, 0.5, size=(101, 101))
75+
data[20:80, 10:20] = 1.4
76+
data[20:30, 20:45] = 1.4
77+
data[20:80, 55:65] = 7.2
78+
data[70:80, 65:87] = 7.2
79+
data[45:55, 65:87] = 7.2
80+
data[20:30, 65:87] = 7.2
81+
data[55:75, 82:92] = 7.2
82+
data[25:45, 82:92] = 7.2
83+
84+
wht = np.ones(data.shape)
85+
wht[0:10, :] = 0.0
86+
err = np.abs(data) / 10.0
87+
model = dm.ImageModel(data, wht=wht, err=err)
88+
model.meta.bunit_data = "MJy/sr"
89+
model.meta.bunit_err = "MJy/sr"
90+
model.meta.photometry.pixelarea_steradians = 1.0e-13
91+
model.meta.wcs = make_gwcs(data.shape)
92+
model.meta.wcsinfo = {
93+
"ctype1": "RA---TAN",
94+
"ctype2": "DEC--TAN",
95+
"dec_ref": 11.99875540218638,
96+
"ra_ref": 22.02351763251896,
97+
"roll_ref": 0.005076934167039675,
98+
"v2_ref": 86.039011,
99+
"v3_ref": -493.385704,
100+
"v3yangle": -0.07385127,
101+
"vparity": -1,
102+
"wcsaxes": 2,
103+
"crpix1": 50,
104+
"crpix2": 50,
105+
}
106+
model.meta.instrument = {
107+
"channel": "LONG",
108+
"detector": "NRCALONG",
109+
"filter": "F2550WR",
110+
"lamp_mode": "NONE",
111+
"module": "A",
112+
"name": "NIRCAM",
113+
"pupil": "CLEAR",
114+
}
115+
model.meta.exposure.type = "NRC_IMAGE"
116+
model.meta.observation.date = "2021-01-01"
117+
model.meta.observation.time = "00:00:00"
118+
119+
return model

0 commit comments

Comments
 (0)