Coverage for python/lsst/display/firefly/footprints.py: 6%
99 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-21 05:13 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-21 05:13 +0000
1# This file is part of {{ cookiecutter.package_name }}.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
23import numpy as np
24from astropy.io.votable.tree import Info
25from astropy.io.votable import from_table
26from astropy.table import Column
28import lsst.geom as geom
29import lsst.afw.table as afwTable
32def recordSelector(record, selection):
33 """Select records from source catalog
35 Parameters:
36 -----------
37 record : `lsst.afw.detect.SourceRecord`
38 record to select
39 selection : `str`
40 'all' to select all records. 'blended parents' to select records with
41 more than zero children. 'deblended children' to select records with
42 non-zero parents. 'isolated' to select records that are not blended,
43 meaning zero parents and zero children.
44 Values to check for sel
45 """
46 # Catalogs missing the 'deblend_nChild' column were not deblended
47 if 'deblend_nChild' in record.schema.getNames():
48 nChildren = record.get('deblend_nChild')
49 else:
50 nChildren = 0
51 parentId = record.getParent()
52 if selection == 'all':
53 return True
54 elif selection == 'blended parents':
55 return (nChildren > 0)
56 elif selection == 'deblended children':
57 return (parentId > 0)
58 elif selection == 'isolated':
59 return ((parentId == 0) and (nChildren == 0))
60 else:
61 raise RuntimeError(
62 f'invalid selection: {selection}\n'
63 'Must be one of "all", "blended parents", "deblended children", "isolated"'
64 )
67def createFootprintsTable(catalog, xy0=None, insertColumn=4):
68 """make a VOTable of SourceData table and footprints
70 Parameters:
71 -----------
72 catalog : `lsst.afw.table.SourceCatalog`
73 Source catalog from which to display footprints.
74 xy0 : tuple or list or None
75 Pixel origin to subtract off from the footprint coordinates.
76 If None, the value used is (0,0)
77 insertColumn : `int`
78 Column at which to insert the "family_id" and "category" columns
80 Returns:
81 --------
82 `astropy.io.votable.voTableFile`
83 VOTable object to upload to Firefly
84 """
85 if xy0 is None:
86 xy0 = geom.Point2I(0, 0)
88 _catalog = afwTable.SourceCatalog(catalog.table.clone())
89 _catalog.extend(catalog, deep=True)
90 sourceTable = _catalog.asAstropy()
92 # Change int64 dtypes so they convert to VOTable
93 for colName in sourceTable.colnames:
94 if sourceTable[colName].dtype.num == 9:
95 sourceTable[colName].dtype = np.dtype('long')
97 inputColumnNames = sourceTable.colnames
99 x0, y0 = xy0
100 # The 'deblend_nChild' column is only present if the catalog was deblended
101 isDeblended = 'deblend_nChild' in catalog.schema.getNames()
102 spanList = []
103 peakList = []
104 familyList = []
105 categoryList = []
106 fpxll = []
107 fpyll = []
108 fpxur = []
109 fpyur = []
110 for record in catalog:
111 footprint = record.getFootprint()
112 recordId = record.getId()
113 spans = footprint.getSpans()
114 scoords = [(s.getY()-y0, s.getX0()-x0, s.getX1()-x0) for s in spans]
115 scoords = np.array(scoords).flatten()
116 scoords = np.ma.MaskedArray(scoords, mask=np.zeros(len(scoords),
117 dtype=np.bool))
118 fpbbox = footprint.getBBox()
119 corners = [(c.getX()-x0, c.getY()-y0) for c in fpbbox.getCorners()]
120 fpxll.append(corners[0][0])
121 fpyll.append(corners[0][1])
122 fpxur.append(corners[2][0])
123 fpyur.append(corners[2][1])
124 peaks = footprint.getPeaks()
125 pcoords = [(p.getFx()-x0, p.getFy()-y0) for p in peaks]
126 pcoords = np.array(pcoords).flatten()
127 pcoords = np.ma.MaskedArray(pcoords, mask=np.zeros(len(pcoords),
128 dtype=np.bool))
129 fpbbox = footprint.getBBox()
130 parentId = record.getParent()
131 nChild = record.get('deblend_nChild') if isDeblended else 0
132 if parentId == 0:
133 familyList.append(recordId)
134 if nChild > 0:
135 # blended parent
136 categoryList.append('blended parent')
137 else:
138 # isolated
139 categoryList.append('isolated')
140 else:
141 # deblended child
142 familyList.append(parentId)
143 categoryList.append('deblended child')
144 spanList.append(scoords)
145 peakList.append(pcoords)
147 sourceTable.add_column(Column(np.array(familyList)),
148 name='family_id',
149 index=insertColumn)
150 sourceTable.add_column(Column(np.array(categoryList)),
151 name='category',
152 index=insertColumn+1)
153 spanColumn = np.fromiter(spanList, dtype=object, count=len(spanList))
154 peakColumn = np.fromiter(peakList, dtype=object, count=len(peakList))
155 sourceTable.add_column(Column(spanColumn), name='spans')
156 sourceTable.add_column(Column(peakColumn), name='peaks')
157 sourceTable.add_column(Column(np.array(fpxll)), name='footprint_corner1_x')
158 sourceTable.add_column(Column(np.array(fpyll)), name='footprint_corner1_y')
159 sourceTable.add_column(Column(np.array(fpxur)), name='footprint_corner2_x')
160 sourceTable.add_column(Column(np.array(fpyur)), name='footprint_corner2_y')
162 outputVO = from_table(sourceTable)
163 outTable = outputVO.get_first_table()
165 outTable.infos.append(Info(name='contains_lsst_footprints', value='true'))
166 outTable.infos.append(Info(name='contains_lsst_measurements', value='true'))
167 outTable.infos.append(Info(name='FootPrintColumnNames',
168 value='id;footprint_corner1_x;footprint_corner1_y;' +
169 'footprint_corner2_x;footprint_corner2_y;spans;peaks'))
170 outTable.infos.append(Info(name='pixelsys', value='zero-based'))
171 # Check whether the coordinates are included and are valid
172 if (('slot_Centroid_x' in inputColumnNames) and
173 ('slot_Centroid_y' in inputColumnNames) and
174 np.isfinite(outTable.array['slot_Centroid_x']).any() and
175 np.isfinite(outTable.array['slot_Centroid_y']).any()):
176 coord_column_string = 'slot_Centroid_x;slot_Centroid_y;ZERO_BASED'
177 elif (('coord_ra' in inputColumnNames) and
178 ('coord_dec' in inputColumnNames) and
179 np.isfinite(outTable.array['coord_ra']).any() and
180 np.isfinite(outTable.array['coord_dec']).any()):
181 coord_column_string = 'coord_ra;coord_dec;EQ_J2000'
182 elif (('base_SdssCentroid_x' in inputColumnNames) and
183 ('base_SdssCentroid_y' in inputColumnNames) and
184 np.isfinite(outTable.array['base_SdssCentroid_x']).any() and
185 np.isfinite(outTable.array['base_SdssCentroid_y']).any()):
186 coord_column_string = 'base_SdssCentroid_x;base_SdssCentroid_y;ZERO_BASED'
187 else:
188 raise RuntimeError('No valid coordinate columns in catalog')
189 outTable.infos.append(Info(name='CatalogCoordColumns',
190 value=coord_column_string))
192 for f in outTable.fields:
193 if f.datatype == 'bit':
194 f.datatype = 'boolean'
196 outTable._config['version_1_3_or_later'] = True
197 outputVO.set_all_tables_format('binary2')
199 return outputVO