From 16eed05615f64b3b471c990ee01ccc1a2cd68093 Mon Sep 17 00:00:00 2001 From: HannoSpreeuw Date: Sat, 13 Dec 2025 12:53:58 +0100 Subject: [PATCH] Restrict "tweaked moments" for "free shape" 1) Consider this the equivalent of commit f3b10b1. That commit tightened the condition for applying "tweaked moments" for forced beams: apply "tweaked moments" only when the rounded barycenter position is part of the island. The current commit tightens the conditions for "tweaked moments" for a "free shape" in the same way in the sense that a maximum pixel value and corresponding position can no longer be used as "basevalue" and "basepos", i.e. can no longer be used as the base point for a "tweaked moments" extrapolation. 2) Fixed a serious bug that occurs for "ill-formed" or large islands: "posx.size==posy.size!=no_pixels" is the source of error. Since we have indices "[0][0]" in the line "i = np.nonzero(mask)[0][0]" this will often still end up right, but we have encountered cases from a collection of 60 GRB images described here: https://github.com/transientskp/pyse/issues/180#issue-3668146799 where a run "pyse GRB201006A/*.fits --vectorized --back-size-x 50 --back-size-y 50" will reveal serious problems, i.e. unrealistic peak brightnesses. This can occur on one machine, while not causing any problems on another machine with exactly the same code. This comes from the way the "np.empty" fills values in these lines in the "extract" module: https://github.com/transientskp/pyse/blob/591717fd72decbb7fc991bf2232be595c3f2b6f3/sourcefinder/extract.py#L2229 "xpositions = np.empty((num_islands, max_pixels), dtype=np.int32)" and https://github.com/transientskp/pyse/blob/591717fd72decbb7fc991bf2232be595c3f2b6f3/sourcefinder/extract.py#L2230 "ypositions = np.empty((num_islands, max_pixels), dtype=np.int32)" What is called "xpositions" and "ypositions" in the "extract" module is propagated to "posx" and "posy" in "fitting.moments_enhanced", respectively. The problem can occur for any island smaller than the largest island, i.e. for "no_pixels < max_pixels" from this line https://github.com/transientskp/pyse/blob/591717fd72decbb7fc991bf2232be595c3f2b6f3/sourcefinder/measure.py#L498 " mask = (posx == rounded_barycenter[0]) & (posy == rounded_barycenter[1]) " which generally causes "mask.size>no_pixels", i.e. "mask" generally becomes (much) larger than the island. This should nnever occur. 3) After the fix from 1) we end up with a maximum ratio "peak/maxi" of 1.62 from the 60 images described above, see this comment: https://github.com/transientskp/pyse/issues/182#issuecomment-3620012664. That is why "upp_bound = 2.0 * basevalue" seems appropriate, in the sense that this bound will never be reached. This implies that we could replace that line by "upp_bound = np.inf", but that seems a bit risky at this stage; we have not explored enough pathological cases yet. 4) The comment about the meaning of the "maxi" input to "measure.moments_enhanced" should be clearer now. --- sourcefinder/measure.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/sourcefinder/measure.py b/sourcefinder/measure.py index 2f095c2..3ea4141 100644 --- a/sourcefinder/measure.py +++ b/sourcefinder/measure.py @@ -369,8 +369,9 @@ def moments_enhanced( island. Units: pixels. maxi : float - Peak pixel value within the island. Units: spectral brightness, - typically Jy/beam. To clarify: source_island[maxpos] == maxi. + Peak pixel value within the island. Pixel values represent spectral + brightness (typically in Jy/beam). + For clarity: source_island[maxpos] == maxi. fudge_max_pix_factor : float Correction factor for underestimation of the peak by considering the @@ -464,6 +465,8 @@ def moments_enhanced( # dump the redundant elements that have undetermined values. source_island = source_island[:no_pixels] noise_island = noise_island[:no_pixels] + posx = posx[:no_pixels] + posy = posy[:no_pixels] # The significance of a source detection is determined in this way. significance[0] = (source_island / noise_island).max() @@ -504,13 +507,7 @@ def moments_enhanced( i = np.nonzero(mask)[0][0] basepos = rounded_barycenter basevalue = source_island[i] - else: - # The rounded barycenter position is not in source_island, so we - # revert to the maximum pixel position, which is always included. - basepos = maxpos[0] - chunkpos[0], maxpos[1] - chunkpos[1] - basevalue = maxi - - deltax, deltay = xbar - basepos[0], ybar - basepos[1] + deltax, deltay = xbar - basepos[0], ybar - basepos[1] if force_beam: # If the restoring beam is forced, we use the beam parameters @@ -595,7 +592,7 @@ def moments_enhanced( else: theta -= math.pi / 2.0 - if np.sign(threshold) == np.sign(basevalue): + if np.sign(threshold) == np.sign(basevalue) and in_source_island: # Implementation of "tweaked moments", equation 2.67 from # Spreeuw's thesis. In that formula the "base position" was the # maximum pixel position. Here that is the rounded @@ -620,9 +617,9 @@ def moments_enhanced( # Gaussian fits. if basevalue > 0: low_bound = 0.5 * basevalue - upp_bound = 1.5 * basevalue + upp_bound = 2.0 * basevalue else: - low_bound = 1.5 * basevalue + low_bound = 2.0 * basevalue upp_bound = 0.5 * basevalue # The number of iterations used for the root finder is also