101 """Find glint trails in a catalog.
105 catalog : `lsst.afw.table.SourceCatalog`
106 Catalog to search for glint trails.
110 result : `lsst.pipe.base.Struct`
111 Results as a struct with attributes:
114 Catalog subsets containing sources in each trail that was found.
115 (`list` [`lsst.afw.table.SourceCatalog`])
117 Ids of all the sources that were included in any fit trail.
120 Parameters of all the trails that were found.
121 (`list` [`GlintTrailParameters`])
126 per_id = collections.defaultdict(list)
127 for match
in matches:
128 per_id[match.first[
"id"]].append(match)
129 counts = {id: len(value)
for id, value
in per_id.items()}
135 for id
in dict(sorted(counts.items(), key=
lambda item: item[1], reverse=
True)):
137 if counts[id] < self.config.min_points
or id
in trailed_ids:
140 self.log.debug(
"id=%d at %.1f,%.1f has %d matches within %d pixels.",
142 per_id[id][0].first.getX(),
143 per_id[id][0].first.getY(),
146 if (trail := self.
_search_one(per_id[id], good_catalog))
is not None:
147 trail, result = trail
149 n_new = len(set(trail[
"id"]).difference(trailed_ids))
151 self.log.info(
"Found %.1f pixel length trail with %d points, "
152 "%d not in any other trail (slope=%.4f, intercept=%.2f)",
153 result.length, len(trail), n_new, result.slope, result.intercept)
155 trailed_ids.update(trail[
"id"])
156 parameters.append(result)
158 self.log.info(
"Found %d glint trails containing %d total sources.",
159 len(trails), len(trailed_ids))
160 return lsst.pipe.base.Struct(trails=trails,
161 trailed_ids=trailed_ids,
162 parameters=parameters)
184 """Search one set of matches for a possible trail.
188 matches : `list` [`lsst.afw.table.Match`]
189 Matches for one anchor source to search for lines.
190 catalog : `lsst.afw.SourceCatalog`
191 Catalog of all sources, to refit lines to.
195 trail, result : `tuple` or None
196 If the no trails matching the criteria are found, return None,
197 otherwise return a tuple of the sources in the trail and the
200 components = collections.defaultdict(list)
202 xy_deltas = {pair.second[
"id"]: (pair.second.getX() - pair.first.getX(),
203 pair.second.getY() - pair.first.getY())
for pair
in matches}
206 for i, (id1, pair1)
in enumerate(xy_deltas.items()):
207 distance = math.sqrt(pair1[0]**2 + pair1[1]**2)
208 for j, (id2, pair2)
in enumerate(xy_deltas.items()):
211 delta = abs(pair1[0] * pair2[1] - pair1[1] * pair2[0])
213 if delta / distance < 2 * self.config.threshold:
214 components[i].append(j)
217 if len(components) == 0:
220 longest, value = max(components.items(), key=
lambda x: len(x[1]))
221 n_points = len(value)
223 if n_points < self.config.min_points:
226 candidate = [longest] + components[longest]
227 trail, result = self.
_other_points(n_points, candidate, matches, catalog)
229 if trail
is None or len(trail) < self.config.min_points:
231 if result.stderr > self.config.threshold:
232 self.log.info(
"Candidate trail with %d sources rejected with stderr %.6f > %.3f",
233 len(trail), result.stderr, self.config.threshold)
239 """Find all catalog records that could lie on this line.
244 Number of sources in this candidate trail.
245 indexes : `list` [`int`]
246 Indexes into matches on this candidate trail.
247 matches : `list` [`lsst.afw.table.Match`]
248 Matches for one anchor sources to search for lines.
249 catalog : `lsst.afw.SourceCatalog`
250 Catalog of all sources, to refit lines to.
254 trail : `lsst.afw.table.SourceCatalog`
255 Sources that are in the fitted trail.
256 result : `GlintTrailParameters`
257 Parameters of the fitted trail.
260 def extract(fitter, x, y, prefix=""):
261 """Extract values from the fit and log and return them."""
262 x = x[fitter.inlier_mask_]
263 y = y[fitter.inlier_mask_]
264 predicted = fitter.predict(x).flatten()
265 stderr = math.sqrt(((predicted - y.flatten())**2).sum())
266 m, b = fitter.estimator_.coef_[0][0], fitter.estimator_.intercept_[0]
267 self.log.debug(
"%s fit: score=%.6f, stderr=%.6f, inliers/total=%d/%d",
268 prefix, fitter.score(x, y), stderr, sum(fitter.inlier_mask_), len(x))
271 length = max(scipy.spatial.distance.pdist(np.hstack((x, y))), default=0)
278 fitter = sklearn.linear_model.RANSACRegressor(residual_threshold=self.config.threshold,
279 loss=
"squared_error",
280 random_state=self.config.seed,
284 x = np.empty(n_points).reshape(-1, 1)
285 x[0] = matches[0].first.getX()
286 x[1:, 0] = [matches[i].second.getX()
for i
in indexes]
287 y = np.empty(n_points).reshape(-1, 1)
288 y[0] = matches[0].first.getY()
289 y[1:, 0] = [matches[i].second.getY()
for i
in indexes]
294 self.log.info(
"Glint trail interpolation could not find a valid fit.")
297 result = extract(fitter, x, y, prefix=
"preliminary")
299 if (n_inliers := sum(fitter.inlier_mask_)) < self.config.min_points:
300 self.log.debug(
"Candidate trail rejected with %d < %d points.",
301 n_inliers, self.config.min_points)
305 x = catalog[
"slot_Centroid_x"]
306 y = catalog[
"slot_Centroid_y"]
307 dist = abs(result.intercept + result.slope * x - y) / math.sqrt(1 + result.slope**2)
310 candidates = (dist < 2 * self.config.threshold).flatten()
312 fitter = sklearn.linear_model.RANSACRegressor(residual_threshold=self.config.threshold,
313 loss=
"squared_error",
314 random_state=self.config.seed,
317 x = x[candidates].reshape(-1, 1)
318 y = y[candidates].reshape(-1, 1)
320 result = extract(fitter, x, y, prefix=
"final")
322 return catalog[candidates][fitter.inlier_mask_], result