@@ -1522,16 +1522,28 @@ def centroid_win(self):
15221522 when the change in centroid position falls below a pre-defined
15231523 threshold or a maximum number of iterations is reached.
15241524
1525- If the windowed centroid falls outside the 1-sigma ellipse shape
1526- based on the image moments, then the isophotal `centroid` will
1527- be used instead.
1525+ If the windowed centroid falls outside the 1-sigma ellipse
1526+ shape based on the image moments, then the isophotal `centroid`
1527+ will be used instead. If the half-light radius is not finite
1528+ (e.g., due to a non-finite Kron radius), then ``np.nan`` will be
1529+ returned.
15281530 """
1529- radius_hl = self .fluxfrac_radius (0.5 ).value
1531+ # Use .copy() to avoid mutating the cached fluxfrac_radius value
1532+ radius_hl = self .fluxfrac_radius (0.5 ).value .copy ()
15301533 if self .isscalar :
15311534 radius_hl = np .array ([radius_hl ])
1532- min_radius = 0.5 # define minimum half-light radius
1533- mask = (radius_hl < min_radius ) | ~ np .isfinite (radius_hl )
1534- radius_hl [mask ] = min_radius
1535+
1536+ # Track which sources have non-finite half-light radii (e.g.,
1537+ # due to NaN kron_radius). These sources cannot have a
1538+ # meaningful windowed centroid.
1539+ nan_hl = ~ np .isfinite (radius_hl )
1540+
1541+ # Apply a minimum half-light radius of 0.5 pixels (matching
1542+ # SourceExtractor) for valid but very small values
1543+ min_radius = 0.5
1544+ small_mask = np .isfinite (radius_hl ) & (radius_hl < min_radius )
1545+ radius_hl [small_mask ] = min_radius
1546+
15351547 kwargs = self ._apermask_kwargs ['cen_win' ]
15361548
15371549 labels = self .labels
@@ -1541,11 +1553,11 @@ def centroid_win(self):
15411553
15421554 xcen_win = []
15431555 ycen_win = []
1544- for label , xcen , ycen , rad_hl in zip (labels , self . _xcentroid ,
1545- self ._ycentroid , radius_hl ,
1546- strict = True ):
1556+ for label , xcen , ycen , rad_hl , nan_hl_ in zip (
1557+ labels , self . _xcentroid , self ._ycentroid , radius_hl ,
1558+ nan_hl , strict = True ):
15471559
1548- if np .any (~ np .isfinite ((xcen , ycen ))):
1560+ if nan_hl_ or np .any (~ np .isfinite ((xcen , ycen ))):
15491561 xcen_win .append (np .nan )
15501562 ycen_win .append (np .nan )
15511563 continue
@@ -1560,7 +1572,11 @@ def centroid_win(self):
15601572 centroid_threshold = 0.0001
15611573 while iter_ < max_iters and dcen > centroid_threshold :
15621574 aperture = CircularAperture ((xcen , ycen ), radius )
1563- aperture_mask = aperture .to_mask (** kwargs )
1575+ aperture_mask = self ._aperture_to_mask (aperture , ** kwargs )
1576+ if aperture_mask is None :
1577+ xcen = np .nan
1578+ ycen = np .nan
1579+ break
15641580
15651581 # For consistency with the isophotal centroid, a local
15661582 # background is not subtracted here
@@ -1607,7 +1623,9 @@ def centroid_win(self):
16071623 ycen_win = np .array (ycen_win )
16081624
16091625 # Reset to the isophotal centroid if the windowed centroid is
1610- # outside the 1-sigma ellipse
1626+ # outside the 1-sigma ellipse or if the iteration failed (NaN
1627+ # from aperture off-image). Sources with NaN half-light radius
1628+ # keep NaN (no valid window size).
16111629 dx = self ._xcentroid - xcen_win
16121630 dy = self ._ycentroid - ycen_win
16131631 cxx = self .cxx .value
@@ -1617,11 +1635,12 @@ def centroid_win(self):
16171635 cxx = (cxx ,)
16181636 cxy = (cxy ,)
16191637 cyy = (cyy ,)
1620- mask = ((cxx * dx ** 2 + cxy * dx * dy + cyy * dy ** 2 ) > 1 )
1621- mask |= (np .isnan (xcen_win ) | np .isnan (ycen_win ))
1622- if np .any (mask ):
1623- xcen_win [mask ] = self ._xcentroid [mask ]
1624- ycen_win [mask ] = self ._ycentroid [mask ]
1638+ reset = ((cxx * dx ** 2 + cxy * dx * dy + cyy * dy ** 2 ) > 1 )
1639+ nan_cen = np .isnan (xcen_win ) | np .isnan (ycen_win )
1640+ reset |= nan_cen & ~ nan_hl
1641+ if np .any (reset ):
1642+ xcen_win [reset ] = self ._xcentroid [reset ]
1643+ ycen_win [reset ] = self ._ycentroid [reset ]
16251644
16261645 return np .transpose ((xcen_win , ycen_win ))
16271646
@@ -2878,6 +2897,27 @@ def local_background(self):
28782897 bkg <<= self ._data_unit
28792898 return bkg
28802899
2900+ def _aperture_to_mask (self , aperture , ** kwargs ):
2901+ """
2902+ Call ``aperture.to_mask()``, but first check that the aperture
2903+ bounding box is not larger than the input data to prevent
2904+ out-of-memory errors from pathologically large apertures.
2905+
2906+ The aperture mask is allocated at the full (unclipped) bounding
2907+ box size by ``to_mask()``, before ``get_overlap_slices`` clips
2908+ it to the data shape. For pathological apertures (e.g., from
2909+ huge Kron radii), this allocation can cause out-of-memory
2910+ issues.
2911+
2912+ Returns `None` if the aperture mask would be unreasonably large.
2913+ """
2914+ bbox = aperture .bbox
2915+ # Limit the aperture mask size to prevent OOM errors
2916+ max_size = max (self ._data .size , 1_000_000 )
2917+ if bbox .shape [0 ] * bbox .shape [1 ] > max_size :
2918+ return None
2919+ return aperture .to_mask (** kwargs )
2920+
28812921 def _make_aperture_data (self , label , xcentroid , ycentroid , aperture_bbox ,
28822922 local_background , * , make_error = True ):
28832923 """
@@ -3193,7 +3233,10 @@ def _measured_kron_radius(self):
31933233
31943234 xcen , ycen = aperture .positions
31953235 # Use 'center' (whole pixels) to compute Kron radius
3196- aperture_mask = aperture .to_mask (method = 'center' )
3236+ aperture_mask = self ._aperture_to_mask (aperture , method = 'center' )
3237+ if aperture_mask is None :
3238+ kron_radius .append (np .nan )
3239+ continue
31973240
31983241 # Prepare cutouts of the data based on the aperture size
31993242 # local background explicitly set to zero for SE agreement
@@ -3234,6 +3277,15 @@ def _calc_kron_radius(self, kron_params):
32343277 pixel units.
32353278 """
32363279 kron_radius = self ._measured_kron_radius .copy ()
3280+
3281+ # Set values exceeding the measurement aperture scale (6.0)
3282+ # to NaN. Such values are unphysical (the Kron radius cannot
3283+ # meaningfully exceed the aperture used to measure it) and are
3284+ # caused by near-cancellation in the denominator of the Kron
3285+ # formula due to outlier pixels or noise.
3286+ max_kron_radius = 6.0
3287+ kron_radius [kron_radius > max_kron_radius ] = np .nan
3288+
32373289 # Set minimum (unscaled) kron radius
32383290 kron_radius [kron_radius < kron_params [1 ]] = kron_params [1 ]
32393291
@@ -3280,7 +3332,11 @@ def kron_radius(self):
32803332
32813333 The `kron_radius` value is the unscaled moment value. The
32823334 minimum unscaled radius can be set using the second element of
3283- the `SourceCatalog` ``kron_params`` keyword.
3335+ the `SourceCatalog` ``kron_params`` keyword. If the measured
3336+ unscaled Kron radius exceeds 6.0 (the measurement aperture
3337+ scale factor), ``np.nan`` will be returned. Such values are
3338+ unphysical, typically caused by near-cancellation in the
3339+ denominator of the Kron formula due to outlier pixels or noise.
32843340
32853341 If either the numerator or denominator above is less than
32863342 or equal to 0, then the minimum unscaled Kron radius
@@ -3505,7 +3561,11 @@ def _aperture_photometry(self, apertures, *, desc='', **kwargs):
35053561 continue
35063562
35073563 xcen , ycen = aperture .positions
3508- aperture_mask = aperture .to_mask (** kwargs )
3564+ aperture_mask = self ._aperture_to_mask (aperture , ** kwargs )
3565+ if aperture_mask is None :
3566+ flux .append (np .nan )
3567+ fluxerr .append (np .nan )
3568+ continue
35093569
35103570 # Prepare cutouts of the data based on the aperture size
35113571 data , error , mask , _ , slc_sm = self ._make_aperture_data (
@@ -3735,7 +3795,10 @@ def _fluxfrac_optimizer_args(self):
37353795 continue
37363796
37373797 aperture = CircularAperture ((xcen , ycen ), r = max_radius_ )
3738- aperture_mask = aperture .to_mask (** kwargs )
3798+ aperture_mask = self ._aperture_to_mask (aperture , ** kwargs )
3799+ if aperture_mask is None :
3800+ args .append (None )
3801+ continue
37393802
37403803 # Prepare cutouts of the data based on the maximum aperture size
37413804 data , _ , mask , xycen , _ = self ._make_aperture_data (
0 commit comments