24from smatch.matcher
import Matcher
32__all__ = [
'IsolatedStarAssociationConnections',
33 'IsolatedStarAssociationConfig',
34 'IsolatedStarAssociationTask']
38 dimensions=(
'instrument',
'tract',
'skymap',),
40 source_table_visit = pipeBase.connectionTypes.Input(
41 doc=
'Source table in parquet format, per visit',
42 name=
'sourceTable_visit',
43 storageClass=
'DataFrame',
44 dimensions=(
'instrument',
'visit'),
48 skymap = pipeBase.connectionTypes.Input(
49 doc=
"Input definition of geometry/bbox and projection/wcs for warped exposures",
50 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
51 storageClass=
'SkyMap',
52 dimensions=(
'skymap',),
54 isolated_star_sources = pipeBase.connectionTypes.Output(
55 doc=
'Catalog of individual sources for the isolated stars',
56 name=
'isolated_star_sources',
57 storageClass=
'DataFrame',
58 dimensions=(
'instrument',
'tract',
'skymap'),
60 isolated_star_cat = pipeBase.connectionTypes.Output(
61 doc=
'Catalog of isolated star positions',
62 name=
'isolated_star_cat',
63 storageClass=
'DataFrame',
64 dimensions=(
'instrument',
'tract',
'skymap'),
69 pipelineConnections=IsolatedStarAssociationConnections):
70 """Configuration for IsolatedStarAssociationTask."""
72 inst_flux_field = pexConfig.Field(
73 doc=(
'Full name of instFlux field to use for s/n selection and persistence. '
74 'The associated flag will be implicity included in bad_flags. '
75 'Note that this is expected to end in ``instFlux``.'),
77 default=
'apFlux_12_0_instFlux',
79 match_radius = pexConfig.Field(
80 doc=
'Match radius (arcseconds)',
84 isolation_radius = pexConfig.Field(
85 doc=(
'Isolation radius (arcseconds). Any stars with average centroids '
86 'within this radius of another star will be rejected from the final '
87 'catalog. This radius should be at least 2x match_radius.'),
91 band_order = pexConfig.ListField(
92 doc=((
'Ordered list of bands to use for matching/storage. '
93 'Any bands not listed will not be matched.')),
95 default=[
'i',
'z',
'r',
'g',
'y',
'u'],
97 id_column = pexConfig.Field(
98 doc=
'Name of column with source id.',
102 ra_column = pexConfig.Field(
103 doc=
'Name of column with right ascension.',
107 dec_column = pexConfig.Field(
108 doc=
'Name of column with declination.',
112 physical_filter_column = pexConfig.Field(
113 doc=
'Name of column with physical filter name',
115 default=
'physical_filter',
117 band_column = pexConfig.Field(
118 doc=
'Name of column with band name',
122 extra_columns = pexConfig.ListField(
123 doc=
'Extra names of columns to read and persist (beyond instFlux and error).',
127 'apFlux_17_0_instFlux',
128 'apFlux_17_0_instFluxErr',
130 'localBackground_instFlux',
131 'localBackground_flag']
133 source_selector = sourceSelectorRegistry.makeField(
134 doc=
'How to select sources. Under normal usage this should not be changed.',
142 source_selector.setDefaults()
144 source_selector.doFlags =
True
145 source_selector.doUnresolved =
True
146 source_selector.doSignalToNoise =
True
147 source_selector.doIsolated =
True
149 source_selector.signalToNoise.minimum = 10.0
150 source_selector.signalToNoise.maximum = 1000.0
152 flux_flag_name = self.
inst_flux_fieldinst_flux_field.replace(
"instFlux",
"flag")
154 source_selector.flags.bad = [
'pixelFlags_edge',
155 'pixelFlags_interpolatedCenter',
156 'pixelFlags_saturatedCenter',
157 'pixelFlags_crCenter',
159 'pixelFlags_interpolated',
160 'pixelFlags_saturated',
164 source_selector.signalToNoise.fluxField = self.
inst_flux_fieldinst_flux_field
165 source_selector.signalToNoise.errField = self.
inst_flux_fieldinst_flux_field +
'Err'
167 source_selector.isolated.parentName =
'parentSourceId'
168 source_selector.isolated.nChildName =
'deblend_nChild'
170 source_selector.unresolved.maximum = 0.5
171 source_selector.unresolved.name =
'extendedness'
175 """Associate sources into isolated star catalogs.
177 ConfigClass = IsolatedStarAssociationConfig
178 _DefaultName = 'isolatedStarAssociation'
183 self.makeSubtask(
'source_selector')
185 self.source_selector.log.setLevel(self.source_selector.log.WARN)
188 input_ref_dict = butlerQC.get(inputRefs)
190 tract = butlerQC.quantum.dataId[
'tract']
192 source_table_refs = input_ref_dict[
'source_table_visit']
194 self.log.
info(
'Running with %d source_table_visit dataRefs',
195 len(source_table_refs))
197 source_table_ref_dict_temp = {source_table_ref.dataId[
'visit']: source_table_ref
for
198 source_table_ref
in source_table_refs}
200 bands = {source_table_ref.dataId[
'band']
for source_table_ref
in source_table_refs}
202 if band
not in self.config.band_order:
203 self.log.
warning(
'Input data has data from band %s but that band is not '
204 'configured for matching', band)
208 source_table_ref_dict = {visit: source_table_ref_dict_temp[visit]
for
209 visit
in sorted(source_table_ref_dict_temp.keys())}
211 struct = self.
runrun(input_ref_dict[
'skymap'], tract, source_table_ref_dict)
213 butlerQC.put(pd.DataFrame(struct.star_source_cat),
214 outputRefs.isolated_star_sources)
215 butlerQC.put(pd.DataFrame(struct.star_cat),
216 outputRefs.isolated_star_cat)
218 def run(self, skymap, tract, source_table_ref_dict):
219 """Run the isolated star association task.
223 skymap : `lsst.skymap.SkyMap`
227 source_table_ref_dict : `dict`
228 Dictionary of source_table refs. Key is visit, value
is dataref.
232 struct : `lsst.pipe.base.struct`
233 Struct
with outputs
for persistence.
237 primary_bands = self.config.band_order
240 primary_star_cat = self.
_match_primary_stars_match_primary_stars(primary_bands, star_source_cat)
242 if len(primary_star_cat) == 0:
243 return pipeBase.Struct(star_source_cat=np.zeros(0, star_source_cat.dtype),
244 star_cat=np.zeros(0, primary_star_cat.dtype))
249 if len(primary_star_cat) == 0:
250 return pipeBase.Struct(star_source_cat=np.zeros(0, star_source_cat.dtype),
251 star_cat=np.zeros(0, primary_star_cat.dtype))
254 inner_tract_ids = skymap.findTractIdArray(primary_star_cat[self.config.ra_column],
255 primary_star_cat[self.config.dec_column],
257 use = (inner_tract_ids == tract)
258 self.log.
info(
'Total of %d isolated stars in inner tract.', use.sum())
260 primary_star_cat = primary_star_cat[use]
262 if len(primary_star_cat) == 0:
263 return pipeBase.Struct(star_source_cat=np.zeros(0, star_source_cat.dtype),
264 star_cat=np.zeros(0, primary_star_cat.dtype))
269 len(primary_star_cat))
272 star_source_cat, primary_star_cat = self.
_match_sources_match_sources(primary_bands,
276 return pipeBase.Struct(star_source_cat=star_source_cat,
277 star_cat=primary_star_cat)
279 def _make_all_star_sources(self, tract_info, source_table_ref_dict):
280 """Make a catalog of all the star sources.
285 Information about the tract.
286 source_table_ref_dict : `dict`
287 Dictionary of source_table refs. Key is visit, value
is dataref.
291 star_source_cat : `np.ndarray`
292 Catalog of star sources.
298 poly = tract_info.outer_sky_polygon
301 for visit
in source_table_ref_dict:
302 source_table_ref = source_table_ref_dict[visit]
303 df = source_table_ref.get(parameters={
'columns': all_columns})
304 df.reset_index(inplace=
True)
306 goodSrc = self.source_selector.selectSources(df)
308 table = df[persist_columns][goodSrc.selected].to_records()
312 table = np.lib.recfunctions.append_fields(table,
315 [np.where(goodSrc.selected)[0],
316 np.zeros(goodSrc.selected.sum(), dtype=np.int32)],
322 tract_use = poly.contains(np.deg2rad(table[self.config.ra_column]),
323 np.deg2rad(table[self.config.dec_column]))
325 tables.append(table[tract_use])
328 star_source_cat = np.concatenate(tables)
330 return star_source_cat
332 def _get_source_table_visit_column_names(self):
333 """Get the list of sourceTable_visit columns from the config.
337 all_columns : `list` [`str`]
339 persist_columns : `list` [`str`]
340 Columns to persist (excluding selection columns)
342 columns = [self.config.id_column,
344 self.config.ra_column, self.config.dec_column,
345 self.config.physical_filter_column, self.config.band_column,
346 self.config.inst_flux_field, self.config.inst_flux_field +
'Err']
347 columns.extend(self.config.extra_columns)
349 all_columns = columns.copy()
350 if self.source_selector.config.doFlags:
351 all_columns.extend(self.source_selector.config.flags.bad)
352 if self.source_selector.config.doUnresolved:
353 all_columns.append(self.source_selector.config.unresolved.name)
354 if self.source_selector.config.doIsolated:
355 all_columns.append(self.source_selector.config.isolated.parentName)
356 all_columns.append(self.source_selector.config.isolated.nChildName)
358 return all_columns, columns
360 def _match_primary_stars(self, primary_bands, star_source_cat):
361 """Match primary stars.
365 primary_bands : `list` [`str`]
366 Ordered list of primary bands.
367 star_source_cat : `np.ndarray`
368 Catalog of star sources.
372 primary_star_cat : `np.ndarray`
373 Catalog of primary star positions
375 ra_col = self.config.ra_column
376 dec_col = self.config.dec_column
380 primary_star_cat = None
381 for primary_band
in primary_bands:
382 use = (star_source_cat[
'band'] == primary_band)
384 ra = star_source_cat[ra_col][use]
385 dec = star_source_cat[dec_col][use]
387 with Matcher(ra, dec)
as matcher:
390 idx = matcher.query_groups(self.config.match_radius/3600., min_match=1)
391 except AttributeError:
393 idx = matcher.query_self(self.config.match_radius/3600., min_match=1)
398 self.log.
info(
'Found 0 primary stars in %s band.', primary_band)
401 band_cat = np.zeros(count, dtype=dtype)
402 band_cat[
'primary_band'] = primary_band
408 if ra.min() < 60.0
and ra.max() > 300.0:
409 ra_temp = (ra + 180.0) % 360. - 180.
415 for i, row
in enumerate(idx):
417 band_cat[ra_col][i] = np.mean(ra_temp[row])
418 band_cat[dec_col][i] = np.mean(dec[row])
422 band_cat[ra_col] %= 360.0
425 if primary_star_cat
is None or len(primary_star_cat) == 0:
426 primary_star_cat = band_cat
428 with Matcher(band_cat[ra_col], band_cat[dec_col])
as matcher:
429 idx = matcher.query_radius(primary_star_cat[ra_col],
430 primary_star_cat[dec_col],
431 self.config.match_radius/3600.)
433 match_indices = np.array([i
for i
in range(len(idx))
if len(idx[i]) > 0])
434 if len(match_indices) > 0:
435 band_cat = np.delete(band_cat, match_indices)
437 primary_star_cat = np.append(primary_star_cat, band_cat)
438 self.log.
info(
'Found %d primary stars in %s band.', len(band_cat), primary_band)
441 if primary_star_cat
is None:
442 primary_star_cat = np.zeros(0, dtype=dtype)
444 return primary_star_cat
446 def _remove_neighbors(self, primary_star_cat):
447 """Remove neighbors from the primary star catalog.
451 primary_star_cat : `np.ndarray`
452 Primary star catalog.
456 primary_star_cat_cut : `np.ndarray`
457 Primary star cat with neighbors removed.
459 ra_col = self.config.ra_column
460 dec_col = self.config.dec_column
462 with Matcher(primary_star_cat[ra_col], primary_star_cat[dec_col])
as matcher:
467 idx = matcher.query_groups(self.config.isolation_radius/3600., min_match=2)
468 except AttributeError:
470 idx = matcher.query_self(self.config.isolation_radius/3600., min_match=2)
473 neighbor_indices = np.concatenate(idx)
475 neighbor_indices = np.zeros(0, dtype=int)
477 if len(neighbor_indices) > 0:
478 neighbored = np.unique(neighbor_indices)
479 self.log.
info(
'Cutting %d objects with close neighbors.', len(neighbored))
480 primary_star_cat = np.delete(primary_star_cat, neighbored)
482 return primary_star_cat
484 def _match_sources(self, bands, star_source_cat, primary_star_cat):
485 """Match individual sources to primary stars.
489 bands : `list` [`str`]
491 star_source_cat : `np.ndarray`
492 Array of star sources.
493 primary_star_cat : `np.ndarray`
494 Array of primary stars.
498 star_source_cat_sorted : `np.ndarray`
499 Sorted and cropped array of star sources.
500 primary_star_cat : `np.ndarray`
501 Catalog of isolated stars,
with indexes to star_source_cat_cut.
503 ra_col = self.config.ra_column
504 dec_col = self.config.dec_column
508 n_source_per_band_per_obj = np.zeros((len(bands),
509 len(primary_star_cat)),
513 with Matcher(primary_star_cat[ra_col], primary_star_cat[dec_col])
as matcher:
514 for b, band
in enumerate(bands):
515 band_use, = np.where(star_source_cat[
'band'] == band)
517 idx = matcher.query_radius(star_source_cat[ra_col][band_use],
518 star_source_cat[dec_col][band_use],
519 self.config.match_radius/3600.)
520 n_source_per_band_per_obj[b, :] = np.array([len(row)
for row
in idx])
522 band_uses.append(band_use)
524 n_source_per_obj = np.sum(n_source_per_band_per_obj, axis=0)
526 primary_star_cat[
'nsource'] = n_source_per_obj
527 primary_star_cat[
'source_cat_index'][1:] = np.cumsum(n_source_per_obj)[:-1]
529 n_tot_source = primary_star_cat[
'source_cat_index'][-1] + primary_star_cat[
'nsource'][-1]
532 source_index = np.zeros(n_tot_source, dtype=np.int32)
533 obj_index = np.zeros(n_tot_source, dtype=np.int32)
536 for i
in range(len(primary_star_cat)):
537 obj_index[ctr: ctr + n_source_per_obj[i]] = i
538 for b
in range(len(bands)):
539 source_index[ctr: ctr + n_source_per_band_per_obj[b, i]] = band_uses[b][idxs[b][i]]
540 ctr += n_source_per_band_per_obj[b, i]
542 source_cat_index_band_offset = np.cumsum(n_source_per_band_per_obj, axis=0)
544 for b, band
in enumerate(bands):
545 primary_star_cat[f
'nsource_{band}'] = n_source_per_band_per_obj[b, :]
548 primary_star_cat[f
'source_cat_index_{band}'] = primary_star_cat[
'source_cat_index']
551 primary_star_cat[f
'source_cat_index_{band}'] = (primary_star_cat[
'source_cat_index']
552 + source_cat_index_band_offset[b - 1, :])
554 star_source_cat = star_source_cat[source_index]
555 star_source_cat[
'obj_index'] = obj_index
557 return star_source_cat, primary_star_cat
559 def _compute_unique_ids(self, skymap, tract, nstar):
560 """Compute unique star ids.
562 This is a simple hash of the tract
and star to provide an
563 id that
is unique
for a given processing.
567 skymap : `lsst.skymap.Skymap`
577 Array of unique star ids.
580 mult = 10**(
int(np.log10(len(skymap))) + 1)
582 return (np.arange(nstar) + 1)*mult + tract
584 def _get_primary_dtype(self, primary_bands):
585 """Get the numpy datatype for the primary star catalog.
589 primary_bands : `list` [`str`]
590 List of primary bands.
594 dtype : `numpy.dtype`
595 Datatype of the primary catalog.
597 max_len = max([len(primary_band) for primary_band
in primary_bands])
599 dtype = [(
'isolated_star_id',
'i8'),
600 (self.config.ra_column,
'f8'),
601 (self.config.dec_column,
'f8'),
602 (
'primary_band', f
'U{max_len}'),
603 (
'source_cat_index',
'i4'),
606 for band
in primary_bands:
607 dtype.append((f
'source_cat_index_{band}',
'i4'))
608 dtype.append((f
'nsource_{band}',
'i4'))
def _get_source_table_visit_column_names(self)
def run(self, skymap, tract, source_table_ref_dict)
def _make_all_star_sources(self, tract_info, source_table_ref_dict)
def runQuantum(self, butlerQC, inputRefs, outputRefs)
def _compute_unique_ids(self, skymap, tract, nstar)
def __init__(self, **kwargs)
def _match_primary_stars(self, primary_bands, star_source_cat)
def _match_sources(self, bands, star_source_cat, primary_star_cat)
def _get_primary_dtype(self, primary_bands)
def _remove_neighbors(self, primary_star_cat)