Loading [MathJax]/extensions/tex2jax.js
LSST Applications g0fba68d861+83433b07ee,g16d25e1f1b+23bc9e47ac,g1ec0fe41b4+3ea9d11450,g1fd858c14a+9be2b0f3b9,g2440f9efcc+8c5ae1fdc5,g35bb328faa+8c5ae1fdc5,g4a4af6cd76+d25431c27e,g4d2262a081+c74e83464e,g53246c7159+8c5ae1fdc5,g55585698de+1e04e59700,g56a49b3a55+92a7603e7a,g60b5630c4e+1e04e59700,g67b6fd64d1+3fc8cb0b9e,g78460c75b0+7e33a9eb6d,g786e29fd12+668abc6043,g8352419a5c+8c5ae1fdc5,g8852436030+60e38ee5ff,g89139ef638+3fc8cb0b9e,g94187f82dc+1e04e59700,g989de1cb63+3fc8cb0b9e,g9d31334357+1e04e59700,g9f33ca652e+0a83e03614,gabe3b4be73+8856018cbb,gabf8522325+977d9fabaf,gb1101e3267+8b4b9c8ed7,gb89ab40317+3fc8cb0b9e,gc0af124501+57ccba3ad1,gcf25f946ba+60e38ee5ff,gd6cbbdb0b4+1cc2750d2e,gd794735e4e+7be992507c,gdb1c4ca869+be65c9c1d7,gde0f65d7ad+c7f52e58fe,ge278dab8ac+6b863515ed,ge410e46f29+3fc8cb0b9e,gf35d7ec915+97dd712d81,gf5e32f922b+8c5ae1fdc5,gf618743f1b+747388abfa,gf67bdafdda+3fc8cb0b9e,w.2025.18
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
piffPsfDeterminer.py
Go to the documentation of this file.
1# This file is part of meas_extensions_piff.
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
22__all__ = ["PiffPsfDeterminerConfig", "PiffPsfDeterminerTask"]
23
24import numpy as np
25import piff
26import galsim
27import re
28import logging
29import yaml
30
31import lsst.utils.logging
32from lsst.afw.cameraGeom import PIXELS, FIELD_ANGLE
33import lsst.pex.config as pexConfig
34import lsst.meas.algorithms as measAlg
35from lsst.meas.algorithms.psfDeterminer import BasePsfDeterminerTask
36from lsst.pipe.base import AlgorithmError
37from .piffPsf import PiffPsf
38from .wcs_wrapper import CelestialWcsWrapper, UVWcsWrapper
39
40
41def _validateGalsimInterpolant(name: str) -> bool:
42 """A helper function to validate the GalSim interpolant at config time.
43
44 Parameters
45 ----------
46 name : str
47 The name of the interpolant to use from GalSim. Valid options are:
48 galsim.Lanczos(N) or Lancsos(N), where N is a positive integer
49 galsim.Linear
50 galsim.Cubic
51 galsim.Quintic
52 galsim.Delta
53 galsim.Nearest
54 galsim.SincInterpolant
55
56 Returns
57 -------
58 is_valid : bool
59 Whether the provided interpolant name is valid.
60 """
61 # First, check if ``name`` is a valid Lanczos interpolant.
62 for pattern in (re.compile(r"Lanczos\‍(\d+\‍)"), re.compile(r"galsim.Lanczos\‍(\d+\‍)"),):
63 match = re.match(pattern, name) # Search from the start of the string.
64 if match is not None:
65 # Check that the pattern is also the end of the string.
66 return match.end() == len(name)
67
68 # If not, check if ``name`` is any other valid GalSim interpolant.
69 names = {f"galsim.{interp}" for interp in
70 ("Cubic", "Delta", "Linear", "Nearest", "Quintic", "SincInterpolant")
71 }
72 return name in names
73
74
75class PiffTooFewGoodStarsError(AlgorithmError):
76 """Raised if too few good stars are available for PSF determination.
77
78 Parameters
79 ----------
80 num_good_stars : `int`
81 Number of good stars used for PSF determination.
82 poly_ndim : `int`
83 Number of dependency parameters (dimensions) used in
84 polynomial fitting.
85 minimum_dof : `int`
86 Minimum number of degree of freedom to do the fit.
87 """
88
90 self,
91 num_good_stars,
92 minimum_dof,
93 poly_ndim,
94 ):
95 self._num_good_stars = num_good_stars
96 self._poly_ndim = poly_ndim
97 self._minimum_dof = minimum_dof
98 super().__init__(
99 f"Failed to determine piff psf: too few good stars ({num_good_stars}) and minimum dof to fit "
100 f"a {poly_ndim} order polynomial is {minimum_dof}."
101 )
102
103 @property
104 def metadata(self):
105 return {
106 "num_good_stars": self._num_good_stars,
107 "poly_ndim": self._poly_ndim,
108 "minimum_dof": self._minimum_dof,
109 }
110
111
112class PiffPsfDeterminerConfig(BasePsfDeterminerTask.ConfigClass):
113 spatialOrderPerBand = pexConfig.DictField(
114 doc="Per-band spatial order for PSF kernel creation. "
115 "Ignored if piffPsfConfigYaml is set.",
116 keytype=str,
117 itemtype=int,
118 default={},
119 )
120 spatialOrder = pexConfig.Field[int](
121 doc="Spatial order for PSF kernel creation. "
122 "Ignored if piffPsfConfigYaml is set or if the current "
123 "band is in the spatialOrderPerBand dict config.",
124 default=2,
125 )
126 piffBasisPolynomialSolver = pexConfig.ChoiceField[str](
127 doc="Solver to solve linear algebra for "
128 "the spatial interpolation of the PSF. "
129 "Ignore if piffPsfConfigYaml is not None.",
130 allowed=dict(
131 scipy="Default solver using scipy.",
132 cpp="C++ solver using eigen.",
133 jax="JAX solver.",
134 qr="QR decomposition using scipy/numpy.",
135 ),
136 default="scipy",
137 )
138 piffPixelGridFitCenter = pexConfig.Field[bool](
139 doc="PixelGrid can re-estimate center if this option is True. "
140 "Will use provided centroid if set to False. Default in Piff "
141 "is set to True. Depends on how input centroids can be trust. "
142 "Ignore if piffPsfConfigYaml is not None.",
143 default=True
144 )
145 samplingSize = pexConfig.Field[float](
146 doc="Resolution of the internal PSF model relative to the pixel size; "
147 "e.g. 0.5 is equal to 2x oversampling. This affects only the size of "
148 "the PSF model stamp if piffPsfConfigYaml is set.",
149 default=1,
150 )
151 modelSize = pexConfig.Field[int](
152 doc="Internal model size for PIFF (typically odd, but not enforced). "
153 "Partially ignored if piffPsfConfigYaml is set.",
154 default=25,
155 )
156 outlierNSigma = pexConfig.Field[float](
157 doc="n sigma for chisq outlier rejection. "
158 "Ignored if piffPsfConfigYaml is set.",
159 default=4.0
160 )
161 outlierMaxRemove = pexConfig.Field[float](
162 doc="Max fraction of stars to remove as outliers each iteration. "
163 "Ignored if piffPsfConfigYaml is set.",
164 default=0.05
165 )
166 maxSNR = pexConfig.Field[float](
167 doc="Rescale the weight of bright stars such that their SNR is less "
168 "than this value.",
169 default=200.0
170 )
171 zeroWeightMaskBits = pexConfig.ListField[str](
172 doc="List of mask bits for which to set pixel weights to zero.",
173 default=['BAD', 'CR', 'INTRP', 'SAT', 'SUSPECT', 'NO_DATA']
174 )
175 minimumUnmaskedFraction = pexConfig.Field[float](
176 doc="Minimum fraction of unmasked pixels required to use star.",
177 default=0.5
178 )
179 interpolant = pexConfig.Field[str](
180 doc="GalSim interpolant name for Piff to use. "
181 "Options include 'Lanczos(N)', where N is an integer, along with "
182 "galsim.Cubic, galsim.Delta, galsim.Linear, galsim.Nearest, "
183 "galsim.Quintic, and galsim.SincInterpolant. Ignored if "
184 "piffPsfConfigYaml is set.",
185 check=_validateGalsimInterpolant,
186 default="Lanczos(11)",
187 )
188 zerothOrderInterpNotEnoughStars = pexConfig.Field[bool](
189 doc="If True, use zeroth order interpolation if not enough stars are found.",
190 default=False
191 )
192 debugStarData = pexConfig.Field[bool](
193 doc="Include star images used for fitting in PSF model object.",
194 default=False
195 )
196 useCoordinates = pexConfig.ChoiceField[str](
197 doc="Which spatial coordinates to regress against in PSF modeling.",
198 allowed=dict(
199 pixel='Regress against pixel coordinates',
200 field='Regress against field angles',
201 sky='Regress against RA/Dec'
202 ),
203 default='pixel'
204 )
205 piffMaxIter = pexConfig.Field[int](
206 doc="Maximum iteration while doing outlier rejection."
207 "Might be overwrite if zerothOrderInterpNotEnoughStars is True and "
208 "ignore if piffPsfConfigYaml is not None.",
209 default=15
210 )
211 piffLoggingLevel = pexConfig.RangeField[int](
212 doc="PIFF-specific logging level, from 0 (least chatty) to 3 (very verbose); "
213 "note that logs will come out with unrelated notations (e.g. WARNING does "
214 "not imply a warning.)",
215 default=0,
216 min=0,
217 max=3,
218 )
219 piffPsfConfigYaml = pexConfig.Field[str](
220 doc="Configuration file for PIFF in YAML format. This overrides many "
221 "other settings in this config and is not validated ahead of runtime.",
222 default=None,
223 optional=True,
224 )
225
226 def setDefaults(self):
227 super().setDefaults()
228 # stampSize should be at least 25 so that
229 # i) aperture flux with 12 pixel radius can be compared to PSF flux.
230 # ii) fake sources injected to match the 12 pixel aperture flux get
231 # measured correctly
232 # stampSize should also be at least sqrt(2)*modelSize/samplingSize so
233 # that rotated images have support in the model
234
235 self.stampSize = 25
236 # Resize the stamp to accommodate the model, but only if necessary.
237 if self.useCoordinates == "sky":
238 self.stampSize = max(25, 2*int(0.5*self.modelSize*np.sqrt(2)/self.samplingSize) + 1)
239
240 def validate(self):
241 super().validate()
242
243 if (stamp_size := self.stampSize) is not None:
244 model_size = self.modelSize
245 sampling_size = self.samplingSize
246 if self.useCoordinates == 'sky':
247 min_stamp_size = int(np.sqrt(2) * model_size / sampling_size)
248 else:
249 min_stamp_size = int(model_size / sampling_size)
250
251 if stamp_size < min_stamp_size:
252 msg = (f"PIFF model size of {model_size} is too large for stamp size {stamp_size}. "
253 f"Set stampSize >= {min_stamp_size}"
254 )
255 raise pexConfig.FieldValidationError(self.__class__.modelSize, self, msg)
256
257
258def getGoodPixels(maskedImage, zeroWeightMaskBits):
259 """Compute an index array indicating good pixels to use.
260
261 Parameters
262 ----------
263 maskedImage : `afw.image.MaskedImage`
264 PSF candidate postage stamp
265 zeroWeightMaskBits : `List[str]`
266 List of mask bits for which to set pixel weights to zero.
267
268 Returns
269 -------
270 good : `ndarray`
271 Index array indicating good pixels.
272 """
273 imArr = maskedImage.image.array
274 varArr = maskedImage.variance.array
275 bitmask = maskedImage.mask.getPlaneBitMask(zeroWeightMaskBits)
276 good = (
277 (varArr != 0)
278 & (np.isfinite(varArr))
279 & (np.isfinite(imArr))
280 & ((maskedImage.mask.array & bitmask) == 0)
281 )
282 return good
283
284
285def computeWeight(maskedImage, maxSNR, good):
286 """Derive a weight map without Poisson variance component due to signal.
287
288 Parameters
289 ----------
290 maskedImage : `afw.image.MaskedImage`
291 PSF candidate postage stamp
292 maxSNR : `float`
293 Maximum SNR applying variance floor.
294 good : `ndarray`
295 Index array indicating good pixels.
296
297 Returns
298 -------
299 weightArr : `ndarry`
300 Array to use for weight.
301
302 See Also
303 --------
304 `lsst.meas.algorithms.variance_plance.remove_signal_from_variance` :
305 Remove the Poisson contribution from sources in the variance plane of
306 an Exposure.
307 """
308 imArr = maskedImage.image.array
309 varArr = maskedImage.variance.array
310
311 # Fit a straight line to variance vs (sky-subtracted) signal.
312 # The evaluate that line at zero signal to get an estimate of the
313 # signal-free variance.
314 fit = np.polyfit(imArr[good], varArr[good], deg=1)
315 # fit is [1/gain, sky_var]
316 weightArr = np.zeros_like(imArr, dtype=float)
317 weightArr[good] = 1./fit[1]
318
319 applyMaxSNR(imArr, weightArr, good, maxSNR)
320 return weightArr
321
322
323def applyMaxSNR(imArr, weightArr, good, maxSNR):
324 """Rescale weight of bright stars to cap the computed SNR.
325
326 Parameters
327 ----------
328 imArr : `ndarray`
329 Signal (image) array of stamp.
330 weightArr : `ndarray`
331 Weight map array. May be rescaled in place.
332 good : `ndarray`
333 Index array of pixels to use when computing SNR.
334 maxSNR : `float`
335 Threshold for adjusting variance plane implementing maximum SNR.
336 """
337 # We define the SNR value following Piff. Here's the comment from that
338 # code base explaining the calculation.
339 #
340 # The S/N value that we use will be the weighted total flux where the
341 # weight function is the star's profile itself. This is the maximum S/N
342 # value that any flux measurement can possibly produce, which will be
343 # closer to an in-practice S/N than using all the pixels equally.
344 #
345 # F = Sum_i w_i I_i^2
346 # var(F) = Sum_i w_i^2 I_i^2 var(I_i)
347 # = Sum_i w_i I_i^2 <--- Assumes var(I_i) = 1/w_i
348 #
349 # S/N = F / sqrt(var(F))
350 #
351 # Note that if the image is pure noise, this will produce a "signal" of
352 #
353 # F_noise = Sum_i w_i 1/w_i = Npix
354 #
355 # So for a more accurate estimate of the S/N of the actual star itself, one
356 # should subtract off Npix from the measured F.
357 #
358 # The final formula then is:
359 #
360 # F = Sum_i w_i I_i^2
361 # S/N = (F-Npix) / sqrt(F)
362 F = np.sum(weightArr[good]*imArr[good]**2, dtype=float)
363 Npix = np.sum(good)
364 SNR = 0.0 if F < Npix else (F-Npix)/np.sqrt(F)
365 # rescale weight of bright stars. Essentially makes an error floor.
366 if SNR > maxSNR:
367 factor = (maxSNR / SNR)**2
368 weightArr[good] *= factor
369
370
371def _computeWeightAlternative(maskedImage, maxSNR):
372 """Alternative algorithm for creating weight map.
373
374 This version is equivalent to that used by Piff internally. The weight map
375 it produces tends to leave a residual when removing the Poisson component
376 due to the signal. We leave it here as a reference, but without intending
377 that it be used (or be maintained).
378 """
379 imArr = maskedImage.image.array
380 varArr = maskedImage.variance.array
381 good = (varArr != 0) & np.isfinite(varArr) & np.isfinite(imArr)
382
383 fit = np.polyfit(imArr[good], varArr[good], deg=1)
384 # fit is [1/gain, sky_var]
385 gain = 1./fit[0]
386 varArr[good] -= imArr[good] / gain
387 weightArr = np.zeros_like(imArr, dtype=float)
388 weightArr[good] = 1./varArr[good]
389
390 applyMaxSNR(imArr, weightArr, good, maxSNR)
391 return weightArr
392
393
395 """A measurePsfTask PSF estimator using Piff as the implementation.
396 """
397 ConfigClass = PiffPsfDeterminerConfig
398 _DefaultName = "psfDeterminer.Piff"
399
400 def __init__(self, config, schema=None, **kwds):
401 BasePsfDeterminerTask.__init__(self, config, schema=schema, **kwds)
402
403 piffLoggingLevels = {
404 0: logging.CRITICAL,
405 1: logging.WARNING,
406 2: logging.INFO,
407 3: logging.DEBUG,
408 }
409 self.piffLogger = lsst.utils.logging.getLogger(f"{self.log.name}.piff")
410 self.piffLogger.setLevel(piffLoggingLevels[self.config.piffLoggingLevel])
411
413 self, exposure, psfCandidateList, metadata=None, flagKey=None
414 ):
415 """Determine a Piff PSF model for an exposure given a list of PSF
416 candidates.
417
418 Parameters
419 ----------
420 exposure : `lsst.afw.image.Exposure`
421 Exposure containing the PSF candidates.
422 psfCandidateList : `list` of `lsst.meas.algorithms.PsfCandidate`
423 A sequence of PSF candidates typically obtained by detecting sources
424 and then running them through a star selector.
425 metadata : `lsst.daf.base import PropertyList` or `None`, optional
426 A home for interesting tidbits of information.
427 flagKey : `str` or `None`, optional
428 Schema key used to mark sources actually used in PSF determination.
429
430 Returns
431 -------
432 psf : `lsst.meas.extensions.piff.PiffPsf`
433 The measured PSF model.
434 psfCellSet : `None`
435 Unused by this PsfDeterminer.
436 """
437 psfCandidateList = self.downsampleCandidates(psfCandidateList)
438
439 if self.config.stampSize:
440 stampSize = self.config.stampSize
441 if stampSize > psfCandidateList[0].getWidth():
442 self.log.warning("stampSize is larger than the PSF candidate size. Using candidate size.")
443 stampSize = psfCandidateList[0].getWidth()
444 else: # TODO: Only the if block should stay after DM-36311
445 self.log.debug("stampSize not set. Using candidate size.")
446 stampSize = psfCandidateList[0].getWidth()
447
448 scale = exposure.getWcs().getPixelScale(exposure.getBBox().getCenter()).asArcseconds()
449
450 match self.config.useCoordinates:
451 case 'field':
452 detector = exposure.getDetector()
453 pix_to_field = detector.getTransform(PIXELS, FIELD_ANGLE)
454 gswcs = UVWcsWrapper(pix_to_field)
455 pointing = None
456
457 case 'sky':
458 gswcs = CelestialWcsWrapper(exposure.getWcs())
459 skyOrigin = exposure.getWcs().getSkyOrigin()
460 ra = skyOrigin.getLongitude().asDegrees()
461 dec = skyOrigin.getLatitude().asDegrees()
462 pointing = galsim.CelestialCoord(
463 ra*galsim.degrees,
464 dec*galsim.degrees
465 )
466
467 case 'pixel':
468 gswcs = galsim.PixelScale(scale)
469 pointing = None
470
471 stars = []
472 for candidate in psfCandidateList:
473 cmi = candidate.getMaskedImage(stampSize, stampSize)
474 good = getGoodPixels(cmi, self.config.zeroWeightMaskBits)
475 fracGood = np.sum(good)/good.size
476 if fracGood < self.config.minimumUnmaskedFraction:
477 continue
478 weight = computeWeight(cmi, self.config.maxSNR, good)
479
480 bbox = cmi.getBBox()
481 bds = galsim.BoundsI(
482 galsim.PositionI(*bbox.getMin()),
483 galsim.PositionI(*bbox.getMax())
484 )
485 gsImage = galsim.Image(bds, wcs=gswcs, dtype=float)
486 gsImage.array[:] = cmi.image.array
487 gsWeight = galsim.Image(bds, wcs=gswcs, dtype=float)
488 gsWeight.array[:] = weight
489
490 source = candidate.getSource()
491 starId = source.getId()
492 image_pos = galsim.PositionD(source.getX(), source.getY())
493
494 data = piff.StarData(
495 gsImage,
496 image_pos,
497 weight=gsWeight,
498 pointing=pointing
499 )
500 star = piff.Star(data, None)
501 star.data.properties['starId'] = starId
502 stars.append(star)
503
504 if self.config.piffPsfConfigYaml is None:
505 # The following is mostly accommodating unittests that don't have
506 # the filter attribute set on the mock exposure.
507 if hasattr(exposure.filter, "bandLabel"):
508 band = exposure.filter.bandLabel
509 else:
510 band = None
511 spatialOrder = self.config.spatialOrderPerBand.get(band, self.config.spatialOrder)
512 piffConfig = {
513 'type': 'Simple',
514 'model': {
515 'type': 'PixelGrid',
516 'scale': scale * self.config.samplingSize,
517 'size': self.config.modelSize,
518 'interp': self.config.interpolant,
519 'centered': self.config.piffPixelGridFitCenter,
520 },
521 'interp': {
522 'type': 'BasisPolynomial',
523 'order': spatialOrder,
524 'solver': self.config.piffBasisPolynomialSolver,
525 },
526 'outliers': {
527 'type': 'Chisq',
528 'nsigma': self.config.outlierNSigma,
529 'max_remove': self.config.outlierMaxRemove,
530 },
531 'max_iter': self.config.piffMaxIter
532 }
533 else:
534 piffConfig = yaml.safe_load(self.config.piffPsfConfigYaml)
535
536 def _get_threshold(nth_order):
537 # number of free parameter in the polynomial interpolation
538 freeParameters = ((nth_order + 1) * (nth_order + 2)) // 2
539 return freeParameters
540
541 if piffConfig['interp']['type'] in ['BasisPolynomial', 'Polynomial']:
542 threshold = _get_threshold(piffConfig['interp']['order'])
543 if len(stars) < threshold:
544 if self.config.zerothOrderInterpNotEnoughStars:
545 self.log.warning(
546 "Only %d stars found, "
547 "but %d required. Using zeroth order interpolation."%((len(stars), threshold))
548 )
549 piffConfig['interp']['order'] = 0
550 # No need to do any outlier rejection assume
551 # PSF to be average of few stars.
552 piffConfig['max_iter'] = 1
553 else:
555 num_good_stars=len(stars),
556 minimum_dof=threshold,
557 poly_ndim=piffConfig['interp']['order'],
558 )
559
560 self._piffConfig = piffConfig
561 piffResult = piff.PSF.process(piffConfig)
562 wcs = {0: gswcs}
563
564 piffResult.fit(stars, wcs, pointing, logger=self.piffLogger)
565
566 nUsedStars = len([s for s in piffResult.stars if not s.is_flagged and not s.is_reserve])
567
568 if piffConfig['interp']['type'] in ['BasisPolynomial', 'Polynomial']:
569 threshold = _get_threshold(piffConfig['interp']['order'])
570 if nUsedStars < threshold:
571 if self.config.zerothOrderInterpNotEnoughStars:
572 self.log.warning(
573 "Only %d after outlier rejection, "
574 "but %d required. Using zeroth order interpolation."%((nUsedStars, threshold))
575 )
576 piffConfig['interp']['order'] = 0
577 # No need to do any outlier rejection assume
578 # PSF to be average of few stars.
579 piffConfig['max_iter'] = 1
580 piffResult.fit(stars, wcs, pointing, logger=self.piffLogger)
581 nUsedStars = len(stars)
582 else:
584 num_good_stars=nUsedStars,
585 minimum_dof=threshold,
586 poly_ndim=piffConfig['interp']['order'],
587 )
588
589 drawSize = 2*np.floor(0.5*stampSize/self.config.samplingSize) + 1
590
591 used_image_starId = {s.data.properties['starId'] for s in piffResult.stars
592 if not s.is_flagged and not s.is_reserve}
593
594 assert len(used_image_starId) == nUsedStars, "Star IDs are not unique"
595
596 if flagKey:
597 for candidate in psfCandidateList:
598 source = candidate.getSource()
599 starId = source.getId()
600 if starId in used_image_starId:
601 source.set(flagKey, True)
602
603 if metadata is not None:
604 metadata["spatialFitChi2"] = piffResult.chisq
605 metadata["numAvailStars"] = len(stars)
606 metadata["numGoodStars"] = nUsedStars
607 metadata["avgX"] = np.mean([s.x for s in piffResult.stars
608 if not s.is_flagged and not s.is_reserve])
609 metadata["avgY"] = np.mean([s.y for s in piffResult.stars
610 if not s.is_flagged and not s.is_reserve])
611
612 if not self.config.debugStarData:
613 for star in piffResult.stars:
614 # Remove large data objects from the stars
615 del star.fit.params
616 del star.fit.params_var
617 del star.fit.A
618 del star.fit.b
619 del star.data.image
620 del star.data.weight
621 del star.data.orig_weight
622
623 return PiffPsf(drawSize, drawSize, piffResult), None
624
625
626measAlg.psfDeterminerRegistry.register("piff", PiffPsfDeterminerTask)
determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None)
getGoodPixels(maskedImage, zeroWeightMaskBits)
applyMaxSNR(imArr, weightArr, good, maxSNR)