@@ -83,64 +83,85 @@ def flag_saturated_pixels(
83
83
84
84
for ints in range (nints ):
85
85
# Work forward through the groups for initial pass at saturation
86
+
87
+ # We want to flag saturation in all subsequent groups after
88
+ # the one in which it was found. Use this boolean array to
89
+ # keep a running tally of pixels that have saturated.
90
+ previously_saturated = np .zeros (shape = (nrows , ncols ), dtype = 'bool' )
91
+
86
92
for group in range (ngroups ):
87
93
plane = data [ints , group , :, :]
88
94
89
- flagarray , flaglowarray = plane_saturation (plane , sat_thresh , dqflags )
90
-
91
95
# for saturation, the flag is set in the current plane
92
96
# and all following planes.
93
- np .bitwise_or (gdq [ints , group :, :, :], flagarray , gdq [ints , group :, :, :])
97
+
98
+ # Update the running tally of all pixels that have ever
99
+ # experienced saturation to account for this.
100
+
101
+ previously_saturated |= (plane >= sat_thresh )
102
+ flagarray = (previously_saturated * saturated ).astype (np .uint32 )
103
+
104
+ gdq [ints , group , :, :] |= flagarray
94
105
95
106
# for A/D floor, the flag is only set of the current plane
96
- np .bitwise_or (gdq [ints , group , :, :], flaglowarray , gdq [ints , group , :, :])
107
+ flaglowarray = ((plane <= 0 )* (ad_floor | dnu )).astype (np .uint32 )
108
+
109
+ gdq [ints , group , :, :] |= flaglowarray
97
110
98
111
del flagarray
99
112
del flaglowarray
100
113
101
114
# now, flag any pixels that border saturated pixels (not A/D floor pix)
102
115
if n_pix_grow_sat > 0 :
103
- gdq_slice = copy .copy (gdq [ints , group , :, :]).astype (int )
104
-
105
- gdq [ints , group , :, :] = adjacent_pixels (gdq_slice , saturated , n_pix_grow_sat )
116
+ gdq_slice = gdq [ints , group , :, :]
117
+ adjacent_pixels (gdq_slice , saturated , n_pix_grow_sat , inplace = True )
106
118
107
119
# Work backward through the groups for a second pass at saturation
108
120
# This is to flag things that actually saturated in prior groups but
109
121
# were not obvious because of group averaging
110
- for group in range (ngroups - 2 , - 1 , - 1 ):
122
+
123
+ for group in range (ngroups - 2 , - 1 , - 1 ):
124
+
111
125
plane = data [ints , group , :, :]
112
126
thisdq = gdq [ints , group , :, :]
113
127
nextdq = gdq [ints , group + 1 , :, :]
114
128
115
129
# Determine the dilution factor due to group averaging
130
+
131
+ # No point in this step if the dilution factor is 1. In
132
+ # that case, there is no way that we would have missed
133
+ # saturation before but flag it now, since the threshold
134
+ # would be the same.
135
+
116
136
if read_pattern is not None :
117
137
# Single value dilution factor for this group
118
138
dilution_factor = np .mean (read_pattern [group ]) / read_pattern [group ][- 1 ]
139
+ if dilution_factor == 1 :
140
+ continue
119
141
# Broadcast to array size
120
142
dilution_factor = np .where (no_sat_check_mask , 1 , dilution_factor )
121
143
else :
122
144
dilution_factor = 1
145
+ continue
123
146
124
- # Find where this plane looks like it might saturate given the dilution factor
125
- flagarray , _ = plane_saturation (plane , sat_thresh * dilution_factor , dqflags )
147
+ # Find where this plane looks like it might saturate given
148
+ # the dilution factor, *and* this group did not already get
149
+ # flagged as saturated or do not use, *and* the next group
150
+ # was flagged as saturated. Result of the line below is a
151
+ # boolean array.
126
152
127
- # Find the overlap of where this plane looks like it might saturate, was not currently
128
- # flagged as saturation or DO_NOT_USE, and the next group had saturation flagged.
129
- indx = np .where ((np .bitwise_and (flagarray , saturated ) != 0 ) & \
130
- (np .bitwise_and (thisdq , saturated ) == 0 ) & \
131
- (np .bitwise_and (thisdq , dnu ) == 0 ) & \
132
- (np .bitwise_and (nextdq , saturated ) != 0 ))
153
+ partial_sat = ((plane >= sat_thresh * dilution_factor ) & \
154
+ (thisdq & (saturated | dnu ) == 0 ) & \
155
+ (nextdq & saturated != 0 ))
133
156
134
- # Reset flag array to only pixels passing this gauntlet
135
- flagarray [:] = 0
136
- flagarray [indx ] = dnu
157
+ flagarray = (partial_sat * dnu ).astype (np .uint32 )
137
158
138
159
# Grow the newly-flagged saturating pixels
139
160
if n_pix_grow_sat > 0 :
140
- flagarray = adjacent_pixels (flagarray , dnu , n_pix_grow_sat )
161
+ adjacent_pixels (flagarray , dnu , n_pix_grow_sat , inplace = True )
141
162
142
163
# Add them to the gdq array
143
- np . bitwise_or ( gdq [ints , group , :, :], flagarray , gdq [ ints , group , :, :])
164
+ gdq [ints , group , :, :] |= flagarray
144
165
145
166
# Add an additional pass to look for things saturating in the second group
146
167
# that can be particularly tricky to identify
@@ -160,25 +181,24 @@ def flag_saturated_pixels(
160
181
mask &= scigp2 > sat_thresh / len (read_pattern [1 ])
161
182
162
183
# Identify groups that are saturated in the third group but not yet flagged in the second
163
- gp3mask = np . where ((np .bitwise_and (dq3 , saturated ) != 0 ) & \
164
- (np .bitwise_and (dq2 , saturated ) == 0 ), True , False )
184
+ gp3mask = ((np .bitwise_and (dq3 , saturated ) != 0 ) & \
185
+ (np .bitwise_and (dq2 , saturated ) == 0 ))
165
186
mask &= gp3mask
166
187
167
188
# Flag the 2nd group for the pixels passing that gauntlet
168
- flagarray = np .zeros_like (mask ,dtype = 'uint8' )
169
- flagarray [mask ] = dnu
189
+ flagarray = (mask * dnu ).astype (np .uint32 )
170
190
171
191
# Add them to the gdq array
172
192
np .bitwise_or (gdq [ints , 1 , :, :], flagarray , gdq [ints , 1 , :, :])
173
-
193
+
174
194
175
195
# Check ZEROFRAME.
176
196
if zframe is not None :
177
197
plane = zframe [ints , :, :]
178
198
flagarray , flaglowarray = plane_saturation (plane , sat_thresh , dqflags )
179
199
zdq = flagarray | flaglowarray
180
200
if n_pix_grow_sat > 0 :
181
- zdq = adjacent_pixels (zdq , saturated , n_pix_grow_sat )
201
+ adjacent_pixels (zdq , saturated , n_pix_grow_sat , inplace = True )
182
202
plane [zdq != 0 ] = 0.0
183
203
zframe [ints ] = plane
184
204
@@ -192,7 +212,7 @@ def flag_saturated_pixels(
192
212
return gdq , pdq , zframe
193
213
194
214
195
- def adjacent_pixels (plane_gdq , saturated , n_pix_grow_sat ):
215
+ def adjacent_pixels (plane_gdq , saturated , n_pix_grow_sat = 1 , inplace = False ):
196
216
"""
197
217
plane_gdq : ndarray
198
218
The data quality flags of the current.
@@ -204,17 +224,65 @@ def adjacent_pixels(plane_gdq, saturated, n_pix_grow_sat):
204
224
Number of pixels that each flagged saturated pixel should be 'grown',
205
225
to account for charge spilling. Default is 1.
206
226
227
+ inplace : bool
228
+ Update plane_gdq in place, returning None? Default False.
229
+
207
230
Return
208
231
------
209
232
sat_pix : ndarray
210
233
The saturated pixels in the current plane.
211
234
"""
212
- cgdq = plane_gdq .copy ()
213
- only_sat = np .bitwise_and (plane_gdq , saturated ).astype (np .uint8 )
235
+ if not inplace :
236
+ cgdq = plane_gdq .copy ()
237
+ else :
238
+ cgdq = plane_gdq
239
+
240
+ only_sat = plane_gdq & saturated > 0
241
+ dilated = only_sat .copy ()
214
242
box_dim = (n_pix_grow_sat * 2 ) + 1
215
- struct = np .ones ((box_dim , box_dim )).astype (bool )
216
- dialated = ndimage .binary_dilation (only_sat , structure = struct ).astype (only_sat .dtype )
217
- return np .bitwise_or (cgdq , (dialated * saturated ))
243
+
244
+ # The for loops below are equivalent to
245
+ #
246
+ #struct = np.ones((box_dim, box_dim)).astype(bool)
247
+ #dilated = ndimage.binary_dilation(only_sat, structure=struct).astype(only_sat.dtype)
248
+ #
249
+ # The explicit loop over the box, followed by taking care of the
250
+ # array edges, turns out to be faster by around an order of magnitude.
251
+ # There must be poor coding in the underlying routine for
252
+ # ndimage.binary_dilation as of scipy 1.14.1.
253
+
254
+ for i in range (box_dim ):
255
+ for j in range (box_dim ):
256
+
257
+ # Explicit binary dilation over the inner ('valid')
258
+ # region of the convolution/filter
259
+
260
+ i2 = only_sat .shape [0 ] - box_dim + i + 1
261
+ j2 = only_sat .shape [1 ] - box_dim + j + 1
262
+
263
+ k1 , k2 , l1 , l2 = [n_pix_grow_sat , - n_pix_grow_sat ,
264
+ n_pix_grow_sat , - n_pix_grow_sat ]
265
+
266
+ dilated [k1 :k2 , l1 :l2 ] |= only_sat [i :i2 , j :j2 ]
267
+
268
+ for i in range (n_pix_grow_sat - 1 , - 1 , - 1 ):
269
+ for j in range (i + n_pix_grow_sat , - 1 , - 1 ):
270
+
271
+ # March from the limit of the 'valid' region toward
272
+ # each edge. Maximum filter ensures correct dilation.
273
+
274
+ dilated [i ] |= ndimage .maximum_filter (only_sat [j ], box_dim )
275
+ dilated [:, i ] |= ndimage .maximum_filter (only_sat [:, j ], box_dim )
276
+ dilated [- i - 1 ] |= ndimage .maximum_filter (only_sat [- j - 1 ], box_dim )
277
+ dilated [:, - i - 1 ] |= ndimage .maximum_filter (only_sat [:, - j - 1 ], box_dim )
278
+
279
+ cgdq [dilated ] |= saturated
280
+
281
+ if inplace :
282
+ return None
283
+ else :
284
+ return cgdq
285
+
218
286
219
287
220
288
def plane_saturation (plane , sat_thresh , dqflags ):
0 commit comments