Coverage for python / lsst / fgcmcal / fgcmBuildStarsBase.py: 19%
238 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-01 08:51 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-01 08:51 +0000
1# This file is part of fgcmcal.
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/>.
21"""Base class for BuildStars using src tables or sourceTable_visit tables.
22"""
24import abc
26import numpy as np
28import lsst.pex.config as pexConfig
29import lsst.pipe.base as pipeBase
30import lsst.afw.table as afwTable
31from lsst.daf.base import PropertyList
32from lsst.daf.base.dateTime import DateTime
33from lsst.meas.algorithms.sourceSelector import sourceSelectorRegistry
35from .fgcmLoadReferenceCatalog import FgcmLoadReferenceCatalogTask
36from .utilities import computeReferencePixelScale, countDetectors
38import fgcm
40REFSTARS_FORMAT_VERSION = 1
42__all__ = ['FgcmBuildStarsConfigBase', 'FgcmBuildStarsBaseTask']
45class FgcmBuildStarsConfigBase(pexConfig.Config):
46 """Base config for FgcmBuildStars tasks"""
48 instFluxField = pexConfig.Field(
49 doc=("Faull name of the source instFlux field to use, including 'instFlux'. "
50 "The associated flag will be implicitly included in badFlags"),
51 dtype=str,
52 default='slot_CalibFlux_instFlux',
53 )
54 minPerBand = pexConfig.Field(
55 doc="Minimum observations per band",
56 dtype=int,
57 default=2,
58 )
59 matchRadius = pexConfig.Field(
60 doc="Match radius (arcseconds)",
61 dtype=float,
62 default=1.0,
63 )
64 isolationRadius = pexConfig.Field(
65 doc="Isolation radius (arcseconds)",
66 dtype=float,
67 default=2.0,
68 )
69 densityCutNside = pexConfig.Field(
70 doc="Density cut healpix nside",
71 dtype=int,
72 default=128,
73 )
74 densityCutMaxPerPixel = pexConfig.Field(
75 doc="Density cut number of stars per pixel",
76 dtype=int,
77 default=1000,
78 )
79 randomSeed = pexConfig.Field(
80 doc="Random seed for high density down-sampling.",
81 dtype=int,
82 default=123456,
83 optional=True,
84 )
85 matchNside = pexConfig.Field(
86 doc="Healpix Nside for matching",
87 dtype=int,
88 default=4096,
89 )
90 coarseNside = pexConfig.Field(
91 doc="Healpix coarse Nside for partitioning matches",
92 dtype=int,
93 default=8,
94 )
95 physicalFilterMap = pexConfig.DictField(
96 doc="Mapping from 'physicalFilter' to band.",
97 keytype=str,
98 itemtype=str,
99 default={},
100 )
101 requiredBands = pexConfig.ListField(
102 doc="Bands required for each star",
103 dtype=str,
104 default=(),
105 )
106 primaryBands = pexConfig.ListField(
107 doc=("Bands for 'primary' star matches. "
108 "A star must be observed in one of these bands to be considered "
109 "as a calibration star."),
110 dtype=str,
111 default=None
112 )
113 doApplyWcsJacobian = pexConfig.Field(
114 doc="Apply the jacobian of the WCS to the star observations prior to fit?",
115 dtype=bool,
116 default=True
117 )
118 doModelErrorsWithBackground = pexConfig.Field(
119 doc="Model flux errors with background term?",
120 dtype=bool,
121 default=True
122 )
123 psfCandidateName = pexConfig.Field(
124 doc="Name of field with psf candidate flag for propagation",
125 dtype=str,
126 default="calib_psf_candidate"
127 )
128 doSubtractLocalBackground = pexConfig.Field(
129 doc=("Subtract the local background before performing calibration? "
130 "This is only supported for circular aperture calibration fluxes."),
131 dtype=bool,
132 default=False
133 )
134 localBackgroundFluxField = pexConfig.Field(
135 doc="Full name of the local background instFlux field to use.",
136 dtype=str,
137 default='base_LocalBackground_instFlux'
138 )
139 sourceSelector = sourceSelectorRegistry.makeField(
140 doc="How to select sources",
141 default="science"
142 )
143 apertureInnerInstFluxField = pexConfig.Field(
144 doc=("Full name of instFlux field that contains inner aperture "
145 "flux for aperture correction proxy"),
146 dtype=str,
147 default='base_CircularApertureFlux_12_0_instFlux'
148 )
149 apertureOuterInstFluxField = pexConfig.Field(
150 doc=("Full name of instFlux field that contains outer aperture "
151 "flux for aperture correction proxy"),
152 dtype=str,
153 default='base_CircularApertureFlux_17_0_instFlux'
154 )
155 doReferenceMatches = pexConfig.Field(
156 doc="Match reference catalog as additional constraint on calibration",
157 dtype=bool,
158 default=True,
159 )
160 fgcmLoadReferenceCatalog = pexConfig.ConfigurableField(
161 target=FgcmLoadReferenceCatalogTask,
162 doc="FGCM reference object loader",
163 )
164 nVisitsPerCheckpoint = pexConfig.Field(
165 doc="Number of visits read between checkpoints",
166 dtype=int,
167 default=500,
168 )
170 def setDefaults(self):
171 sourceSelector = self.sourceSelector["science"]
172 sourceSelector.setDefaults()
174 sourceSelector.doFlags = True
175 sourceSelector.doUnresolved = True
176 sourceSelector.doSignalToNoise = True
177 sourceSelector.doIsolated = True
178 sourceSelector.doRequireFiniteRaDec = True
180 sourceSelector.signalToNoise.minimum = 10.0
181 sourceSelector.signalToNoise.maximum = 1000.0
183 # FGCM operates on unresolved sources, and this setting is
184 # appropriate for the current base_ClassificationExtendedness
185 sourceSelector.unresolved.maximum = 0.5
188class FgcmBuildStarsBaseTask(pipeBase.PipelineTask, abc.ABC):
189 """
190 Base task to build stars for FGCM global calibration
191 """
192 def __init__(self, initInputs=None, **kwargs):
193 super().__init__(**kwargs)
195 self.makeSubtask("sourceSelector")
196 # Only log warning and fatal errors from the sourceSelector
197 self.sourceSelector.log.setLevel(self.sourceSelector.log.WARN)
199 def fgcmMakeAllStarObservations(self, groupedHandles, visitCat,
200 sourceSchema,
201 camera,
202 calibFluxApertureRadius=None):
203 """
204 Compile all good star observations from visits in visitCat.
206 Parameters
207 ----------
208 groupedHandles : `dict` [`list` [`lsst.daf.butler.DeferredDatasetHandle`]]
209 Dataset handles, grouped by visit.
210 visitCat : `afw.table.BaseCatalog`
211 Catalog with visit data for FGCM
212 sourceSchema : `lsst.afw.table.Schema`
213 Schema for the input src catalogs.
214 camera : `lsst.afw.cameraGeom.Camera`
215 calibFluxApertureRadius : `float`, optional
216 Aperture radius for calibration flux.
217 inStarObsCat : `afw.table.BaseCatalog`
218 Input observation catalog. If this is incomplete, observations
219 will be appended from when it was cut off.
221 Returns
222 -------
223 fgcmStarObservations : `afw.table.BaseCatalog`
224 Full catalog of good observations.
226 Raises
227 ------
228 RuntimeError: Raised if doSubtractLocalBackground is True and
229 calibFluxApertureRadius is not set.
230 """
231 raise NotImplementedError("fgcmMakeAllStarObservations not implemented.")
233 def fgcmMakeVisitCatalog(self, camera, groupedHandles, useScienceDetectors=False):
234 """
235 Make a visit catalog with all the keys from each visit
237 Parameters
238 ----------
239 camera : `lsst.afw.cameraGeom.Camera`
240 Camera from the butler
241 groupedHandles : `dict` [`list` [`lsst.daf.butler.DeferredDatasetHandle`]]
242 Dataset handles, grouped by visit.
243 useScienceDetectors : `bool`, optional
244 Limit to science detectors?
246 Returns
247 -------
248 visitCat: `afw.table.BaseCatalog`
249 """
251 self.log.info("Assembling visitCatalog from %d visits", len(groupedHandles))
253 nCcd = countDetectors(camera, useScienceDetectors)
255 schema = self._makeFgcmVisitSchema(nCcd)
257 visitCat = afwTable.BaseCatalog(schema)
258 visitCat.reserve(len(groupedHandles))
259 visitCat.resize(len(groupedHandles))
261 visitCat['visit'] = list(groupedHandles.keys())
262 visitCat['used'] = 0
263 visitCat['sources_read'] = False
265 defaultPixelScale = computeReferencePixelScale(camera, useScienceDetectors=useScienceDetectors)
267 # No matter what, fill the catalog. This will check if it was
268 # already read.
269 self._fillVisitCatalog(visitCat, groupedHandles, defaultPixelScale)
271 return visitCat
273 def _fillVisitCatalog(self, visitCat, groupedHandles, defaultPixelScale):
274 """
275 Fill the visit catalog with visit metadata
277 Parameters
278 ----------
279 visitCat : `afw.table.BaseCatalog`
280 Visit catalog. See _makeFgcmVisitSchema() for schema definition.
281 groupedHandles : `dict` [`list` [`lsst.daf.butler.DeferredDatasetHandle`]]
282 Dataset handles, grouped by visit.
283 defaultPixelScale : `float`
284 Default pixel scale to use if not in visit summary (arcsecond/pixel).
285 """
287 # Guarantee that these are sorted.
288 for i, visit in enumerate(sorted(groupedHandles)):
289 if (i % self.config.nVisitsPerCheckpoint) == 0:
290 self.log.info("Retrieving metadata for visit %d (%d/%d)", visit, i, len(groupedHandles))
292 handle = groupedHandles[visit][0]
293 summary = handle.get()
295 summaryRow = summary.find(self.config.referenceCCD)
296 if summaryRow is None:
297 # Take the first available ccd if reference isn't available
298 summaryRow = summary[0]
300 visitInfo = summaryRow.getVisitInfo()
301 physicalFilter = summaryRow['physical_filter']
302 # Compute the median psf sigma and fwhm if possible.
303 if 'pixelScale' in summary.schema:
304 # This is not available in the older test summaries
305 pixelScales = summary['pixelScale']
306 else:
307 pixelScales = np.full(len(summary['psfSigma']), defaultPixelScale)
308 psfSigmas = summary['psfSigma']
309 psfFwhms = psfSigmas * pixelScales * np.sqrt(8.*np.log(2.))
310 goodSigma = ((np.nan_to_num(psfSigmas) > 0) & (np.nan_to_num(pixelScales) > 0))
311 psfSigmas[~goodSigma] = -9999.0
312 psfFwhms[~goodSigma] = -9999.0
313 if goodSigma.size > 2:
314 psfSigma = np.median(psfSigmas[goodSigma])
315 psfFwhm = np.median(psfFwhms[goodSigma])
316 elif goodSigma.size > 0:
317 psfSigma = psfSigmas[goodSigma[0]]
318 psfFwhm = psfFwhms[goodSigma[0]]
319 else:
320 self.log.warning("Could not find any good summary psfSigma for visit %d", visit)
321 psfSigma = 0.0
322 psfFwhm = 0.0
323 # Compute median background if possible
324 goodBackground, = np.where(np.nan_to_num(summary['skyBg']) > 0.0)
325 if goodBackground.size > 2:
326 skyBackground = np.median(summary['skyBg'][goodBackground])
327 elif goodBackground.size > 0:
328 skyBackground = summary['skyBg'][goodBackground[0]]
329 else:
330 self.log.warning('Could not find any good summary skyBg for visit %d', visit)
331 skyBackground = -1.0
333 rec = visitCat[i]
334 rec['visit'] = visit
335 rec['physicalFilter'] = physicalFilter
336 # TODO DM-26991: Use the wcs to refine the focal-plane center.
337 radec = visitInfo.getBoresightRaDec()
338 rec['telra'] = radec.getRa().asDegrees()
339 rec['teldec'] = radec.getDec().asDegrees()
340 rec['telha'] = visitInfo.getBoresightHourAngle().asDegrees()
341 rec['telrot'] = visitInfo.getBoresightRotAngle().asDegrees()
342 rec['mjd'] = visitInfo.getDate().get(system=DateTime.MJD)
343 rec['exptime'] = visitInfo.getExposureTime()
344 # convert from Pa to millibar
345 # Note that I don't know if this unit will need to be per-camera config
346 rec['pmb'] = visitInfo.getWeather().getAirPressure() / 100
347 # Flag to signify if this is a "deep" field. Not currently used
348 rec['deepFlag'] = 0
349 # Relative flat scaling (1.0 means no relative scaling)
350 rec['scaling'][:] = 1.0
351 # Median delta aperture, to be measured from stars
352 rec['deltaAper'] = -9999.0
353 rec['deltaAperDetector'][:] = -9999.0
354 rec['psfSigma'] = psfSigma.item()
355 rec['psfFwhm'] = psfFwhm.item()
356 # This is keyed by detector.
357 rec['psfFwhmDetector'][summary["id"]] = psfFwhms
358 rec['skyBackground'] = skyBackground
359 rec['used'] = 1
361 def _makeSourceMapper(self, sourceSchema):
362 """
363 Make a schema mapper for fgcm sources
365 Parameters
366 ----------
367 sourceSchema: `afwTable.Schema`
368 Default source schema from the butler
370 Returns
371 -------
372 sourceMapper: `afwTable.schemaMapper`
373 Mapper to the FGCM source schema
374 """
376 # create a mapper to the preferred output
377 sourceMapper = afwTable.SchemaMapper(sourceSchema)
379 # map to ra/dec
380 sourceMapper.addMapping(sourceSchema['coord_ra'].asKey(), 'ra')
381 sourceMapper.addMapping(sourceSchema['coord_dec'].asKey(), 'dec')
382 sourceMapper.addMapping(sourceSchema['slot_Centroid_x'].asKey(), 'x')
383 sourceMapper.addMapping(sourceSchema['slot_Centroid_y'].asKey(), 'y')
384 # Add the mapping if the field exists in the input catalog.
385 # If the field does not exist, simply add it (set to False).
386 # This field is not required for calibration, but is useful
387 # to collate if available.
388 try:
389 sourceMapper.addMapping(sourceSchema[self.config.psfCandidateName].asKey(),
390 'psf_candidate')
391 except LookupError:
392 sourceMapper.editOutputSchema().addField(
393 "psf_candidate", type='Flag',
394 doc=("Flag set if the source was a candidate for PSF determination, "
395 "as determined by the star selector."))
397 # and add the fields we want
398 sourceMapper.editOutputSchema().addField(
399 "visit", type=np.int64, doc="Visit number")
400 sourceMapper.editOutputSchema().addField(
401 "ccd", type=np.int32, doc="CCD number")
402 sourceMapper.editOutputSchema().addField(
403 "instMag", type=np.float32, doc="Instrumental magnitude")
404 sourceMapper.editOutputSchema().addField(
405 "instMagErr", type=np.float32, doc="Instrumental magnitude error")
406 sourceMapper.editOutputSchema().addField(
407 "jacobian", type=np.float32, doc="Relative pixel scale from wcs jacobian")
408 sourceMapper.editOutputSchema().addField(
409 "deltaMagBkg", type=np.float32, doc="Change in magnitude due to local background offset")
410 sourceMapper.editOutputSchema().addField(
411 "deltaMagAper", type=np.float32, doc="Change in magnitude from larger to smaller aperture")
413 return sourceMapper
415 def fgcmMatchStars(self, visitCat, obsCat, lutHandle=None):
416 """
417 Use FGCM code to match observations into unique stars.
419 Parameters
420 ----------
421 visitCat: `afw.table.BaseCatalog`
422 Catalog with visit data for fgcm
423 obsCat: `afw.table.BaseCatalog`
424 Full catalog of star observations for fgcm
425 lutHandle: `lsst.daf.butler.DeferredDatasetHandle`, optional
426 Data reference to fgcm look-up table (used if matching reference stars).
428 Returns
429 -------
430 fgcmStarIdCat: `afw.table.BaseCatalog`
431 Catalog of unique star identifiers and index keys
432 fgcmStarIndicesCat: `afwTable.BaseCatalog`
433 Catalog of unique star indices
434 fgcmRefCat: `afw.table.BaseCatalog`
435 Catalog of matched reference stars.
436 Will be None if `config.doReferenceMatches` is False.
437 """
438 # get filter names into a numpy array...
439 # This is the type that is expected by the fgcm code
440 visitFilterNames = np.zeros(len(visitCat), dtype='S30')
441 for i in range(len(visitCat)):
442 visitFilterNames[i] = visitCat[i]['physicalFilter']
444 # match to put filterNames with observations
445 visitIndex = np.searchsorted(visitCat['visit'],
446 obsCat['visit'])
448 obsFilterNames = visitFilterNames[visitIndex]
450 if self.config.doReferenceMatches:
451 # Get the reference filter names, using the LUT
452 lutCat = lutHandle.get()
454 stdFilterDict = {filterName: stdFilter for (filterName, stdFilter) in
455 zip(lutCat[0]['physicalFilters'].split(','),
456 lutCat[0]['stdPhysicalFilters'].split(','))}
457 stdLambdaDict = {stdFilter: stdLambda for (stdFilter, stdLambda) in
458 zip(lutCat[0]['stdPhysicalFilters'].split(','),
459 lutCat[0]['lambdaStdFilter'])}
461 del lutCat
463 referenceFilterNames = self._getReferenceFilterNames(visitCat,
464 stdFilterDict,
465 stdLambdaDict)
466 self.log.info("Using the following reference filters: %s" %
467 (', '.join(referenceFilterNames)))
469 else:
470 # This should be an empty list
471 referenceFilterNames = []
473 # make the fgcm starConfig dict
474 starConfig = {'logger': self.log,
475 'useHtm': True,
476 'filterToBand': self.config.physicalFilterMap,
477 'requiredBands': self.config.requiredBands,
478 'minPerBand': self.config.minPerBand,
479 'matchRadius': self.config.matchRadius,
480 'isolationRadius': self.config.isolationRadius,
481 'matchNSide': self.config.matchNside,
482 'coarseNSide': self.config.coarseNside,
483 'densNSide': self.config.densityCutNside,
484 'densMaxPerPixel': self.config.densityCutMaxPerPixel,
485 'randomSeed': self.config.randomSeed,
486 'primaryBands': self.config.primaryBands,
487 'referenceFilterNames': referenceFilterNames}
489 # initialize the FgcmMakeStars object
490 fgcmMakeStars = fgcm.FgcmMakeStars(starConfig)
492 # make the primary stars
493 # note that the ra/dec native Angle format is radians
494 # We determine the conversion from the native units (typically
495 # radians) to degrees for the first observation. This allows us
496 # to treate ra/dec as numpy arrays rather than Angles, which would
497 # be approximately 600x slower.
498 conv = obsCat[0]['ra'].asDegrees() / float(obsCat[0]['ra'])
499 fgcmMakeStars.makePrimaryStars(obsCat['ra'] * conv,
500 obsCat['dec'] * conv,
501 filterNameArray=obsFilterNames,
502 bandSelected=False)
504 # and match all the stars
505 fgcmMakeStars.makeMatchedStars(obsCat['ra'] * conv,
506 obsCat['dec'] * conv,
507 obsFilterNames)
509 if self.config.doReferenceMatches:
510 fgcmMakeStars.makeReferenceMatches(self.fgcmLoadReferenceCatalog)
512 # now persist
514 objSchema = self._makeFgcmObjSchema()
516 # make catalog and records
517 fgcmStarIdCat = afwTable.BaseCatalog(objSchema)
518 fgcmStarIdCat.reserve(fgcmMakeStars.objIndexCat.size)
519 for i in range(fgcmMakeStars.objIndexCat.size):
520 fgcmStarIdCat.addNew()
522 # fill the catalog
523 fgcmStarIdCat['fgcm_id'][:] = fgcmMakeStars.objIndexCat['fgcm_id']
524 fgcmStarIdCat['ra'][:] = fgcmMakeStars.objIndexCat['ra']
525 fgcmStarIdCat['dec'][:] = fgcmMakeStars.objIndexCat['dec']
526 fgcmStarIdCat['obsArrIndex'][:] = fgcmMakeStars.objIndexCat['obsarrindex']
527 fgcmStarIdCat['nObs'][:] = fgcmMakeStars.objIndexCat['nobs']
529 obsSchema = self._makeFgcmObsSchema()
531 fgcmStarIndicesCat = afwTable.BaseCatalog(obsSchema)
532 fgcmStarIndicesCat.reserve(fgcmMakeStars.obsIndexCat.size)
533 for i in range(fgcmMakeStars.obsIndexCat.size):
534 fgcmStarIndicesCat.addNew()
536 fgcmStarIndicesCat['obsIndex'][:] = fgcmMakeStars.obsIndexCat['obsindex']
538 if self.config.doReferenceMatches:
539 refSchema = self._makeFgcmRefSchema(len(referenceFilterNames))
541 fgcmRefCat = afwTable.BaseCatalog(refSchema)
542 fgcmRefCat.reserve(fgcmMakeStars.referenceCat.size)
544 for i in range(fgcmMakeStars.referenceCat.size):
545 fgcmRefCat.addNew()
547 fgcmRefCat['fgcm_id'][:] = fgcmMakeStars.referenceCat['fgcm_id']
548 fgcmRefCat['refMag'][:, :] = fgcmMakeStars.referenceCat['refMag']
549 fgcmRefCat['refMagErr'][:, :] = fgcmMakeStars.referenceCat['refMagErr']
551 md = PropertyList()
552 md.set("REFSTARS_FORMAT_VERSION", REFSTARS_FORMAT_VERSION)
553 md.set("FILTERNAMES", referenceFilterNames)
554 fgcmRefCat.setMetadata(md)
556 else:
557 fgcmRefCat = None
559 return fgcmStarIdCat, fgcmStarIndicesCat, fgcmRefCat
561 def _makeFgcmVisitSchema(self, nCcd):
562 """
563 Make a schema for an fgcmVisitCatalog
565 Parameters
566 ----------
567 nCcd: `int`
568 Number of CCDs in the camera
570 Returns
571 -------
572 schema: `afwTable.Schema`
573 """
575 schema = afwTable.Schema()
576 schema.addField('visit', type=np.int64, doc="Visit number")
577 schema.addField('physicalFilter', type=str, size=30, doc="Physical filter")
578 schema.addField('telra', type=np.float64, doc="Pointing RA (deg)")
579 schema.addField('teldec', type=np.float64, doc="Pointing Dec (deg)")
580 schema.addField('telha', type=np.float64, doc="Pointing Hour Angle (deg)")
581 schema.addField('telrot', type=np.float64, doc="Camera rotation (deg)")
582 schema.addField('mjd', type=np.float64, doc="MJD of visit")
583 schema.addField('exptime', type=np.float32, doc="Exposure time")
584 schema.addField('pmb', type=np.float32, doc="Pressure (millibar)")
585 schema.addField('psfSigma', type=np.float32, doc="PSF sigma (median); pixels")
586 schema.addField('psfFwhm', type=np.float32, doc="PSF FWHM (median); arcseconds")
587 schema.addField('psfFwhmDetector', type='ArrayF', doc="PSF FWHM per detector; arcseconds", size=nCcd)
588 schema.addField('deltaAper', type=np.float32, doc="Delta-aperture")
589 schema.addField('deltaAperDetector', type='ArrayF', doc='Delta-aperture per detector', size=nCcd)
590 schema.addField('skyBackground', type=np.float32, doc="Sky background (ADU) (reference CCD)")
591 # the following field is not used yet
592 schema.addField('deepFlag', type=np.int32, doc="Deep observation")
593 schema.addField('scaling', type='ArrayD', doc="Scaling applied due to flat adjustment",
594 size=nCcd)
595 schema.addField('used', type=np.int32, doc="This visit has been ingested.")
596 schema.addField('sources_read', type='Flag', doc="This visit had sources read.")
598 return schema
600 def _makeFgcmObjSchema(self):
601 """
602 Make a schema for the objIndexCat from fgcmMakeStars
604 Returns
605 -------
606 schema: `afwTable.Schema`
607 """
609 objSchema = afwTable.Schema()
610 objSchema.addField('fgcm_id', type=np.int32, doc='FGCM Unique ID')
611 # Will investigate making these angles...
612 objSchema.addField('ra', type=np.float64, doc='Mean object RA (deg)')
613 objSchema.addField('dec', type=np.float64, doc='Mean object Dec (deg)')
614 objSchema.addField('obsArrIndex', type=np.int32,
615 doc='Index in obsIndexTable for first observation')
616 objSchema.addField('nObs', type=np.int32, doc='Total number of observations')
618 return objSchema
620 def _makeFgcmObsSchema(self):
621 """
622 Make a schema for the obsIndexCat from fgcmMakeStars
624 Returns
625 -------
626 schema: `afwTable.Schema`
627 """
629 obsSchema = afwTable.Schema()
630 obsSchema.addField('obsIndex', type=np.int32, doc='Index in observation table')
632 return obsSchema
634 def _makeFgcmRefSchema(self, nReferenceBands):
635 """
636 Make a schema for the referenceCat from fgcmMakeStars
638 Parameters
639 ----------
640 nReferenceBands: `int`
641 Number of reference bands
643 Returns
644 -------
645 schema: `afwTable.Schema`
646 """
648 refSchema = afwTable.Schema()
649 refSchema.addField('fgcm_id', type=np.int32, doc='FGCM Unique ID')
650 refSchema.addField('refMag', type='ArrayF', doc='Reference magnitude array (AB)',
651 size=nReferenceBands)
652 refSchema.addField('refMagErr', type='ArrayF', doc='Reference magnitude error array',
653 size=nReferenceBands)
655 return refSchema
657 def _getReferenceFilterNames(self, visitCat, stdFilterDict, stdLambdaDict):
658 """
659 Get the reference filter names, in wavelength order, from the visitCat and
660 information from the look-up-table.
662 Parameters
663 ----------
664 visitCat: `afw.table.BaseCatalog`
665 Catalog with visit data for FGCM
666 stdFilterDict: `dict`
667 Mapping of filterName to stdFilterName from LUT
668 stdLambdaDict: `dict`
669 Mapping of stdFilterName to stdLambda from LUT
671 Returns
672 -------
673 referenceFilterNames: `list`
674 Wavelength-ordered list of reference filter names
675 """
677 # Find the unique list of filter names in visitCat
678 filterNames = np.unique(visitCat.asAstropy()['physicalFilter'])
680 # Find the unique list of "standard" filters
681 stdFilterNames = {stdFilterDict[filterName] for filterName in filterNames}
683 # And sort these by wavelength
684 referenceFilterNames = sorted(stdFilterNames, key=stdLambdaDict.get)
686 return referenceFilterNames